Promise 패턴 간단 정리

세하·2025년 9월 8일

JavaScript

목록 보기
5/10

Promise 패턴이란?

Promise 패턴은 비동기 처리(Asynchronous Operation)를 마치 동기 처리(Synchronous Operation)처럼 순차적으로 표현하고 관리하기 위한 디자인 패턴이다.
복잡한 콜백 지옥(Callback Hell)을 해결하고 코드의 가독성과 유지보수성을 높이기 위해 등장했다.

Promise는 말 그대로 약속이라는 뜻인데, 비동기 작업이 "미래의 어느 시점에 결과를 알려주겠다" 는 약속을 하는 객체라고 생각하면 쉽다.

Promise의 세 가지 상태

Promise는 세 가지 상태 중 하나를 가진다.

  • Pending (대기): 비동기 처리가 아직 시작되지 않았거나 진행 중인 초기 상태
  • Fulfilled (이행): 비동기 처리가 성공적으로 완료되어 결과 값을 반환한 상태
    .then() 메서드로 결과 값을 받을 수 있다.
  • Rejected (실패): 비동기 처리가 실패하거나 오류가 발생한 상태
    .catch() 메서드로 실패 이유를 받을 수 있다.

이 상태들은 한 방향으로만 흐른다.
즉, Pending 상태에서 Fulfilled나 Rejected 상태로 한 번 바뀌면 더 이상 다른 상태로 변경되지 않음

MDN | Promise

특징

1. 비동기 로직과 콜백 함수의 분리

Promise의 가장 큰 장점은 비동기 로직에서 콜백 함수를 분리한 것이다.

전통적인 콜백 방식은 비동기 함수를 호출할 때, 성공 콜백과 실패 콜백을 모두 인자로 넘겨야 했다. 코드가 길어지고 중첩되면 '콜백 지옥'이 발생했다.

asyncFunction(param, function(result) {
    // 성공 콜백
}, function(error) {
    // 실패 콜백
});

그러나 Promise 방식은 비동기 작업 자체를 담는 객체를 먼저 반환한다. 그리고 그 결과(성공 또는 실패)가 나왔을 때 실행할 함수는 .then() 이나 .catch()를 사용해 나중에 등록한다.

const promise = asyncFunction(param);
promise.then(result => {
    // 성공 시 실행할 함수 (then의 인자로 등록)
}).catch(error => {
    // 실패 시 실행할 함수
});

2. 체이닝(Chaining)을 통한 순차적 처리

Promise의 가장 강력한 기능 중 하나는 .then()연결(chaining) 하여 비동기 작업을 순서대로 처리할 수 있다는 점이다. 이를 통해 비동기 코드임에도 불구하고 마치 동기 코드처럼 위에서 아래로 흐름을 이해하기 쉽게 작성할 수 있다.

job1()
    .then(resultFromJob1 => job2(resultFromJob1))
    .then(resultFromJob2 => job3(resultFromJob2))
    .then(resultFromJob3 => {
        // 모든 작업이 순차적으로 성공했을 때의 로직
    })
    .catch(error => {
        // 어느 한 작업이라도 실패했을 때의 로직
    });

코드 실행 순서

아래 코드의 실행 순서를 봐보자.

const promise = new Promise((resolve, reject) => {
  console.log('Promise executor');
  setTimeout(() => {
    resolve('Done');
    console.log('Promise resolved');
  }, 1000);
});

promise.then((result) => {
  console.log(`Then: ${result}`);
});

console.log('End');
  1. Promise executor 출력
    new Promise()가 생성될 때, 인자로 전달된 실행자 함수(executor)는 즉시 동기적으로 실행된다.

  2. End 출력
    setTimeout은 비동기 함수이므로, 타이머를 설정하고 바로 다음 코드로 넘어간다. 따라서 promise.then()은 아직 실행되지 않고, console.log('End')가 먼저 실행된다.

  3. (대략 1초 후) Promise resolved 출력
    대략 1초가 지나면 setTimeout의 콜백 함수가 실행된다. (setTimeout 자체는 콜백함수를 web API에 등록한 후 바로 반환된다) resolve('Done')이 호출되어 Promise의 상태가 Pending에서 Fulfilled 로 바뀐다.
    그 직후 같은 콜백 함수 내에 있는 console.log('Promise resolved')가 실행된다.

  4. (마지막) Then: Done 출력
    Promise의 상태가 Fulfilled로 바뀌었기 때문에, .then()에 등록해 두었던 콜백 함수가 실행된다. resolve 함수에 인자로 전달된 'Done' 값이 result로 들어온다.

Promise executor
End
Promise resolved
Then: Done

길게 말로 풀어서 설명해보자면

promise를 선언하면 실행자 함수(executor)가 즉시 동기적으로 실행되고, 그 안에 콜백함수가 실행돼서 setTimeout이 실행되고, setTimeout의 콜백함수web API에 위임됨. 이제 resolve(“Done”)은 독립적으로 진행됨.
이때 프로미스 안의 콜백함수가 종료되었으므로 new Promise 객체가 반환됨. 콜백함수의 resolve() 함수는 아직 실행 안된상태로 존재.

