Home 콜백 함수
Post
Cancel

콜백 함수

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

제어권

호출 시점

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

setInterval : 매개변수로 func, delay를 필수로 받고 3번째 인자부터 선택적으로 받는 함수로 func을 delay마다 실행되며, 나머지 인자는 func함수를 실행할 때 사용할 수 있는 매개변수이고 어떤 값도 리턴하지 않는다.

1
2
3
4
5
6
7
8
9
/* 콜백 함수 예제 */
var count = 0;
var timer;
var cbFunc = function () {
  console.log(count);
  if (++count > 4) clearInterval(timer); // 중간 종료
};

timer = setInterval(cbFunc, 300);

위 코드를 보면 setInterval에게 cbFunc함수 넘겨주고 setInterval이 스스로의 판단에 따라 적절한 시점에 익명 함수를 실행한다. 이처럼 콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수 호출 시점에 대한 제어권을 가진다.

code호출 주체제어권
cbFunc();사용자사용자
setInterval(cbFunc, 300);setIntervalsetInterval

인자

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

map : 매개변수로 callback 함수, 생략 가능한 두 번째 인자 thisArg로 콜백 함수 내부에서 사용할 수 있는 this로 인식할 대상을 특정할 수 있다. 당연하듯 생략할 경우 전역객체가 바인딩된다.

map메서드는 배열의 모든 요소들을 콜백 함수로 반복 호출하고, 콜백 함수의 실행 결과를 배열형태로 저장한다.

1
2
3
4
5
6
7
8
9
10
/* 콜백 함수 예제 */
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
2
3
4
5
6
/* 콜백 함수 예제 2 */
var newArr = [10, 20, 30].map(function (index, currentValue) {
  console.log(index, currentValue);
  return currentValue + 5;
});
console.log(newArr);

Q. 위의 코드의 결과가 어떻게 될까?

A. 10 0, 20 1, 30 2, [5, 6, 7]라는 결과가 나오는데 이는 콜백 함수 제어권은 map메서드 에게 있기 때문이다. 사용자가 index, currentValue로 넘겨도 결정은 map이 하기 때문에 인자 결정은 map메서드가 한다.


this

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

1
2
3
4
5
6
7
8
9
/* 간단하게 구현한 map 메서드 */
Array.prototype.map = function (callback, thisArg) {
  var mappedArr = [];
  for (var i = 0; i < this.length; i++) {
    // map 은 callback: function (currentValue, index, array) 처럼 구현되어 있기 때문이다.
    mappedArr[i] = callback.call(thisArg || window, this[i], i, this);
  }
  return mappedArr;
};

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

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 콜백 함수 내부에서의 this */
setTimeout(function () {
  console.log(this);     // (1) Window { ... }
}, 300);

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

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

(1)의 경우 setTimeout메서드는 call 첫 인자로 전역 객체를 넘기기 때문이고, (2)의 경우 foreach메서드의 경우 this를 지정하지 않으면 전역 객체를 가르키고, (3)의 경우 addEventListener는 내부에서 콜백 함수를 호출할 때 call 메서드의 첫 번째 인자에 addEventListener메서드의 this를 그대로 넘기도록 정의돼 있기 때문에 콜백 함수 내부에서의 this가 addEventListener를 호출한 주체인 HTML 엘리먼트를 가르키게 된다.


콜백 함수는 함수다.

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

Q. 함수와 메서드의 차이점은?

A. 함수를 호출하는 객체가 있는 경우 메서드라고 말하며, 함수를 호출하는 객체가 없는 경우 함수라고 말한다.

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

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

obj.logValues(1,2)의 경우 메서드의 이름 앞에 점이 있으므로 메서드로서 호출이다. [4].forEach(obj.logValues);의 경우 가르키는 함수만 전달하여 obj 와 직접적인 연관이 없어진다. 즉, 전역 객체를 바라보게 된다.


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

객체의 메서드를 콜백함수로 전달하면 해당 객체를 this로 바라볼 수 없게 된다.

그럼에도 콜백 함수 내부에서 this가 객체를 바라보게 하기위해 전통적으로 this를 다른 변수에 담아 콜백 함수로 활용할 함수에서는 this 대신 그 변수를 사용하게 하고, 이를 클로저로 만드는 방식으로 많이 쓰였다.

콜백 함수 내부의 this에 다른 값을 바인딩하는 방법

1
2
3
4
5
6
7
8
9
10
11
var obj1 = {
  name: 'obj1',
  func: function () {
    var self = this; // this를 변수로 저장
    return function () {
      console.log(self.name); // 사용
    };
  }
};
var callback = obj1.func();
setTimeout(callback, 1000);

