코드/JavaScript

콜백함수 (Callback)

juundev 2024. 5. 17. 18:09

콜백 함수란?

콜백 함수는 다른 코드의 인자로 넘겨주는 함수입니다.

뜻을 살펴보면 callback은 '호출'의 의미를 갖는 call과 '되돌아오다'의 의미를 갖는 back의 합성어로 되돌아 호출해달라는 명령입니다.

쉽게 말해서 어떤 함수 X를 호출하면서 '특정 조건을 만족할 때' 함수 Y를 실행해서 반환해달라는 요청을 함께 보내는 것입니다.

 

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

 

제어권

호출 시점

다음은 setInterval 함수를 통한 콜백 함수 예제입니다.

var count = 0;
var callbackFunc = function() {
    console.log(count);
    if (++count > 4) clearInterval(timer);
}
// timer 변수에는 setInterval의 id 값이 담깁니다.
// setInterval에 전달한 첫 번째 인자인 callbackFunc 함수는 0.3초마다 자동으로 실행될 것입니다.
// 콜백 함수 내부에서는 count 값을 출력하고, count를 1만큼 증가시킨 다음, 그 값이 4보다 크면 반복 실행을 종료하라고 요청합니다.
var timer = setInterval(callbackFunc, 300);

// 0 (0.3초)
// 1 (0.6초)
// 2 (0.9초)
// 3 (1.2초)
// 4 (1.5초)

 

  호출 주체 제어권
callbackFunc() 사용자 사용자
setInterval(callbackFunc, 300) setInterval setInterval

코드 실행 방식과 제어권

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

 

인자

다음은 Array.prototype.map을 사용한 콜백 함수 예제입니다.

var newArr = [10, 20, 30].map(function (value, index) {
    console.log(value, index); // (1)
    return value + 5; // (2)
});
console.log(newArr); // (3)

// 10 0
// 20 1
// 30 2
// [15, 25, 35]

배열 [10, 20, 30]의 각 요소를 처음부터 하나씩 꺼내어 콜백 함수를 실행합니다.

0번 인덱스에 대한 콜백 함수는 value에 10이 index에는 0이 담긴채 실행됩니다.

 

(1) 에서는 value 값(10)과 index 값(0)이 출력됩니다.

(2) 에서는 value값(10)에 5를 더한 값(15)를 반환합니다.

반환된 값은 map 메서드를 통해 만들어진 새로운 배열 객체의 요소가 됩니다.

(3) 반환된 새로운 배열 객체 [15, 25, 35]가 출력됩니다.

 

다음은 Array의 prototype에 담긴 map 메서드의 구조

 

Array.prototype.map(callback[, thisArg])
callback: function(value, index, array)
  • map 메서드는 메서드의 대상이 되는 배열의 모든 요소들을 처음부터 끝까지 하나씩 꺼내어 콜백 함수를 반복 호출하고, 함수의 실행 결과들을 모아 새로운 배열을 만듭니다.
  • 생략 가능한 두 번째 인자로 콜백 함수 내부에서 this로 인식할 대상을 특정할 수 있습니다. thisArg를 생략할 경우에는 일반적인 함수와 마찬가지로 전역객체가 바인딩됩니다.

this

콜백 함수도 함수이기 때문에 기본적으로는 this가 전역객체를 참조하지만, 제어권을 넘겨받을 코드에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조하게 됩니다.

 

다음은 Array.prototype.map을 임의로 구현한 코드입니다.

Array.prototype.map = function(callback, thisArg) {
    var mappedArr = [];
    for(var i = 0; i < this.length; i++) {
    	// thisArg 값이 존재하면 그 값을, 없을 경우에는 전역객체를 지정
    	var mappedValue = callback.call(thisArg || window, this[i], i, this);
        mappedArr[i] = mappedValue;
    }
    return mappedArr;
};

 

콜백 함수는 함수

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

 

다음은 메서드를 콜백 함수로 전달한 경우의 코드입니다.

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

// (1)
obj.logValues(1, 2); // { vals: [1, 2, 3], logValues: f } 1 2

// (2)
[4, 5, 6].forEach(obj.logValues);
// window { ... } 4 0
//		 		  5 1
// 		 		  6 2

 

obj 객체의 logValues는 메서드로 정의되었습니다. 

(1) 에서는 메서드의 이름 앞에 점(.)이 있으니 메서드로서 호출한 것입니다.

따라서 (1) 에서의 this는 obj를 가리키고, 인자로 넘어온 (1, 2)가 출력됩니다.

 

(2) 에서는 logValues 메서드를 forEach 함수의 콜백 함수로서 전달했습니다.

obj를 this로 하는 메서드를 그대로 전달한 것이 아니라, obj,logValues가 가리키는 함수만 전달한 것입니다.

따라서 (2) 에서는 forEach에 의해 콜백이 함수로서 호출되고, 별도로 this를 지정하는 인자가 없기 때문에 함수 내부에서의 this는 전역객체를 가리키고 있습니다.

 

콜백 지옥과 비동기 제어

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

 

주로 이벤트 처리나 서버 통신과 같이 비동기적인 작업을 수행하기 위해 이런 형태가 자주 등장하는데, 가독성이 떨어지고 유지보수하기도 어렵습니다.

* 비동기는 동기의 반대말입니다. 동기적인 코드는 현재 실행 중인 코드가 완료된 후에야 다음 코드를 실행하는 방식이고, 비동기적인 코드는 현재 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드를 수행할 수 있습니다.

 

다음은 콜백 지옥 예시 코드입니다.

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

 

위의 코드는 0.5초마다 커피 목록을 수집하고 출력합니다. 

해당 코드를 수행하면 목적 달성에는 지장이 없지만 들여쓰기 수준이 과도하게 깊어졌고 가독성이 낮습니다.

 

다음은 콜백 지옥을 해결한 예시 코드입니다.

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);
    await _addCoffee('카페라떼');
    console.log(coffeeList);
};

coffeMaker();

 

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

 

정리

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

 

코어 자바스크립트: 핵심 개념과 동작 원리로 이해하는 자바스크립트 프로그래밍을

공부하며 이해한 내용을 기반으로 정리한 게시글입니다.

'코드 > JavaScript' 카테고리의 다른 글

자바스크립트 클래스  (0) 2024.05.29
자바스크립트 프로토타입  (0) 2024.05.24
클로저  (0) 2024.05.22
undefined와 null의 차이  (0) 2024.05.17
얕은 복사와 깊은 복사  (0) 2024.05.17