\(@^0^@)/

[BOOK] 코어 자바스크립트, 콜백 함수 본문

BOOKS/코어 자바스크립트

[BOOK] 코어 자바스크립트, 콜백 함수

minjuuu 2022. 3. 18. 18:17
728x90

오늘 읽은 범위 : (콜백 함수) p.94 ~ p.114


< 책에서 기억하고 싶은 내용 >

콜백 함수

  • 콜백 함수 (callback function)는 다른 코드의 인자로 넘겨주는 함수.
  • 콜백 함수는 제어권과 관련이 깊다.
  • 콜백 함수는 다른 코드(함수 또는 메서드)에게 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수.
  • 콜백 함수를 위임받은 코드는 자체적인 내부 로직에 의해 이 콜백 함수를 적절한 시점에 실행할 것.

제어권

var count = 0;
var cbFunc = function () {
    console.log(count);

    if (++count > 4) clearInterval(timer);
};

var timer = setInterval(cbFunc, 300);

// -- 실행 결과 --
// 0 (0.3초)
// 1 (0.6초)
// 2 (0.9초)
// 3 (1.2초)
// 4 (1.5초)

이 코드를 실행하면 콘솔창에는 0.3초에 한 번씩 숫자가 0부터 1씩 증가하며 출력되다가 4가 출력된 이후 종료된다.
setInterval이라고 하는 '다른 코드'에 첫 번째 인자로서 cbFunc 함수를 넘겨주자 제어권을 넘겨받은 setInterval이 스스로의 판단에 따라 적절한 시점에(0.3초마다) 이 익명 함수를 실행.
콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수 호출 시점에 대한 제어권을 갖는다.


var newArr = [10, 20, 30].map(function (currentValue, index) {
    console.log(currentValue, index);
    return currentValue + 5;
});
console.log(newArr);

// -- 실행 결과 --
// 10 0
// 20 1
// 30 2
// [15, 25, 35]

콜백 함수의 첫 번째 인자에는 배열의 요소 중 현재 값이, 두 번째 인자에는 현재 값의 인덱스가, 세 번째 인자에는 map 메서드의 대상이 되는 배열 자체가 담긴다.
배열 [10, 20, 30]의 각 요소를 처음부터 하나씩 꺼내어 콜백 함수를 실행한다.
첫 번째(인덱스 0)에 대한 콜백 함수는 currentValue에 10, index에는 인덱스 0을 담고, 15 (10 + 5)를 반환.
두 번째(인덱스 1)에 대한 콜백 함수는 currentValue에 20, index에는 인덱스 1을 담고, 20 (20 + 5)를 반환.
세 번째(인덱스 2)에 대한 콜백 함수는 currentValue에 30, index에는 인덱스 2를 담고, 30 (30 + 5)를 반환.
세 번째에 대한 콜백 함수까지 실행을 마치고 나면, [15, 25, 35]라는 새로운 배열이 만들어져서 변수 newArr에 담기고, 출력된다.


this

콜백 함수도 함수이기 때문에 기본적으로는 this가 전역객체를 참조하지만, 제어권을 넘겨받을 코드에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조하게 된다.


콜백 함수는 함수다

콜백 함수로 어떤 객체의 메서드를 전달하더라도 그 메서드는 메서드가 아닌 함수로서 호출된다.

var obj = {
    vals: [1, 2, 3],
    logValues: function(v, i) {
        console.log(this, v, i);
    }
};

obj.logValues(1, 2);                // {vals: [1, 2, 3], logValues: f} 1 2
[4, 5, 6].forEach(obj.logValues);   // Window { ... } 4 0
                                    // Window { ... } 5 1
                                    // Window { ... } 6 2

obj.logValues(1, 2)는 이 메서드의 이름 앞에 점이 있으니 메서드로서 호출한 것으로, this는 obj를 가리키고 인자로 넘어온 1, 2가 출력된다.

하지만, [4, 5, 6].forEach(obj.logValues) 에서는 이 메서드를 forEach 함수의 콜백 함수로서 전달했다.
obj를 this로 하는 메서드를 그대로 전달한 것이 아니라, obj.logValues가 가리키는 함수만 전달한 것으로, obj와 직접적인 연관이 없고, forEach에 의해 콜백이 함수로서 호출되고, 별도로 this를 지정하는 인자를 지정하지 않았으므로 함수 내부에서의 this는 전역 객체를 바라보게 된다.


콜백 함수 내부의 this에 다른 값 바인딩하기

  • 콜백 함수 내부의 this에 다른 값을 바인딩하는 전통적인 방법
var obj1 = {
    name: 'obj1',
    func: function () {
        var self = this;
        return function () {
            console.log(self.name);
        };
    }
};
var callback = obj1.func();
setTimeout(callback, 1000);

이 방식은 실제로 this를 사용하지도 않을뿐더러 번거로워서 차라리 this를 아예 안 쓰는 편이 더 나을지도 모른다.


  • 콜백 함수 내부의 this에 다른 값을 바인딩하는 방법 - bind 메서드 활용
var obj1 = {
    name: 'obj1',
    func: function () {
        console.log(this.name);
    }
};

setTimeout(obj1.func.bind(obj1), 1000);

var obj2 = { name: 'obj2' };
setTimeout(obj1.func.bind(obj2), 1500);

ES5에서 등장한 bind 메서드를 이용하면, 전통적인 방식의 아쉬움을 보완할 수 있다.


콜백 지옥과 비동기 제어

