[JS] 콜백함수란?

고정원·2021년 6월 22일
0

1Q/1Day

목록 보기
7/13
post-thumbnail

정재남,『코어자바스크립트』를 읽고 정리한 내용입니다. 이해가 부족한 부분은 책과 동일하게 작성하였습니다.

1. 콜백함수란?

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

Call+Back
어떤 함수 X를 호출하면서 동시에 '특정 조건일 때 함수 Y를 실행해서 나에게 알려줘!'라는 요청을 함께 보내는 것이다. 이 요청을 받은 함수X는 해당 조건이 갖춰졌는지 여부를 스스로 판단해서 Y를 직접 호출한다.

2. 제어권

2.1 호출시점

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

아래 예제1-1를 살펴보면,

<예제1>
let count = 0;
let countTime = function () {
  	console.log(count);
  
  if(++count > 4) clearInterval(timer);
};
let timer = setInterval(countTime, 300);

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

1) setInterval이라고 하는 '다른 코드'에 첫 번째 인자로서 contTime 함수를 넘겨주자,
2) 제어권을 넘겨 받은 setInterval이 스스로 판단에 따라 적절한 시점(0.3초마다)이 익명 함수를 실행했다.
📍 콜백 함수의 제어권을 넘겨받은 코드는 콜백함수 호출 시점에 대한 제어권을 갖는다.

2.2 인자

<예제2>
let 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]

🧐우선, map()메서드는 어떻게 동작할까?
Array.map(callback[,thisArg]) callback: function(currentValue, index, array)

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

첫 번째 인자로 callback함수를 받고, 생략 가능한 두 번째 인자로 콜백 함수 내부에서 this로 인식할 대상을 특정할 수 있다. thisArg를 생략하면 일반적인 함수처럼 전역객체에 바인딩된다. map메서드는 메서드의 대상이 되는 배열의 모든 요소들을 처음부터 끝까지 하나씩 꺼내서 콜백함수를 반복 호출하고, 콜백 함수의 실행 결과들을 모아 새로운 배열을 만든다.

다시 예제2를 보면
1) 배열[10,20,30]의 각 요소를 처음부터 하나씩 꺼내서 콜백 함수를 실행
2) 첫 번째(index 0)에 대한 콜백함수는 currentValue에 10이, index에는 인덱스 0이 담겨 실행
3) 각 값을 출력하고, 15(10+5)를 반환
...두 번째(index 1), 세 번째(index 2) 각 각 동일하게 콜백함수를 실행하고 나면
4)[15, 25, 35]라는 새로운 배열을 만들어 newArr에 담긴다.

만약 map메서들를 제이쿼리의 방식처럼 인자의 순서를 바꾸어 사용하면?
우리는 순서바뀌더라도 index, currentValue 단어로 접근하니까 순서상관없이 각 단어의 의미가 바뀌는 것이 아니니까 문제 없을거라 생각할 수 있다. 하지만, 저 단어들은 그저 이름일뿐이다.
컴퓨터는 그저 첫 번째, 두 번째의 순서에 의해서만 각각을 구분하고 인식한다!

<예제2.1>
let newArr2 = [10,20,30].map(function(index, currentValue){
  console.log(index, currentValue);
  return currentValue+5;
});
console.log(newArr2);
// --실행 결과--
//10 0 
//20 1 
//30 2 
//[5, 6, 7]

따라서 예제2.1를 실행하면 [5, 6, 7]이라는 결과가 나온다. currentValue라는 인자의 위치가 두 번째라서 컴퓨터는 여기에 인덱스 값을 부여했기 때문이다.

