javascript & Node.js/javascript

자바스크립트 콜백 함수

반응형

콜백함수란?

콜백 함수란 다른 코드(함수 또는 메소드)에게 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수이다.

콜백 함수를 위임받은 코드는 자체적인 내부 로직에 의해 이 콜백 함수를 적절한 시점에 실행한다.

let count = 0;

const cbFunc = function () {
  console.log(count);
  if (++count > 4) {
    clearInterval(timer);
  }
};

const timer = setInterval(cbFunc, 300);
  • cbFunc()의 호출 주체 및 제어권 = 사용자
  • setInterval(cbzFunc, 300)의 호출주체 및 제어권 ⇒ setInterval

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


this

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


콜백 함수는 함수다

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

obj.logValues(1, 2); // === obj (메소드로서의 호출)
[4, 5, 6].forEach(obj.logValues); // Window (함수로서의 호출)


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

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

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

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

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

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

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

setTimeout(obj1.func.bind(obj1), 1000); // obj1

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


콜백 지옥과 비동기 제어

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

step1(function (value1) {
    step2(function (value2) {
        step3(function (value3) {
            step4(function (value4) {
                step5(function (value5) {
                    step6(function (value6) {
                        // Do something with value6
                    });
                });
            });
        });
    });
});

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


비동기 vs 동기

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

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

예를 들어 사용자의 요청에 의해 특정 시간이 경과되기 전까지 어떤 함수의 실행을 보류한다거나(setTimeInterval), 사용자의 직접적인 개입이 있을 때 비로소 어떤 함수를 실행하도록 대기한다거나(addEventListener), 웹 브라우저 자체가 아닌 별도의 대상에 무언가를 요청하고 그에 대한 응답이 왔을 때 비로소 어떤 함수를 실행하도록 대기하는 등(XMLHttpRequest), 별도의 요청, 실행 대기, 보류 등과 관련된 코드는 비동기적인 코드이다.


콜백지옥을 해결하는 방법

자바스크립트 진영은 비동기적인 일련의 작업을 동기적으로, 혹은 동기적인 것 처럼 보이게끔 처리해주는 장치를 마련하고자 끊임없이 노력해왔다.

ES6에서는 Promise, Generator 등이 도입됐고, ES2017에서는 async, await가 도입됐다.


Promise

return new Promise(function (resolve) {
  setTimeout(function () {
    const name = "바보";
    console.log(name);
    resolve(name);
  }, 500);
}).then(function (prevName) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      const name = prevName + " 멍청이";
      console.log(name);
      resolve(name);
    }, 500);
  });
});

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


Generator

const addBabo = function (prevName, name) {
  setTimeout(function () {
    baboMaker.next(prevName ? prevName + ", " + name : name);
  }, 500);
};

const baboGenerator = function* () {
  const babo = yield addBabo("", "바보");
  console.log(babo);
  const babo2 = yield addBabo(babo, "멍청이");
  console.log(babo2);
};

const baboMaker = baboGenerator();
baboMaker.next();

// 바보
// 바보, 멍청이

ES6의 Generator를 이용할 수도 있다.

*이 붙은 함수가 바로 Generator 함수이다. Generator 함수를 실행하면 Iterator가 반환되는데, Iterator는 next라는 메소드를 가지고 있다. 이 next 메소드를 호출하면 Generator 함수 내부에서 가장 먼저 등장하는 yield에서 함수의 실행을 멈춘다. 이후 다시 next 메소드를 호출하면 앞서 멈췄던 부분터 시작해서 그 다음 등장하는 yield에서 함수의 실행을 멈춘다.


Promise + async&await

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

const baboMaker = async function () {
  let baboList = "";
  const _addBabo = async function (name) {
    baboList += (baboList ? "," : "") + (await addBabo(name));
  };

  await _addBabo("바보");
  console.log(baboList);

  await _addBabo("멍청이");
  console.log(baboList);
};

baboMaker();

이처럼 ES2017에서는 가독성이 뛰어나면서 작성법도 간단한 새로운 기능이 추가됐다. (async, await)

비동기 작업을 수행하고자 하는 함수 앞에 async를 표기하고, 함수 내부에서 실질적인 비동기 작업이 필요한 위치마다 await를 표기하는 것만으로 뒤의 내용을 Promise로 자동 전환하고, 해당 내용이 resolve된 이후에야 다음으로 진행한다. (즉 Promise의 then과 흡사한 효과를 얻을 수 있다.)

반응형