뮁이의 개발새발

[코어자바스크립트] Chapter 4. 콜백함수 본문

Front-end/Javascript

[코어자바스크립트] Chapter 4. 콜백함수

뮁뮁이 2024. 5. 18. 11:04

Chapter 4. 콜백함수

콜백함수란?

  • 다른 코드의 인자로 넘겨주는 함수
    • ex) 알람을 설정하는 함수(시계의 제어권 X) / 수시로 시간을 확인하는 함수 (시계의 제어권 O)
  • 즉 다른 코드(함수 또는 메서드)에게 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수

제어권

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

// -- 실행 결과 --
// 0  (0.3초)
// 1  (0.6초)
// 2  (0.9초)
// 3  (1.2초)
// 4  (1.5초)
  • 여기서 setInterval의 구조는

    var intervalID = scope.setInterval(func, delay[, param1, param2,…]);

    • scope에는 Window 객체 또는 Worker의 인스턴스가 들어올 수 있음
    • 일반적인 브라우저에서는 window를 생략해서 함수처럼 활용이 가능 (위 예제코드처럼)
    • func에 넘겨준 함수는 매 delay(ms) 마다 실행됨.
    • 리턴값 X
  • 위 코드에서의 제어권

    • cbFunc(); ⇒ 호출주체 : 사용자 / 제어권 : 사용자
    • setInterval(cbFunc, 300); ⇒ 호출주체 : setInterval / 제어권 : setInterval

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

인자

var 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.prototype.map(callback[, thisArg])
    • callback : function(current, index, array)
    • thisArg는 생략이 가능. 생략할 경우 전역객체가 바인딩됨
    • 메서드 대상이 되는 배열의 모든 요소들을 처음부터 끝까지 하나씩 꺼내어 콜백함수를 반복 호출하고 실행 결과들을 모아 새로운 배열을 만듬
  • 위 코드에서는 10,20,30의 결과를 함수 내 인자로 넣음으로써 15,25,35라는 새로운 배열을 newArr에 형성해서 넘김
  • 함수를 호출할때 넘기는 인자의 위치는 함수가 원하는대로 넣어줘야함.
    • 콜백함수의 제어권을 넘겨받은 코드는 콜백함수를 호출할 때 인자에 어떤 값들을 어떤 순서로 넘길것인지에 대한 제어권을 가짐.

this

  • 콜백 함수도 함수이기 때문에 기본적으로는 this가 전역객체를 참조하지만, 제어권을 넘겨받을 코드에서 콜백함수에 별도로 this 가 될 대상을 지정한 경우에는 그 대상을 참조하게 됨.
Array.prototype.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;
};
  • this로 지정하고픈 인자가 있는 경우엔 해당 값을 지정, 없으면 window(전역객체)로 지정
  • 제어권을 넘겨받을 코드에서 call/apply 메서드의 첫번째 인자에 콜백 함수 내부에서의 this가 될 대상을 명시적으로 바인딩함
setTimeout(function() {
  console.log(this);
}, 300); // (1) Window { ... }

[1, 2, 3, 4, 5].forEach(function(x) {
  console.log(this); // (2) Window { ... }
});

document.body.innerHTML += '<button id="a">클릭</button>';
document.body.querySelector('#a').addEventListener(
  'click',
  function(e) {
    console.log(this, e); // (3) <button id="a">클릭</button>
  } // MouseEvent { isTrusted: true, ... }
);
  • (1)은 첫번째 인자에 전역객체를 넘기기 때문에 this가 전역객체
  • (2)도 this를 넘겨주지 않음
  • (3)은 this로 addEventListener의 this를 넘기도록 되어있어서, HTML 엘리먼트를 가르키게 됨

콜백 함수는 함수다

  • 콜백 함수로 어떤 객체의 매서드를 전달하더라도 그 메서드는 메서드가 아닌 함수로서 호출됨
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
  • logValues는 메서드로 정의됨.
  • this는 obj
  • 뒤에는 해당 함수를 함수로써 활용
  • 어떤 함수의 인자에 객체의 메서드를 전달하더라도, 이는 결국 메서드가 아닌 함수

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

  • 객체의 메서드를 콜백 함수로 전달하면 해당 객체를 this로 바라볼 수 없게 됨
  • 콜백 함수 내부에서 this가 객체를 바라보게 하려면?
    • 별도의 인자로 this를 받는 함수의 경우에는 원하는 값을 넘겨주면 됨
    • 그렇지 않은 경우에는 this의 제어권도 넘겨주게 되어버림
      • this를 다른 변수에 담아 콜백 함수로 활용할 함수에서는 this 대신 그 변수를 사용하게 하고, 이를 클로저로 만드는 방식이 자주 사용
  1. 전통적 방식