이 방식은 실제로 this를 사용하지도 않고 번거롭기 때문에 차라리 this를 사용하지 않는 방법이 있다.

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

1
2
3
4
5
6
7
var obj1 = {
  name: 'obj1',
  func: function () {
    console.log(obj1.name); // this 대신 obj1 사용
  }
};
setTimeout(obj1.func, 1000);

관결하고 직관적이지만 이 역시 this를 이용하지 않기 때문에 재활용성이 떨어진다.

func 함수 재활용

1
2
3
4
5
6
7
8
9
10
11
12
/* func 함수 재활용 방법 */
var obj2 = {
  name: 'obj2',
  func: obj1.func
};

var callback2 = obj2.func();          // obj2의 func(obj1 메서드를 복사한) 실행한 결과를 담아둠
setTimeout(callback2, 1500);          // obj2

var obj3 = {name: 'obj3'};
var callback3 = obj1.func.call(obj3); // call 메서드를 통해 this 재설정
setTimeout(callback3, 2000);          // obj1

위와 같은 방법으로 this를 설정하여 원하는 객체를 바라보는 콜백 함수를 만들 수 있다.

1
2
3
4
5
6
7
8
9
10
/* func 함수 재활용 방법 : bind 메서드 활용 */
var obj1 = {
  name: 'obj1',
  func: function () {
    console.log(this.name);
  }
};

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

콜백 지옥과 비동기 제어

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

  • 비동기적인 작업을 수행하기 위해 이런 형태가 자주 등장한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 콜백 지옥 */
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, '에스프레소');

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* promise 방식 1 */
new Promise(function (resolve) {
  setTimeout(function () {
    var name = '에스프레소';
    console.log(name);
    resolve(name);
  }, 500);
}).then(function (preName) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      var name = preName + ', 아메리카노';
      console.log(name);
      resolve(name);
    }, 500);
  });
});//.then(function (prevName) {

new 연산자와 함꼐 호출한 Promise의 인자로 넘겨주는 콜백 함수는 호출할 때 바로 실행되지만 그 내부에 resolve또는 reject함수를 호출하는 구문이 있을 경우 둘 중 하나가 실행되기 전까지는 then, catch로 넘어가지 않음

따라서 비동기 작업이 완료될 때 비로소 resolve,reject를 호출하는 방법으로 비동기 작업을 동기적 표현이 가능하다.ㅁ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* promise 방식 2 */
var addCoffee = function (name) {
  return function (prevName) {
    return new Promise(function (resolve) {
      setTimeout(function () {
        var newName = prevName ? (prevName + ', ' + name) : name;
        console.log(newName);
        resolve(newName);
      }, 500);
    });
  };
};

addCoffee('에스프레소')()
.then(addCoffee('아메리카노'))
.then(addCoffee('카페모카'))
.then(addCoffee('카페라떼'));

2,3 번째 줄에서 클로저를 사용했는데, 자세한 내용은 다음 장에서 자세히 다룬다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Generator 함수 */
var addCoffee = function (prevName, name) {
  setTimeout(function () {
    coffeeMaker.next(prevName ? prevName + ', ' + name : name);
  }, 500);
};

var coffeeGenerator = function* () { // function 뒤에 *가 붙은 함수를 Generator 함수
  var espresso = yield addCoffee('', '에스프레소');
  console.log(espresso);
  var americano = yield addCoffee(espresso, '아메리카노');
  console.log(americano);
  //var mocha ...
};
var coffeeMaker = coffeeGenerator();
coffeeMaker.next();

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

next메서드를 호출하면 Generator 함수 내부에 가장 먼저 등장하는 yield에서 함수 실행을 멈춘다. 그후 next가 호출되면 다음 yield로 넘어간다. 즉, 비동기 작업이 완료 시점에 next를 호출하여 순차적 진행이 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* promise 방식 3 - Async/await */
var addCoffee = function (name) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve(name);
    }, 500);
  });
};
// async : 비동기 작업 수행하고자 하는 함수 앞에 async 명시
var coffeeMaker = async function () {
  var coffeeList = '';
  var _addCoffee = async function (name) {
    coffeeList += (coffeeList ? ',' : '') + await addCoffee(name);
  };
  // await 표기시 뒤의 내용을 자동 Promise로 전환이후 해당 내용이 resolve 이후 다음 진행
  await _addCoffee('에스프레소');
  console.log(coffeeList);
  await _addCoffee('아메리카노');
  console.log(coffeeList);
  //await _addCoffee('카페모카'); ...
}
coffeeMaker();

async/await를 통해 Promise의 then과 흡사한 효과를 얻을 수 있다.

This post is licensed under CC BY 4.0 by the author.