본문 바로가기
CoreJavaScript-study/dil

[DIL] 04 콜백 함수

by 어느새벽 2024. 7. 13.

01 콜백 함수란

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

callback은 '부르다', '호출(실행)하다'는 의미인 call과, '뒤돌아오다', '되돌다'는 의미인 back의 합성어로, '되돌아 호출해달라'는 명령이다. 어떤 함수 X를 호출하면서 '특정 조건일 때 함수 Y를 실행해서 나에게 알려달라'는 요청을 함께 보낸다. 이 요청을 받은 함수 X의 입장에서는 해당 조건이 갖춰졌는지 여부를 스스로 판단하고 Y를 직접 호출한다.

이처럼 콜백 함수는 다른 코드 (함수 또는 메서드)에게 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수이다. 콜백 함수를 위임받은 코드는 자체적인 내부 로직에 의해 이 콜백 함수를 적절한 시점에 실행할 것이다.

 

02 제어권

4-2-1 호출 시점

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

 

1번째 줄에서 count 변수를 선언하고 0을 할당했다. 2번째 줄에서는 timer 변수를 선언하고 여기에 setInterval을 실행한 결과를 할당했다. setInterval 메서드를 제공하기 때문인데, 일반적인 브라우저 환경에서는 window를 생략해서 함수처럼 사용 가능할 것이다. 매개변수로는 func, dalay 값을 반드시 전달해야 하고, 세 번째 매개변수부터는 선택적이다. func는 함수이고, delat는 밀리초(ms) 단위의 숫자이며, 나머지(param1, param2, ...)는 func 함수를 실행할 때 매개변수로 전달할 인자이다. func에 넘겨준 함수는 매 delay(ms)마다 실행되며, 그 결과 어떠한 값도 리턴하지 않는다. setInterval를 실행하면 반복적으로 실행되는 내용 자체를 특정할 수 있는 고유한 ID 값이 반환된다. 이를 변수에 담는 이유는 반복 실행되는 중간에 종료(clearInterval)할 수 있게 하기 위해서이다. 

 

4-2-2 인자

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]

 

1번째 줄에서 newArr 변수를 선언하고 우항의 결과를 할당했다. 5번째 줄에서 그 결과를 확인하고자 한다. 1번째 줄의 우항은 배열[10, 20, 30]에 map메서드를 호출하고 있다. 이때 첫번째 매개변수로 익명 함수를 전달한다. 우선 map 메서드가 어떤 방식으로 동작하는지를 알아야 5번째 줄의 결과를 예상할 수 있다. Array의 prototype에 담긴 map 메서드는 다음과 같은 구조로 이뤄져있다. 

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

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

 

map메서드를 호출해서 원하는 배열을 얻으려면 map 메서드에 정의된 규칙에 따라 함수를 작성해야 한다. map 메서드에 정의된 규칙에는 콜백 함수의 인자로 넘어올 값들 및 그 순서도 포함돼 있다. 콜백 함수를 호출하는 주체가 사용자가 아닌 map메서드이므로 map메서드가 콜백함수를 호출할때 인자에 어떤 값들을 어떤 순서로 넘길 것인지가 전적으로 map메서드에게 달린 것이다. 이처럼 콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수를 호출할 때 인자에 어떤 값들을 어떤 순서로 넘길 것인지에 대한 제어권을 가진다.

 

4-2-3 this

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에는 thisArg 값이 있을 경우에 그 값을, 없을 경우에는 전역객체를 지정하고, 첫번째 인자에는 메서드의 this가 배열을 가리킬 것이므로 배열의 i번째 요소들 값을, 두번째 인자에는 i값을, 세번째 인자에는 배열 자체를 지정해 호출한다. 그 결과가 변수 mappedValue에 담겨 mappedArr의 i번째 인자에 할당된다. 

이를 통해 제어권을 넘겨 받을 코드에서 call/ apply 메서드의 첫번째 인자에 콜백함수 내부에서의 this가 될 대상을 명시적으로 바인딩한다는 것을 알 수 있다. 

 

03 콜백 함수는 함수다

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

