코어자바스크립트 스터디

4장 콜백함수

려낭 2024. 7. 10. 22:56
1. 콜백함수란?

 

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

 

일상생활에서 예를 들어보자.

 

A와 B는 다음 날 아침 8시에 만나기로 하고 잠을 잔다.

약속 장소에 가려면 늦어도 6시에는 일어나야 한다.

A는 불안한 마음에 수시로 깨어 시계를 확인한다.

계속 잠을 설치다가 결국 5시 즈음 포기하고 일어나고야 만다.

한편 B는 알람시계를 세팅한다.

시계가 정한 시각에 울리지 않을 염려는 없고 평소 알람소리에 쉽게 눈을 뜨곤 했던지라 

안심하고 꿀잠을 잔다. 6시가 되자 시계의 알람소리를 듣고 상쾌하게 일어난다.

A는 수시로 시간을 구하는 함수를 직접 호출한다. 반면 B는 시계의 알람을 설정하는 함수를 호출했고,

해당 함수는 호출당시에는 아무것도 하지 않다가 B가 정해준 시각이 됐을 때 '알람을 울리는' 결과를 반환했다.

시간 정보를 제공하는 시계 입장에서 생각해 보면 A의 경우 요청할 때마다 수동적으로 시간 정보를 제공하기만 했다면

B의 경우에는 요청을 받은 뒤 자체적으로 무언가를 수행하다가 적절한 시점에 적극적으로 통보했다.

A의 경우 시계 함수의 제어권은 A에게 있고, 시계는 그저 요청받은 내용을 이행할 뿐이다.

그런데 B는 시계 함수에게 요청을 하면서 알람을 울리는 명령에 대한 제어권을 시계에게 넘겨준 것이다.

이처럼 콜백 함수는 제어권과 관련이 깊다.

 

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

어떤 함수 X를 호출하면서 '특정 조건일 떄 함수 Y를 실행해서 나에게 알려달라'는 요청을 함께 보내는 것이다.

이 요청을 받은 함수 X의 입장에서는 해당 조건이 갖춰졌는지 여부를 스스로 판단하고 Y를 직접 호출한다.

 

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

 

 

2. 제어권

 

4-2-1 호출 시점

 

콜백 함수 예제 setInterval

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

 

1번째 줄에서 count 변수를 선언하고 0을 할당했다.

2번째 줄에서는 timer 변수를 선언하고 여기에 setInterval을 실행한 결과를 할당했다.

setInterval을 호출할 때 두 개의 매개변수를 전달한다. 

그중 첫째는 익명함수, 두 번째는 300이라는 숫자이다.

 

setInterval의 구조를 살펴보자

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

 

scope에는 window 객체 또는 Worker의 인스턴스가 들어올 수 있다. 

두 객체 모드 setInerval에서 메서드를 제공하기 때문이다. 일반적인 브라우저 환경에서는 window를 생략하여 함수처럼 사용이 가능하다.

매개변수로는 func, delay값을 반드시 전달해야 하고, 세 번째 매개변수부터는 선택적이다.

func는 함수이고, delay는 밀리초 단위의 숫자이며, 나머지는 func함수를 실행할 때 매개변수로 전달할 인자이다.

func에 넘겨준 함수는 매 delay 마다 실행되며, 그 결과 어떤 값도 리턴하지 않는다.

setInterval을 실행하면 반복적으로 실행되는 내용 자체를 특정할 수 있는 고유한 ID값이 반환된다.

이를 변수에 담는 이유는 반복 실행되는 중간에 종료할수 있게 하기 위해서 이다.

 

예제(2)

