TIL DAY.46 [코어 자바스크립트] 콜백 함수

Dan·2020년 12월 1일
0

오늘은 내가 평소 제일 헷갈리고 알고 싶었던 콜백 함수에 대해서 코어 자바스크립트 내용에 있는 핵심 내용을 써가면서 공부해보는 시간을 갖도록 하겠다.

콜백 함수

콜백 함수란?

Callback Function(콜백 함수)는 다른 코드의 인자로 넘겨주는 함수이다. 예를 들면 어떤 함수 X를 호출하면서 '특정 조건일 때 함수 Y를 실행해서 나에게 알려달라'는 요청을 함께 보낸다. 그리고 이 요청을 받은 함수 X 입장에서는 해당 조건이 갖춰졌는지 여부를 스스로 판단하고 Y를 직접 호출한다.

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

제어권

호출 시점

setInterval의 구조를 살펴보면 다음과 같다.

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

매개변수로는 func, delay 값은 반드시 전달해야하고, 세번 째부터는 선택적이다. func에 넘겨준 함수는 매 delay(ms) 마다 실행되며, 그 결과 어떠한 값도 리턴하지 않는다. 다음 예제를 보며 콜백함수를 좀 더 살펴보자.

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

// -- 실행 결과 --
//0 (0.3초)
//1 (0.6초)
//2 (0.9초)
//3 (1.2초)
//4 (1.5초)

timer 변수에는 setInterval 의 ID 값이 담긴다. setInterval에 전달한 첫 번째 인자인 cbFunc함수(콜백 함수)는 0.3초마다 자동으로 실행 될것이다. 콜백 함수 내부에서는 count값을 출력하고, count를 1만큼 증가시킨 다음, 그 값이 4보다 크면 반복 실행을 종료하라고 한다.

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

인자

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]

위의 코드를 해석해보자. 배열 [10,20,30]의 각 요소를 처음부터 하나씩 떠내어 콜백 함수를 실행한다. 우선 첫 번째(인덱스 0)에 대한 콜백 함수는 currentValue에 10이, index에는 인덱스 0이 담긴채 실행된다. 각 값을 출력한 다음, 15(10+5)를 반환하게 될것이다. 나머지 20과, 30또한 똑같은 방식으로 반환되어 마지막 콜백 함수까지 실행되고 나면 새로운 배열인 [15,25,35]가 newArr에 담기게 되고 출력 될 것이다.

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

콜백 함수는 함수다

콜백 함수로 어떤 객체의 메서드를 전달하더라도 그 메서드는 메서드가 아닌 함수로서 호출되게 된다. 예제를 보며 살펴보자.

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는 메서드롤 정의했다. 그리고 이 메서드의 이름 앞에 점(.)이 있으니 메서드로서 호출된 것이다. 따라서 this는 obj를 가리키고, 인자로 넘어온 1,2 가 출력된다.

하지만 위에 정의한 메서드를 forEach함수의 콜백 함수로서 전달을 했을 때 어떤 결과값이 나올까? obj를 this로 하는 메서드를 그대로 전달한 것이 아니라, obj.logValue가 가리키는 함수만 전달한 것이다.그러므로 forEach에 의해 콜백이 함수로서 호출되고, 별도로 this를 지정하는 인자를 지정하지 않았으므로 함수 내부에서의 this는 전역개체를 바라보게 된다.

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

콜백 함수 내부에서 this가 객체를 바라보게 하고 싶다면 어떻게 해야 할까? 별도의 인자로 this를 받는 함수의 경우에는 여기에 원하는 값을 넘겨주면 되지만 그렇지 않은 경우에는 this의 제어권도 넘겨주게 되므로 사용자가 임의로 값을 바꿀 수 없다. 그래서 앞 장에서 배웠던 obj1.func메서드 내부에서 self변수에 this를 담고 익명 함수를 선언과 동시에 반환하는 방식과 this자체를 사용하지 않는 방식 있지만 둘 다 사용하는데 불편함이 있다. 그리하여 이를 보완하기위해 ES5에서 등장한 bind메서드를 이용하는 방법이 있다. 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);

콜백 지옥과 비동기 제어

콜백 지옥 (callback hell)은 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상이다. 주로 이벤트 처리나 서버 통신과 같은 비동기적인 작업을 수행하기 위해 이런 형태가 자주 등장하는데, 가독성이 떨어질뿐더러 코드도 수정하기 어렵다.

