Callback Hell


비동기 방식은 특정 코드가 실행완료될 때까지 기다리지 않고, 다음 코드를 먼저 수행하는 방식이기 때문에 만약, 비동기 작업 결과에 따라서 또 다른 작업을 수행해야 될 때(순차적인 비동기 작업)는 콜백함수를 사용했다.

콜백함수는 비동기 작업이 완료되면 호출되는 함수라는 의미이다. 대게 비동기 함수의 매개변수로 이 콜백 함수를 넘겨준다. 그래서 비동기 작업의 결과를 받아서 콜백 함수의 인자로 주면, 후속 작업을 진행할 수 있었다.

하지만 가독성이 떨어지는 문제가 있었다. 여러 개의 비동기 작업을 순차적으로 수행해야 할 때는 콜백 함수가 중첩되어 코드의 깊이가 깊어지는 현상이 발생하기 때문이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
step1((err1, result1) => {
    if (err1) {
        handleError(err1);
    } else {
        step2(result1, (err2, result2) => {
            if (err2) {
                handleError(err2);
            } else {
                step3(result2, (err3, result3) => {
                    if (err3) {
                        handleError(err3);
                    } else {
                        console.log(result3);
                    }
                });
            }
        });
    }
});

각각의 단계에 대한 에러처리가 중첩되어 있고, 코드 흐름을 따라가기가 어렵다. 이를 해결하기 위해서 Promise가 나왔다.

Promise라는 이름은 약속이라는 의미다.


Promise라는 이름은 비동기 작업이 끝날 때까지 결과를 기다리는 것이 아니라, 미래의 어떤 시점에 비동기 작업에 대한 결과를 제공하겠다는 “약속”을 반환한다는 의미에서 명명했다고 한다.

우리가 카페를 가면, 커피를 주문하고 진동벨을 받고 기다리게 되는데 Promise가 이 진동벨이라고 생각하면 이해하기가 쉽다.

Promise의 3가지 상태


Promise는 비동기 작업 상태를 나타내는 객체이다. 3가지 상호 배타적인 상태 중 하나를 가진다.

fulfilled : promise.then(func)을 호출했을 때, func를 가능한 빨리 호출하는 상태
rejected : promise.then(func,r)을 호출했을 때, r를 가능한 빨리 호출하는 상태
pending : 이행도 거부도 아닌 초기 상태

Promise Chaining


1
2
3
4
5
6
7
8
9
10
11
const myPromsise  = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("foo");
    }, 300);
})

myPromise
  .then(handleFullfiledA, handleRejectedA)// (1)
  .then(andleFullfiledB, handleRejectedB)// (2)
  .then(andleFullfiledC, handleRejectedC);// (3)
  .error(handleError(error))

이전 콜백함수의 반환값을 다음 콜백 함수에 사용해야 하는 경우에 콜백함수를 사용하게 되고, 그래서 발생하는 것이 콜백지옥이다. Promise가 이 콜백지옥을 해결하는 핵심적인 기능이 ‘체이닝’이다.

.then()은 최대 2가지 인자를 받는데, 첫 번째 인자는 fullfiled상태에 대한 콜백 onFulfilled, 두 번째 인자는 rejected에 대한 콜백 onRejected이다. 그리고 각 .then()은 연쇄적으로 사용할 수 있다.

위와 같은 코드가 있다고 했을 때, 1번 .then()myPromise가 아닌 promiseB라고 하는 새로운 Promise 객체를 즉시 반환한다. 마찬가지로 2번 .then()promiseB에 연결(chaining)되어 새로운 promiseC를 반환한다.

이처럼 각 .then()은 이전 Promise를 기다리는 것이 아니라, 이전 .then()이 반환한 새로운 Promise를 기다리고 있는 구조이다.

Resolved vs Fulfilled


여기서 resolved라는 단어가 fulfilled랑 헷갈려서 따로 찾아봤다. resolved는 Promise가 직접적으로 가지는 상태(State)는 아니지만, 그 ‘운명(Fate)’을 설명하는 용어로 Promise의 ‘운명(Fate)’은 resolved(결정됨) 또는 unresolved(미결정됨) 둘 중 하나이다.

  • unresolved: Promise가 여전히 pending 상태이고, 다른 Promise나 thenable에 의해 결정될 수 있는 상태
  • resolved: Promise가 더 이상 다른 Promise나 thenable에 의해 결정되지 않는 상태. 이는 fulfilled이거나 rejected일 수 있다.

즉, resolved Promise는 settled(완료된) Promise를 의미하며, 이는 fulfilled 또는 rejected 상태를 모두 포함한다.

Promise의 실제 사용 예시


앞서 살펴본 콜백 지옥 코드를 Promise로 해결하면, 다음과 같은 코드가 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
step1
  .then(result1 => {
    console.log(result1)
    return step2(result1)
  })
  .then(result2 => {
    console.log(result2)
    return step3(result3)
})
  .then(result3 => {
    console.log(result3)
    console.log(finish)
  }
  )
  .catch(error => {
    console.log(error)
  })

마무리


Promise는 JavaScript의 비동기 프로그래밍에서 콜백 지옥 문제를 해결하고, 코드의 가독성과 유지보수성을 크게 향상시켰다. Promise의 핵심 개념들을 정리하면:

  1. 상태 관리: pending → fulfilled/rejected로 상태가 변화하며, 한 번 결정되면 변경되지 않는다.
  2. 체이닝: .then()을 통해 순차적인 비동기 작업을 평탄한 구조로 작성할 수 있다.
  3. 에러 처리: .catch()를 통해 중앙집중식 에러 처리가 가능하다.

현재는 async/await 문법이 더 널리 사용되지만, Promise의 기본 개념을 이해하는 것은 JavaScript 비동기 프로그래밍의 기초이자 필수이다. Promise를 제대로 이해하면 async/await도 더 깊이 있게 활용할 수 있게 된다.

카테고리:

업데이트: