콜백 함수

정수·2023년 3월 9일
0

JavaScript

목록 보기
5/15
post-thumbnail

콜백 함수(callback function)란 다른 코드의 인자로 자신이 넘어가 적절한 시점에 실행이 되어지는 함수입니다.

우선 setInterval의 구조를 살펴보면

var intervalID = scope.setInterval(func, delay[, param1, param2, ...]);

func으로 넘겨준 함수는 매 delay(ms)마다 실행되며, 그 결과 어떠한 값도 반환하지 않습니다. 다만 setInterval을 실행하면 반복적으로 실행되는 내용 자체를 특정할 수 있는 고유한 ID값이 반환됩니다. 이를 이용하여 특정 시점에 반복 실행을 종료(clearInterval)할 수도 있습니다.

콜백 함수를 인자로 넘겨받은 setInterval의 예시를 보면

var count = 0;
var cbFunc = function(){
  console.log(count);
  if (++count > 4) clearInterval(timer);
};
var timer = setInterval(cbFunc, 300);

콜백 함수가 된 cbFuncsetInterval의 판단에 따라 적절한 시점(0.3초 마다)에 실행되었습니다. 콜백 함수의 제어권이 인자로 넘겨준 setInterval에게로 넘어가게 된 것입니다.

이와 같이 콜백 함수를 인자로 받는 함수들에 대해 더 알아보겠습니다.

map

map 메서드의 작동 방식

Array.prototype.map(callback[, thisArg])
callback: function(currentValue, index, array)

map 메서드는 첫 번째 인자로 callback 함수를 받고, 생략 가능한 두 번째 인자로 콜백 함수 내부에서 this로 인식할 대상을 특정할 수 있습니다. 이를 생략할 경우에는 전역객체가 바인딩됩니다. 또한 콜백 함수의 첫 번째 인자에는 배열의 요소 중 현재값이, 두 번째 인자에는 현재값의 인덱스가, 세 번째 인자에는 map 메서드의 대상이 되는 배열 자체가 담깁니다.

map 메서드의 동작 방식은 메서드의 대상이 되는 배열의 모든 요소들을 처음부터 끝까지 하나씩 꺼내서 콜백 함수를 반복 호출하고, 콜백 함수의 실행 결과들을 모아 새로운 배열을 만듭니다.

var newArr = [10, 20, 30].map(function(currentValue, index){
  return currentValue + 5;
})
console.log(newArr); // [15, 25, 35]

this에 대한 이해

별도의 this를 지정하는 방식 및 제어권에 대한 이해를 높이기 위해 간단하게 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;
}

call / apply 메서드의 첫 번째 인자에 콜백 함수 내부에서의 this가 될 대상을 명시적으로 바인딩하기 때문에 this에 다른 값이 담길 수 있었던 것입니다.

콜백 함수는 함수다.

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

[3, 4].forEach(obj.logValues); // Window { ... } 3 4

logValuesobj객체의 메서드로 정의되었습니다. 그래서 obj.logValues(1, 2)을 실행했을 때 thisobj를 가리키고 인자로 넘어온 1 2가 출력됩니다.

반면 forEach 함수의 콜백 함수로 전달된 obj.logValues의 경우 함수만 전달한 것이기 때문에 obj와의 직접적인 연관이 없어지며 forEach에 의해 함수로서 호출되어지게 됩니다. 즉, 별도로 this를 지정하지 않았기에 함수 내부에서의 this는 전역객체를 바라보게 됩니다.

그럼 콜백 함수 내부에서의 this에 다른 값을 바인딩 할 수는 없을까요?

전통적으로는 this를 다른 변수에 담아 콜백 함수로 활용할 함수에서는 그 변수를 사용하게 하고, 이를 클로저로 만드는 방식이 많이 쓰였습니다.

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

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

var obj3 = { name: 'obj3' };
var callback3 = obj1.func.call(obj3);
setTimeout(callback3, 2000); // obj1 { ... }

Q. 여기서 callback 식별자에 함수를 할당한 이유는 무엇일까요?

하지만 ES5에 bind라는 메서드가 추가되면서 훨씬 간단하게 구현이 가능해졌습니다.

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

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