동기적인 코드는 현재 실행 중인 코드가 완료된 후에야 다음 코드를 실행하는 방식이라면, 비동기는 반대로 현재 실행 중인 코드의 완료 여부와는 관계없이 즉시 다음 코드로 넘어간다.

CPU의 계산에 의해 즉시 처리 가능한 코드나 계산식이 복잡해서 CPU가 계산하는데 시간이 많이 필요한 경우 둘 다 동기적인 코드이다.

사용자의 요청에 의해 특정시간이 경과되기 전까지 어떤 함수 실행을 보류(setTimeout), 사용자의 직접적인 개입이 있을 때 비로소 어떤 함수를 실행하도록 대기한다거나(addEventListent), 웹브라우저 자체가 아닌 별도의 대상에 무언가를 요청하고 그에 대한 응답이 왔을 때 비로소 어떤 함수를 실행하도록 대기하는(XMLHttpRequest), 별도의 요청, 실행 대기, 보류 관련된 코드는 모두 비동기적인 코드이다.

다음은 콜백지옥의 예시를 살펴보자.

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초 주기마다 커피 목록을 수집하고 출력한다. 각 콜백은 커피 이름을 전달하고 목록에 이름을 추가한다. 목적 달성에는 지장이 없지만 가독성 문제가 심각하다. 이러한 문제를 해결하기위해 콜백함수를 모두 기명함수로 전환하는 방법도 나왔지만 일회성 함수를 전부 변수에 할당하는 것이 오히려 헷갈릴 소지가 되기도 했다고 한다.

자바스크립트는 이러한 비동기적인 일련의 작업을 동기적으로, 혹은 그 반대로 처리해주는 장치를 마련하고자 끊임없이 노력해왔고 ES6에서는 promise,Generator 등이 도입됐고, ES2017에서는 async/await가 도입 됐다. 이러한 새로운 기능들을 예제들을 통해 알아보도록 하자.

비동기 작업의 동기적 표현 - promise

new Promise(function(resolve) {
	setTimeout(function() {
    		var name = '에소프레소'ㅣ
            	console.log(name);
                resolve(name);
               },500);
               }).then(function (prevName){
               	return new Promise(function(resolve) {
                	setTimeout(function () {
                    	var name = prevName + ',아메리카노';
                        console.log(name);
                        resolve(name);
                      }, 500);
                    });
              }).then(function (prevName){
               	return new Promise(function(resolve) {
                	setTimeout(function () {
                    	var name = prevName + ',카페모카';
                        console.log(name);
                        resolve(name);
                      }, 500);
                    });
             }).then(function (prevName){
               	return new Promise(function(resolve) {
                	setTimeout(function () {
                    	var name = prevName + ',카페라떼';
                        console.log(name);
                        resolve(name);
                      }, 500);
                    });
   }0;
    	

ES6의 Promise를 이용한 방식이다. new 연산자와 함께 호출한 Promise의 인자로 넘겨주는 콜백 함수는 호출할 때 바로 실행되지만 그 내부에 resolve 또는 reject함수를 호출하는 구문이 있을 경우 둘 중 하나가 실행되기 전까지는 다음(then) 또는 오류 구문(catch)로 넘어가지 않는다. 따라서 비동기 작업이 완료될 때 비로소 resolve 또는 reject를 호출하는 방법으로 비동기 작업의 동기적 표현이 가능해진다.

비동기 작업의 동기적 표현 - Promise + Async/await

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 ? ',' : '') + awiat addCoffe(name);
    ];
    await _addCoffee('에소프레소');
    console.log(coffeeList);
    await _addCoffee('아메리카노');
    console.log(coffeeList);
    await _addCoffee('카페모카');
    console.log(coffeeList);
    await _addCoffee('카페라떼');
    console.log(coffeeList);
  };
coffeeMake();

비동기 작업을 수행하고자 하는 함수 앞에 async를 표기하고, 함수 내부에서 실질적인 비동기 작업이 필요한 위치마다 await를 표기하는 것만으로 뒤의 내용을 promise를 자동 전환하고, 해당 내용이 resolve된 이후에야 다음으로 진행한다. 즉 promise의 then과 흡사한 효과를 갖는다.

profile
만들고 싶은게 많은 개발자

2개의 댓글

comment-user-thumbnail
2020년 12월 1일

크으.. 그의 성실함에 취한다...

호균님 근데 코드 적을때 ```뒤에 jsx 쓰면 색깔 예쁘게 돼요.

//`.``jsx
// 여기에 코드
//```
이렇게용!

1개의 답글