JavaScript - 콜백함수

자바스크립트 - 콜백함수

  • 콜백 함수 : 다른 코드의 인자로 넘겨주는 함수
  • 제어권과 관련이 깊음
  • 어떤 함수 X를 호출하면서 ‘특정 조건일 때 함수 Y를 실행해서 나에게 알려달라’는 요청을 함께 보냄
    • 이 요청을 받은 함수 X의 입장에서는 해당 조건이 갖춰졌는지 여부를 스스로 판단하고 Y를 직접 호출 👉🏻 다른 코드(함수 또는 메서드)에게 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수

제어권

호출 시점

  • timer 변수를 선언하고 여기에 setInterval 실행한 결과 할당
  • setInterval을 호출할 때 두개의 매개변수 전달
    • 첫 번째, 익명함수
    • 300이라는 숫자
//setInterval의 구조
var intervalID = scope.setInterval(func, delay[, param1, param2, ...]);
  • 매개변수로는 func, delay값을 반드시 전달해야 하고, 세 번째 매개변수부서는 선택적
  • setInterval를 실행하면 반복적으로 실행되는 내용 자체를 특정할 수 있는 고유한 ID값 반환
    • 변수에 담는 이유 : 반복 실행되는 중간에 종료(clearInterval)할 수 있게 하기 위해서

인자

Array의 prototype에 담긴 map 메서드의 구조

Array.prototype.map(callback[, thisArg])
callback:function(currentValue, index, array)
  • 첫 번째 인자로 callback 함수를 받고, 생략 가능한 두 번째 인자로 콜백 함수 내부에서 this로 인식할 대상을 특정
  • map 메서드는 메서드의 대상이 되는 배열의 모든 요소들을 처음부터 끝까지 하나씩 꺼내어 콜백 함수를 반복 호출 -> 콜백함수의 실행 결과들을 모아 새로운 배열로 만듬
  • 콜백 함수의 첫 번째 인자에는 배열의 요소 중 현재값이,
  • 두 번째 인자에는 현재값의 인덱스가,
  • 세 번째 인자에는 map의 대상이 되는 배열 자체

this

  • this에는 thisArg 값이 있을 경우에는 그 값을, 없을 경우에는 전역객체를 지정
  • 첫 번째 인자엔느 메서드의 this가 배열을 가리킬 것이므로 배열의 i번째 요소 값
  • 두 번째 인자에는 i 값
  • 세 번째 인자에는 배열 자체를 지정해 호출

=> 제어권을 넘겨받을 코드에서 call/apply 메서드의 첫 번째 인자에 콜백 함수 내부에서의 this가 될 대상을 명시적으로 바인딩 하기 때문

콜백 함수는 함수다

  • 콜백 함수는 어떤 객체의 메서드를 전달하더라도 그 메서드는 메서드가 아닌 함수로서 호출
1 var obj = {
2  vals:[1,2,3],
3   logValues: function (v, i) {
4     console.log(this, v, i);
5   }
6 };
7 obj.logValues(1,2);
8 [4,5,6].forEach(obj.logValues);

  • 7️⃣ 번째 줄 : 메서드의 이름 앞에 점(.)이 있으니 메서드로서의 호출
    • this는 obj를 가리킴
  • 8️⃣ 번째 줄 : 메서드를 forEach 함수의 콜백 함수로서 전달
    • obj.logValues가 가리키는 함수만 전달
    • forEach함수에 의해 콜백이 함수로서 호출되고, 별도로 this를 지정하는 인자를 지정하지 않았으므로 => 함수 내부에서의 this는 전역 객체

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

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

var obj3 = { name: "obj3" };
var callback3 = obj1.func.call(obj3);
setTimeout(callback3, 2000);
  • callback2에서는 obj2의 func를 실행한 결과를 담아 이를 콜백으로 사용
  • callback3의 경우 obj1의 func를 실행하면서 this를 obj3가 되도록 지정해 이를 콜백으로 사용
  • this를 우회적으로나마 활용함으로써 다양한 상황에서 원하는 객체를 바라보는 콜백 함수를 만들 수 있는 방법