😎콜백 함수의 제어권을 가진 코드(함수 또는 메서드) 규칙을 따라야 한다!
콜백함수를 받아서 처리할 코드, 제어권을 가지고 있는 코드(함수 또는 메서드)에 정의된 규칙에 따라 함수를 작성해야 한다.
즉,map()메서드를 호출해서 원하는 배열을 얻으려면 map메서드에 정의된 규칙에 따라 함수를 작성해야한다. 콜백함수를 호출하는 주체가 사용자가 아닌 map메서드이므로 map메서드가 콜백 함수를 호출할 때 인자에 어떤 값들을 어떤 순서로 넘길 것인지가 전적으로 map메서드에 달린 것이다.
콜백 함수의 제어권을 넘겨받은 코드는 콜백 함수를 호출할 때 인자에 어떤 값들을 어떤 순서로 넘길 것인지에 대한 제어권을 가진다.

2.3 this

  • 콜백 함수도 함수이기 때문에 기본적으로 this가 전역객체를 참조
  • 하지만 제어권을 넘겨 받을 코드에서 콜백 함수에 별도로 this가 될 대상을 지정한 경우에는 그 대상을 참조
Array.prototype.map = function (callback, thisArg) {
  let mappedArr = [];
  for (let i = 0; i < this.length; i++) {
    const mappedValue = callback.call(thisArg || window, this[i], i, this);
    mappedArr[i] = mappedValue;
  }
  return mappedArr;
}

const result = [10, 20, 30].map(function (value, index) {
  console.log(this, value, index);
  return value + 100;
});

console.log(result);

// 실행 결과
// Window 10 0
// Window 20 1
// Window 30 2
// [110, 120, 130]

const anotherResult = [10, 20, 30].map(function (value, index) {
  console.log(this, value, index);
  return value + 100;
}, [1, 2, 3]);

console.log(anotherResult);

// 실행 결과
// [1, 2, 3] 10 0
// [1, 2, 3] 20 1
// [1, 2, 3] 30 2
// [110, 120, 130]

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

  • thisArg가 있는 경우에는 해당 값을 콜백 함수의 this로 지정
  • thisArg가 없는 경우에는 전역 객체(window)를 this로 지정

📍 그리고 thisArg로 넘기는 것은 콜백 함수의 this를 지정하는 것이지, map메서드의 this를 지정하는 게 아니라서 결과 값에는 영향이 없다.(둘 다 [110, 120, 130])

const cbFunc = function () {
  console.log(this);
};

setTimeout(cbFunc, 300);
// 실행 결과
// Window

const arr = [1, 2, 3, 4, 5];
arr.forEach(cbFunc);
// 실행 결과
// Window
// Window
// Window
// Window
// Window

arr.forEach(cbFunc, arr);
// 실행 결과
// [1, 2, 3, 4, 5]
// [1, 2, 3, 4, 5]
// [1, 2, 3, 4, 5]
// [1, 2, 3, 4, 5]
// [1, 2, 3, 4, 5]

document.body.innerHTML += '<button id="a">클릭</button>';
document.body.querySelector('#a')
  .addEventListener('click', cbFunc);
// 실행 결과
// <button id="a">클릭</button>
  • setTimeout
    : 내부에서 콜백 함수를 호출할 때 call 메서드의 첫번째 인자에 전연 객체를 넘기기 때문에 콜백 함수 내부에서의 this는 전역 객체를 가리킴

  • forEach
    : 별도의 인자로 this를 받는 경우. 콜백 함수 다음 인자로 this로 지정할 객체를 넘기지 않은 경우에는 this가 전역 객체를 가리키고, this로 지정할 객체를 넘긴 경우에는 this가 해당 객체를 가리킴

  • addEventListener
    : 내부에서 콜백 함수를 호출 할 때 call 메서드의 첫번째 인자에 addEventListener 메서드의 this를 그대로 넘기도록 정의돼 있기 때문에 콜백 함수 내부에서 this가 addEventListener를 호출 한 주체 HTML 엘리먼트를 가리킴

3. 콜백함수는 함수다

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

const obj = {
  vals: [1, 2, 3],
  logValues: function (v, i) {
    console.log(this, v, i);
  }
};

obj.logValues(1, 2); // 🔹여기서 logValues는 obj객체의 메서드임!
// 따라서 this는 obj를 가리킴
// obj, 1, 2

