2025-10-07 13:11

  • 자바스크립트 프로미스는 비동기 작업을 깔끔하게 처리하기 위해 콜백 지옥을 대체하는 객체다.

  • 프로미스는 ‘대기’, ‘이행’, ‘거부’라는 세 가지 상태를 가지며, 한 번 결정된 상태는 바뀌지 않는다.

  • then, catch, finally를 통해 비동기 작업의 성공, 실패, 완료 시점을 제어하며 async/await 문법의 기반이 된다.

자바스크립트 비동기의 구원자 프로미스 완벽 핸드북

자바스크립트는 본질적으로 ‘싱글 스레드(Single-threaded)’ 기반 언어다. 한 번에 하나의 작업만 처리할 수 있다는 의미다. 하지만 웹 애플리케이션에서는 데이터를 서버에서 가져오거나, 파일을 읽는 등 시간이 걸리는 작업이 필수적이다. 만약 이런 작업이 끝날 때까지 프로그램 전체가 멈춘다면 사용자는 끔찍한 경험을 하게 될 것이다.

이 문제를 해결하기 위해 자바스크립트는 ‘비동기(Asynchronous)’ 처리 모델을 사용한다. 시간이 걸리는 작업을 일단 다른 곳에 위임하고, 그 작업이 끝나면 알려달라고 요청하는 방식이다. 초기 자바스크립트에서는 이 ‘알림’을 받기 위해 ‘콜백 함수(Callback Function)‘를 사용했다. 하지만 비동기 작업이 여러 개 중첩되면 코드의 가독성이 급격히 떨어지는 ‘콜백 지옥(Callback Hell)‘이라는 문제가 발생했다.

이 콜백 지옥의 혼돈 속에서 질서를 부여하기 위해 등장한 개념이 바로 **프로미스(Promise)**다. 이 핸드북은 프로미스가 왜 탄생했고, 어떤 구조로 이루어져 있으며, 어떻게 사용해야 하는지에 대한 모든 것을 깊이 있게 다룬다.


1. 왜 프로미스가 탄생했는가 콜백 지옥 탈출기

프로미스를 이해하려면 그것이 해결하고자 했던 문제, 즉 콜백 지옥을 먼저 이해해야 한다.

1.1. 지옥의 서막 콜백 패턴

예를 들어, 사용자 정보를 가져오고, 그 정보로 게시글을 가져온 다음, 그 게시글에 달린 댓글을 가져오는 3단계 비동기 작업이 있다고 상상해보자. 콜백 함수를 사용하면 코드는 다음과 같은 형태가 된다.

JavaScript

getUser(userId, (user) => {
  getPosts(user.id, (posts) => {
    getComments(posts[0].id, (comments) => {
      console.log(comments);
      // 또 다른 작업이 있다면...
        // 또 다른 작업이 있다면...
          // 끝없이 깊어지는 들여쓰기
    });
  });
});

코드가 오른쪽으로 끝없이 파고드는 피라미드 형태, 이것이 바로 콜백 지옥(Callback Hell) 또는 **운명의 피라미드(Pyramid of Doom)**다. 이런 코드는 몇 가지 심각한 문제를 안고 있다.

  • 가독성 저하: 코드의 흐름을 파악하기 매우 어렵다. 어떤 작업이 어떤 순서로 실행되는지 한눈에 들어오지 않는다.

  • 에러 처리의 어려움: 각 콜백마다 개별적으로 에러를 처리해야 한다. 중앙에서 에러를 관리하기 어렵고, 코드가 누락되면 디버깅이 힘들어진다.

  • 제어권 역전(Inversion of Control): 콜백 함수의 호출 시점과 횟수에 대한 제어권이 내가 작성한 코드가 아닌, 비동기 작업을 수행하는 외부 함수에 넘어간다. 이는 예측 불가능한 버그를 유발할 수 있다.

1.2. 프로미스의 등장 약속의 개념

프로미스는 이러한 문제들을 해결하기 위해 고안된 디자인 패턴이자 자바스크립트 ES6에 정식으로 추가된 객체다.

프로미스를 한마디로 정의하면 **“미래의 어떤 시점에 결과를 제공하겠다는 약속”**이다.

