Javascript

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

이 챕터를 읽고 다음의 질문에 답할 수 있어야 한다.

  • 콜백 함수란 무엇인가?
  • 콜백 함수의 제어권은?
  • 콜백 함수로 전달된 메서드 내부의 this가 참조하는 객체는?
  • callback 함수 내부에 this를 바인딩하는 방법은 어떤 것들이 있는가?
  • 콜백 지옥이란 무엇인가?
  • 비동기적 코드란 무엇인가?
  • 콜백 지옥을 해결하기 위한 방법들에는 어떤 것들이 있는가?
  • Promise란?
  • Generator란?
  • async/await란?

 

콜백 함수란

다른 코드의 인자로서 넘겨주는 함수

 

콜백 함수의 제어권

콜백 함수의 제어권은 콜백 함수를 전달받아 사용하는 코드에서 갖는다.

setInterval(cb, 300);

여기서, 콜백 함수 cb의 제어권은 setInterval이 갖는다.

 

콜백 함수의 this

콜백 함수는 말 그대로 함수로서 호출되기 때문에 기본적으로 this는 전역 객체를 가리킨다.

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

setTimeout(obj.func.bind(obj), 1000);

var obj2 = { name: 'obj2'};

setTimeout(obj.func.bind(obj2), 1000);

bind 메서드를 통해 this에 다른 값을 바인딩할 수 있다.

 

콜백 지옥이란?

콜백 지옥은 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상으로, 자바스크립트에서 흔히 발생하는 문제입니다.

예시.

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

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

 

비동기적 코드

동기적인 코드는 현재 실행 중인 코드가 완료된 후에 다음 코드를 실행하는 방식이다.

비동기적인 코드는 그 반대로 현재 실행 중인 코드의 완료 여부와 관계없이 다음 코드를 실행하는 방식이다.

 

콜백 지옥 해결

콜백으로 전달된 각각의 익명함수를 분리시켜 기명 함수로 선언하여 사용한다.

그러나 일일이 함수를 선언하거나 변수에 할당해줘야 하고,

선언해야 될 함수가 많을수록 코드의 흐름을 파악하기 어렵다.

 

Promise를 써서 콜백 지옥을 어느 정도 해결할 수 있다.

그 전에 프라미스가 무엇인지 알아보자.

 

프라미스 객체가 생성되면 그 내부의 함수는 자동으로 실행된다. 프라미스 객체는 new Promise 생성자 함수를 통해 만들 수 있다. new Promise의 인수로는 실행자 함수(executor)가 전달되고, 그 실행자 함수의 인수로는 resolvereject가 전달된다. resolve와 reject는 자바스크립트에서 기본적으로 제공하는 콜백이다. 둘 중 하나는 반드시 호출되어야 한다.

  • resolve(value): 성공적으로 코드가 실행된 후 그 결과인 value와 함께 호출
  • reject(error): 에러가 발생하면 에러 객체인 error와 함께 호출
let promise = new Promise(function (resolve) {
  setTimeout(function (){
    const name = 'aiden';
    resolve(name);
  }, 500);
})

console.log(promise);

// Promise { <pending> }
// resolve가 실행되기 전에 콘솔 로그가 실행되어 pending이 출력

생성자 함수에 의해 만들어진 프라미스 객체는 "pending"이라는 상태 프로퍼티를 갖는다.

만약 코드가 성공적으로 수행되고 resolve가 호출되면 상태는 "fulfilled"로 바뀌고

resolve 콜백의 인수로 들어간 value가 result로 들어가게 된다.

 

reject가 호출되는 경우엔 "rejected" 상태로 바뀌게 되고 result의 값은 error가 된다.

let promise = new Promise(function (resolve) {
  setTimeout(function (){
    const name = 'aiden';
    resolve(name);
    console.log(promise);
  }, 500);
})

// Promise { 'aiden' }
// resolve가 실행된 후에는 value가 제대로 반환된 것을 확인 가능

프라미스는 성공 또는 실패만 한다.

만약 resolve가 한 번 호출되면 그 이후에 호출된 resolve나 reject는 모두 무시된다.

 

then 메서드를 통해 일련의 비동기 작업을 동기적으로 표현할 수 있다.

// 프라미스 체이닝

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve(1), 1000);
}).then(function(result) {
  console.log(result);
  return result * 2;
}).then(function(result) {
  console.log(result);
  return result * 3;
}).then(function(result) {
  console.log(result);
  return result * 2;
}).then(function(result){
  console.log(result);
});

// 1
// 2
// 6
// 12

이를 프라미스 체이닝이라고 한다.

 

위에 있는 커피 예제를 프라미스를 통해 아래 코드처럼 바꿀 수 있다.

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를 사용할 수도 있다.

그게 뭔지 먼저 알아보자.

 

하나의 값을 반환하는 일반 함수와 달리 제너레이터는

여러 개의 값을 필요에 따라 하나씩 반환할 수 있는 함수이다.

각각의 반환을 yield라고 하며 function키워드 뒤에 *이 붙는 것이 특징이다.

let example = function* () {
  yield 1;
  yield 2;
  return 3;
}

console.log(example);

// [GeneratorFunction: example]

console.log(example());

// Object [Generator] {}

위 코드처럼 제너레이터를 생성하여 변수에 할당해도 되고, 함수선언문으로 생성해도 된다.

 

제너레이터를 실행하면,

함수의 본문 코드는 실행되지 않은 채로 제너레이터 객체가 반환된다.

 

함수의 본문 코드를 실행하기 위해서는 next() 메서드를 호출하면 된다.

next()를 호출하게 되면 yield를 만날 때까지 코드를 실행하다가,

yield가 산출하는 값을 value속성으로 리턴하고 boolean 값을 갖는 done속성도 함께 반환한다.

  • value: 산출 값
  • done: 함수의 전체 코드 실행이 끝났으면 true, 아니면 false
let example = function* () {
  yield 1;
  yield 2;
  return 3;
}

let values = example();

console.log(values.next());
console.log(values.next());
console.log(values.next());

// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: true }

next() 메서드를 쓸 수 있는 이유는 제너레이터 객체가 Interator이기 때문이다.

따라서 for문을 통해서도 값을 얻을 수 있다.

 

위에서 프라미스로 구현한 코드를 제너레이터로 아래와 같이 구현할 수 있다.

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

// 결과는 같음

또 다른 방법으로,

async/await를 사용할 수 있다.

 

ES2017에서 도입된 async/await를 통해 프라미스를 좀 더 쉽게 사용할 수 있다.

function 앞에 async를 붙이면 해당 함수는 마치 프라미스 생성자와 같이 프라미스 객체를 반환한다.

async function f() {
  return 1;
}

console.log(f());

// Promise { 1 }

 

await 키워드는 이전의 프라미스가 처리될 때까지 기다렸다가

해당 내용이 reslove된 이후에 다음으로 진행한다.

async function f() {

  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("done"), 1000)
  });

  let result = await promise; // 기다리는 지점

  alert(result); // "done"
}

f();

 

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

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