본문 바로가기

JavaScript/[책] 코어 자바스크립트 -ing

04. 콜백 함수 in JavaScript

 

01. 콜백함수(Callback Function)란?

- callback: 회신하다, 답신하다

- callback function: 회신되는 함수

- 함수 또는 메서드에(1)에 인자(argument)로 들어가는 함수(2)를 콜백함수(2)라 한다. ➡  콜백함수(2)에 대한 제어권을 함수(1)에다가 넘긴다는 뜻이다.

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

 

 

 

02. 제어권(콜백 함수를 넘겨받은 코드가 제어권을 가진다!)

1) 호출 시점 2) 인자 3) this

 

1) 호출 시점: 콜백 함수를 넘겨 받은 코드가 콜백 함수를 언제 호출할 지 결정한다. ex) setInteval 주기함수

- 위 예제에서는 콜백함수를 넘겨 받은 함수, 즉 setInterval 함수가 빨간색 영역인 콜백함수의 호출 시점을 결정한다.

 

- 위 예제에서는 콜백 함수(cbFunc)의 제어권을 넘겨 받은 setInterval는 콜백 함수(cbFunc) 호출 시점에 대한 제어권을 가진다.

그러니깐 cbFunc 함수를 언제 호출할 지에 대한 결정권? 제어권이 setInterval에 있다는 말이다.

 

2) 인자: 콜백 함수를 호출할 때 콜백 함수에 들어갈 인자에 어떤 값들을 어떤 순서로 넘길 것인지도 콜백 함수를 넘겨받은 코드에게 결정권이 있다.

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 메서드는 메서드의 대상이 되는 배열, 즉 [10, 20, 30]의 모든 요소들을 처음부터 끝까지 하나씩 꺼내어 콜백 함수(빨간색 표시)를 반복 호출하고, 콜백 함수의 실행 결과들을 모아 새로운 배열을 만든다.

- 콜백함수를 넘겨 받은 map 메서드에게 콜백 함수의 인자로 넘어올 값들 및 그 순서에 대한 결정권, 제어권이 있다.

@mdn

 

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

// 콜백 함수 내부에서의 this

// 1. setTimeout
setTimeout(function () { console.log(this), 300}); // (1) Window { ... }

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

// 3. addEventListener
document.body.innerHTML += '<button id="a">클릭</button>';
document.body.querySelector('#a')
  .addEventListener('click', function (e) {
    console.log(this); // (3) <button id="a">클릭</button>
  })

- setTimeout은 내부에서 콜백함수를 호출 할 때 call 메서드의 첫 번째 인자에 전역 객체를 넘기기 때문에 콜백 함수 내부에서의 this가 전역 객체를 가리킨다.

- forEach별도의 인자로 this를 넘겨주지 않았기 때문에 전역 객체를 가리킨다.

- addEventListener 는 내부에서 콜백 함수를 호출할 때 call 메서드의 첫 번째 인자에 addEventListenr 메서드의 this를 그대로 넘기도록 정의돼 있기 때문에 콜백 함수 내부에서의 this가 addEventListenr를 호출한 주체인 HTML 엘리먼트를 가리키게 된다.

 

 

 

03. 콜백 함수는 함수다.

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

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

obj.logValues(1, 2); // obj { ... } 1 2
[4, 5, 6].forEach(obj.logValues); // Window { ... } * 3

 

 

 

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

위 내용에서 콜백 함수로 어떤 객체의 메서드를 전달하더라도 그 메서드는 메서드가 아닌 함수로 호출되기 때문에 this는 전역 객체를 가리키는 것을 확인했다. 

그렇다면 콜백 함수 내부에서 this가 전역 객체가 아니라 해당 객체를 바라보게 하고 싶다면 어떻게 해야할까?

 

1) 전통적인 방식

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

var callback = obj1.func();
// var callback = function () { var self = this; return function () { console.log(self.name); } } 
// this는 obj1, 왜냐고? obj1.func() 형태로 보아 객체 메서드로 함수를 호출했으니깐 this는 해당객체를 가리킨다.
// var callback = function () { console.log('obj1'); }

setTimeout(callback, 300);
// setTimeout(function () { console.log('obj1'); }, 300)

- 그런데 위 예제는 실제로 코드 내에서 this를 사용하지 않음

 

2) call method로 thisArg 를 원하는 객체로 연결

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


var obj2 = {
  name: 'obj2',
  func: obj1.func
};

// var obj2 = {
//   name: 'obj2',
//   func: function () {
//     var self = this;
// 	return function () {
//       console.log(self.name);
//     }
//   }
// };

// var callback2 = obj2.func();
var callback2 = function () { console.log('obj2') };
setTimeout(callback2, 1500); // obj2

var obj3 = { name: 'obj3' };
var callback3 = obj1.func.call(obj3);
setTimeout(callback3, 2000); // obj3

 

