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

신윤철·2022년 1월 26일
0

코어자바스크립트

목록 보기
4/8
post-thumbnail

콜백 함수

콜백 함수란?

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

간단히 콜백 함수의 예를 살펴보며 구체적으로 이해해 보겠습니다.

// 직접 구현해본 callback 함수
function func(callback) {
  callback();
}

function callBackTest() {
  console.log('콜백 함수 호출');
}

func(callBackTest);     // 콜백 함수 호출

이처럼 func함수의 매개변수에 인자로 callBackTest함수를 주어 콜백 함수로서 호출했습니다.

이때 콜백 함수의 특징을 찾을 수 있는데, func(callBackTest)에서 callBackTest는 함수임에도 callBackTest()으로 작성하지 않는다는 것입니다.

즉 실행권한()이 callBackTest 자신이 아닌 func에 있고 이는 제어권이 위임받은 함수에 있다는 것을 의미합니다.

제어권

제어권은 콜백 함수의 중요 특징으로서 자신을 호출하는 함수또는 메서드에게 자신의 제어권한을 위임합니다.

setInterval 메서드를 통해 콜백 함수의 제어권을 좀더 자세히 알아보겠습니다.

// setInterval
var count = 0;

var intervalTest = function () {
  console.log(count);
  if (++count > 3) clearInterval(counter);
};

var counter = setInterval(intervalTest, 500);	// 0, 1, 2

setInterval의 실행 결과 0.5초간격으로 0, 1, 2가 실행되었습니다.

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

인자

또한 콜백 함수를 넘겨받은 함수(또는 메서드)는 자신만의 규칙으로 콜백 함수에 어떤 타입의 인자가 어떤 순서로 들어가야하는지를 결정합니다.

map 메서드의 구조와 예제를 통해 자세히 알아보겠습니다.
Array.prototype.map(callback[, thisArg(생략가능)])
callback: function(value, index, array)

map 메서드는 첫번째 매개변수로 callback함수, 두번째 매개변수로 콜백 함수가 가리키는 this값을 특정합니다.(두번째 생략가능)

그런데 이때 map메서드의 콜백 함수는 첫번째 값엔 map의 객체인 Array의 value, 두번째 값엔 index, 세번째 값엔 array가 들어갑니다.

var newArr = [10, 20, 30].map(function (value, index, arr) {
  console.log(value, index, arr);
});

console.log(newArr);
/* 결과
10 0 (3) [10, 20, 30]
20 1 (3) [10, 20, 30]
30 2 (3) [10, 20, 30]
*/

이처럼 제어권에는 실행권한 외에도 콜백 함수의 매개변수에 관한 권한도 갖고 있습니다.
때문에 콜백 함수는 호출 주체의 규칙에 맞춰 인자를 받아야합니다.

vscode에서도 호출 주체에 마우스를 올려 놓으면 어떤식으로 매개변수를 사용하는지 규칙을 볼 수 있습니다.

콜백 함수는 함수다.

당연한 말일 수 있지만 콜백 함수도 함수이기 때문에 함수의 특징을 가지고 있습니다.

그렇다면 만약 메서드를 콜백 함수로서 사용한다면 어떻게 될까요?

var obj = {
  arr: [1, 2, 3],
  logValues: function (value, index) {
    console.log(this, value, index);
  },
};

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

/*
실행 결과 
{arr: Array(3), logValues: ƒ} 1 2
window{} 4 0
window{} 5 1
window{} 6 2
*/

같은 obj.logValues 메서드를 그냥 호출한 결과 (this는 obj), (value, index는 1, 2)를 출력했습니다.

하지만 콜백 함수로서 obj.logValues 메서드를 호출한 결과 (this는 window), (value는 array[4,5,6]의 값), index는 자동 매칭이 되었습니다.

이처럼 호출 주체에 의해 콜백이 함수로서 호출될 때에는 메서드는 자신의 객체와는 직접적인 연관이 사라지고 함수로써 동작합니다.

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

위에서 본듯 객체의 메서드를 콜백 함수로 전달하면 해당 객체를 this로 바라볼 수 없게 됩니다. (함수로서 사용됨)

하지만 이번에는 기존처럼 this가 자신의 객체를 바라보게 하는 방법을 살펴보겠습니다.

// bind 메서드를 활용하여 콜백 함수 내부의 this에 다른 값을 바인딩
var obj1 = {
  name: 'object',
  func: function () {
    console.log(this);
  },
};
setTimeout(obj1.func, 500);			// window{}
setTimeout(obj1.func.bind(obj1), 1000);		// obj1 {}

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

기존처럼 호출 주체(setTimeout)에 객체의 메서드를 호출하면 this는 전역 객체 window를 바라봅니다.

하지만 bind로 바라볼 객체를 명시해 준다면 this는 해당 객체를 가리키게 됩니다.

콜백 지옥과 비동기 제어

자바스크립트이 대표적인 문제 중 하나인 콜백 지옥과 비동기 제어에 대해 알아보겠습니다.

콜백 지옥 : 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상