콜백 지옥과 비동기 제어

콜백 지옥(callback hell)은 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상입니다.

동기적인(synchronous) 코드

  • 현재 실행 중인 코드가 완료된 후에야 다음 코드를 실행하는 방식입니다.
  • CPU의 계산에 의해 즉시 처리가 가능한 대부분의 코드는 동기적인 코드입니다.

비동기적인(asynchronous) 코드

  • 현재 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드를 실행합니다.
  • setTimeout, addEventListener, XMLHttpRequest와 같이 별도의 요청, 실행 대기, 보류 등과 관련된 코드는 비동기적인 코드입니다.

그렇지만 웹의 복잡도가 높아지면서 비동기적인 코드의 비중이 훨씬 높아졌고 Javascript 진영은 비동기적인 일련의 작업을 동기적으로, 혹은 그렇게 보이게끔 처리해주는 장치를 마련하고자 끊임없이 노력해 왔습니다. ES6에서는 Promise, Generator 등이 도입됐고, ES2017에서는 async/await가 도입되었습니다.

setTimeout(function(name){
  var coffeeList = name;
  
  setTimeout(function(name){
    coffeeList += ', ' + name;
    
    setTimeout (function(name){
	  ...
	}, 500, '카페라떼');
  }, 500, '아메리카노');
}, 500, '에스프레소');

이를 기명함수로 변환하면 아래와 같이 변환할 수 있습니다.

var coffeeList = '';

var addEspresso = function(name){
  coffeeList = name;
  setTimeout(addAmericano, 500, '아메리카노');
}

var addAmericano = function(name){
  coffeeList += ', ' + name;
  setTimeout(addLatte, 500, '카페라떼');
}
...

가독성 문제가 해결되긴 했지만 변수가 너무 많이 할당되어 헷갈릴 소지가 있습니다.

var addCoffee = function(name){
  return function(prevName){
    return new Promise(function(resolve){
      setTimeout(function(){
        var newName = prevName ? (prevName + ', ' + name) : name;
        resolve(newName);
      }, 500);
    });
  };
};

addCoffee('에스프레소')()
  .then(addCoffee('아메리카노'))
  .then(addCoffee('카페라떼'));
  ...

Promise를 이용한 방식입니다. new 연산자와 함께 호출한 Promise의 인자로 넘겨주는 콜백 함수는 호출할 때 바로 실행되지만 그 내부에 resolve 또는 reject 함수를 호출하는 구문이 있을 경우 둘 중 하나가 실행되기 전까지는 then 또는 catch 구문으로 넘어가지 않습니다.

var addCoffee = function(prevName, name){
  setTimeout(function(){
    coffeeMaker.next(prevName ? prevName + ', ' + name : name);
  }, 500);
};

var coffeeGenerator = function*(){
  var espresso = yield addCoffee('', '에스프레소');
  var americano = yield addCoffee(espresso, '아메리카노');
  ...
};
  
var coffeeMaker = coffeeGenerator();
coffeeMaker.next();

Generator를 이용한 방식입니다. *이 붙은 함수가 Generator함수인데 이는 next라는 메서드를 가지고 있는 Iterator(반복자)가 반환됩니다. 이 next 메서드를 호출하면 Generator 함수 내부의 yield에서 실행을 멈추며 다시 next 메서드를 호출하면 멈췄던 부분부터 다시 코드를 실행합니다.

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

var coffeeMaker = async function(){
  var coffeeList = '';
  var _addCoffee = async function(){
    coffeeList += (coffeeList ? ', ' : '') + await addCoffee(name);
  }
  await _addCoffee('에스프레소');
  await _addCoffee('아메리카노');
  ...
}

ES2017에서 도입된 async/await를 이용한 방식입니다. 비동기 작업을 수행하고자 하는 함수 앞에 async를 표기하고, 함수 내부에서 비동기 작업이 필요한 위치마다 await을 표기합니다. 이것만으로도 뒤의 내용을 Promise로 자동 전환하고, 해당 내용이 resolve 된 이후에야 다음으로 진행합니다.

profile
해피한하루

0개의 댓글