너굴 개발 일지

[리뷰] 코어 자바스크립트 4장 - 콜백 함수 본문

독서

[리뷰] 코어 자바스크립트 4장 - 콜백 함수

너굴냥 2023. 6. 5. 23:06

이 글은 코어 자바스크립트 책을 읽으며 정리한 글입니다.

 

4장. 콜백 함수

4-1. 콜백 함수란?

콜백 함수란 다른 코드의 인자로 넘겨주는 함수로 제어권과 관련이 깊습니다.

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

 

4-2. 제어권

(1) 호출 시점

setInterval 콜백 함수 예제 (코드 실행 방식과 제어권)

var count = 0;
var cbFunc = function () {
  console.log(count);
  if (++count > 4) clearInterval(timer);
};
var timer = setInterval(cbFunc, 300);
code 호출 주체 제어권
cbFunc(); 사용자 사용자
setInterval(cbFunc, 300); setInterval  

위 코드 실행시 콘솔창에 0.3초에 한번씩 숫자가 0부터 1씩 증가하며 출력되다 4가 출력된 후 종료됩니다.

setInterval이라고 하는 '다른 코드'에 첫 번째 인자로서 cbFunc 함수를 넘겨주자 제어권을 받은 setInterval이 스스로 판단에 따라 적절한 시점에 이 익명 함수를 실행했습니다.

즉, 콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수 호출 시점에 대한 제어권을 가집니다.

 

(2) this

콜백 함수 예제 - Array.prototype.map 구현

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;
};

위는 Array.prototype.map 메소드를 임의로 구현한 코드입니다. this에는 thisArg 값이 있을 경우 그 값을, 없으면 전역 객체를 지정하고, 첫 번째 인자에는 메서드의 this가 배열을 가리킬 것이므로 배열의 i번째 요소의 값을, 두 번째 인자에는 i 값을, 세 번째 인자에는 배열 자체를 지정해 호출합니다.

위처럼 제어권을 넘겨 받을 코드에서 call/apply 메서드의 첫 번째 인자에 콜백 함수 내부에서의 this가 될 대상을 명시적으로 바인딩하기에 this에 다른 값이 담기게 됩니다.

결론적으로, 콜백 함수도 함수기에 this는 기본적으로 전역 객체를 참조하지만, 제어권을 넘겨 받을 코드에서 콜백 함수에 별도로 this가 될 대상을 지정할 수 있습니다.

 

 

4-3. 콜백 함수는 함수다

메서드를 콜백 함수로 전달한 경우

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 ...

콜백 함수로 어떤 객체의 메서드를 전달해도 그 메서드는 메서드가 아닌 함수로 호출하게 됩니다.

위 예시의 8번쨰 줄에서 obj를 this로 하는 메서드를 그대로 전달한 것이 아니라, obj의 logValues가 가리키는 함수만 전달한 것 뿐입니다. 

어떤 함수의 인자에 객체의 메서드를 전달해도, 이는 메서드가 아닌 함수일뿐이라는 차이가 중요합니다.

 

 

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

객체의 메소드를 콜백 함수로 전달하게 되면 해당 객체를 this로 바라볼 수 없게 됩니다. 그럼에도 함수 내부에서 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

전통적으로 this를 다른 변수에 담아 콜백함수로 활용할 함수에서는 this 대신 그 변수를 사용하고, 클로저를 만드는 방식을 사용했었습니다. 하지만 실제로 this를 사용하지도 않고 번거롭다는 느낌이 듭니다.

 

(2) 콜백 함수 내부의 this에 다른 값을 바인딩 하는 방법 - bind

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

var obj2 = {
  name: "obj2",
};
setTimeout(obj1.func().bind(obj2), 1000); // obj2

ES5에 등장한 bind 메서드를 이용해 위 방식의 아쉬움을 보완할 수 있습니다.

 

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

콜백 지옥이란 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상으로 주로 비동기 작업 수행시 나타납니다.

