반응형

콜백 함수 (Callback Function)


콜백 함수란, 

영어 뜻 그대로 callback은 call (부르다, 호출하다) 과 back (뒤돌아오다, 되돌다) 의 합성어로 되돌아 호출해달라는 명령입니다.

 

쉽게 설명하면 함수 A 를 호출하면서 '특정 조건일 때 함수 B 를 실행하여 나에게 알려줘!'라는 뜻이죠

어려운 용어로 설명하면, 

콜백함수는 다른 코드 (함수 또는 메서드) 에게  인자로 넘겨줌으로써 그 제어권도 함께 위임하는 함수란 것입니다.

 

콜백 함수를 위임받은 코드는 자체적인 내부 로직에 의해 이 콜백 함수를 적절한 시점에 실행할 것입니다.

 

줄글로 설명하면, 어려우니 예제를 통해 구체적으로 살펴보겠습니다.

function fn_sum(a, b, callback) {
  var sum = a + b;
  callback(sum);
}
 
// fn_sum 함수에 익명 함수를 인자로 전달
fn_sum(3, 7, function (result) {
  console.log(result);
});

위 코드는 fn_sum 함수에 callback 이라는 매개변수를 만들고, 해당 함수 내부에서 callback 매개변수를 함수형태로서 실행하는 것입니다.

익명함수에 fn_sum 함수를 호출하여 sum 이 익명함수로 전달되면서 console 에 실행되는 거죠

 

콜백 함수가 맨 뒤에 위치하기 때문에 모든 로직이 처리되고 난 시점에 콜백 함수가 호출된다는 것을 알 수 있습니다.

 

예제를 통해 배웠으니, 콜백 함수의 구조를 살펴보자면,

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

map 메서드는 첫 번째 인자로 callback 함수를 받고, 

생략할 수 있는 두 번째 인자로 콜백 함수 내부에서 this 로 인식할 대상을 특정할 수 있습니다.

 

콜백 함수의 

첫 번째 인자에는 배열의 요소 중 현재값, 

두 번째 인자에는 현재값의 인덱스,

세 번째 인자에는 map 메서드의 대상이 되는 배열 자체가 담기게 됩니다.

 

해당 인자들의 순서는 고정되어있으며, 인자의 이름만 변경할 수 있습니다! 

jQuery 에서는 첫 번째 인자가 index, 두 번째 인자가 currentValue 가 오니까 헷갈리지 않도록 주의합시다!! 

 

this 

이전 글에서 설명했듯이 

콜백 함수도 함수이기 때문에 기본적으로 this 가 전역 객체를 참조하지만,

제어권을 넘겨받을 코드에서 콜백 함수에 별도로 this 가 될 되상을 지정한 경우 그 대상을 참조하게 됩니다. 

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

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

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

위 코드를 살펴봅시다.

setTimeout 내부에서 콜백 함수를 호출했을 때, call 메서드의 첫 번째 인자가 전역객체를 넘기기 때문에 여기서의 this 는 

전역객체를 가리킵니다. 

forEach 문에서의 this 는 '별도의 인자로 this 를 받는 경우' 에 해당하지만 별도 인자를 넘기지 않았기 때문에 해당 this 또한 

전역객체를 가리키게 되는 것이죠.

마지막으로 addEventListener 는 내부에서 콜백 함수를 호출할 때 call 메서드의 첫 번째 인자에 해당 메서드의 this 를 그대로 넘기도록 정의했기 때문에 콜백 함수 내부에서 this 가 addEventListener 를 호출한 주체인 HTML 을 가리키게 됩니다. 

 

콜백 지옥과 비동기 제어

콜백 지옥 (Callback Hell) 이란,

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

JavaScript 에서 흔히 발생할 수 있는 문제죠.

 

비동기 (asynchronous) 는 동기 (synchronous) 의 반댓말입니다.

동기적 코드는 현재 실행중인 코드가 완료된 후 다음 코드를 실행하는 반면,

비동기적 코드는 현재 실행중인 코드의 완료 여부와 관계없이 즉시 다음 코드로 넘어갑니다. 

 

CPU 의 계산에 의해 즉시 처리 가능한 대부분의 코드는 동기적 코드입니다. 