커피숍에서 주문하는 상황을 비유로 들어보자.

  1. 주문 (프로미스 생성): 당신이 카운터에서 커피를 주문하면, 점원은 “커피가 준비되면 진동벨을 울려드릴게요”라고 말하며 진동벨을 준다. 이 진동벨이 바로 프로미스다.

  2. 대기 (Pending 상태): 당신은 진동벨을 들고 자리에 앉아 다른 일을 할 수 있다. 커피가 만들어지는 동안 당신의 시간은 멈추지 않는다. 이것이 비동기 처리의 핵심이다.

  3. 결과 (Settled 상태):

    • 성공 (Fulfilled): 잠시 후, 진동벨이 울린다. 당신은 카운터로 가서 약속된 커피를 받는다. 약속이 성공적으로 이행된 것이다.

    • 실패 (Rejected): 만약 원두가 다 떨어졌다면, 점원은 당신을 불러 주문을 취소하고 환불해 줄 것이다. 약속이 실패한 것이다.

이처럼 프로미스는 비동기 작업의 ‘상태’를 관리하고, 그 결과(성공 또는 실패)를 처리할 수 있는 명확한 방법을 제공한다. 이를 통해 들여쓰기가 깊어지는 대신, 순차적인 코드 흐름을 만들 수 있다.


2. 프로미스의 핵심 구조 파헤치기

프로미스는 단순한 객체가 아니라, 명확한 상태와 규칙을 가진 하나의 작은 시스템이다.

2.1. 세 가지 상태

모든 프로미스 객체는 다음 세 가지 상태 중 하나를 가진다.

상태 (State)설명비유
pending (대기)비동기 작업이 아직 완료되지 않은 초기 상태.진동벨이 아직 울리지 않은 상태.
fulfilled (이행)비동기 작업이 성공적으로 완료되어 결과 값을 반환한 상태.진동벨이 울려 커피를 받은 상태.
rejected (거부)비동기 작업이 실패하여 에러(이유)를 반환한 상태.원두가 떨어져 주문이 취소된 상태.

가장 중요한 규칙: 프로미스는 pending 상태에서 fulfilled 또는 rejected 상태로 단 한 번만 변경될 수 있다. 한번 이행되거나 거부된 프로미스는 다시는 다른 상태로 돌아갈 수 없다. 이를 settled(처리됨) 상태라고 부른다.

2.2. 프로미스 객체 생성하기

프로미스는 new Promise() 생성자를 통해 만들어진다. 이 생성자는 **실행 함수(executor function)**라는 하나의 함수를 인자로 받는다.

JavaScript

const myPromise = new Promise((resolve, reject) => {
  // 비동기 작업을 수행하는 코드
  // 예: setTimeout, fetch, readFile 등

  const success = true; // 작업 성공 여부를 가정

  if (success) {
    // 작업이 성공하면 resolve 함수를 호출
    resolve("작업 성공! 결과 데이터");
  } else {
    // 작업이 실패하면 reject 함수를 호출
    reject(new Error("작업 실패! 에러 메시지"));
  }
});
  • 실행 함수: new Promise가 생성될 때 즉시 실행된다.

  • resolve(value): 이 함수가 호출되면 프로미스는 pending 상태에서 fulfilled 상태로 변경된다. 인자로 전달된 value는 프로미스의 결과 값이 된다.

  • reject(error): 이 함수가 호출되면 프로미스는 pending 상태에서 rejected 상태로 변경된다. 인자로 전달된 error는 프로미스가 실패한 이유가 된다.


3. 프로미스 사용법 정복하기

프로미스를 만드는 것만큼이나 중요한 것은 만들어진 프로미스의 결과를 ‘소비(consuming)‘하는 것이다.

3.1. 결과 값 받아오기 .then()

.then() 메서드는 프로미스가 **이행(fulfilled)**되었을 때 실행될 콜백 함수를 등록하는 역할을 한다.

JavaScript

myPromise.then((result) => {
  // 프로미스가 성공적으로 이행되었을 때 실행되는 코드
  console.log(result); // "작업 성공! 결과 데이터"
});

.then()은 두 개의 함수를 인자로 받을 수 있다. 첫 번째는 성공 콜백, 두 번째는 실패 콜백이다.

JavaScript

myPromise.then(
  (result) => { console.log(result); },
  (error) => { console.error(error); }
);

3.2. 에러 처리하기 .catch()

.catch() 메서드는 프로미스가 **거부(rejected)**되었을 때 실행될 콜백 함수를 등록한다. 이는 .then(null, rejectionCallback)과 동일하게 동작하는 가독성 좋은 축약형이다.

JavaScript