promise는 아직 pending 상태.
promise.then동기함수 이다. 동기적으로 실행되며, 미래에 실행될 콜백 함수를 등록하는 역할만 하고 바로 다음 코드로 넘어간다. 따라서 그냥 바로 반환 됨. 그래서 만약 또 .then이 있으면 얘는 또 바로 실행 되어서 콜백을 등록한다. 여튼, .then은 파라미터를 받아서 promise 객체 안에 콜백을 등록해주는 함수임.
then이 콜백함수 등록해주면 얘는 끝나.

아직 실행 안된거는 setTimeout으로 등록했던 콜백함수랑, 프로미스 객체 안에 then으로 등록했던 콜백함수가 실행 안됨.

그럼 언제 실행될까?
대략 1초뒤에 setTimeout의 콜백이 실행되면서 resolve()가 호출되면, 프로미스의 상태가 fulfilled로 바뀐다. 상태가 fulfilled로 바뀌는 것을 기점으로, .then()으로 등록해 두었던 콜백 함수가 실행된다.

헷갈리는것 정리

1. 생성자 함수 vs 실행자 함수

  • 생성자 함수 (Constructor): Promise 자체
    Promise는 새로운 프로미스 객체를 만드는 설계도 또는 틀과 같다.
    new Promise()는 이 설계도를 바탕으로 실제 프로미스 객체 인스턴스를 생성하는 명령어

  • 실행자 함수 (Executor): (resolve, reject) => { ... }
    생성자 함수 Promise에 인자로 전달된 이 화살표 함수를 실행자(executor) 함수라고 부른다.
    이 함수는 new Promise()가 실행될 때 즉시 동기적으로 호출되며, 비동기 작업을 시작하는 로직을 담는다.
    resolve와 reject는 이 실행자 함수에 자동으로 전달되는 콜백 함수

2. .then() 콜백은 언제, 어떻게 실행될까? (Microtask Queue)

setTimeout의 콜백 함수는 Task Queue (Macrotask Queue) 에 들어간다.
resolve()가 호출되면, .then()으로 등록된 콜백 함수Microtask Queue라는 다른 줄에 들어간다.
이벤트 루프(Event Loop)Microtask Queue를 항상 우선적으로 처리한다. 즉, 현재 실행 중인 코드가 끝나면 Task Queue에 아무리 많은 작업이 쌓여있어도 Microtask Queue를 먼저 확인해서 비울 때까지 실행한다. 이것이 Promise가 다른 비동기 작업보다 우선적으로 처리되는 이유이다.

resolve().then() 콜백Microtask Queue에 등록하고, 이벤트 루프가 이를 가져와 실행한다.

3. .then 등록 시점 vs 실행 시점

.then()은 메서드가 코드에서 호출되는 즉시 동기적으로 프로미스 객체에 콜백을 등록한다.
콜백의 실행은 비동기적이지만, 콜백의 등록은 즉시 일어난다.

  • 등록 시점 (Synchronous)
    자바스크립트 엔진이 promise.then(...) 코드를 읽는 순간, .then 메서드는 바로 실행된다. 이때 .then이 하는 일은 단 하나, "이 콜백 함수를 기억해뒀다가 이 프로미스가 성공(fulfilled)하면 실행해 줄게"라고 약속하며 내부적으로 콜백을 등록하는 것. 그리고는 바로 다음 줄의 코드로 넘어간다.

  • 실행 시점 (Asynchronous)
    프로미스 내부의 비동기 작업이 끝나고 resolve()가 호출되면, 프로미스의 상태가 fulfilled로 바뀐다. 이때 이벤트 루프는 이 프로미스에 이미 등록되어 있던 콜백 함수를 Microtask Queue로 보낸다. 그리고 현재 실행 중인 동기 코드가 모두 끝난 후에 Microtask Queue에서 해당 콜백을 꺼내 실행한다.

2. .then이 여러 개일 때의 동작 방식 (Chaining)

.then()은 콜백을 여러 개 등록하는 것이 아니라, 항상 새로운 Promise 객체를 반환한다.

const promise1 = new Promise(resolve => setTimeout(() => resolve(1), 500));

const promise2 = promise1.then(result => {
  console.log(result); // 1
  return result + 1;   // 새로운 Promise가 이 값을 가지고 이행된다.
});

const promise3 = promise2.then(result => {
  console.log(result); // 2
  return result + 1;
});

// promise1, promise2, promise3는 모두 다른 Promise 객체

위 코드를 체이닝으로 바꾸면 아래와 같다.

new Promise(resolve => setTimeout(() => resolve(1), 500))
  .then(result => { // 첫 번째 then은 promise1에 등록됨
    console.log(result); // 1
    return result + 1;   // 여기서 반환된 값으로 '새로운 Promise'가 결정됨
  })
  .then(result => { // 두 번째 then은 바로 위 then이 반환한 '새로운 Promise'에 등록됨
    console.log(result); // 2
    return result + 1;
  });

즉, .then을 줄줄이 쓰는 것은 하나의 Promise에 콜백을 여러 개 다는 것이 아니라, .then반환한 새로운 Promise에 다음 .then연결되는 릴레이 방식이다.
이것이 Promise 체이닝(Chaining)의 핵심 원리이다.

0개의 댓글