2021. 7. 29. 17:09ㆍ개발공부/자바스크립트
콜백 지옥과 비동기 제어
콜백지옥이란?
콜백 함수를 익명함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상
비동기란?
동기적인 코드는 현재 실행 중인 코드가 완료된 후에야 다음 코드를 실행하는 방식
반면, 비동기적인 코드는 현재 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어가는 방식을 말한다.
ex) setTimeout, addEventListener, XMLHttpRequest
요즘엔 웹의 복잡도가 높아진 만큼 비동기적인 코드의 비중도 높아지면서 콜백 지옥에 빠지기가 쉬워졌다.
// 콜백지옥의 예시
// 0.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);
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, '에스프레소');
가독성은 높아졌지만 이 역시 일회성 함수를 전부 변수에 할당해야하는 번거러운 작업일 수 있기때문에 ES6에서 도입된Prormise, Generator, ES2017에서 도입된 async/await를 사용해보자.
비동기 작업의 동기적 표현(1) - 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연산자와 함께 호출한 prormise의 인자로 넘겨주는 콜백 함수는 호출할 때 바로 실행되지만 극 내부에 resolve또는 reject 함수를 호출하는 구문이 있을 경우 둘 중 하나가 실행되기 전까지는 then또는 catch구문으로 넘어가지 않는다. 따라서 비동기 작업이 완료될 때 비로소 resolve 또는 reject를 호출하는 방법으로 비동기 작업의 동기적 표현이 가능하다!!
하지만 뭔가 효율적인 방법같지만 오히려 콜백지옥에 빠졌던 코드와 길이도 비슷하고 함수가 반복되어 실행되고있는 점이 마음에 안든다.
이 반복적인 내용을 함수화해서 간결하게 표현해보자.
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('카페라떼'));
훤씬 깔끔해졌다!
👀삼형연산자를 이용해 prevName이 있다면 prevName + 현재 add된 name이 구현되었는데, 여기서 prevName을 어디서 담는거지..?
그렇다면 generator을 이용한 비동기 작업의 동기적 표현은 어떨까?
var addCoffee = function (prevName, name) {
setTimeout(function() {
coffeeMaker.next(prevName ? prevName + ',' + name : name);
}, 500);
};
var coffeeGenerator = function* () {
var expresso = yield addCoffee('', '에스프레소');
console.log(expresso);
var americano = yield addCoffee(expresso, '아메리카노');
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함수이다.
1. Generator 함수를 실행하면 Iterator가 반환된다. Iterator는 next라는 메서드를 가지고있다.
2. 이 next메서드를 호출하면 Generator 함수 내부에서 가장 먼저 등장하는 yield에서 함수의 실행을 멈춘다.
3. 이후 다시 next메서드를 호출하면 앞서 멈췄던 부분부터 시작해 그 다음 등장하는 yield에서 함수의 실행을 멈춘다.
즉, 비동기 작업이 완료되는 시점마다 next메서드를 호출해준다면 Generator 함수 내부의 소스가 위에서부터 아래로 순차적으로 진행된다.
하지만 Generator은 여러가지 이유로 사용이 권장되지않는다고한다. 그렇다면 어떤걸 써야할까?
가장 가독성이 뛰어나면서 간단하게 작성할 수 있는 기능인 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된 이후에야 다음으로 진행한다. Promise의 then과 흡사한 효과이다.
>> 비동기와 promise, async/awiat를 조금 더 공부하러 가보자!
출처: 정재남, 『코어 자바스크립트』, 위키북스(2019), 4장 콜백 함수
'개발공부 > 자바스크립트' 카테고리의 다른 글
[코어자바스크립트] 스코프, 스코프 체인 (0) | 2021.07.30 |
---|---|
[JS] 비동기, promise, async/await 정리 (0) | 2021.07.07 |
[코어 자바스크립트] 클로저① - 클로저의 의미 및 원리 이해 (0) | 2021.06.27 |
[코어 자바스크립트] 콜백 함수 (0) | 2021.06.27 |
[코어 자바스크립트] this② 함수의 메소드 call, apply, bind (0) | 2021.06.25 |