[JS] 콜백 함수 - 제어권

이승혜·2021년 9월 12일
0

JS/JQUERY

목록 보기
10/10
post-thumbnail

이 글은 📕코어 자바스크립트 책을 바탕으로 정리한 글입니다.

콜백 함수란

콜백 함수(callback function)는 다른 코드의 인자로 넘겨주는 함수이다.
콜백 함수를 넘겨받은 코드는 이 콜백 함수를 필요에 따라 적절한 시점에 실행할 것이다.

콜백 함수를 이해하기 위한 간단한 예시를 보자.
A와 B는 다음 날 아침 8시에 약속이 있다.
A는 수시로 잠에서 깨어 시계를 확인하고, 반면 B는 알람시계를 세팅한다.

A는 수시로 시간을 구하는 함수를 직접 호출했다.
반면 B는 시계의 알람을 설정하는 함수를 호출했고, 해당 함수는 호출 당시에는 아무것도 하지 않다가 B가 정해준 시각이 됐을 때 비로소 '알람을 울리는' 결과를 반환한다.

시간 정보를 제공하는 시계 입장에서 생각해보면 A의 경우 요청할 때마다 수동적으로 시간 정보를 제공하기만 한 반면,
B의 경우에는 요청을 받은 뒤 자체적으로 무언가를 수행하다가 적절한 시점에 적극적으로 통보했다.

A의 경우 시계 함수의 제어권은 A에게 있고, B는 시계 함수에게 요청을 하면서 알람을 울리는 명령에 대한 제어권을 시계에게 넘겨준 것이다.

🤔 callback은 '부르다', '호출(실행)하다'의 의미인 call과, '뒤돌아오다', '되돌다'는 의미인 back의 합성어로, '되돌아 호출해달라'는 명령이다.

이처럼 콜백 함수는 다른 코드(함수 또는 메서드)에게 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수라고 할 수 있다.

제어권

호출 시점

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

// 0
// 1
// 2
// 3
// 4

우선 setInterval의 구조를 살펴보자

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

우선 scope에는 Window 객체 또는 Worker의 인스턴스가 들어올 수 있다.
두 객체 모두 setInterval 메서드를 제공하기 때문인데, 일반적인 브라우저 환경에서는 window를 생략해서 함수처럼 사용 가능하다.
매개변수로는 func, delay값을 반드시 전달해야하고, 세 번째 매개 변수부터는 선택적이다.
func에 넘겨준 함수는 매 delay(ms)마다 실행되며, 그 결과 어떠한 값도 리턴하지 않는다.

setInterval를 실행하면 반복적으로 실행되는 내용 자체를 특정할 수 있는 고유한 ID 값이 반환된다.
이를 변수에 담는 이유는 반복 실행되는 중간에 종료(clearInterval)할 수 있게 하기 위함이다.

위 코드를 좀 더 확인하기 쉽게 수정해보자.

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

var timer = setInterval(cbFunc, 300);

// 0 (0.3s)
// 1 (0.6s)
// 2 (0.9s)
// 3 (1.2s)
// 4 (1.5s)
code호출 주체제어권
cbFunc()사용자사용자
setInterval(cbFunc, 300)setIntervalsetInterval

setInterval이라고 하는 '다른 코드'에 첫 번째 인자로서 cbFunc 함수를 넘겨주자 제어권을 넘겨받은 setInterval이 스스로의 판단에 따라 적절한 시점에(0.3초마다) 이 익명함수를 실행했다.
이처럼 콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수 호출 시점에 대한 제어권을 가진다.

인자

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

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

map 메서드는 첫 번째 인자로 callback 함수를 받고, 생략 가능한 두 번째 인자로 콜백 함수 내부에서 this로 인식할 대상을 특정할 수 있다.
thisArg를 생략할 경우에는 일반적인 함수와 마찬가지로 전역객체가 바인딩된다.

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

map 메서드에 정의된 규칙에는 콜백 함수의 인자로 넘어올 값들 및 그 순서도 포함이 돼있다.
콜백 함수를 호출하는 주체가 사용자가 아닌 map 메서드이므로,
map 메서드가 콜백 함수를 호출할 때 인자에 어떤 값들을 어떤 순서로 넘길 것인지가 전적으로 map 메서드에 달린 것이다.

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

this

별도의 this를 지정하는 방식 및 제어권에 대한 이해를 높이기 위해 map 메서드를 직접 구현해보자

Array.prototype.map = function (callback, thisArg) {
  var mappedArr = [];
  for (let i = 0; i < this.length; ++i) {
    var mappedValue = callback.call(thisArg || window, this[i], i, this);
    mappedArr[i] = mappedValue;
  }
  return mappedArr;
};

메서드 구현의 핵심은 call/apply메서드에 있다.
this에는 thisArg값이 있을 경우에는 그 값을, 없을 경우에는 전역객체를 지정하고, 첫 번째 인자에는 메서드의 this가 배열을 가리킬 것이므로 배열의 i번째 요소 값을, 두 번째 인자에는 i 값을, 세 번째 인자에는 배열 자체를 지정해 호출한다.
그 결과가 변수 mappedValue에 담겨 mappedArr의 i번째 인자에 할당된다.

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

setTimeout(function () {
  console.log(this); // (1) window {...}
}, 300);

[1, 2, 3, 4, 5].forEach(function (x) {
  console.log(this); // (2) window {...}
});

document.body.innerHTML += '<button id="a">클릭</button>';
document.body.querySelector('#a').addEventListener('click', function (e) {
  console.log(this, e);
  // (3)
  // <button id="a">클릭</button>
  // PointerEvent {isTrusted: true, ...}
});

(1)의 setTimeout은 내부에서 콜백 함수를 호출할 때 call 메서드의 첫 번째 인자에 전역객체를 넘기기 때문에 콜백 함수 내부에서의 this가 전역객체를 가리킨다.
(2)의 forEach는 별도의 인자로 this를 받는 경우에 해당되지만 별도의 인자로 this를 넘겨주지 않았기 때문에 전역객체를 가리킨다.
(3)의 addEventListener는 내부에서 콜백 함수를 호출할 때 call 메서드의 첫 번째 인자에 addEventListener 메서드의 this를 그대로 넘기도록 정의돼있기 때문에 콜백 함수 내부에서의 thisaddEventListener를 호출한 주체인 HTML 엘리먼트를 가리키게 된다.

profile
더 높이

0개의 댓글