콜백 지옥과 비동기 제어

  • 콜백지옥 : 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상
  • 비동기 : 동기의 반대말로, 현재 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어가는 것
    • 사용자의 요청에 의해 특정 시간이 경과되기 전까지 어떤 함수의 실행을 보류하거나(setTimeout)
    • 사용자의 직접적인 개입이 있을 때 비로소 어떤 함수를 실행하도록 대기한다거나(addEventListener)
    • 웹 브라우저 자체가 아닌 별도의 대상에서 무언가를 요청하고 그에 대한 응답이 왔을 때 비로소 어떤 함수를 실행하도록 대기하는 등 (XMLHttpRequest)
    • 별도의 요청, 실행 대기, 보류 등

콜백지옥 예시

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,
  "에스프레소"
);

콜백지옥 해결 - 기명함수로 변환

var coffeeList = "";

var addEspresso = function (name) {
  cofffeeList = name;
  console.log(coffeeList);
  setTimeout(addAmericano, 500, "아메리카노");
};

var addAmericano = function (name) {
  cofffeeList = "," + name;
  console.log(coffeeList);
  setTimeout(addMocha, 500, "카페모카");
};

var addMocha = function (name) {
  cofffeeList = "," + name;
  console.log(coffeeList);
  setTimeout(addLatte, 500, "카페라떼");
};

var addLatte = function (name) {
  cofffeeList = "," + name;
  console.log(coffeeList);
};
setTimeout(addEspresso, 500, "에스프레소");
  • 코드의 가독성
  • 함수 선언과 함수 호출만 구분할 수 있다면 위체서 부터 아래로 순서대로 읽어내려가는데 어려움 ❌
  • 변수를 최상단으로 끌어올림으로써 외부에 노출되게 됐지만 전체를 즉시 실행 함수 등으로 감싸면 간단히 해결

  • 코드명을 일일이 따라다녀야므로, 오히려 헷갈릴 소지 ⭕️

비동기 작업의 동기적 표현 - Promise

new Promise(function (resoleve) {
  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);
  }).then(function(prevName) {
  return new Promise(function (resolve) {
    setTimeout(function() {
      var name = prevName + ', 카페라떼';
      console.log(name);
      resolve(name);
    },500);
  });
});
  • ES6의 Promise를 이용한 방식
  • new 연산자와 함께 호출한 Promise의 인자로 넘겨주는 콜백 함수는 호출할 때 바로 실행되지만 그 내부에 resolve 또는 reject 함수를 호출하는 구문이 있을 경우 둘 중 하나가 실행되기 전까지 다음(then) 또는 오류(catch)로 넘어가지 않음
  • 비동기 작업이 완료될 때 비로소 resolve 또는 reject를 호출하는 방법
  • 비동기 작업의 동기적 표현이 가능

비동기 작업의 동기적표현 - 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("카페모카"))
  .then(addCoffee("카페라떼"));

비동기 작업의 동기적 표현 - 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();
  • ’*‘이 붙은 함수가 바로 Generator 함수
  • Generator 함수를 실행하면 Iterator가 반환, Iterator는 next라는 메서드 소유
  • next 메서드를 호출하면 Generator 함수 내부에서 가장 먼저 등장하는 yield에서 함수의 실행 중단
  • 비동기 작업이 완료되는 시점마다 next 메서드를 호출해준다면 Generator 함수 내부의 소스가 위에서부터 아래로 순차적으로 진행

비동기 작업의 동기적 표현 - Promise + 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();
  • ES2017에서 가독성이 뛰어나면서 작성법도 간단한 새로운 기능 추가 => async/await
  • 비동기 작업을 수행하고자 하는 함수 앞에 async 표기
  • 함수 내부에서 실질적인 비동기 작업이 필요한 위치마다 await를 표기
    • 뒤의 내용을 Promise로 자동 전환
    • 해당 내용이 resolve 된 이후에야 다음으로 진행
  • Promise의 then과 흡사한 효과

참고 : 코어 자바스크립트(Core JavaScript), 정재남