[JS] 콜백 함수

Haegu·2023년 9월 19일

1. 콜백 함수란?

콜백 함수 callback function : 다른 코드의 인자로 넘겨주면서 제어권도 함께 주는 함수

call (부르다, 호출하다) + back (되돌아오다, 되돌다) = 되돌아 호출해달라

콜백 함수를 넘겨받은 코드는 이 콜백 함수를 필요에 따라 적절한 시점에 실행

  • A와 B는 아침 6시에 깨어나야 한다. 제어권

    A는 불안한 마음에 수시로 깨어 시계를 확인한다.
    A는 수시로 시간을 구하는 함수를 직접 호출

    B는 알람시계 6시로 설정하고 잠에 든다.
    B는 시계의 알람을 설정하는 함수 호출 6시에 알람을 울리는 결과 반환


2. 제어권

2-1. 호출 시점

2-1-1. 콜백 함수 setInterval (1)

첫 번째 매개변수 setInterval
두 번째 매개변수 300

var count = 0;
var timer = setInterval(function () {
  console.log(count);
  if (++count > 4) clearInterval(timer);
}, 300);
setInterval 구조
var intervalID = scope.setInterval(func, delay[, param1, param2, ...]);
  • scope : Window, Worker 객체 모두 setInterval 메서드 제공
  • func : 함수
  • delay : 밀리초 ms
  • 나머지 : param1, param2, ...

func에 넘겨준 함수는 매 delay(ms)마다 실행

setInterval 실행하면 반복적으로 실행되는 내용 자체를 특정할 수 있는 고유한 ID 값 반환
변수에 담는 이유 : 반복 실행되는 중간에 종료(clearInterval)할 수 있게 하기 위해서


2-1-2. 콜백 함수 setInterval (2)

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

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보다 크면 반복 실행 종료


코드 실행 방식과 제어권
code호출 주체제어권
cbFunc();사용자사용자
setInterval(cbFunc, 300);setIntervalsetIntervlal

콘솔창에는 0.3초에 한 번씩 숫자가 0부터 1씩 증가하며 출력되다가 4가 출력된 이후 종료됨

setInterval이라고 하는 '다른 코드'에 첫 번째 인자로서 cbFunc 함수를 넘겨주자
제어권을 넘겨받은 setInterval이 스스로 판단에 따라
적절한 시점에 (0.3초마다) 이 익명 함수를 실행

=> 콜백 함수의 제어권을 넘겨 받은 코드는 콜백 함수 호출 시점에 대한 제어권 가짐



2-2. 인자

2-2-1. Array.prototype.map

var newArr = [10, 20, 30].map(function (currentValue, index) {
  console.log(currentValue, index);
  return currentValue + 5;
});
console.log(newArr);

// 실행 결과
// currentValue index
// 10 0
// 20 1
// 30 2
// [15, 25, 35]

배열 [10, 20, 30]map 메서드 호출

첫 번째 매개변수 : 익명 함수

Array의 prototype에 담긴 map 메서드 구조
Array.prototype.map(callback[, thisArg])
callback: function(cureentValue, index, array)
  • 첫 번째 인자 : callback 함수
  • 생략가능한 두 번째 인자 : 콜백 함수 내부에서 this로 인식할 대상 특정
  • 생략할 경우 : 일반적인 함수와 같이 전역객체가 바인딩 됨

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

콜백 함수의 첫 번째 인자 cureentValue : 배열의 요소 중 현재값
콜백 함수의 두 번째 인자 index : 현재값의 인덱스
콜백 함수의 세 번째 인자 array : map 메서드의 대상이 되는 배열 자체



2-3. this

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

2-3-1. Array.prototype.map 구현

Array.porototype.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 값,
세 번째 인자 : 배열 자체를 지정해 호출
그 결과가 변수 mappedValue에 담겨 mappedArr의 i 번째 인자에 할당됨

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

2-3-2. 콜백 함수 내부에서의 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);  // <button id="a">클릭</button>
  }						   // MouseEvent { isTrusted: true, ... }
);

addEventListener 내부에서 콜백 함수를 호출할 때 call 메서드의 첫 번째 인자에 addEventListener 메서드 this를 그대로 넘기도록 정의되어 있기 때문에
콜백 함수 내부에서의 thisaddEventListener를 호출한 주체인 HTML 엘리먼트를 가리키게 됩니다.


3. 콜백 함수는 함수다

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

3-1. 메서드를 콜백 함수로 전달한 경우

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

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

4-1. 전통적인 방식

this 우회적으로 활용함으로써 다양한 상황에서 원하는 객체 바라보는 콜백 함수
실제로 this를 사용하지도 않고 번거로움

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

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

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

callbacksetTimeout 함수에 인자로 전달하면
1초(1000ms) 뒤 callback 실행되면서 'obj1'을 출력

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

