ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JavaScript] 4. 콜백 함수
    개발자 이야기/JavaScript 2021. 7. 15. 20:16

    콜백 함수(callback function)

    • 다른 코드의 인자로 넘겨주는 함수
    • 다른 코드(함수 or 메서드)에게 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수

     

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

     

    제어권

    호출 시점

    setInterval의 구조

    let intervalId = scope.setInterval(func, delay[, param1, param2, ...]);

    scope에는 setInterval 메서드를 제공하는 Window 객체 또는 Worker의 인스턴스가 들어올 수 있다. 브라우저 환경에서는 Window를 생략할 수 있다. 매개변수로는 func(함수), delay(ms) 값을 반드시 전달해야 한다. 세 번째 매개변수부터는 func 함수를 실행할 때 매개변수로 전달할 인자다.

    func에 넘겨준 함수는 매 delay(ms)마다 실행되고, 그 결과로 아무런 값을 리턴하지 않는다. setInterval를 실행하면 반복적으로 실행되는 내용 자체를 특정할 수 있는 고유 ID 값이 반환된다.

    let count = 0;
    let func = function() {
      console.log(count);
      if (++count > 4) clearInterval(timer);
    };
    
    let timer = setInterval(func, 300);
    
    // 실행 결과
    // 0 (0.3s)
    // 1 (0.6s)
    // 2 (0.9s)
    // 3 (1.2s)
    // 4 (1.5s)

    위 코드 예제에서 콜백 함수는 func 함수다.

    콜백 함수 func는 0.3초마다 자동으로 실행된다. timer 변수에는 setInterval의 ID 값이 담긴다. setInterval를 변수에 담은 이유는 clearInterval을 활용해 반복 실행 중 종료하기 위해서다.

     

    setInterval의 첫 번째 인자로 func 함수를 넘겨줌으로써 func 함수의 제어권은 setInterval이 가지게 된다. 

     

    인자

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

    위 코드는 map 메서드의 구조다. map 메서드는 첫 번째 인자로 콜백 함수를 받는다. 생략 가능한 두 번째 인자로 콜백 함수 내부에서 this로 인식할 대상을 특정할 수 있다. thisArg를 생략할 경우 일반 함수와 마찬가지로 전역 객체가 바인딩된다.

     

    map 메서드는 메서드의 대상이 되는 배열의 모든 요소들을 하나씩 꺼내어 콜백 함수를 반복 호출하고, 콜백 함수의 실행 결과를 새로운 배열로 만들어 반환한다.

    let newArr = [5, 6, 7].map(function (currentValue, index) {
      console.log(currentValue, index);
      return currentValue * 10;
    });
    
    console.log(newArr);
    
    // 실행 결과
    // 5 0
    // 6 1
    // 7 2
    // [50, 60, 70]

     

    this

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

     

    콜백 함수는 함수다

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

    let obj = {
      vals: [1, 2, 3],
      logValues: function(v, i) {
        console.log(this, v, i);
      }
    };
    
    obj.logValues(1, 2);               // {vals: Array(3), logValues: ƒ} 1 2
    [4, 5, 6].forEach(obj.logValues);
    
    // [4, 5, 6].forEach(obj.logValues) 실행 결과
    // Window {…} 4 0
    // Window {…} 5 1
    // Window {…} 6 2

    위 예제를 실행시키면, 메서드로서 호출하여 얻은 결과와 forEach 함수로 실행한 결과가 다르다.

    그 이유는 forEach에 의해 콜백이 함수로서 호출되고, 별도로 this를 지정하는 인자를 지정하지 않았기 때문에 this에는 전역 객체가 바인딩된다. 즉, 함수의 인자에 객체의 메서드를 전달하더라도 결국 메서드가 아닌 함수라는 것을 명심해야 한다.

     

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

    별도의 인자로 this를 받는 함수가 아닌 경우, this의 제어권도 넘겨주게 되므로 사용자가 임의로 값을 바꿀 수 없다.

    그래서 콜백 함수로 활용할 함수에서는 this를 다른 변수에 담아 사용하게 되었고, 이를 클로저로 만드는 방식이 많이 쓰였다.

     

    아래 예제 코드는 bind 메서드를 활용하여 콜백 함수 내부의 this에 다른 값을 바인딩하는 방법이다.

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

     

    콜백 지옥과 비동기 제어

    콜백 지옥(callback hell)은 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상이다. 주로 이벤트 처리나 서버 통신과 같이 비동기적인 작업을 수행하기 위해 이런 형태가 등장하는데, 가독성이 떨어지고 코드 수정이 힘들다는 단점을 가진다.

     

    비동기(asynchronous)는 동기(synchronous)의 반대말로, 현재 실행 중인 코드의 완료 여부와는 무관하게 즉시 다음 코드로 넘어가는 것을 말한다. 별도의 요청, 실행 대기, 보류 등과 관련된 코드는 비동기적인 코드다.

     

    비동기적인 작업을 동기적으로, 혹은 동기적인 것처럼 보이게끔 하고자 ES6에서는 Promise, Generator 등이 도입되었고, ES2017에서는 async/await이 도입되었다.

     

    Promise를 이용한 비동기 처리

    new Promise(function (resolve) {
      setTimeout(function () {
        let name = '에스프레소';
        console.log(name);
        resolve(name);
      }, 500);
    }).then(function (prevName) {
      return new Promise(function (resolve) {
        setTimeout(function () {
          let name = prevName + ', 아메리카노';
          console.log(name);
          resolve(name);
        }, 500);
      });
    }).then(function (prevName) {
      return new Promise(function (resolve) {
        setTimeout(function () {
          let name = prevName + ', 카페모카';
          console.log(name);
          resolve(name);
        }, 500);
      });
    }).then(function (prevName) {
      return new Promise(function (resolve) {
        setTimeout(function () {
          let name = prevName + ', 카페라테';
          console.log(name);
          resolve(name);
        }, 500);
      });
    });
    
    // 실행 결과
    // Promise {<pending>}
    // 에스프레소
    // 에스프레소, 아메리카노
    // 에스프레소, 아메리카노, 카페모카
    // 에스프레소, 아메리카노, 카페모카, 카페라테

    Promise의 인자로 넘겨주는 콜백 함수는 호출할 때 바로 실행된다. 그리고 내부에 resolve 또는 reject 함수를 호출하는 구문이 있을 경우 둘 중 하나가 실행되기 전까지 then(다음) 또는 catch(오류 구문)으로 넘어가지 않는다. 그러므로 비동기 작업이 완료될 때 비로소 resolve 또는 reject 함수를 호출하는 방법으로 비동기 작업의 동기적 표현이 가능해진다.

     

    아래 예제는 위 코드 예제에서 반복적인 내용을 함수화하여 더 짧게 표현한 것이다.

    2, 3번째 줄에는 클로저가 활용되었다.

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

     

    Generator를 이용한 비동기 처리

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

    Generator 함수를 실행하면 Iterator가 반환된다. Iterator는 next라는 메서드를 가진다. next 메서드를 호출하면 Generator 함수 내부에서 가장 먼저 등장하는 yield에서 함수의 실행을 중단한다. 이후 다시 next 메서드를 호출하면 앞서 멈췄던 부분부터 다시 시작한다. 만약 또 yield를 만난다면 함수 실행을 멈추고 next 메서드가 호출되면 또다시 시작하는 과정을 거친다. 

     

    Promise + async / await를 이용한 비동기 처리

    let addCoffee = function (name) {
      return new Promise(function (resolve) {
        setTimeout(function() {
          resolve(name);
        }, 500);
      });
    };
    let coffeeMaker = async function () {
      let coffeeList = '';
      let _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();

    비동기 작업을 수행하고자 하는 함수 앞에 async를 표기하고, 함수 내부에서 실질적인 비동기 작업이 필요한 위치마다 await을 표기함으로써 뒤의 내용을 Promise로 자동 전환한다. 그리고 해당 내용이 resolve된 이후에야 다음으로 진행된다. 

    댓글

Designed by Tistory.