var obj1 = {
  name: 'obj1',
  func: function() {
    var self = this;
    return function() {
      console.log(self.name);
    };
  },
};
var callback = obj1.func();
setTimeout(callback, 1000);
  • self 변수에 this를 담아 사용
    • 번거로움
  1. 콜백 함수 내부에서 this를 사용하지 않은 경우
var obj1 = {
  name: 'obj1',
  func: function() {
    console.log(obj1.name);
  },
};
setTimeout(obj1.func, 1000);
  • 간결하지만 this를 다양한 상황에 재활용 할 수 없게 됨.
  • 불편하며 메모리를 낭비
  1. func 함수 재활용
var obj1 = {
  name: 'obj1',
  func: function() {
    console.log(obj1.name);
  },
};
var obj2 = {
  name: 'obj2',
  func: obj1.func,
};
var callback2 = obj2.func();
setTimeout(callback2, 1500);

var obj3 = { name: 'obj3' };
var callback3 = obj1.func.call(obj3);
setTimeout(callback3, 2000);
  • this를 우회적으로나마 다양한 상황에서 원하는 객체를 바라보는 콜백함수를 만들 수 있는 방법
  1. ES5에서 등장한 bind 메서드를 이용하는 방식
var obj1 = {
  name: 'obj1',
  func: function() {
    console.log(this.name);
  },
};
setTimeout(obj1.func.bind(obj1), 1000);

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

콜백 지옥과 비동기 제어

  • 콜백 지옥

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

      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,
      '에스프레소'
      );
    • 위를 해결하기 위한 가장 간단한 방법은 익명의 콜백함수를 모두 기명함수로 전환하는 것

      var coffeeList = '';
      
      var addEspresso = function(name) {
      coffeeList = name;
      console.log(coffeeList);
      setTimeout(addAmericano, 500, '아메리카노');
      };
      var addAmericano = function(name) {
      coffeeList += ', ' + name;
      console.log(coffeeList);
      setTimeout(addMocha, 500, '카페모카');
      };
      var addMocha = function(name) {
      coffeeList += ', ' + name;
      console.log(coffeeList);
      setTimeout(addLatte, 500, '카페라떼');
      };
      var addLatte = function(name) {
      coffeeList += ', ' + name;
      console.log(coffeeList);
      };
      
      setTimeout(addEspresso, 500, '에스프레소');
    • 가독성을 높였으나, 일회성 함수를 전부 변수에 할당해야함.

  • 동기적인 코드

    • 현재 실행중인 코드가 완료된 후에 다음 코드를 실행하는 방식
    • 대부분 즉시 처리가 가능한 코드
  • 비동기적인 코드

    • 현재 실행중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어가는 방식
    • 별도의 요청, 실행 대기, 보류 등과 관련된 코드

⇒ 자바스크립트에서는 비동기적인 코드를 동기적으로 보이게끔 처리해주고자 하였음

비동기적 작업의 동기적 표현

  1. Promise (ES6)

     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);
         });
       });
    • 콜백함수는 호출할때 바로 실행되지만 내부에 resolve 또는 reject 함수가 호출될 경우 둘 중 하나가 실행되기 전까지는 다음(then) 또는 catch로 넘어가지 않음
    • 비동기 작업이 완료될 때 비로소 resolve 또는 reject를 호출하는 방법으로 비동기 작업의 동기적 표현이 가능함
  2. Promise(2) - 클로저 활용

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

     var addCoffee = function(prevName, name) {
       setTimeout(function() {
         coffeeMaker.next(prevName ? prevName + ', ' + name : name);
       }, 500);
     };
     var coffeeGenerator = function*() { // Generator
       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();
    • *이 붙은 함수가 Generator
    • Itegrator를 반환함.
      • next라는 메서드를 가지고 있음
      • 이는 가장 먼저 등장하는 yield의 실행을 멈춤
    • 비동기 작업이 완료되는 시점마다 next 메서드를 호출해준다면 Generator 함수 내부의 소스가 위에서서부터 순차적으로 진행됨
  4. Promise + Async/Await (ES2017)

     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);
     };
     coffeeMaker();
    • 비동기를 수행하고자 하는 함수 앞에 async를 표기
    • 실질적 비동기 작업이 필요한 위치마다 await을 표기
    • 뒤의 내용을 Promise로 자동 전환하고, 해당 내용이 resolve 된 이후에 다음으로 진행함

정리

  • 콜백 함수는 다른 코드에 인자로 넘겨줌으로써 그 제어권도 함께 위임한 함수
  • 콜백 함수를 호출하는 시점을 스스로 판단해서 실행
  • 인자로 넘겨줄 순서는 따라야함
  • this를 바꾸고 싶을 경우 bind 메서드를 활용하면 됨
  • 어떤 함수에 인자로 메서드를 전달하더라도 이는 결국 함수로서 실행됨
  • 최근 ECMAScript에서는 Promise, Generator, async/await 등 콜백 지옥에서 벗어날 수 있는 다양한 방법이 나오고 있음
Comments