본 내용은 책 코어 자바스크립트(2019, 정재남)를 세 번 정독 후, 글쓴이가 이해한 내용과 실제 업무에서 겪은 경험을 기술하였습니다. 다소 형식적이지 못합니다.
1. 콜백함수란?
콜백함수는 어떤 함수의 인자로 활용되어, 그 제어권또한 함께 위임받은 함수다.
우리가 흔히 addEventListener, forEach와 같은 반복 메소드 등의 첫번째 인자로 넘기는 함수가 곧 콜백함수로, 원하는 시점(버튼 클릭 등)에 특정 함수를 호출할 수 있도록 할 수 있다.
2. ★ 콜백함수는 함수다.
너무 당연한 말이지만 이는 매우x100 중요한 의미이다.
다음의 메소드를 콜백함수로 넘기면 (1)과 (2)의 this는 각각 무엇일까?
var obj = {
name: "juj",
func: function (val, idx) {
console.log(this, val, idx);
}
}
obj.func(10, 1); // (1)
[10, 20, 30].forEach(obj.func); // (2)
3장에서 배웠듯이 함수의 this는 함수를 호출한 주체가 된다.
따라서 (1)은 객체의 메소드로서 호출되었으므로 obj, (2)는 콜백함수로서 obj가 가리키는 func이라는 함수만 전달하였으므로 콜백함수의 this 정의가 없는 forEach이므로 전역 객체가 된다.
어떤 함수의 인자에 객체의 메서드를 전달하더라도 이는 결국 메서드가 아닌 함수일 뿐입니다.
3. 콜백함수에 this 바인딩 하기
이미 3장에서 명시적으로 this를 바인딩하는 방법을 보았다.
1. call, apply, bind
2. 화살표 함수
3. thisArg
만약 이 this 바인딩 방법들을 몰랐다면 어떻게 구현하였을까?
var obj = {
name: "juj",
func: function () {
var self = this;
return function (v, i) {
console.log(self, v, i)
}
}
}
[10, 20, 30].forEach(obj.func())
// {name: "juj", func: ƒ} 10 0
// {name: "juj", func: ƒ} 20 1
// {name: "juj", func: ƒ} 30 2
self라는 식별자에 this를 미리 저장해두고 해당 변수를 내부 함수로 전달하여 함수를 리턴했다. (== 클로저)
매번 이렇게 self를 저장하여 활용하는 방법은 번거롭기 그지 없으므로 이제 명시적인 this 바인딩을 활용하여 이를 개선해보겠다.
var obj = {
name: "juj",
func: function (val, idx) {
console.log(this, val, idx);
}
}
[10,20,30].forEach(obj.func.bind(obj))
// {name: "juj", func: ƒ} 10 0
// {name: "juj", func: ƒ} 20 1
// {name: "juj", func: ƒ} 30 2
bind로 this를 바인딩하니까 훨씬 깔끔하다!
4. 콜백 지옥, 비동기 제어
콜백 지옥이란, 익명 함수를 인자로 넘기는 과정이 반복되면서 코드의 깊이가 읽을 수 없을 정도로 깊어지는 현상을 말한다.
대게 여러 번의 서버 통신과 함께 복잡한 로직으로 처리되는 비동기 작업에서 많이 발생한다.
학생 때 많이 경험했었다.
비동기란, CPU에 의해 즉시 처리되는 동기와는 반대로 실행을 보류, 대기, 브라우저 외부의 대상에 별도로 데이터를 요청하는 상황 등에서 발생하며, 현재 실행 중인 코드와 상관없이 바로 다음 코드로 넘어간다.
[관련 포스팅]
https://coffeeandcakeandnewjeong.tistory.com/17?category=906460
콜백 지옥은 이 복잡한 비동기 작업을 처리할 때 많이 발생하는데 다음의 예시를 보자.
setTimeout(function(coffee) {
var coffeeList = coffee;
console.log(coffee);
setTimeout(function (coffee) {
coffeeList+=coffee;
console.log(coffee);
setTimeout(function (coffee) {
coffeeList += coffee;
console.log(coffee);
setTimeout(function (coffee) {
coffeeList += coffee;
console.log(coffee);
}, 1000, "에스프레소");
}, 1000, "카페모카");
}, 1000, "카페라떼");
}, 1000, "아메리카노");
// (1초 후) 아메리카노
// (2초 후) 카페라떼
// (3초 후) 카페모카
// (4초 후) 에스프레소
만약에 커피를 100개 등록하고자 한다면 들여쓰기 수준이 감당이 되지 않을 정도로 깊어질 것이다.
게다가 읽는 순서가 아래에서 위로 읽어야 하기 때문에 가독성이 현저히 떨어진다.
책에서는 다음의 4가지 방법(혹은 단계)로 콜백 지옥의 해결책을 제시한다.
4-1. 익명함수를 기명함수로
var coffeeList = "";
var addAmericano = function (coffee){
coffeeList += coffee;
console.log(coffee);
setTimeout(addCafeLatte, 1000, "카페라떼")
}
var addCafeLatte = function (coffee){
coffeeList += coffee;
console.log(coffee);
setTimeout(addCafeMocha, 1000, "카페모카")
}
var addCafeMocha = function (coffee){
coffeeList += coffee;
console.log(coffee);
setTimeout(addEspresso, 1000, "에스프레소")
}
var addEspresso = function (coffee){
coffeeList += coffee;
console.log(coffee);
}
setTimeout(addAmericano, 1000, "아메리카노");
이제 어느정도 가독성이 좋아졌지만 여전히 커피가 100개 있을때, 100개의 함수를 만드는 것은 부담스러운 일이다.
4-2. Promise
커피 리스트에 커피를 추가하고, 추가한 커피를 출력하는 일이 반복되므로 이를 공통 함수로 묶은 다음에 Promise 객체를 활용한 방법이다.
var addCoffee = function (coffee) {
return function(prev) {
return new Promise(function (resolve) {
setTimeout(function () {
var result = prev ? prev + coffee : coffee;
console.log(coffee);
resolve(result);
}, 1000)
});
}
}
addCoffee("아메리카노")()
.then(addCoffee("카페라떼"))
.then(addCoffee("카페모카"))
.then(addCoffee("에스프레소"))
.catch(function (e) {console.log(e.message)})
4-3. Generator
ES6에서 등장한 제너레이터를 활용한 방법이다.
제너레이터는 함수 앞에 *를 붙이면 제너레이터 함수로써 동작하는데, Iterator를 반환하여, next를 통해 yield 시점까지 함수를 실행하고 일시중지했다가 또다시 next를 호출하면 다음 시점부터 함수를 실행하여 다음 yield 혹은 함수의 마지막까지 실행한다.
제너레이터와 관련하여 Iterable, Iterator에 대해 공부를 한 후, 포스팅해보고 싶다.
제너레이터를 활용하여 해당 비동기 작업을 구현하면 다음과 같다.
var addCoffee = function (prev, coffee) {
setTimeout(function() {
makeCoffee.next(prev ? prev+coffee : coffee)
}, 1000);
}
function* coffeeGenerator () {
var americano = yield addCoffee("", "아메리카노");
console.log(americano);
var cafelatte = yield addCoffee(americano, "카페라떼");
console.log(cafelatte);
var cafemocha = yield addCoffee(cafelatte, "카페모카");
console.log(cafemocha);
var espresso = yield addCoffee(cafemocha, "에스프레소");
console.log(espresso);
}
var makeCoffee = coffeeGenerator();
makeCoffee.next();
4-4. async/await
ES2017에서 등장한 async/await 은 더욱 가독성이 뛰어난 비동기 작업을 수행할 수 있게끔 해준다.
비동기 작업을 수행하는 함수 앞에 async를 붙이고, 실제 비동기 작업이 이루어지는 코드에 await을 붙이는 것만으로도 비동기 작업을 완료한 후에(즉, Promise에서 resolve() 후에) 다음 작업을 진행한다.
주의할 점은 await은 Promise를 받기 때문에 setTimeout 함수를 Promise로 감싸주어야 의도한 대로 비동기 작업을 수행할 수 있다.
[참고]
https://victorydntmd.tistory.com/87
var addCoffee = function (coffee) {
return new Promise(function(resolve) {
setTimeout(function() {
console.log(coffee);
resolve(coffee);
}, 1000);
});
}
var _addCoffee = async function (list, coffee) {
return list + await addCoffee(coffee);
}
var makeCoffee = async function () {
var addAmericano = await _addCoffee("", "아메리카노");
var addCafeLatte = await _addCoffee(addAmericano, "카페라떼");
var addCafeMocha = await _addCoffee(addCafeLatte, "카페모카");
var addEspresso = await _addCoffee(addCafeMocha, "에스프레소");
return addEspresso
}
makeCoffee().then(result => console.log(result));
5. Sync/Async와 Blocking/Non-Blocking
이전 글에서 React로 Toast 알람 구현하기(1)에서 콜백함수는 함수라는 의미를 setInterval 콜백함수에 bind를 적용해야 하는 이유에서 아! 하고 이해가 되었다.
[참고]
https://coffeeandcakeandnewjeong.tistory.com/21?category=906460
여기서 show 함수는 어쨌든 함수이기 때문에 this가 전역 객체로 잡힐 것이기 때문에 this를 인스턴스로 바인딩해주어야 한다.
더불어 늘 햇갈리는 동기/비동기, 블로킹/논블로킹을 다시 꼼꼼히 정리했다.
여러 글에서 동기/비동기와 블로킹/논블로킹은 늘 햇갈리지만 이렇게 이해하는게 가장 쉬웠다.
※ 동기/비동기: 해당 작업을 누가 신경쓰는가
- 동기: 호출 주체와 호출된 대상이 모두 신경씀
- 비동기: 호출된 대상만 신경씀
[실생활 예시]
주체: 엄마
대상: 나
엄마: 방청소좀 해라
나: 네, 지금할게요
엄마: (내가 방청소를 끝낼 때까지 문앞에서 지켜봄) - 동기
엄마: (바로 다른 방 청소하러 가심) - 비동기
※ 블로킹/논블로킹: 작업의 통제권은 누가 가지고 있는가
- 블로킹: 작업이 끝날 때까지 호출된 대상이 가지고 있음
- 논블로킹: 호출 후 호출 주체에게 통제권을 바로 넘겨줌
[실생활 예시]
주체: 엄마
대상: 나
엄마: 방청소좀 해라
나: 네, 지금 할게요!
엄마: (바로 다른 방 청소하러 갈려는데....) - 비동기로 가정
나: 엄마 가지말고 잘하고 있는지 봐줘요 - 블로킹
나: (붙잡지 않음) - 논블로킹
따라서 동기/비동기, 블로킹/논블로킹은 호출 주체 관점이냐 호출 대상 관점이냐에 따라 달라진다.
다음 두 속성에 따라 네 가지 상황이 펼쳐질 수 있다.
- 동기, 블로킹(Sync, Blocking)
- 동기, 논블로킹(Sync, Non-Blocking)
- 비동기, 블로킹(Async, Blocking)
- 비동기, 논블로킹(Async, Non-Blocking)
5-1. 동기, 블로킹 예시
주체: 엄마
대상: 나, 동생
엄마: '나'에게 방청소를 지시
엄마: 내가 방청소 끝낼때까지 지켜봄 - 동기
나: 내가 방청소를 끝낼때까지 엄마를 놔주지 않음 - 블로킹
--- '나'가 방청소를 끝내고 엄마한테 보고 ---
엄마: '동생'에게 방청소를 지시
엄마: '동생'이 방청소 끝낼때까지 지켜봄 - 동기
동생: '동생'이 방청소를 끝낼때까지 엄마를 놔주지 않음 - 블로킹
--- '동생'이 방청소를 끝내고 엄마한테 보고 ---
엄마: 당신이 하실 일을 하러 감
5-2. 동기, 논블로킹 예시
엄마: '나'에게 방청소를 지시
엄마: 내가 방청소 끝낼때까지 지켜봄 - 동기
나: 엄마가 보든말든 신경안쓰고 방청소함 - 논블로킹
--- '나'가 방청소를 끝내고 엄마한테 보고 ---
엄마: '동생'에게 방청소를 지시
엄마: '동생'이 방청소 끝낼때까지 지켜봄 - 동기
동생: 엄마가 보든말든 신경안쓰고 방청소함 - 논블로킹
--- '동생'이 방청소를 끝내고 엄마한테 보고 ---
엄마: 당신이 하실 일을 하러 감
5-3. 비동기, 블로킹 예시
엄마: '나'에게 방청소를 지시
엄마: '동생'에게 방청소를 지시하려 가려고 함 - 비동기
나: 내가 방청소를 끝낼때까지 엄마를 놔주지 않음 - 블로킹
--- '나'가 방청소를 끝내고 엄마한테 보고 ---
엄마: '동생'에게 방청소를 지시
엄마: 할 일을 하러 가려고 함 - 비동기
동생: '동생'이 방청소를 끝낼때까지 엄마를 놔주지 않음 - 블로킹
--- '동생'이 방청소를 끝내고 엄마한테 보고 ---
엄마: 당신이 하실 일을 하러 감
5-4. 비동기, 논블로킹 예시
엄마: '나'에게 방청소를 지시
엄마: '동생'에게 방청소를 지시하려 감 - 비동기
나: 엄마가 보든말든 신경안쓰고 방청소함 - 논블로킹
엄마: '동생'에게 방청소를 지시
동생: 엄마가 보든말든 신경안쓰고 방청소함 - 논블로킹
엄마: 당신이 하실 일을 하러 감
--- '나'가 방청소를 끝내고 엄마한테 보고 ---
--- '동생'이 방청소를 끝내고 엄마한테 보고 ---
[참고]
https://siyoon210.tistory.com/147
(시스템 I/O 관점에서 이해하기)
'Books > 코어 자바스크립트' 카테고리의 다른 글
06. 프로토타입 (0) | 2020.04.16 |
---|---|
05. 클로저 (0) | 2020.04.12 |
03. this와 객체 (0) | 2020.03.05 |
02. 실행 컨텍스트 (0) | 2019.12.31 |
01. 데이터 타입 (0) | 2019.12.31 |