var obj = {
	vals: [1, 2, 3],
    logValues: function(v, i) {
    	console.log(this, v, i);
    }
} ;
obj.logValues(1,2); // {vals: [1, 2, 3], logValues: f} 1 2
[4, 5, 6].forEach(obj.logValues); // window {...} 4 0
 // window {...} 5 1 
 // window {...} 6 2

obj 객체의 logValues는 메서드로 정의됐다. 7번째 줄에서는 이 메서드의 이름 앞에 점이 있으니 메서드로서 호출한 것이다. 따라서 this는 obj를 가리키고, 인자로 넘어온 1, 2가 출력된다.

한편 8번째 줄에서는 이 메서드를 forEach 함수의 콜백 함수로서 전달했다. obj를 this로 하는 메서드를 그대로 전달한 것이 아니라, obj.logValues가 가리키는 함수만 전달한 것이다. 이 함수는 메서드로서 호출할 때가 아닌 한 obj와의 직접적인 연관이 없어진다. forEach에 의해 콜백이 함수로서 호출되고, 별도로 this를 지정하는 인자를 지정하지 않았으므로 함수 내부에서의 this는 전역객체를 바라보게 된다. 그래서 어떤 함수의 인자에 객체의 메서드를 전달하더라도 이는 결국 메서드가 아닌 함수인 것이다.

 

04 콜백 함수 내부의 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.func 메서드 내부에서 self  변수에 this를 담고, 익명 함수를 선언과 동시에 반환했습니다. 이제 10번째 줄에서 obj1.func를 호출하면 앞서 선언한 내부함수가 반환되어 callback 변수에 담긴다. 11번째 줄에서 이 callback을 setTimeout 함수에 인자로 전달하면 1초(1000ms) 뒤 callback이 실행되면서 'obj1'을 출력할 것이다. 

하지만 이 방식은 실제로 this를 사용하지도 않을 뿐더러 번거롭기 그지 없다. 차라리 안 쓰는게 나을 수도 있다.

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

 

this를 사용하지 않았을때의 결과이다. 간결해졌지만 다양한 상황에서 this를 이용해 재활용할 수 없게 됐다. 

 

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

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

 

callback2에는 obj2의 func를 실행한 결과를 담아 이를 콜백으로 사용했다. callback3의 경우 obj1의 func를 실행하면서 this를 obj3가 되도록 지정해 이를 콜백으로 사용했다. 예제를 실행해보면 실행 시점으로부터 1.5초 후에는 'obj2'가, 실행 시점으로부터 2초 후에는 'obj3'이 출력된다. 이처럼 번거롭긴 하지만 this를 우회적으로나마 활용하여 다양한 상황에서 원하는 객체를 바라보는 콜백 함수를 만들 수 있다.

하지만 앞전의 예제의 경우 처음부터 바라볼 객체를 명시적으로 obj1로 지정했기 때문에 어떤 방법으로도 다른 객체를 바랄보게끔 할 수가 없다. 이런 문제들을 보완하기 위해 bind 메서드를 이용하면 된다.

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

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

 

05 콜백 지옥과 비동기 제어

콜백 지옥은 콜백함수를 익명함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상을 말한다. 비동기는 동기의 반댓말로 동기적인 코드는 현재 실행 중인 코드가 완료된 후에야 다음 코드를 실행하지만 비동기적은 코드는 현재 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어간다. 

즉시 처리가 가능한 대부분의 코드는 동기적인 코드이고 별도의 대상에 무언가를 요청하고 그에 대한 응답이 왔을 때 비로소 어떤 함수를 실행하도록 대기하는 등, 별도의 요청, 실행대기, 보류 등과 관련된 코드는 비동기적인 코드이다.

콜백 지옥에 빠지지 않기 위해 모두 변수를 선언하여 가독성을 높일 수 있지만 가성비가 안 좋다. 그래서 이를 보완하고자 Promise, Generator등이 도입됐고, 이후 async/await가 도입됐다.

 

정리

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

'CoreJavaScript-study > dil' 카테고리의 다른 글

[DIL] 06 프로토타입  (4) 2024.07.22
[DIL] 05 클로저  (2) 2024.07.15
[DIL] 03 this  (0) 2024.07.10
[DIL] 02 실행 컨텍스트  (0) 2024.07.04
[DIL] 01 데이터타입  (0) 2024.07.02