본문 바로가기
📖 책 찢기/코어 자바스크립트

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

by 짱돌보리 2024. 6. 14.
728x90

[4장] 콜백 함수

1. 콜백 함수란?

  • 콜백함수: 다른 코드의 인자로 넘겨주는 함수
  • 콜백함수는 제어권과 관련이 깊음
  • 어떤 함수 X를 호출하면서 특정 조건일 때 함수 Y를 실행해서 나에게 알려달라는 요청을 보내는 것임.

2. 제어권

📍호출시점

❓제어권
실행흐름 or 프로세스

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초)
// 0 (0.9초)
// 1 (1.2초)
// 0 (1.5초)
code 호출 주체 제어권
cbFunc() 사용자 사용자
setInterval(cbFunc, 300) setInterval setInterval

📍인자

map 메서드는 첫 번째 인자로 callback함수를 받고, 생략 가능한 두 번째 인자로 콜백함수 내부에서 this로 인식할 대상을 특정할 수 있다.

const numbers = [1, 2, 3];

numbers.map(function(currentValue, index) {
    console.log(`Index ${index}: ${currentValue}`);
});

// Index 0: 1
// Index 1: 2
// Index 2: 3
// 예시 배열
const numbers = [1, 2, 3, 4, 5];

// map 메서드를 사용하여 각 요소를 제곱한 새로운 배열을 생성하는 예시
const squaredNumbers = numbers.map(function(currentValue, index, array) {
    console.log(`Current Value: ${currentValue}, Index: ${index}, Array: ${array}`);
    return currentValue * currentValue;
});

console.log(squaredNumbers); // [1, 4, 9, 16, 25]

📍this

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

console.log(this === window); // true (전역 컨텍스트에서 this는 window 객체를 가리킴)

function sayHello() {
    console.log(`Hello, ${this.name}!`);
}

const person = {
    name: 'bori',
    greet: sayHello
};

sayHello(); // Hello, undefined! (this는 전역 객체(window)를 가리킴)
person.greet(); // Hello, bori! (this는 person 객체를 가리킴)

---

<button id="myButton">Click me</button>

document.getElementById('myButton').addEventListener('click', function() {
    console.log(this); // 이벤트가 발생한 요소(<button id="myButton">)
    this.textContent = 'Clicked!'; // this를 사용하여 요소의 텍스트를 변경
});

3. 콜백함수는 함수

  • 콜백 함수는 함수다.
  • 콜백 함수로 어떤 객체의 매서드를 전달하더라도 그 메서드는 메서드가 아닌 함수로서 호출된다.
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
  • this는 호출된 메서드가 속한 객체인 obj를 가리킴 → { vals: [1, 2, 3], logValues: f } 1 2
  • forEach 메서드는 콜백 함수 내에서 this를 설정하지 않는다. 단순히 함수 자체로 전달되기 때문에 this는 전역 객체가 된다.

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

🤔위의 해결 방법?!!

  1. 콜백 함수 내부의 this에 다른 값을 바인딩 하는 방법 - 전통적인 방식
    var obj1 = {
      name: 'obj1',
      func: function () {
        var self = this
        return function () {
          console.log(self.name)
        }
      },
    }
    var callback = obj1.func()
    setTimeout(callback, 1000)
     

2. 콜백 함수 내부에서 this를 사용하지 않은 경우

var obj1 = {
  name: 'obj1',
  func: function () {
    console.log(obj1.name)
  },
}
setTimeout(obj1.func, 1000)

 

3. 콜백 함수 내부의 this에 다른 값을 바인딩 하는 방법 - bind 메서드 활용 👍🏻👍🏻

→ forEach의 각 콜백 함수에서 this가 obj 객체를 가리키게 된다!

[4, 5, 6].forEach(obj.logValues.bind(obj))

5. 콜백 지옥과 비동기 제어

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

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

❓비동기
현재 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어감
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,
  "에스프레소"
);

🤔위의 해결 방법?!!

  1. 기명 함수로 변환
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
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);
    });
  });

new 연산자와 함께 호출한 Promise의 인자로 넘겨주는 콜백 함수는 호출할 때 바로 실행되지만 그 내부에 resolve 또는 reject 함수를 호출하는 구문이 있을 경우 둘 중 하나가 실행되기 전까지는 다음(then) 또는 오류 구문(catch)으로 넘어가지 않는다. 

  1. Generator
var addCoffee = function (prevName, name) {
  setTimeout(function () {
    coffeeMaker.next(prevName ? prevName + ", " + name : name);
  }, 500);
};
var coffeeGenerator = function* () {
  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 함수
- `yield` 키워드를 사용하여 비동기 작업을 기다리고, `setTimeout`을 통해 커피 이름을 순차적으로 추가한다.

  1. Promise + async/await
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();

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