[JavaScript] 콜백 함수

빵호·2021년 11월 19일
0

JavaScript

목록 보기
18/28
post-thumbnail

콜백 함수

콜백 함수는 다른 코드에게 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수이다.

제어권

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초)

timer 변수에는 setInerval의 ID 값이 담긴다. setInerval에 전달한 첫 번째 인자인 cbFunc 함수(이 함수가 곧 콜백 함수이다)는 0.3초마다 자동으로 실행될 것이다. 콜백 함수 내부에서는 count 값을 출력하고, count를 1만큼 증가시킨 다음, 그 값이 4보다 크면 반복 실행을 종료한다.

setInterval의 첫 번째 인자로서 cbFunc 함수를 넘겨주자 제어권을 넘겨받은 setInterval이 스스로의 판단에 따라 적절한 시점에 이 익명 함수를 실행했다. 이처럼 콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수 호출 시점에 대한 제어권을 가진다.

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]

1번째 줄에서 newArr 변수를 선언하고 우항의 결과를 할당한다. 이후 map 메서드를 호출하였고 이때 첫 번째 매개변수로 익명 함수를 전달한다. map 메서드는 첫 번째 인자로 callback 함수를 받고 배열의 모든 요소들에 콜백 함수를 반복 호출한다. 그리고 콜백 함수의 실행 결과들을 모아 새로운 배열을 만든다.

Array.prototype.map = function (callback, thisArg) {
  var mappedArr = [];
  for (var i =0; i < this.length; i ++) {
    var mappedValue = callback.call(thisArg || window, this[i], i, this);
    mappedArr[i] = mappedValue;
  }
  return mapperdArr;
};
// map의 두 번째 매개변수에 this의 값이 있으면 그 값을 없으면 window를 바인딩
// 첫 번째 인자에는 this가 배열을 가리키므로 배열의 i 번째 요소 값을
// 두 번째 인자에는 i 값을
// 세 번째 인자에는 배열 자체를 지정해 호출

콜백 함수도 함수이기 때문에 기본적으로는 this가 전역 객체를 참조하지만, 제어권을 넘겨받을 코드에서 콜백 함수에 call / apply를 통해 this가 될 대상을 명시적으로 바인딩 할 수 있다.

setTimeout(function () {
  console.log(this);
}, 300); // (1) Window {...}

[1, 2, 3, 4, 5].forEach(function (x) {
  console.log(this); // (2) Window {...}
});

document.body.innerHTML += '<button id="a">클릭</button>';
document.body.querySelector('#a').addEventListener('click', function (e) {
  console.log(this, e); // (3) <button id="a">클릭</button>
});

(1), (2)는 콜백 함수에서 따로 바인딩을 하지 않으면 this는 전역 객체를 가리키기 때문에 전역 객체를 가리키고 (3)의 경우도 콜백 함수이긴 하지만 addEventListener의 경우에는 내부에서 콜백 함수를 호출할 때 call 메서드의 첫 번째 인자에 addEventListener 메서드의 this를 그대로 넘기도록 정의돼 있어 this가 HTML 엘리먼트를 가리킨다.

콜백 함수는 함수다

당연한 소리이지만 콜백 함수로 어떤 객체의 메서드를 전달하더라도 그 메서드는 함수로서 호출된다.

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는 메서드로 정의됐다. 7번째 줄에서는 이 메서드를 메서드로서 호출하였고 따라서 this는 obj를 가리키고, 인자로 넘어온 1, 2가 출력된다.

8번째 줄에서는 이 메서드를 forEach 함수의 콜백 함수로서 전달했다. 이때 obj를 this로 하는 메서드를 그대로 전달한 것이 아니라, obj.logValues가 가리키는 함수만 전달한다. 그래서 별도로 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);
// 1초 뒤에 obj1을 출력

콜백 함수로 활용할 함수에서는 this 대신 그 변수를 사용하게 하여, 이를 클로저로 만드는 방식이 많이 쓰였다.

this를 다른 변수(self)에 담고, 익명 함수를 선언과 동시에 반환했다. 이제 obj1.func를 호출하면 앞서 선언한 내부함수가 반한되어 callback 변수에 담긴다. 그리고 setTimeout 함수에 인자로 전달하면 1초뒤 callback이 실행되면서 ojb1을 출력한다. 하지만 이 방식은 실제로 this를 사용하지도 않고 번거롭다.

var obj1 = {
  name: 'obk1',
  func: function () {
    console.log(obj1.name);
  }
};
setTimeout(obj1.func, 1000);

또 다른 방법으로는 this를 쓰지 않는 방법이 있다. 하지만 이 방법은 훨씬 간결하고 직관적이지만 작성한 함수를 this를 이용해 다양한 상황에 재활용할 수 없다.

var obj1 = {
  name: 'obk1',
  func: function () {
    console.log(this.name);
  }
};
setTimeout(obj1.func.bind(obj1), 1000);

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

이런 아쉬움을 보완하는 방법이 ES5에 등장한 bind 메서드를 이용하는 방법이다.

콜백 지옥과 비동기 제어

콜백 지옥은 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상을 말하고 자바스크립트에서 흔히 발생하는 문제이다. 이런 경우 가독성도 떨어지고 코드를 수정하기도 어렵다.

setTimeout(
  function (name) {
    let coffeeList = name;
    console.log(coffeeList);

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

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

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

위 코드는 콜백 지옥의 예시이다. 0.5초 주기마다 커피 목록을 수집하고 출력한다. 각 콜백은 커피 이름을 전달하고 목록에 이름을 추가한다. 들여 쓰기도 너무 과하고 값이 전달되는 순서가 아래에서 위로 향하고 있어 어색하게 느껴진다.

var coffeeList = '';

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

setTimeout(addEspresso, 500, '아메리카노');

가독성과 어색함을 해결하는 방법은 콜백 함수를 모두 기명함 수로 전환하는 방법이 있다. 이 방식은 코드의 가독성도 높이고 함수 선언과 함수 호출만 구분할 수 있다면 위에서 아래로 읽는데 어려움이 없다. 하지만 일회성 함수를 전부 변수에 할당하는 게 단점이다.

new Promise(function (resolve) {
  setTimeout(function () {
    let name = '에스프레소';
    console.log(name);
    resolve(name);
  }, 500);
})
  .then(function (prevName) {
    return new Promise(function (resolve) {
      setTimeout(function () {
        let name = prevName + ', 아메리카노';
        console.log(name);
        resolve(name);
      }, 500);
    });
  })
  .then(function (prevName) {
    return new Promise(function (resolve) {
      setTimeout(function () {
        let name = prevName + ', 카페모카';
        console.log(name);
        resolve(name);
      }, 500);
    });
  })
  .then(function (prevName) {
    return new Promise(function (resolve) {
      setTimeout(function () {
        let name = prevName + ', 카페모카';
        console.log(name);
        resolve(name);
      }, 500);
    });
  });

하지만 위의 문제들을 ES6에서 도입된 Promise 와 ES8에서 도입된 async/await을 사용해 해결이 가능하다. new 연산자와 함께 호출한 Promise의 인자로 넘겨주는 콜백 함수는 호출할 때 바로 실행되지만 그 내부에 resolve 또는 reject 함수를 호출하는 구문이 있을 경우 둘 중 하나가 실행되기 전까지는 다음(then) 또는 오류 구문(cathc)으로 넘어가지 않는다. 그래서 비동기 작업의 동기적 표현이 가능하다.

profile
늘 한결같이 꾸준히

0개의 댓글