myPromise.catch((error) => {
  // 프로미스가 거부되었을 때 실행되는 코드
  console.error(error); // Error: "작업 실패! 에러 메시지"
});

에러 처리는 .catch()를 사용하는 것이 가독성과 일관성 측면에서 권장된다.

3.3. 성공/실패 여부와 상관없이 실행하기 .finally()

.finally() 메서드는 프로미스가 settled(이행 또는 거부) 상태가 되면 항상 실행될 콜백 함수를 등록한다. 로딩 스피너를 숨기는 등, 성공/실패 여부와 관계없이 항상 수행해야 하는 정리 작업에 유용하다.

JavaScript

myPromise
  .then((result) => console.log(result))
  .catch((error) => console.error(error))
  .finally(() => {
    console.log("프로미스 작업이 완료되었습니다.");
  });

3.4. 프로미스의 꽃 프로미스 체이닝(Promise Chaining)

.then().catch() 메서드는 항상 새로운 프로미스 객체를 반환한다. 이것이 프로미스의 가장 강력한 특징이며, 콜백 지옥을 해결하는 핵심 원리다.

반환된 프로미스는 이전 프로미스의 결과에 따라 상태가 결정된다.

  • .then()의 콜백 함수에서 값을 반환하면, 체인의 다음 프로미스는 그 값으로 이행된다.

  • .then()의 콜백 함수에서 새로운 프로미스를 반환하면, 체인의 다음 .then()은 그 새로운 프로미스가 settled될 때까지 기다린다.

  • 에러가 발생하거나 .catch()가 실행되면, 에러가 처리된 후 체인은 계속 진행될 수 있다.

콜백 지옥 예제를 프로미스 체이닝으로 바꿔보자.

JavaScript

getUser(userId)
  .then((user) => {
    console.log("사용자 정보 획득:", user);
    return getPosts(user.id); // 다음 작업(프로미스)을 반환
  })
  .then((posts) => {
    console.log("게시글 목록 획득:", posts);
    return getComments(posts[0].id); // 또 다른 프로미스 반환
  })
  .then((comments) => {
    console.log("댓글 획득:", comments);
  })
  .catch((error) => {
    // 체인 전체에서 발생하는 모든 에러를 여기서 한 번에 처리
    console.error("오류 발생:", error);
  })
  .finally(() => {
    console.log("모든 데이터 요청 작업 완료");
  });

코드가 위에서 아래로 순차적으로 흐르는 것을 볼 수 있다. 들여쓰기가 깊어지지 않고, 에러 처리가 단일 .catch() 블록으로 통합되어 훨씬 깔끔하고 직관적이다.


4. 심화 탐구 프로미스, 더 깊게 이해하기

프로미스의 기본 사용법을 익혔다면, 이제 내부 동작 원리와 강력한 정적 메서드들을 탐구할 차례다.

4.1. 프로미스와 이벤트 루프 마이크로태스크 큐

자바스크립트 런타임 환경에는 **이벤트 루프(Event Loop)**라는 것이 있다. 이벤트 루프는 콜 스택(Call Stack)이 비었을 때 태스크 큐(Task Queue)에서 작업을 가져와 실행하는 역할을 한다.

여기서 중요한 점은 태스크 큐가 하나가 아니라는 것이다.

  • 매크로태스크 큐 (Macrotask Queue, 또는 그냥 Task Queue): setTimeout, setInterval, I/O 작업 등의 콜백이 들어간다.

  • 마이크로태스크 큐 (Microtask Queue): 프로미스의 .then(), .catch(), .finally() 콜백MutationObserver 등이 들어간다.

이벤트 루프는 현재 실행 중인 스크립트가 끝나면, 마이크로태스크 큐에 있는 모든 작업을 먼저 처리한 뒤, 매크로태스크 큐에서 작업을 하나 가져와 처리한다.

이 우선순위 때문에 프로미스 콜백은 setTimeout 콜백보다 항상 먼저 실행된다.

JavaScript

console.log("스크립트 시작"); // 1

setTimeout(() => {
  console.log("setTimeout 콜백"); // 4
}, 0);

Promise.resolve().then(() => {
  console.log("프로미스 콜백"); // 3
});

console.log("스크립트 끝"); // 2

// 출력 순서:
// 스크립트 시작
// 스크립트 끝
// 프로미스 콜백
// setTimeout 콜백

