프론트엔드 개발

코어 자바스크립트 - 콜백 함수 본문

후기/코어 자바스크립트

코어 자바스크립트 - 콜백 함수

태나미 2021. 10. 4. 04:18
이 글은 코어 자바스크립트 책에서 콜백 함수를 공부하고 정리하는 목적으로 남깁니다.

목표

  • 콜백 함수의 개념과 비동기 개념, 비동기 제어에 대해서 알 수 있습니다.

목차

  1. 콜백함수란?
  2. 콜백 함수는 함수다
  3. 콜백 함수 내부의 this에 다른 값 바인딩하기
  4. 콜백 지옥과 비동기 제어
  5. 정리

콜백 함수란?

callback은 '호출하다'는 의미인 call과 '뒤돌아오다' back의 합성어로 '되돌아 호출해달라'는 의미입니다.

콜백 함수(Callback Function)는 다른 함수나 메서드에게 인자로 넘겨주는 함수입니다. 콜백 함수를 넘겨받은 코드는 이 콜백 함수를 필요에 따라 적절한 시점에 따라 실행합니다. 

콜백 함수는 함수다

콜백 함수는 함수입니다. 메서드를 콜백 함수로 전달한 경우, 메서드가 아닌 함수로서 호출됩니다.

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

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

단순히 메서드로서 호출을 한 방식은, 앞에 객체가 출력되는 것을 볼 수 있지만, forEach문에 콜백 함수로 전달된 것은 콜백이 함수로서 호출되었는데, 별도로 this를 지정하는 인자를 지정하지 않았으므로 this는 전역 객체를 바라보게 되어, Window 객체가 출력되는 것을 볼 수 있습니다.

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

객체의 메서드를 콜백 함수로 전달하면, 해당 객체를 this로 바라볼 수 있는 방법은 this를 다른 변수에 담아 콜백 함수로 활용할 함수에서는 this대신 그 변수를 사용하게 하고, 이를 클로저로 만드는 방식이 있습니다.

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

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

ES 5에 나온 bind 메서드를 이용하여 this에 바인딩하는 방법은 다음과 같습니다

const obj1 = {
  name: 'obj1',
  func: function() {
    console.log(this.name);
  }
};

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

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

콜백 지옥과 비동기 제어

콜백 지옥(callback hell)은 콜백 함수를 함수로 전달하는 과정이 반복되어 코드의 들여 쓰기가 깊어지는 현상입니다. 주로 비동기적인 작업을 수행할 때, 나타나는 문제입니다. 별도의 요청(XMLHttpRequest), 실행 대기(addEventListener), 보류(setTimeout) 등과 관련된 코드는 비동기 코드입니다.

콜백 지옥 예시

setTimeout(function (name) {
  let coffeeList = name;
  console.log(coffeeList);
  
  setTimeout(function (name) {
    coffeeList = `${coffeeList}, ${name}`
    console.log(coffeeList);
    
    setTimeout(function (name) {
      coffeeList = `${coffeeList}, ${name}`
      console.log(coffeeList);
      
      setTimeout(function (name) {
        coffeeList = `${coffeeList}, ${name}`
        console.log(coffeeList);
      }, 500, 'Affogato');
    }, 500, 'Caffe Latte');
  }, 500, 'Americano');
}, 500, 'Espresso');
// Espresso
// Espresso, Americano
// Espresso, Americano, Caffe Latte
// Espresso, Americano, Caffe Latte, Affogato

이런 콜백 지옥을 해결하기 위해, ES6에서는 Promise, Generator 등이 도입되었고, ES2017에서는 async/await가 도입됐습니다.

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

const addCoffee = function (name){
  return function (prevName){
    return new Promise(function (resolve){
      setTimeout(function () {
        const newName = prevName ? `${prevName} , ${name}` : name;
        console.log(newName);
        resolve(newName);
      }, 500);
    });
  };
};

addCoffee('Espresso')()
    .then(addCoffee('Americano'))
    .then(addCoffee('Caffe Latte'))
    .then(addCoffee('Affogato'));
    
// Espresso
// Espresso, Americano
// Espresso, Americano, Caffe Latte
// Espresso, Americano, Caffe Latte, Affogato

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

const addCoffee = function (prevName, name){
  setTimeout(function () {
    coffeeMaker.next(prevName ? `${prevName}, name` : name);
  }, 500);
};

const coffeeGenerator = function*(){
  const espresso = yield addCoffee('', 'Espresso');
  console.log(espresso);
  
  const americano = yield addCoffee(espresso, 'Americano');
  console.log(americano);
  
  const caffeLatte = yield addCoffee(americano, 'Caffe Latte');
  console.log(caffeLatte);
  
  const affogato = yield addCoffee(affogato, 'Affogato');
  console.log(affogato);
};

const coffeeMaker = coffeeGenerator();
coffeeMaker.next();
    
// Espresso
// Espresso, Americano
// Espresso, Americano, Caffe Latte
// Espresso, Americano, Caffe Latte, Affogato

Promise + Async/await - 비동기 작업의 동기적 표현

const addCoffee = function (name){
  return new Promise(function (resolve){
    setTimeout(function () {
      resolve(name);
    }, 500);
  });
};

const coffeeMaker = async function(){
  let coffeeList = '';
  const _addCoffee = async function(name){
    coffeeList += (coffeeList ? ',' : '') + await addCoffee(name);
  };
  
  await _addCoffee('Espresso');
  console.log(coffeeList);
  
  await _addCoffee('Americano');
  console.log(coffeeList);
  
  await _addCoffee('Caffe Latte');
  console.log(coffeeList);
  
  await _addCoffee('Affogato');
  console.log(coffeeList);
};

coffeeMaker();

// Espresso
// Espresso, Americano
// Espresso, Americano, Caffe Latte
// Espresso, Americano, Caffe Latte, Affogato

함수 앞에 async를 표기하고, 함수 내부에서 비동기 작업이 필요한 위치마다 await를 표기하면 뒤의 내용을 Promise로 반환되고 resolve 된 이후 다음으로 진행됩니다. 즉 Promise와 then의 흡사한 효과를 얻을 수 있습니다. 자칫하면 Promise형태도 들여 쓰기가 깊게 될 수 있는데 asnyc await을 사용하면 예방할 수 있습니다.


정리

  • 콜백 함수는 다른 코드에 인자로 넘겨줌으로써 제어권까지 함께 위임한 함수
  • 제어권을 넘겨받은 코드가 다음과 같은 제어권을 가짐
    1. 콜백 함수를 호출하는 시점을 스스로 판단해서 실행
    2. 콜백 함수를 호출할 때 인자로 넘겨줄 값들 및 그 순서가 정해져 있음
    3. 콜백 함수의 this가 무엇을 바라보도록 할지가 정해져 있는 경우도 있음. 정하지 않은 경우에는 전역 객체를 바라봄. 사용자가 임의로 this를 바꾸고 싶을 경우 bind 메서드를 활용하면 됨
  • 어떤 함수에 인자로 메서드를 전달하더라도 이는 결국 함수로서 실행됨
  • 최근의 ECMAScript에는 Promise, Generator, async/await 등 콜백 지옥에서 벗어날 수 있는 방법들이 등장하였음
Comments