콜백 지옥(callback hell) : 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상.

  • 이벤트 처리나 서버 통신과 같이 비동기적인 작업을 수행하기 위해 이런 형태가 자주 등장.
    가독성이 떨어질뿐더러 코드를 수정하기도 어렵다.
  • 동기적인 코드 : 현재 실행 중인 코드가 완료된 후, 다음 코드를 실행하는 방식
    (CPU의 계산에 의해 즉시 처리가 가능한 대부분의 코드)
  • 비동기적인 코드 : 현재 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어가는 방식
    (별도의 요청, 실행 대기, 보류)
setTimeout(function (name) {
    var coffeeList = name;
    console.log(coffeeList);

    setTimeout(function (name) {
        coffeeList += ', ' + name;
        console.log(coffeeList);

        setTimeout(function (name) {
        coffeeList += ', ' + name;
        console.log(coffeeList);
        }, 500, '카페라떼');
    }, 500, '카페모카');
}, 500, '아메리카노');

제대로 출력은 되지만 들여쓰기 수준이 과도하게 깊어졌고, 값이 전달되는 순서가 '아래에서 위로'향하고 있어 어색하게 느껴짐.

  • 콜백 지옥 해결 방안
    • 1. 기명함수로 변환
      • 코드의 가독성을 높일 뿐 아니라 함수 선언과 함수 호출만 구분할 수 있다면 위에서부터 아래로 순서대로 읽어 내려가는데 어려움이 없음.
      • 하지만, 일회성 함수를 전부 변수에 할당해야 하므로, 오히려 헷갈리고 비효율적일 수 있음.
    • 2. ES6에서는 Promise, Generator 도입 (비동기 작업의 동기적 표현
      • Promise(1)
        new Promise(function (resolve) {
            setTimeout(function () {
                var name = '아메리카노';
                console.log(name);
                resolve(name);
            }, 500);
        }).then(function (prevName) {
            return new Promise(function (resolve) {
                setTimeout(function () {
                    var name = prevName + ', 카페모카';
                    console.log(name);
                    resolve(name);
                }, 500);
            });
        }).then(function (prevName) {
            return new Promise(function (resolve) {
                setTimeout(function () {
                    var name = prevName + ', 카페라떼';
                    console.log(name);
                    resolve(name);
                }, 500);
            });
        });

      • Promise(2)
        반복적인 내용을 함수 화해서 짧게 표현.
        var addCoffee = function (name) {
            return function (prevName) {
                return new Promise(function (resolve) {
                    setTimeout(function () {
                        var newName = prevName ? (prevName + ', ' + name) : name;
                        console.log(newName);
                        resolve(newName);
                    }, 500);
                });
            };
        };
        addCoffee('아메리카노')()
                  .then(addCoffee('카페모카'))
                  .then(addCoffee('카페라떼'));

      • Generator
var addCoffee = function (prevName, name) {
    setTimeout(function () {
        coffeeMaker.next(prevName ? prevName + ', '+ name : name);
    }, 500);
};
var coffeeGenerator = function* () {
    var americano = yield addCoffee('', '아메리카노');
    console.log(americano);
    var mocha = yield addCoffee('', '카페모카');
    console.log(mocha);
    var latte = yield addCoffee('', '카페라떼');
    console.log(latte);
};
var coffeeMaker = coffeeGenerator();
coffeeMaker.next();
  • 3. ES2017에서는 async/await 도입 (비동기 작업의 동기적 표현)
    • var addCoffee = function (name) {
          return new Promise(function (resolve) {
              setTimeout(function () {
                  resolve(name);
              }, 500);
          });
      };
      
      var coffeeMaker = async function () {
          var coffeeList = '';
          var _addCoffee = async function (name) {
              coffeeList += (coffeeList ? ',' : '') + await addCoffee(name);
          };
          await _addCoffee('아메리카노');
          console.log(coffeeList);
          await _addCoffee('카페모카');
          console.log(coffeeList);
          await _addCoffee('카페라떼');
          console.log(coffeeList);
      };
      coffeeMaker();
    • 비동기 작업을 수행하고자 하는 함수 앞에 async를 표기하고, 함수 내부에서 실질적인 비동기 작업이 필요한 위치마다 await를 표기하는 것만으로 뒤의 내용을 Promise로 자동 전환하고, 해당 내용이 resolve 된 이후에야 다음으로 진행한다. 즉, Promise의 then과 흡사한 효과를 얻을 수 있다.

< 정리 >

  • 콜백 함수는 다른 코드에 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수이다.
  • 제어권을 넘겨받은 코드는 다음과 같은 제어권을 가진다.
    • 콜백 함수를 호출하는 시점을 스스로 판단해서 실행한다.
    • 콜백 함수를 호출할 때 인자로 넘겨줄 값들 및 그 순서를 따라야 한다.
    • 콜백 함수의 this가 무엇을 바라보도록 할지가 정해져 있는 경우도 있음.
      (정하지 않은 경우에는 전역 객체, 사용자 임의로 this를 바꾸고 싶을 경우 bind 메서드를 활용)
    • 어떤 함수에 인자로 메서드를 전달하더라도 이는 결국 함수로서 실행됨.
    • 비동기 제어를 위해 콜백 함수를 사용하다 보면 콜백 지옥에 빠지기 쉬운데, 이에 대한 해결책으로 Promise, Generator, async/await 등이 있다.

[ 출처 : 코어 자바스크립트 ]

728x90