비동기 함수의 결과값을 처리하기 위해 Callback패턴을 사용한다.
하지만, 연속적인 비동기 함수의 호출과 처리는 수많은 Callback 함수를 필요로하고, 인해 가독성이 안좋아진 코드를 Callback hell이라고 부른다. 어떤 상황인지 직접 코드로 확인해보자.
특정 {nickname}을 가진 사용자의 uid 정보 호출 API
GET https://asyncstudy.com/user/{nickname}
response {
uid: string
}
특정 {uid}로 작성된 게시글 목록 호출 API
GET https://asyncstudy.com/post/{uid}
response {
posts: [{title: string, contents: string}] // 게시글 목록
}
const get = (url, callback) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url)
xhr.send()
xhr.onload = () => {
if (xhr.status === 200)
callback(JSON.parse(xhr.response))
else console.error('ERROR')
}
}
// A 함수 : 닉네임이 shelly인 사용자의 uid정보를 GET 요청한다.
get('https://asyncstudy.com/user/shelly', ({uid}) => {
// B 함수 : A 함수의 callback 함수이다. 전달받은 uid로 post 목록을 GET 요청한다.
get(`https://asnycstudy.com/post/${uid}`, ({posts}) => {
// C 함수 : B 함수의 callback 함수이다. 포스트 목록을 출력한다.
console.log(posts)
})
})
위의 코드를 한번 이해해보자.
// A 함수 : 닉네임이 shelly인 사용자의 uid정보를 GET 요청한다.
// callback은 B 함수이다.
get('https://asyncstudy.com/user/shelly', callback )
// B 함수 : A 함수의 콜백함수이다. A 함수에서 전달해준 uid로 post 목록을 GET 요청한다.
// callback은 C 함수이다.
({uid}) => {
get(`https://asnycstudy.com/post/${uid}`, callback)
}
// C 함수 : B 함수의 콜백함수이다. B 함수에서 전달해준 post 목록을 출력한다.
({posts}) => {
console.log(posts)
}
이러한 요구사항은 빈번하게 발생한다. 물론 이 보다 더 연속적으로 비동기 함수를 호출하는 요구사항도 있다. 그런 상황에서 중첩은 더욱 깊어지며, 중첩이 깊어질수록 가독성 또한 점점 더 안좋아진다.
try {
throw new Error('에러가 발생했습니다')
console.log('success')
} catch(e){
console.error(e)
}
우리는 에러에 대응하기 위해 try catch문을 사용한다. 위의 코드의 경우, try 스코프 내부에서 throw new Error('에러가 발생했습니다.') 코드로 인해 에러가 발생하기 때문에 success 문구를 출력하지 않고, catch로 넘어가 console.error(e)를 실행한다.즉, 정상적으로 에러를 처리한다.
그렇다면, 아래의 코드는 어떨까?
try {
setTimeout(() => {throw new Error('에러가 발생했습니다')}, 1000)
} catch(e){
console.error(e)
}
결론부터 말하자면, 이 코드는 에러를 처리하지 못한다.
.
.
그 이유에 대해서 알아보자.
우선 에러는 콜스택 아래로 전파된다는 사실을 알아야한다.

만약, C 컨텍스트에서 에러가 발생했다고 가정하자.
1. 콜스택에서 C컨텍스트 아래에 있는 B 컨텍스트로 에러를 전파한다.
2. B 에서 에러를 catch하지 못했다면 A 컨텍스트로 에러를 다시 내려준다.
3. A 컨텍스트에서 catch문이 있다면, 에러를 캐치하고 더이상 에러를 전파하지 않는다.
다시 코드로 돌아와보자. 아래의 코드는 콜스택이 어떻게 되어있을까?
try {
setTimeout(() => {throw new Error('에러가 발생했습니다')}, 1000)
} catch(e){
console.error(e)
}

1. 빈 콜스택이 있다.
2. 전역 컨텍스트가 콜스택에 push 되어 코드가 실행된다.
3. setTimeout 컨텍스트가 콜스택에 push되어 스케쥴링을 한다.
4. 스케쥴링을 완료한 후 setTimeout 컨텍스트는 콜스택에서 pop된다.
5. 코드 실행이 끝났기 때문에 전역 컨텍스트가 콜스택에서 pop된다.
6. 콜택이 빈 것을 보고 테스크큐에 push되어있는 setTimeout의 콜백함수를 pop하여 콜스택에 push한다.
7. Error 가 발생하고, 콜스택 아래로 에러를 전파한다.
8. Error 컨텍스트가 콜스택에서 pop된다.
만약 이벤트 루프에 대한 내용을 알고 싶다면, 이 글을 참고하자.
에러는 콜스택 아래로 전파된다.
그런데 Error가 발생하는 7번 과정을 보면, throw new Error 컨텍스트의 아래엔 아무런 컨텍스트가 없다. 즉, 선언했던 try catch는 이미 종료된 상태이기 때문에 정상적으로 에러가 처리되지 않는다.
이러한 문제점을 해결하기 위해 Promise 패턴이 등장한다.
Promise는 다음 글에서 살펴보자!
콜백 패턴이 에러 처리에 한계가 있다는 부분이 이해가 안됐었는데, 에러가 콜 스택을 통해 전파된다는 설명을 보고 이해하게 됐어요. 감사합니다!