[JavaScript] 콜백 함수 (Callback Function)


1. 콜백 함수 (Callback Function)

  • 다른 코드(함수 또는 메서드)에게 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수

2. 제어권

(1) 호출 시점

첫번째로 setInterval 구조를 알아야함

var intervalID = scope.setInterval(func,delay[,param1,param2,...]);

  • 매개변수로 func(함수) , delay(ms)값은 필수값
  • 세 번째 param1,param2 (매개변수)는 선택값
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초)

[Done] exited with code=0 in 1.558 seconds

코드 실행 방식과 제어권을 알아보면

  • timer 변수는 setInterval의 ID값이 담김
  • **cbFunc 함수는 0.3초 마다 실행 , callback 내부에서 count값을 출력하고 , count값을 1만큼 증가하고 4보다 크면 루프 종료**
code호출 주체사용자
cbFunc사용자사용자
setInterval(cbFunc, 300)setIntervalsetInterval
  • 이 코드는 제어권을 위임 받은 setInterval이 스스로의 판단에 따라 적절한 시점(0.3초)에 익명 함수를 실행 한것

(2) 인자

(1) Array.prototype.map 예시

Array.prototype.map(callback[,thisArg]) callback:function(currentValue,index,array)

  • map 메서드는 첫 번째 인자는 callback 함수 , 생략가능한 두 번째 인자로 콜백 함수 내부에서 인식할 this
  • 콜백 함수의 첫 번째 인자는 배열 요소중 현재값 , 두번째 인자는 현재값의 인덱스 세번째 인자에선 map 메서드의 대상이 되는 배열
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 ]

[Done] exited with code=0 in 0.065 seconds

(3) this

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 mappedArr;
};
  • 첫번째 인자에 메서드 this의 배열 i번째값
  • 두번째 인자에 i
  • 세번째 인자에는 배열 자체를 지정해 호출한다

결과값으로 mappedValue에 담겨 mappedArri번째 인자에 할당됌

3. 메서드를 콜백 함수로 전달할 경우

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

obj.logValues(1, 2);
[(4, 5, 6)].forEach(obj.logValues);

// { vals: [ 1, 2, 3 ], logValues: [Function: logValues] } 1 2
// Window {...} 4 0
// Window {...} 5 1
// Window {...} 6 2
  • 메서드를 forEach 함수의 콜백 함수로 전달
  • forEach에 의해 콜백이 함수로서 호출되고 , 별도로 this를 지정하는 인자를 지정하지 않았으니 함수 내부의 this는 전역객체를 바라봄

4. 콜백 함수 내부의 this 바인딩

(1): 콜백 함수 내부의 this에 다른 값 바인딩 방법

var obj1 = {
  name: "obj1",
  func: function () {
    var self = this;
    return function () {
      console.log(self.name);
    };
  },
};

var callback = obj1.func();
setTimeout(callback, 1000);

// obj1
[Done] exited with code=0 in 1.055 seconds

(2): Func 함수를 재활용 할 경우

var obj1 = {
  name: "obj1",
  func: function () {
    var self = this;
    return function () {
      console.log(self.name);
    };
  },
};

var callback = obj1.func();
setTimeout(callback, 1000);

var obj2 = {
  name: "obj2",
  func: obj1.func,
};

var callback2 = obj2.func();
setTimeout(callback2, 1000);

var obj3 = { name: "obj3" };
var callback3 = obj1.func.call(obj3);
setTimeout(callback3, 2000);
  • callback2에는 obj2func를 실행한 결과를 담아 콜백으로 사용
  • callback3에는 obj1func를 실행하면서 thisobj3가 되도록 지정해 콜백 사용

(3): 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);

5. 콜백지옥과 비동기 제어

  • 콜백 지옥 : 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상
  • 동기 코드 : 현재 실행 중인 코드가 끝나고 다음 코드를 실행하는 방식
  • 비동기 코드 : 현재 실행 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어

비동기 코드를 사용하는 경우 예시

  • 사용자의 요청에 의해 특정 시간이 경과되기 전까지 어떤 함수의 실행을 보류하는 경우
  • 사용자의 직접적인 개입이 있을때 비로소 어떤 함수를 실행하도록 대기
  • 웹브라우저 자체가 아닌 별도의 대상에 무언가를 요청하고 그에 대한 응답이 왔을때, 어떤 함수를 실행하도록 대기
  • 별도의 요청 , 실행 대기 , 보류 코드

(1) 0.5초 주기 마다 커피 목록 수집

(1) 콜백 지옥

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);
            setTimeout(
              function (name) {
                coffeeList += "," + name;
                console.log(coffeeList);
              },
              500,
              "카페라떼"
            );
          },
          500,
          "카페모카"
        );
      },
      500,
      "아메리카노"
    );
  },
  500,
  "에스프레소"
);

에스프레소
에스프레소,아메리카노
에스프레소,아메리카노,카페모카
에스프레소,아메리카노,카페모카,카페라떼

값 전달 순서가 아래에서 위로 향하고 있고 가독성이 매우 떨어진다

(2) 콜백 지옥 해결 - 기명 함수 변환

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(addLatte, 500, "카페라떼");
};
var addLatte = function (name) {
  coffeeList = name;
  console.log(coffeeList);
};

setTimeout(addEspresso, 500, "에스프레소");
에스프레소
아메리카노
카페모카
카페라떼

(3) 콜백 지옥 해결 - ES6 Promise

  • new 연산자와 함께 호출한 Promise의 인자로 넘겨주는 콜백 함수가 실행
  • resolve 또는 reject 함수를 호출하는 구문이 있을 경우 둘중 하나가 실행되기 전까지 then 또는 catch로 넘어가지 않음
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("카페모카"))
  .then(addCoffee("카페라떼"));
에스프레소
에스프레소,아메리카노
에스프레소,아메리카노,카페모카
에스프레소,아메리카노,카페모카,카페라떼

(4) 콜백 지옥 해결 - ES6 Generator

  • *이 붙은 함수가 generator 함수
  • generator 함수를 실행하면 Iterator가 반환되고 Iteratornext라는 메서드를 가짐
  • next 메서드를 호출하면 generator 함수 내부에서 먼저 등장하는 yierd에서 함수 실행을 멈춤
  • 비동기 작업이 완료되는 시점마다 next 메서드를 호출하면 generator 함수가 위에서 아래로 순차적으로 진행함
var addCoffee = function (prevName, name) {
  setTimeout(function () {
    coffeeMaker.next(prevName ? prevName + "," + name : name);
  }, 500);
};

var CoffeeGenerator = function* () {
  var espresso = yield addCoffee("", "에스프레소");
  console.log(espresso);
  var americano = yield addCoffee(espresso, "아메리카노");
  console.log(americano);
  var mocha = yield addCoffee(americano, "카페모카");
  console.log(mocha);
  var latte = yield addCoffee(mocha, "카페라떼");
  console.log(latte);
};

var coffeeMaker = CoffeeGenerator();
coffeeMaker.next();
에스프레소
에스프레소,아메리카노
에스프레소,아메리카노,카페모카
에스프레소,아메리카노,카페모카,카페라떼

(5) 콜백 지옥 해결 - ES2017 async/await

  • 비동기 작업을 수행할 함수 앞에 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);
  await _addCoffee("카페라떼");
  console.log(coffeeList);
};

coffeeMaker();
에스프레소
에스프레소,아메리카노
에스프레소,아메리카노,카페모카
에스프레소,아메리카노,카페모카,카페라떼