setTimeout의 지연 시간이 0임에도 불구하고 프로미스 콜백이 먼저 실행되는 이유는 바로 이 마이크로태스크 큐의 우선순위 때문이다.

4.2. 정적 메서드 완전 정복

프로미스는 여러 개의 프로미스를 조합하여 더 복잡한 비동기 로직을 처리할 수 있는 강력한 정적 메서드들을 제공한다.

메서드설명반환 값
Promise.all(iterable)모든 프로미스가 이행될 때까지 기다린다. 하나라도 거부되면 즉시 거부된다.모든 프로미스의 결과 값을 담은 배열.
Promise.race(iterable)가장 먼저 settled(이행 또는 거부)되는 프로미스의 결과/이유를 따른다.가장 먼저 처리된 프로미스의 결과 또는 이유.
Promise.allSettled(iterable)모든 프로미스가 settled(이행 또는 거부)될 때까지 기다린다.각 프로미스의 상태(status)와 결과(value 또는 reason)를 담은 객체 배열.
Promise.any(iterable)가장 먼저 이행되는 프로미스의 결과를 따른다. 모든 프로미스가 거부되면 AggregateError와 함께 거부된다.가장 먼저 이행된 프로미스의 결과.
  • Promise.all: 여러 API를 동시에 호출하고, 모든 응답이 와야 다음 작업을 처리할 수 있을 때 유용하다. “전원 성공해야 성공”.

  • Promise.race: 여러 소스 중 가장 빠른 응답을 사용하거나, 특정 시간 내에 응답이 오지 않으면 타임아웃 처리할 때 유용하다. “1등만 기억하는 세상”.

  • Promise.allSettled: 여러 작업의 성공 여부와 관계없이 모든 작업의 결과를 확인하고 싶을 때 사용한다. all과 달리 실패가 있어도 중단되지 않는다. “결과가 어떻든 모두 끝까지 간다”.

  • Promise.any: 여러 미러 서버 중 가장 빠른 서버로부터 데이터를 가져올 때 유용하다. “누구든 성공만 하면 된다”.


5. 프로미스를 넘어 async/await

ES2017(ES8)에서는 프로미스를 더욱 쉽게 사용할 수 있도록 async/await 문법이 도입되었다. 이는 프로미스 기반의 비동기 코드를 마치 동기 코드처럼 보이게 만드는 **문법적 설탕(Syntactic Sugar)**이다.

async 함수는 항상 프로미스를 반환하고, await 키워드는 프로미스가 settled될 때까지 함수의 실행을 일시 중지시킨다.

앞서 본 프로미스 체이닝 예제를 async/await으로 바꿔보자.

JavaScript

async function fetchUserData(userId) {
  try {
    const user = await getUser(userId);
    console.log("사용자 정보 획득:", user);

    const posts = await getPosts(user.id);
    console.log("게시글 목록 획득:", posts);

    const comments = await getComments(posts[0].id);
    console.log("댓글 획득:", comments);

    return comments;
  } catch (error) {
    console.error("오류 발생:", error);
  } finally {
    console.log("모든 데이터 요청 작업 완료");
  }
}

fetchUserData(userId);
  • 비동기 작업 앞에 await을 붙여 결과가 올 때까지 기다린다.

  • 에러 처리는 익숙한 try...catch 구문을 사용한다.

  • 코드의 흐름이 위에서 아래로 자연스럽게 이어져 가독성이 극적으로 향상된다.

async/await은 내부적으로 프로미스를 사용하므로, 프로미스에 대한 깊은 이해는 async/await을 더 효과적으로 사용하는 데 필수적이다.


결론 프로미스는 현대 자바스크립트의 심장

프로미스는 자바스크립트 비동기 프로그래밍의 패러다임을 바꾼 혁신적인 개념이다. 콜백 지옥의 복잡성을 명확하고 순차적인 코드로 해결했으며, async/await이라는 더 발전된 패턴의 기반이 되었다.

웹 애플리케이션이 점점 더 복잡해지고 실시간 데이터 통신이 중요해지는 오늘날, 비동기 처리는 선택이 아닌 필수다. 프로미스의 상태, 체이닝, 그리고 이벤트 루프와의 상호작용을 깊이 이해하는 것은 모든 현대 자바스크립트 개발자가 갖추어야 할 핵심 역량이라 할 수 있다. 프로미스는 단순한 문법을 넘어, 비동기적 사고의 근간을 이루는 중요한 철학이다.

레퍼런스(References)

프로미스