콜백 지옥 (Callback Hell)
콜백지옥이란, 콜백 함수를 익명 함수로 전달하는 과정이 반복되면서 코드 Depth가 알아보기 힘들만큼 깊어지는 상황을 이야기한다. 보통 EventHandler나 비동기 처리를 수행할 때 이런 형태가 많이 반복될 때가 있는데, 코드 Depth가 많이 길어지게 되고 가독성이 떨어지면서 수정하기도 두려운 상태가 된다.
아래의 코드를 통해 콜백 지옥을 맛보도록 하자. (a.k.a 앱등이 콜백지옥)
setTimeout(
(product) => {
let appleProducts = product;
console.log(appleProducts);
setTimeout(
(product) => {
appleProducts += ", " + product;
console.log(appleProducts);
setTimeout(
(product) => {
appleProducts += ", " + product;
console.log(appleProducts);
setTimeout(
(product) => {
appleProducts += ", " + product;
console.log(appleProducts);
setTimeout(
(product) => {
appleProducts += ", " + product;
console.log(appleProducts);
},
1000,
"AirPod Pro"
);
},
1000,
"Mac Mini"
);
},
1000,
"Apple Watch"
);
},
1000,
"iPad"
);
},
1000,
"iPhone"
);
// Result in console
// iPhone (1 sec)
// iPhone, iPad (2 sec)
// iPhone, iPad, Apple Watch (3 sec)
// iPhone, iPad, Apple Watch, Mac Mini (4 sec)
// iPhone, iPad, Apple Watch, Mac Mini, AirPod Pro (5 sec)
** 위의 코드에서는 앱등이의 비애를 표현하고 있다.
위의 코드에서는 1초에 한번씩 콜백 함수를 통해 Apple 제품 이름을 넘겨 받아 제품 목록에 제품을 추가한다. 코드 자체는 정상적으로 실행되지만 코드만 봐도 머리가 지끈지끈하며, 가독성이 매우매우 떨어진다.
그렇다면 이런 콜백지옥을 탈출할 방법은 어떤것들이 있을까?
콜백지옥을 탈출하자! (Let's escape Callback Hell!)
Solution 1 - 기명함수 표현식
기명함수 표현식이란 함수를 정의할 때 함수의 이름이 있다면 이를 기명 함수 표현식이라고 한다. (강은 강이고 바다는 바다입니다~ 같은 말인가..?) 어찌됐건 가독성의 문제를 해결할 수 있을 것이고, 이를 통해 코드를 보기 좀 더 쉬워질 수 있을 것이다.
let appleProducts = ''
const addIPhone = (name) => {
appleProducts = name;
console.log(appleProducts)
setTimeout(addIPad, 1000, 'iPad')
}
const addIPad = (name) => {
appleProducts += ', ' + name;
console.log(appleProducts)
setTimeout(addAppleWatch, 1000, 'Apple Watch')
}
const addAppleWatch = (name) => {
appleProducts += ', ' + name;
console.log(appleProducts)
setTimeout(addMacMini, 1000, 'Mac Mini')
}
const addMacMini = (name) => {
appleProducts += ', ' + name;
console.log(appleProducts)
setTimeout(addAirpodPro, 1000, 'AirPod Pro')
}
const addAirpodPro = (name) => {
appleProducts += ', ' + name;
console.log(appleProducts)
}
setTimeout(addIPhone, 1000, 'iPhone')
확실히 콜백지옥 예시 코드보다 가독성도 높아져서, 위에서 부터 아래로 코드를 읽기 쉬워졌다.
하지만 일회성 함수들을 전부 변수에 할당해야하는 이유 때문에 코드를 트래킹할때 오히려 혼란을 유발할 소지가 있다.
Solution 2 - Promise
ES6에서 발표된 Promise를 활용한 방식이 콜백지옥을 탈출하는데 도움을 줄 수 있다.
Promise는 자바스크립트의 내장 객체로, Promise를 활용하여 콜백함수 대신 비동기를 간편하게 처리할 수 있다.
우선, Promise는 클래스이기 때문에 new 연산자와 함께 새 객체를 생성한다. 생성한 객체의 인자로 넘겨주는 콜백 함수는 호출할 때 바로 실행되지만 Promise 내부의 resolve 또는 reject 함수를 호출하는 구문이 있을 경우, 둘 중 하나가 실행되기 전까지는 then 또는 catch로 넘어가지 않는다. 즉, 비동기 작업이 완료될 때 resolve 또는 reject를 호출하는 방법으로 비동기 작업의 동기적 표현이 가능하다.
new Promise((resolve) => {
setTimeout(() => {
let product = "iPhone"
console.log(product)
resolve(product)
}, 1000)
})
.then((prevProduct) => {
return new Promise((resolve) => {
setTimeout(() => {
let product = prevProduct + ", iPad"
console.log(product)
resolve(product)
}, 1000)
})
})
.then((prevProduct) => {
return new Promise((resolve) => {
setTimeout(() => {
let product = prevProduct + ", Apple Watch"
console.log(product);
resolve(product)
}, 1000)
})
})
.then((prevProduct) => {
return new Promise((resolve) => {
setTimeout(() => {
let product = prevProduct + ", Mac Mini"
console.log(product)
resolve(product)
}, 1000)
})
})
.then((prevProduct) => {
return new Promise((resolve) => {
setTimeout(() => {
let product = prevProduct + ", AirPod Pro"
console.log(product)
resolve(product)
}, 1000)
})
})
//==============================================
// Use Chaining
//==============================================
const collectAppleProduct = (product) => {
return (prevProduct) => {
return new Promise((resolve) => {
setTimeout(() => {
const newProduct = prevProduct ? `${prevProduct}, ${product}` : product;
console.log(newProduct)
resolve(newProduct)
}, 1000)
})
}
}
collectAppleProduct("iPhone")()
.then(addCoffee("iPad"))
.then(addCoffee("Apple Watch"))
.then(addCoffee("Mac Mini"))
.then(addCoffee("Airpod Pro"))
Solution 3 - Promise & async/await
ES2017에 추가 된 async/await를 활용하여 콜백지옥 문제를 해결할 수 있다. 비동기 작업을 수행하고자 하는 함수 앞에 async를 추가한 후, 함수 내부에서 실제로 비동기 처리가 필요한 곳에 await를 추가하는 것만으로 함수의 내용을 Promise로 전환하고, 해당 내용이 resolve 또는 reject 이후 다음으로 진행된다. 아래의 코드를 보며 어떤 내용인지 살펴보도록 하자.
const collectAppleProduct = (product) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(product)
}, 1000)
})
}
const appleCollector = async () => {
let appleProducts = ""
let addProduct = async (product) => {
appleProducts += (appleProducts ? ", " : "") + (await collectAppleProduct(product));
}
await addProduct("iPhone")
console.log(appleProducts)
await addProduct("iPad")
console.log(appleProducts)
await addProduct("Apple Watch")
console.log(appleProducts)
await addProduct("Mac Mini")
console.log(appleProducts)
await addProduct("Airpod PRo")
console.log(appleProducts)
}
appleCollector()
Solution 4 - Generator
ES6에서 추가된 Generator 역시 콜백지옥을 탈출하는데 도움이 되는 녀석이다. function* () 과 같은 형태로 구현된 함수를 Generator 함수라고 한다. Generator 함수가 실행되면 Iterator가 반환이 되며, Iterator는 next라는 메서드를 가지고 있다. next 메서드를 호출하면 앞에 멈췄던 부분부터 시작해서 yield 부분에서 실행을 멈춘다.
이를 활용해 비동기 작업이 완료되는 시점에 next 메서드를 호출하면 Generator 함수의 내부 소스를 통해 코드를 진행한다.
아래의 코드를 보며 어떤 내용인지 살펴보도록 하자.
const addProduct = (prevProduct, product) => {
setTimeout(() => {
appleCollector.next(prevProduct ? `${prevProduct}, ${product}` : product)
}, 1000)
}
const appleGenerator = function* () {
const iPhone = yield addProduct("", "iPhone")
console.log(iPhone)
const iPad = yield addProduct("iPhone", "iPad")
console.log(iPad)
const appleWatch = yield addProduct("iPad", "Apple Watch")
console.log(appleWatch)
const macMini = yield addProduct("appleWatch", "Mac Mini")
console.log(macMini)
const airPodPro = yield addProduct("macMini", "AirPod Pro")
console.log(airPodPro)
}
const appleCollector = appleGenerator()
appleCollector.next()
마무리하며..
어떤 Solution이 제일 좋다! 라고 말할 수 없지만, 개개인의 코딩스타일 또는 함께 협업하는 동료들과의 합의점을 잘 마련하여 공통된 코딩스타일로 비동기 처리를 진행하는 것이 Best라고 생각한다.
'Development > JavaScript' 카테고리의 다른 글
Javascript - Set을 알아보자 (0) | 2022.04.27 |
---|---|
Javascript - 맵(Map)을 알아보자 (0) | 2022.04.27 |
구조 분해 할당(Destructuring Assignment) - 객체, 중첩구조 편 (0) | 2022.04.26 |
구조 분해 할당(Destructuring Assignment) - 배열편 (0) | 2022.04.25 |
호이스팅(Hoisting)을 쉽게 알아보자 (0) | 2022.04.22 |