사용자의 요청에 의해 특정 시간이 경과되지 전까지 어떤 함수의 실행을 보류(setTimeout)하거나,

사용자의 직접적인 개입이 있을 때 어떤 함수를 실행하도록 대기(addEventListener)하거나, 

웹 브라우저 자체가 아니라 별도의 대상에 무언가를 요청하고 그 응답이 왔을 때 어떤 함수를 실행하도록 대기(XMLHttpRequest)하는 등

별도의 요청, 실행 대기, 보류 등과 관련된 코드는 비동기적 코드입니다. 

step1(function (value1) {
    step2(function (value2) {
        step3(function (value3) {
            step4(function (value4) {
                step5(function (value5) {
                    step6(function (value6) {
                        // Do something with value6
                    });
                });
            });
        });
    });
});

콜백 지옥의 예시입니다. 

한눈에 봐도 가독성이 떨어지고, 들여쓰기가 과하기 때문에 끝맺음을 찾기 어려울 수 있습니다. 

회사에서 프로젝트를 진행하면서 흔히 겪는 일이죠. 

 

위와 같은 콜백 지옥에서 벗어나려면 어떻게 해야 할까요?

가장 간단한 방법은 익명의 콜백 함수를 기명함수로 변경하는 것입니다.

step1(afterStep1);
function afterStep1(value1) {
    step2(afterStep2);
}
function afterStep2(value2) {
    step3(afterStep3);
}
... 

위와 같은 방식은 코드의 가독성을 높일 뿐만 아니라 함수 선언과 함수 호출만 구분할 수 있다면

위에서 아래로 읽어내려가는데 어려움이 없습니다. 

 

다른 방법은 Promise 의 사용입니다. 

ES6 에서 Promise 와 Generator 등이 도입되었고, ES2017 에서는 async / await 가 도입되었습니다.

(TMI : 제가 Promise 를 처음 만난 것은 개발공부를 시작할 때 Final Project 였습니다. 실행순서를 지정할 때 처음 접했죠..)

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

new 연산자와 함께 호출한 Promise 의 인자로 넘겨주는 콜백 함수는 호출할 때 바로 실행되지만, 

그 내부의 resolve 또는 reject 함수를 호출하는 구문이 있을 경우 둘 중 하나가 실행되기 전까지는

다음(then) 또는 오류 구문(catch) 로 넘어가지 않습니다. 

따라서 비동기 작업이 완료될 시점에 resolve 또는 reject 함수를 호출하는 방법으로 비동기 작업의 동기적 표현이 가능해지는 것이죠.

 

또 다른 방법은 Generaotr 의 사용입니다. 

var addCoffee = function (prevName, name) {
  setTimeout(function () {
    coffeeMarker.next(prevName ? prevName + ', ' + name : name);
  }, 500);
}

var coffeGenerator = function* () {
  var espresso = yield addCoffee('', '에스프레소');
  console.log(espresso);
  var americano = yield addCoffee(espresso, '아메리카노');
  console.log(americano);
  var latte = yield addCoffee(americano, '카페라떼');
  console.log(latte);
};

var coffeeMarker = coffeGenerator();
coffeeMarker.next();

위 코드에서 '*' 이 붙은 함수가 Generator 함수입니다. 

Generator 함수를 실해하면 Iterator 가 반환되는데 Iterator 는 next 라는 메서드를 가지고 있습니다. 

next 를 호출하면 Generator 함수 내부에 가장 먼저 등장하는 yield 에서 함수 실행을 멈춥니다. 

이후 다시 next 를 호출하면 앞에서 멈췄던 부분에서 다시 시작하여 다음 등장하는 yield 에서 또 멈추게 되는 것입니다.

비동기 작업이 완료되는 시점마다 next 메서드를 호출하면 Generator 함수 내부의 소스가 위 →아래로 순차실행되는 것이죠.

 

다음은 클로저 (Closure) 에 대해 다뤄보겠습니다 : )

 

 

* 출처 : '코어 자바스크립트 - 핵심 개념과 동작 원리로 이해하는 자바스크립트 프로그래밍', 정재남 지음

반응형
  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 라이프코리아트위터 공유하기
  • shared
  • 카카오스토리 공유하기