[JS] Callback function

CheolHyeon Park·2022년 11월 21일
0

JavaScript

목록 보기
15/23

콜백 함수(callback function)은 다른 코드의 인자로 넘기는 함수를 의미한다. 넘겨받은 코드는 콜백 함수를 적절한 시기에 호출하여 실행시킨다. 콜백함수의 예로 자주 쓰이는 것이 있다.

6시에 일어나기 위해 알람을 맞춰놓고 푸욱 자는 A
6시에 일어나기 위해 매번 일어나 6시인지를 확인하는 B

A는 시계에게 알림에 대한 권한을 위임하고 제어권을 넘긴 것이고, B는 자신이 제어권을 가지고 계속 함수를 호출하는 것이다. 이렇듯 callback은 제어권과 관련이 깊다.

제어권

호출 시점에 대한 제어

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

위 코드는 0.3초마다 count변수의 값을 1씩 늘리고 count값이 4를 초과하면 중단하라는 cbFunc함수의 실행을 setInterval에게 넘겼다. 이로써 호출 주체는 setInterval이고 제어권도 setInterval에게 있게 된다.

인자에 대한 제어

const newArr = [10, 20, 30].map((value, index) => {
  console.log(value, index);
  return value + 5;
})

console.log(newArr)  // [15, 25, 35]

위 코드에서 map메소드의 콜백함수는 첫번째 인자로 현재 값, 두번째 인자로 인덱스를 받기로 정의해두었다. map메소드의 인자를 (value,index)에서 (index,value)로 변경한다고 해도 실제로 첫번째 인자로 들어오는 값은 [10, 20, 30]의 값(10, 20, 30)이 된다. 즉, 정의된 규칙에 따라 콜백함수를 작성해야한다.

this

callback함수는 "함수"다.

기존에 정리한 내용(this)를 보면 콜백 함수의 this는 전역객체를 가리킨다. 콜백함수도 "함수"이기 때문이다. 하지만 제어권을 넘겨받을 코드에서 콜백에 별도로 this가 될 대상을 지정한 경우 this는 해당 객체를 가리키게 된다. 이외에도 this가 가르킬 객체를 직접 명시할 수도 있다.

Array.prototype.customFilter = function(callback, thisArgs) {
  const filteredArr = [];
  for(let i = 0; i < this.length; i++) {
    if(callback.call(thisArgs || window, this[i],i)) {
      filteredArr.push(this[i])
    }
  }
  return filteredArr;
}

const arr = [10, 20, 30]
console.log(arr.customFilter((value,i) => value > 10)); // [20, 30]
console.log(arr.customFilter((_,i) => i> 0)); // [30]

콜백함수의 this를 명시적으로 만들기 위해 customFilter를 만들어 보았다. call메소드를 통해 thisArgs가 없을 때는 window객체를 가르키도록 명시하였다.

const consoleObj = {
  state: "loading",
  getState: function() {
    console.log("the state of console is", this.state);
  }
}

consoleObj.getState() // the state of console is loading
setTimeout(consoleObj.getState, 0); // the state of console is undefined

consoleObjgetState메소드는 this.state를 출력한다. consoleObj.getState()는 호출주체인 consoleObjstate값을 출력하는 반면, setTimeout의 콜백함수로 호출을 하게 되면 정상적으로 출력되지 않게 된다. setTimeout의 콜백함수는 "함수"로서 호출되었다는 얘기다. this를 직접 바인딩 해주어야 올바르게 작동할 수 있다.

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

콜백함수가 "함수"와 "메소드" 두 곳에 모두 사용하기 위해, bind메소드를 이용하면 좋다.


const consoleObj = {
  state: "loading",
  getState: function() {
    console.log("the state is", this.state);
  }
}

setTimeout(consoleObj.getState.bind(consoleObj), 0)  // the state is loading

const drawObj = {
  state: "drawing",
}

setTimeout(consoleObj.getState.bind(drawObj), 0) // the state is drawing

콜백 지옥과 비동기 제어

콜백함수는 비동기 처리에서 많이 사용된다. 비동기 처리를 동기적으로 실행하기 위해, 콜백함수를 여러번 중첩해서 사용하다보면 들여쓰기가 많아져 가독성이 떨어지게 되고, 코드 수정도 어렵게 된다.

setTimeout((name) => {
  let coffeeList = name;
  console.log(coffeeList);
  setTimeout((name) => {
    coffeeList += ', ' + name;
    console.log(coffeeList);
    setTimeout((name) => {
      coffeeList += ', ' + name;
      console.log(coffeeList)
      setTimeout((name) => {
        coffeeList += ', ' + name;
        console.log(coffeeList)
      }, 500, "카페모카");
    }, 500, "카페라떼");
  }, 500, '아메리카노');
}, 500, '에스프레소');

코드를 아래에서부터 읽어 위를 해석해야 하는 난해한 상황이 된다.

비동기 표현을 보다 동기적으로 표현하기 위해, ES6+에서는 Promise, async/await, Generator와 같은 방식을 이용하여 비동기를 처리하고 있다.

Promise

new Promise((resolve, reject) => {
  setTimeout(() => {
    var name = "에스프레소";
    console.log(name);
    resolve(name);
  }, 500);
}).then(prevName => {
  return new Promise(resolve => {
    setTimeout(() => {
      var name = prevName + ", 아메리카노";
      console.log(name)
      resolve(name);
    }, 500);
  }) 
}).then(prevName => {
  return new Promise(resolve => {
    setTimeout(() => {
      var name = prevName + ", 카페모카";
      console.log(name)
      resolve(name);
    }, 500);
  }) 
}).then(prevName => {
  return new Promise(resolve => {
    setTimeout(() => {
      var name = prevName + ", 카페라떼";
      console.log(name)
      resolve(name);
    }, 500);
  }) 
})

코드는 들여쓰기가 되지 않고, 차례대로 위부터 순서대로 실행되고, 읽는 방향도 위에서 아래로 일반적이기 때문에 중첩콜백보다 가독성이 좋아졌다. ES2017에서는 이보다 더 가독성을 올려주는 async/await가 있다.

async/await

const addCoffee = function(name) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(name);
    }, 500);
  })
}

const coffeeMaker = async function() {
  let coffeeList = "";
  const _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);
}

coffeeMaker();

then메소드의 체이닝을 통해 동기적으로 사용하는 방식보다 조금 더 동기적인 표현으로 느껴진다. 비동기적으로 실행되지만 코드는 위에서 아래로 그대로 읽으면 되기 때문에 가독성이 좋다.

Generator

const addCoffee = function(prevName, name) {
 setTimeout(() => {
   coffeeMaker.next(prevName ? prevName + ", " + name : name);
 }, 500);
}

const coffeeGenerator = function* () {
 const espresso = yield addCoffee("","에스프레소")
 console.log(espresso);
 const americano = yield addCoffee(espresso, "아메리카노");
 console.log(americano)
 const mocha = yield addCoffee(americano, "카페모카");
 console.log(mocha)
 const latte = yield addCoffee(mocha, "카페라떼");
 console.log(latte)
}

const coffeeMaker = coffeeGenerator();
coffeeMaker.next();

Generator에 대한 설명은 여기서 확인해보면 된다. Generatoryield 키워드를 통해 값을 리턴하면서 동시에 suspend를 하며, 이를 통해 순차적으로 실행할 수 있도록 도와주는 키워드이다. 흐름 제어도 같이 할 수 있다는 장점이 있다.

참고
코어 자바스크립트 - 정재남

profile
나무아래에 앉아, 코딩하는 개발자가 되고 싶은 박철현 블로그입니다.

0개의 댓글