간결하고 직관적이지만 this를 이용해 다양한 상황에 재활용할 수 없음
처음부터 바라볼 객체를 명시적으로 obj1로 지정했기 때문에 다른 객체를 바라보게 할 수 없음
-> 전통적인 방식 통용됨

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

4-3. (4-1) func 함수 재활용

다양한 객체에 재활용할 필요성이 없는 경우 사용

...
var obj2 = {
  name: 'obj2',
  func: obj1.func
};

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

// obj1의 func를 실행하면서 this를 obj3가 되도록 지정해 콜백
var obj3 = { name: 'obj3' };
var callback3 = obj1.func.call(obj3);
setTimeout(callback3, 2000);

4-4. 콜백 함수 내부의 this에 다른 값을 바인딩하는 방법 - bind 메서드 활용

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

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

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

콜백 지옥 callback hell

  • 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상
  • 주로 이벤트 처리나 서버 통신과 같이 비동기적인 작업 수행할 때 자주 등장
  • 가독성 저하, 코드 수정 어려움

비동기 asynchronous

  • 현재 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어감
  • 별도의 요청, 실행 대기, 보류 등과 관려된 코드
  • 사용자의 요청에 의해 특정 시간이 경과되기 전까지 어떤 함수의 실행을 보류 setTimeout
  • 사용자의 직접적인 개입이 있을 때 비로소 어떤 함수를 실행하도록 대기 addEventListener
  • 웹브라우저 자체가 아닌 별도의 대상에 무언가를 요청하고 그에 대한 응답이 왔을 때 비로소 어떤 함수를 실행하도록 대기 XMLHttpRequest

동기 synchronous

  • 현재 실행 중인 코드가 완료된 후 다음 코드 실행
  • CPU 계산에 의해 즉시 처리 가능한 대부분의 코드

5-1-1. 콜백 지옥

0.5초 주기마다 커피 목록 수집, 출력
목적 달성에는 지장 없으나 과도하게 깊어짐
전달되는 순서 아래 -> 위

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, '에스프레소');

5-1-2. 콜백 지옥 해결 - 기명함수로 변환

var coffeeList = '';

var addEspresso = function (name) {
  coffeeList = name;
  console.log(coffeList);
  setTimeout(addAmericano, 500, '아메리카노');
}; 
var addAmericano = function (name) {
  coffeeList = name;
  console.log(coffeList);
  setTimeout(addMocha, 500, '카페모카');
};
var addMocha = function (name) {
  coffeeList = name;
  console.log(coffeList);
  setTimeout(addLatte, 500, '카페라떼');
};
var addLatte = function (name) {
  coffeeList = name;
  console.log(coffeList);
};

setTimeout(addEspresso, 500, '에스프레소');

5-2-1. 비동기 작업의 동기적 표현 - Promise (1)

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

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

5-2-2. 비동기 작업의 동기적 표현 - Promise (2)

함수화해서 더 짧게 표현

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

5-2-3. 비동기 작업의 동기적 표현 - Generator

ES6 Generator 함수 : *가 붙은 함수
Generator 함수 실행하면 Iterator가 반환되는데, Iteratornext라 메서드를 가지고 있음
next 메서드를 호출하면 Generator 함수 내부에서 가장 먼저 등장하는 yield에서 함수의 실행을 멈춤
이후 다시 next 메서드를 호출하면 앞서 멈췄던 부분부터 시작해서 그다음에 등장하는 yield에서 함수의 실행을 멈춤
비동기 작업이 완료되는 시점마다 next 메서드를 호출해준다면 Generator 함수 내부의 소스가 위 -> 아래 순차적으로 진행됨

var addCoffee = function (prevName, name) {
  setTimeout(funtion () {
    coffeeMaker.next(prevName ? (prevName + ', ' + name) : name);
  }, 500);
};
var coffeeGenerator = function* () {
  var espresso = yield addCoffee('', '에스프레소');
  console.log(espresso);
  var americano = yield addCoffee(espresso, '아메리카노');
  console.log(americano);
  var mocha = yield addCoffee(americano, '모카');
  console.log(mocha);
  var latte = yield addCoffee(mocha, '카페라떼');
  console.log(latte);
};
var coffeeMaker = coffeeGenerator();
coffeeMaker.next();

5-2-2. 비동기 작업의 동기적 표현 - Promise (2)

함수화해서 더 짧게 표현

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

5-2-4. 비동기 작업의 동기적 표현 - Promise + async/await

ES2017 async / await
가독성 뛰어나며, 작성법 간단함
비동기 작업을 수행하고자 하는 함수 앞에 async를 표기
함수 내부에서 실질적인 비동기 작업이 필요한 위치마다 await를 표기하는 것만으로
뒤의 내용을 Promise로 자동 전환하고, 해당 내용이 resolve된 이후에야 다음으로 진행함
=> Promise의 then과 흡사한 효과

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();
profile
사용자 경험을 위해 모험을 떠나는 해구

0개의 댓글