간단한 콜백 지옥 예제를 살펴보겠습니다.

// 콜백 지옥 예시
setTimeout(
  function (coffeeMenu) {
    var coffeeList = coffeeMenu;
    console.log(coffeeList);

    setTimeout(
      function (coffeeMenu) {
        coffeeList += ', ' + coffeeMenu;
        console.log(coffeeList);

        setTimeout(
          function (coffeeMenu) {
            coffeeList += ', ' + coffeeMenu;
            console.log(coffeeList);
          },
          500,'카페모카');
      },
      500,'아메리카노');
  },
  500,'에스프레소');

/*
출력
에스프레소
에스프레소, 아메리카노
에스프레소, 아메리카노, 카페모카
*/

콜백 지옥이라고 하기에 코드가 깊진 않지만 이런 방식으로 콜백 함수가 더욱 깊어진다면 콜백 지옥이 됩니다.

또한 이러한 코드는 결과가 순차적으로 나오지않고 역으로 출력되어 더욱 혼란이 가중됩니다.

자바스크립트는 이러한 비동기적인 일련의 작업을 동기적으로 처리하기 위한 여러 방법을 고안했습니다.

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

/*
결과 
에스프레소
에스프레소 아메리카노
에스프레소 아메리카노 카페모카
*/

앞선 콜백 지옥의 예제를 ES6의 Promise를 구현한 방식입니다.

  • Promise의 인자로 넘겨주는 콜백 함수는 호출시 바로 실행 됩니다.
  • 실행 후 내부의 resolve또는 then이 있을 경우 둘 중 하나가 실행될떄까지 대기합니다.
  • resolve또는 then이 실행된 후 다음(then)코드로 넘어가거나 오류(catch)구문으로 넘어갑니다.

이런식으로 비동기 코드를 동기적으로 표현하고 실행 결과도 위에서 아래로 순차적으로 실행되는 것을 확인할 수 있습니다.

다음으로 Generator를 알아보겠습니다.

// 비동기 작업의 동기적 표현 - 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 coffeeMaker = coffeeGenerator();
coffeeMaker.next();

/*
결과 
에스프레소
에스프레소 아메리카노
에스프레소 아메리카노 카페모카
*/

Generator을 이용해 구현한 방식 입니다.

  • '*'이 붙은 함수가 Generator 함수를 뜻합니다.
  • Generator 함수를 실행시 Iterator가 반환되는데 Iterator은 next라는 메서드를 가지고 있습니다.
  • next 메서드를 호출하면 Generator 함수 내부에서 가장 먼저 등장하는 yield에서 함수의 실행을 멈춥니다.
  • 이후 다시 next 메서드를 실행하여 그 다음에 등장하는 yield에서 함수의 실행을 멈춥니다.

이런 방식으로 비동기 작업이 완료되는 시점마다 next 메서드를 호출해 준다면 Generator 함수 내부의 결과가 위에서 아래로 순차적으로 진행됩니다.

마지막으로 Async/await 방식을 소개하겠습니다.

// 비동기 작업의 동기적 표현 - 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);
};

coffeeMaker();

/*
결과 
에스프레소
에스프레소 아메리카노
에스프레소 아메리카노 카페모카
*/

가장 가독성이 뛰어나고 작성하기 간단한 Async/await을 이용한 방식입니다.

  • 비동기 작업이 필요한 함수 앞에 async를 표기합니다.
  • 함수 내부에서 실질적인 비동기 작업이 필요한 위치마다 await을 표기합니다.
  • 그럼 자동으로 뒤의 내용을 Promise로 전환하고 해당 내용이 resolve된 이후에야 다음 코드를 진행합니다.
  • 즉 Promise의 then과 await을 비슷하다 생각하면 됩니다.

전 처음 비동기 작업을 동기 작업으로 변환하는 것에 대해 궁금한 점이 많았었습니다.

싱글 스레드의 단점을 보완하고자 비동기적으로 코드를 실행함으로써 대기시간을 줄이고 효율적인 실행을 할 수 있다 라고 생각했는데
이런 비동기 코드를 다시 동기적으로 실행되게 만드는 것이 무슨 비효율적인 작업인가 참 이해가 안갔습니다.

이를 이해하기 위해선 런타임과 자바스크립트의 이벤트루프에 대해 이해할 필요가 있습니다. ==> (글쓰고 링크삽입예정)

정리

  • 콜백 함수는 다른 코드에 인자로 넘겨 줌으로써 그 제어권도 함께 위임한 함수입니다.

  • 제어권에는 "실행권한", "콜백 함수의 인자 값들의 종류와 순서", "콜백 함수의 this가 바라보는 객체지정"이 있습니다.

  • 어떤 함수의 인자로 전달되는 메서드도 결국 함수로서 실행됩니다.

  • 비동기 제어를 위해 콜백 함수를 사용하다 보면 콜백 지옥에 빠질 수 있고 해결하기 위해선 Promise, Generator, async/await 등을 사용하면 됩니다.

profile
기본을 탄탄하게🌳

0개의 댓글