var count = 0;
var cbFunc = function () {
	console.log(count);
    if(++count>4) cleartInterval(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보다 크면 반복 실행을 종료하라고 한다.

 

이 코드를 실행하면 콘솔창에는 0.3초에 한 번씩 숫자가 0부터 1씩 증가하며 출력되다가 4가 출력된 이후 종료된다.

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

 

4-2-2 인자

 

예제 Array.prototype.map

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번째 줄의 우항은 배열에 map 메서드를 호출하고 있다.

이때 첫 번째 매개변수로 익명 함수를 전달한다.

Array의 prototype에 담긴 map 메서드는 이런 구조이다.

 

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

 map 메서드는 첫 번째 인자로 callback 함수를 받고, 생략 가능한 두 번째 인자로 콜백함수 내부에서 this로 인식할 대상을 특정할 수 있다.

thisArg를 생략할 경우에는 전역 객체가 바인딩된다.

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

콜백 함수의 첫 번째 인자에는 배열의 요소 중 현재값이, 두 번째 인자에는 현재값의 인덱스가, 세 번째 인자에는 map메서드의 대상이 되는 배열 자체가 담긴다.

 

4-2-3 this

 

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

 

Array.prototype.map 예제

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 값을, 세 번째 인자에는 배열 자체를 지정해 호출한다.

그 결과 변수 mappedValued에 담겨 mappedArr의 i 번째 인자에 할당된다.

 

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

 

3. 콜백 함수는 함수다

 

메서드를 콜백 함수로 전달한 경우

var obj = {
	vals:[1,2,3],
    logValus: 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가 출력된다.

한편 8번째 줄에서는 이 메서드를 forEach 함수의 콜백 함수로서 전달했다.

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

 

어떤 함수의 인자에 객체의 메서드를 전달하더라도 이는 결국 메서드가 아닌 함수일 뿐이다.

 

 

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

 

콜백 함수 내부에서 this가 객체를 바라보게 하고 싶다면 어떻게 해야할까?

 

별도의 인자로 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);

 

obj.func 메서드 내부에서 self 변수에 this를 담고, 익명 함수를 선언과 동시에 반환했다.

이제 obj1.func를 호출하면 앞서 선언한 내부 함수가 반환되어 callback 변수에 담긴다.

이 callback을 setTimeout 함수에 인자로 전달하면 1초 뒤 callback이 실행되면서 obj1을 출력할 것이다.

 

이 방식은 별로임.

 

콜백 함수 내부에서 this를 사용하지 않은 경우

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

 

this를 사용하지 않았지만 훨씬 간결하고 직관적이다 하지만 좀 아쉬운 부분도 있다. 

이제는 작성한 함수를 this를 이용해 다양한 상황에 재활용할 수 없게 되었다.

 

func함수 재활용

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

this를 우회적으로나마 활용하므로써 다양한 상황에서 원하는 객체를 바라보는 콜백 함수를 만들 수 있는 방법이다.

 

이제는 전통적인 방식의 아쉬움을 보완하는 훌륭한 방법이 있다. 

ES5에서 등장한 bind 메서드를 이용하는 방법이다.

 

콜백 함수 내부의 this에 다른 값을 바인딩하는 방법 - 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);

 

 

5. 콜백 지옥과 비동기 제어

 

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

자바스크립트에서 흔히 발생하는 문제이다.

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

 

비동기는 동기의 반대말이다. 동기적인 코드는 현재 실행중인 코드가 완료된 후에야 다음 코드를 실행하는 방식이다. 

반대로 비동기적인 코드는 현재 실행중인 코드의 완료 여부와 무관하게 바로 다음 코드로 넘어간다.

 

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

계산식이 복잡해서 CPU가 계산하는 데 시간이 많이 필요한 경우라 하더라도 이는 동기적인 코드이다.

 

반면 사용자의 요청에 의해 특정 시간이 경과되기 전까지 어떤 함수의 실행을 보류한다거나, 사용자의 직접적인 개입이 있을 때

어떤 함수를 실행하도록 대기한다거나, 웹 브라우저 자체가 아닌 별도의 대상에 무언가를 요청하고 그에 대한 응답이 왔을 때 어떤 함수를 실행하도록 대기하는 등 별도의 요청, 실행, 대기, 보류 등과 관련된 코드는 비동기적인 코드이다.

 

그러나 현대의 자바스크립트는 웹의 복잡도가 높아진 만큼 비동기적인 코드의 비중에 예전보다 훨씬 높아진 상황이다.

그와 동시에 콜백 지옥에 빠지기도 훨씬 쉬워졌다.

 

콜백 지옥 예시

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초마 주기마다 커피 목록을 수집하고 출력한다.

각 콜백은 커피 이름을 전달하고 목록에 이름을 추가한다. 

목적 달성에는 지장이 없지만 들여쓰기 수준도 과도하게 깊어졌고 

값이 전달되는 순서가 '아래에서 위로'향하고 있어 어색하게 느껴진다.

가독성 문제와 어색함을 동시에 해결하는 가장 간단한 방법은 익명의 콜백함수를 모두 기명함수로 전달하는 것이다.

 

콜백 지옥 해결-기명함수로 변환

이 방식은 코드의 가독성을 높일 뿐 아니라 함수 선언과 함수 호출만 구분할 수 있다면 위에서부터 아래로 순서대로 읽어내려가는 데 어려움이 없다.

변수를 최상단으로 끌어올림으로써 외부에 노출되게 됐지만 전체를 즉시 실행 함수 등으로 감싸면 간단히 해결된다

 


마무리 정리

 

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

'코어자바스크립트 스터디' 카테고리의 다른 글

5장 클로저  (0) 2024.07.15
3,4강 발표자료  (0) 2024.07.13
3장 this  (0) 2024.07.08
1장 데이터 타입, 2장 실행 컨텍스트 발표자료  (0) 2024.07.05
코어 자바스크립트 - 1장 : 데이터 타입  (0) 2024.07.03