콜백함수

2024년 06월 8일 by지우진

콜백함수란?

콜백함수란 다른 코드의 인자로 넘겨주는 함수를 의미합니다. 이때, 제어권도 함께 위임합니다.
콜백함수를 위임받은 코드는 자체적인 내부 로직에 의해 콜백 함수를 적절한 시점에 실행합니다.

제어권

호출 시점

콜백 함수 예제 setInterval
var count = 0;
var timer = setInterval(function () {
  console.log(count);
  if (++count > 4) clearInterval(timer);
}, 300);
setInterval 함수
var intervalID = scope.setInterval(func, delay[, param1, param2, ...]);

setInterval 함수 설명

  • scope에는 Window 객체 또는 Worker의 인스턴스가 들어올 수 있습니다. 일반적인 브라우저 환경에서는 window를 생략해서 함수처럼 사용 가능합니다.
  • 매개변수로 func, delay 값을 반드시 전달해야하고 세 번째 매개변수부터는 선택적입니다.
  • func은 함수로 delay는 밀리초 단위 숫자이고 나머지 파라미터들은 func함수를 실행할 때 매개변수로 전달할 인자입니다.
  • func에 넘겨준 함수는 매 delay마다 실행되며, 결과를 반환하지 않습니다. setInterval를 실행하면 반복적으로 실행되는 내용 자체를 특정할 수 있는 고유한 ID 값이 반환됩니다. ← 이를 변수에 담는 이유는 반복 실행되는 중간에 종료(clearInterval)을 할 수 있게하기 위해서 입니다.
콜백 함수 예제 2 setInterval
var count = 0;
var cbFunc = function() {
  console.log(count);
  if (++count > 4) clearInterval(timer);
};
var timer = setInterval(cbFunc, 300);
// 0, 1, 2, 3, 4 (0.3초 간격)

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

인자

콜백 함수 예제 Array.prototype.map
var newArr = [10, 20, 30].map(function (currentValue, index) {
  console.log(currentValue, index);
  return currentValue + 5;
});
console.log(newArr);
// 10 0 / 20 1 / 30 2 / [15, 25, 35]
map메서드 구조
Array.prototype.map(callback[, thisArg])
callback: function(currentValue, index, array)

map 메서드 설명

  • 첫 번째 인자로 callback함수를 받고, 생략 가능한 두 번째 인자로 콜백함수 내부에서 this로 인식할 대상을 특정할 수 있습니다.
  • map메서드는 메서드의 대상이 되는 배열의 모든 요소들을 처음부터 끝까지 하나씩 꺼내어 콜백 함수를 반복 호출하고, 콜백 함수의 실행 결과들을 모아 새로운 배열을 만듭니다.
  • 콜백함수의 첫 번째 인자에서는 배열의 요소 중 현재값이, 두 번째 인자는 현재값의 인덱스가, 세 번째 인자에는 map메서드의 대상이 되는 배열 자체가 담깁니다.

이처럼 콜백 함수의 제어권을 넘겨받은 코드는 콜백함수를 호출할 때 인자에 어떤 값들을 순서로 넘길 것인지에 대한 제어권을 가집니다.

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

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

콜백 함수는 함수다

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

콜백 함수로 메서드 전달
var obj = {
  vals: [1, 2, 3],
  logValues: function(v, i) {
    console.log(this, v, i);
  }
};
obj.logValues(1, 2); // {obj} 1 2
[4, 5, 6].forEach(obj.logValues); // Window 4 0 / Window 5 1 / Window 6 2

어떤 함수의 인자에 객체의 메서드를 전달하더라도 이는 결국 메서드가 아닌 함수일 뿐입니다.

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

콜백 함수 내부에서 this가 객체를 바라보게 하는 법

  • 별도의 인지로 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

콜백 지옥과 비동기 제어

콜백 지옥이란 콜백함수를 익명함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상으로, JS에서 흔히 발생하는 문제입니다.
주로 이벤트 처리나 서버 통신과 간이 비동기적인 작업을 수행하기 위해 이런 형태가 자주 등장합니다. 그러나 가독성 떨어지고, 코드 수정 어렵다는 단점이 존재합니다.

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

콜백 지옥의 예시
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, '에서프레소');

0.5초 주기마다 커피 목록을 수집하고 출력하는 코드입니다.
그러나 들여쓰기 수준이 과도하고 값이 전달되는 순서가 아래에서 위로 향하고 있어 어색하게 느껴질 수 있습니다.
이를 개선하기 위해 익명의 콜백함수를 모두 기명함수로 전환하는 방법이 있습니다.

익명함수의 콜백함수를 모두 기명함수로 전환하는 것
var coffeeList = '';
 