비동기적인 코드는 현재 실행중인 코드의 완료 여부와 무관하게 다음 코드로 넘어갑니다. 별도의 요청(XMLHttpRequest), 실행대기(setTimeout), 보류(addEventListener)와 관련된 코드는 비동기적인 코드입니다.

 

콜백 지옥 예시

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

위 코드는 커피 이름을 전달하고 목록에 이름을 추가합니다. 목적 달성에는 지장이 없지만 들여쓰기 수준이 과도하게 깊고 값이 전달되는 순서가 아래 -> 위로 향해 어색한 느낌이 듭니다.

 

 

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

var coffeeList = "";

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

가독성 문제와 어색함을 해결하기 위해 익명의 콜백 함수를 모두 기명함수로 전환했습니다. 해당 방식은 코드의 가독성을 높이고 함수 선언, 호출을 구분할 수 있다면 위 -> 아래 방향으로 읽어내려가는 데 어려움이 없습니다.

하지만 일회성 함수를 전부 변수에 할당하는 것이 단점입니다. 또한 코드명을 일일이 따라다녀야 하므로 오히려 헷갈릴 수도 있습니다.

JS는 비동기적인 작업을 동기적으로, 혹은 동기적인 것처럼 보이게끔 하기위해 ES6에서는 Promise, Generator가 도입됐고, ES2017에서는 async/await이 도입되었습니다. 

 

 

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

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

 

 

한편 ES2017에서는 가독성이 뛰어나며 작성법도 간단한 새로운 기능이 추가됐는데, 바로 async/await입니다. 비동기 작업을 수행하고자 함수 앞에 async를 표기하고, 함수 내부에서 실질적인 비동기 작업이 필요한 위치마다 await을 표기하는 것만으로도 뒤의 내용을 Promise로 자동 전환하고, 해당 내용이 resolve된 이후에야 다음으로 진행합니다. 

즉, Promise의 then과 흡사한 효과를 얻을 수 있습니다.

 

4-6. 정리

  • 콜백함수란?
    • 다른 코드의 인자로 넘겨줌으로써 제어권도 함께 위임한 함수
  • 제어권을 넘겨 받은 코드는 다음과 같은 제어권을 가짐
    • 콜백함수의 호출 시점을 스스로 판단해 실행
    • 콜백함수 호출시 인자로 넘겨줄 값들 및 순서가 정해져 있음
    • 콜백 함수의 this를 임의로 바꿀 수 있음
  • 어떤 함수의 인자로 객체의 메서드 전달시, 함수로서 실행됨
  • 비동기 제어를 위해 콜백 함수 사용시, 콜백 지옥에 빠질 수 있으며 콜백 지옥에서 벗어나기 위한 방법으로 Promise, Generator, async/await이 있음

 


Quiz

콜백함수란 무엇이며 어디에 주로 사용될까요?

더보기

콜백함수란 다른 코드의 인자로 넘겨줌으로써 제어권도 같이 위임한 함수를 의미합니다. 주로 비동기 제어를 위해 사용됩니다.

 

콜백 함수 사용시 어떤 주의사항이 있을까요?

더보기

콜백 함수를 익명 함수로 전달하는 과정이 반복되 코드의 들여쓰기 수준이 깊어지는 콜백 지옥 현상이 일어날 수 있으며 주로 비동기적인 작업 수행시 발생하게 됩니다. 이럴 땐, Promise, Generator, async/await을 사용해 콜백 지옥에서 벗어날 수 있습니다.

 

 

콜백 함수의 this는 무엇이며, 임의로 this를 지정하는 방법은 무엇이 있을까요?

더보기

콜백 함수도 함수기 떄문에 기본적으로 this는 전역 객체이며, 사용자 임의로 this를 변경하고 싶다면 bind 메소드를 활용할 수 있습니다.


관련 글

Callback, Promise, async & await