3) bind 메서드를 이용하여 this를 원하는 객체와 연결

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), 2000);

 

 

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

콜백 지옥은 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상이다. 주로 이벤트 처리서버 통신과 같이 비동기적인 작업을 수행하기 위해 이런 형태가 자주 등장하곤 한다. 

 

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

 

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

비동기적인 코드는 

- 사용자의 요청에 의해 특정 시간이 경과되기 전까지 어떤 함수의 실행을 보류한다거나(setTimeout)

- 사용자의 직접적인 개입이 있을 때 비로소 어떤 함수를 실행하도록 대기한다거나(addEventListener)

- 웹브라우저 자체가 아닌 별도의 대상에 무언가를 요청하고 그에 대한 응답이 왔을 때 비로소 어떤 함수를 실행하도록 대기하는(XMLHttpRequest)

이와 같이 별도의 요청, 실행 대기, 보류 등과 관련된 코드는 비동기적인 코드다.

 

자바스크립트 진영은 비동기적인 일련의 작업을(이벤트 처리, 서버 통신과 실행중인 코드 완료 여부 상관없이 다음 코드로 넘어가는)

→ 동기적(순차적으로 실행되는)으로, 혹은 동기적인 것처럼 보이게끔 처리해주는 장치를 마련했다.

즉, 이벤트 처리나 서버 통신과 관련된 코드는 자동적으로 비동기적으로 진행되는데 즉 실행을 다 마치지 않고 다음 코드로 넘어가는데, 
실행을 다 마치면 그 다음에 필요한 코드를 실행할 수 있도록 설정하는 것이 콜백함수를 등록하는 것이다.

 

콜백지옥을 해결하는 방법

1) 기명함수로 변환

익명의 콜백 함수를 모두 기명 함수로 전환

 

2) 비동기 작업의 동기적 표현, Promise(1)

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) 또는 오류 구문으로 넘어가지 않는다.

 

3) 비동기 작업의 동기적 표현 - 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, 'latte');
  console.log(latte);
};

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

- '*'이  붙은 함수가 Generator 함수이다.

제너레이터 함수를 실행하면 Iterator가 반환되는데, Iterator는 next 라는 메서드를 가지고 있다.

이 next 메서드를 호출하면 Generator 함수 내부에서 가장 먼저 등장하는 yield에서의 함수의 실행을 멈춘다.

비동기 작업이 완료되는 시점마다 next 메서드를 호출해준다면 Generator 함수 내부의 소수가 위에서부터 아래로 순차적으로 진행되고 있다고 함..

 

 

회고

이번 스터디를 통해서 나는 지금까지 나름 콜백함수를 쓰고는 있었지만, 콜백 함수의 의미 용도 등을 제대로 알고 쓰지 않았다는 걸 깨달았다. 그러나 스터디(코어 자바스크립트 책, 스터디원들과의 대화)만으로는 너무 부족했다. 기초 지식이 너무 없어서 어려워서 이해될 때까지 계속 봐야 했었는데 이어서 공부하지 못했다. 모르는 개념이 있으면 하루하루 이어서 이해할 때까지 공부해야한다. 안 그러면 시간을 너무 많이 쓰게 되니 주의하자. 

 

콜백함수 스터디 2주 후에, 다시 찬찬히 공부했다. 이번 주 내내 콜백 관련 책이랑 영상 글을 봤는데 다시 보니깐 스콜백함수의 뜻, 용도 등등이 정확하게 이해 안됐었는데.. 이제 좀 이해된다. ㅠㅠㅠㅠ 아래는 계속 이해되지 않았던 부분을 이해하고 그 내용을 간략하게 정리해보았다.

 

"동기"의 의미는 : 순차적으로 진행된다. 이 말은 다 완료되고 난후에 다음 코드가 실행된다는 말이다.

"비동기": 순차적으로 진행되지 않는다. 실행시켜 놓고 다 완료되지 않아도 다음 코드를 실행한다. 실행시켜 놓은 코드가 완료되면 다음을 실행하기 위해서 이 때 콜백함수를 쓴다.

콜백함수는 비동기적인 코드를 동기적으로 만들어주거나 또는 그렇게 보이게 한다.

 

 

추가로 공부하면 좋을 것들

- call 과 bind 차이.... 잘 모르겟....

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

setTimeout(obj1.func.bind(obj1), 3000); // 1초 뒤에 obj1 출력
setTimeout(obj1.func.call(obj1), 3000); // 바로 obj1 출력
// 왜 둘이 차이가 나는지 모르겠다 ㅠㅠ

- 참고자료: https://kongda.tistory.com/20

 

[javascript] 콜백(callback) 함수 사용이유 ,,?

callback() 함수란 무엇이고 왜 사용하는가,,? 간단히 말하자면, 자바스크립트의 비동기 처리 방식의 문제점을 해결해주기 위해 특정 시점에서 호출이 되도록 사용하는 함수이다. javascript의 비동기

kongda.tistory.com