var addEspresso = function (name) {
  coffeeList = name;
  console.log(coffeeList);
  setTimeout(addAmericano, 500, '아메리카노');
};
 
var addAmericano = function (name) {
  coffeeList = ', ' + name;
  console.log(coffeeList);
  setTimeout(addAmericano, 500, '카페모카');
};
 
var addMocha = function (name) {
  coffeeList = ', ' + name;
  console.log(coffeeList);
  setTimeout(addAmericano, 500, '카페라떼');
};
 
var addLatte = function (name) {
  coffeeList = ', ' + name;
  console.log(coffeeList);
};
 
setTimeout(addEspresso, 500, '에서프레소');

하지만, 익명함수를 기명함수로 전환해서 개선한 코드도 일회성 함수를 전부 변수에 할당하는 것이 좋지 않다는 단점이 존재합니다.

비동기 작업의 동기적 표현(1) - Promise(1)
new Promise(function (resolve) {
  setTimeout(function () {
    var name = '에스프레소';
    console.log(name);
    resolve(name);
  }, 500);
}).then(function (preName) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      var name = prevName + '아메리카노';
      console.log(name);
      resolve(name);
    }, 500);
  });
}).then(function (preName) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      var name = prevName + '카페모카';
      console.log(name);
      resolve(name);
    }, 500);
  });
}).then(function (preName) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      var name = prevName + '카페라떼';
      console.log(name);
      resolve(name);
    }, 500);
  });
});

위 코드는 ES6의 Promise를 이용한 방식으로 비동기 작업이 완료될 때 비로소 resolve 또는 reject를 호출하는 방법으로 비동기 작업의 동기적 표현이 가능합니다.

비동기 작업의 동기적 표현(2) - Promise(2)
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('카페라떼'));

위 코드는 반복적인 내용을 함수화해서 더욱 짧게 표현한 것입니다. 클로저는 다음 장에서 설명합니다.

비동기 작업의 동기적 표현(3) - 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 latte = yield addCoffee(mocha, '카페라떼');
  console.log(latte);
};
var coffeeMaker = coffeeGenerator();
coffeeMaker.next();

위 코드는 ES6의 Generator를 이용한 코드로 *이 붙은 함수가 Generator함수입니다.

Generator함수를 실행하면 Iterator가 반환되는데, Iteratornext라는 메서드를 가지고 있습니다. 이 next메서드를 호출하면 Generator함수 내부에서 가장 먼저 등장하는 yield에서 함수 실행을 멈춥니다.
이후 다시 next메서드를 호출하면 앞서 멈췄던 부분부터 시작해서 그 다음에 등장하는 yield에서 함수의 실행을 멈춥니다.
비동기 작업이 완료되는 시점마다 next메서드를 호출해 준다면 Generator함수 내부의 소스가 위에서 아래로 순차적으로 진행됩니다.

비동기 작업의 동기적 표현(4) - Promise + Async/await
var addCoffee = function(nama) {
  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();

위코드 처럼 ES2017에서 추가된 async/await을 통해 더 가독성 좋게 작성할 수 있습니다.
비동기 함수 앞에 aysnc를 표기하고, 함수 내부에서 실질적인 비동기 작업이 필요한 위치마다 await를 표기하는 것만으로 뒤 내용을 Promise로 자동 전환하고 해당 내용이 resolve된 이후에야 다음으로 진행합니다.
Promisethen과 흡사한 효과를 얻을 수 있습니다.

정리

  • 콜백 함수는 다른 코드에 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수입니다.
  • 제어권을 넘겨받은 코드는 다음과 같은 제어권을 가집니다.
    • 콜백 함수를 호출하는 시점을 스스로 판단해서 실행합니다.
    • 콜백 함수를 호출할 때 인자로 넘겨줄 값들 및 그 순서가 정해져 있습니다. 이 순서를 따르지 않고 코드를 작성하면 엉뚱한 결과를 얻게 됩니다.
    • 콜백 함수의 this가 무엇을 바라보도록 할지가 정해져 있는 경우도 있습니다. 정하지 않은 경우에는 전역객체를 바라봅니다. 사용자 임의로 this를 바꾸고 싶을 경우 bind 메서드를 활용하면 됩니다.
  • 어떤 함수에 인자로 메서드를 전달하더라도 이는 결국 함수로서 실행됩니다.
  • 비동기 제어를 위해 콜백 함수를 사용하다 보면 콜백 지옥에 빠지기 쉽습니다. 최근의 ECMAScript에는 Promise, Generator, async/await등 콜백 지옥에서 벗어날 수 있는 훌륭한 방법들이 등장하고 있습니다.