[4, 5, 6].forEach(obj.logValues); //🔹여기서는 obj.logValues가 가리키는 함수만 전달! forEach에 의해 콜백이 함수로서 호출되고, 별도의 this지정인자 없으니까 함수내부에서 this는 전역객체를 바라봄
// 실행 결과
// Window, 4, 0
// Window, 5, 1
// Window, 6, 2

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

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

객체의 메서드를 콜백 함수로 전달하면 객체의 메서드가 아니라 함수로 전달되기 때문에 해당 객체를 this로 바라볼 수 없다. 그럼에도 콜백함수 내부에서 this가 객체를 바라보게 하고 싶다면??

1) 전통적인 방식

const obj1 = {
  name: 'obj1',
  func: function () {
    const self = this;
    return function () {
      console.log(self.name);
    }
  }
};

const callback = obj1.func();
setTimeout(callback, 1000);

// 실행 결과
// obj1
  • 메서드 내부에서 self 변수에 this를 담아서 사용
  • 실제로 this를 사용하지도 않을뿐더러 번거로움

2) this를 사용하지 않은 경우

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

setTimeout(obj1.func, 1000);

// 실행 결과
// obj1
  • 훨씬 간결하고 직관적이지만 아쉬운 부분도 있음
  • this를 이용해 다양한 상황에 재활용할 수 없게 됨

3) func함수 재활용

const obj1 = {
  name: 'obj1',
  func: function () {
    const self = this;
    return function () {
      console.log(self.name);
    }
  }
};
const callback = obj1.func();
setTimeout(callback, 1000);
// 실행 결과
// obj1

const obj2 = {
  name: 'obj2',
  func: obj1.func
};
const callback2 = obj2.func();
setTimeout(callback2, 1500);
// 실행 결과
// obj2

const obj3 = { name: 'obj3' };
const callback3 = obj1.func.call(obj3);
setTimeout(callback3, 2000);
// 실행 결과
// obj3
  • 위의 예제처럼 this를 우회적으로 활용한 경우에는 재사용이 가능
  • 하지만 this를 사용하지 않고, 객체 이름을 직접 사용할 경우에는 재사용이 불가능

4) ✨bind메서드 활용

const obj1 = {
  name: 'obj1',
  func: function () {
    console.log(this.name);
  }
};

setTimeout(obj1.func.bind(obj1), 1000);
// 실행 결과
// obj1

const obj2 = { name: 'obj2' };
setTimeout(obj1.func.bind(obj2), 1500);
// 실행 결과
// obj2
  • ES5에 추가 된 bind 메서드를 이용한다면 깔끔하게 해결 가능

정리

  • 콜백 함수는 다른 코드에 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수이다.
  • 제어권을 넘겨받은 코드는 다음과 같은 제어권을 가진다.
    1) 콜백 함수를 호출하는 시점을 스스로 판단해서 실행한다.
    2) 콜백 함수를 호출할 때 인자로 넘겨줄 값들 및 그 순서가 정해져 있다.이 순서를 따르지 않고 코드를 작성하면 엉뚱한 결과를 얻게 된다.
    3) 콜백 함수의 this가 무엇을 바라보도록 할지가 정해져 있는 경우도 있다. 정하지 않은 경우에는 전역객체를 바라본다. 사용자 임의로 this를 바꾸고 싶을 경우 bind메서드를 활용하면 된다.
  • 어떤 함수에 인자로 메서드를 전달하더라도 이는 결국 함수로서 실행된다.

🤓이어서 콜백지옥과 비동기 제어에 대해 알아보자

[참고한자료]
정재남, 『코어자바스크립트』, 위키북스(2019)
https://velog.io/@modolee/core-javascript-04-part1
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/map

profile
해결문제에 대해 즐겁게 대화 할 수 있는 프론트엔드 개발자

1개의 댓글

comment-user-thumbnail
2021년 6월 23일

퍼가요~♡

답글 달기