💻 프로그래밍

Promise

프로미스

비동기 작업의 최종 완료/실패를 나타내는 객체. then, catch, finally로 처리. async/await의 기반.

📖 상세 설명

Promise는 JavaScript에서 비동기 작업의 최종 완료(또는 실패)와 그 결과 값을 나타내는 객체입니다. ES6(ECMAScript 2015)에서 정식으로 도입되어, 콜백 기반 비동기 처리의 복잡성을 해결하고 더 직관적인 비동기 프로그래밍을 가능하게 했습니다. Promise는 "미래의 어떤 시점에 결과를 제공하겠다"는 약속을 의미합니다.

Promise는 세 가지 상태(state)를 가집니다. pending(대기)은 초기 상태로 아직 이행도 거부도 되지 않은 상태입니다. fulfilled(이행)는 작업이 성공적으로 완료된 상태이며, rejected(거부)는 작업이 실패한 상태입니다. 한 번 fulfilled나 rejected가 되면 상태가 변경되지 않으며, 이를 settled(처리됨)라고 합니다.

Promise의 핵심 강점은 체이닝(chaining)입니다. then() 메서드는 새로운 Promise를 반환하므로 .then().then().catch() 형태로 연속된 비동기 작업을 순차적으로 처리할 수 있습니다. catch()로 에러를 한 곳에서 처리하고, finally()로 성공/실패와 관계없이 실행할 코드를 정의할 수 있어 콜백 지옥을 해결합니다.

ES2017에서 도입된 async/await은 Promise를 더 동기 코드처럼 작성할 수 있게 해주는 문법적 설탕(syntactic sugar)입니다. async 함수는 항상 Promise를 반환하고, await 키워드는 Promise가 settled될 때까지 실행을 일시 중지합니다. 내부적으로 여전히 Promise를 사용하므로, Promise의 동작 원리를 이해하는 것이 async/await을 올바르게 사용하는 기초가 됩니다.

💻 코드 예제

// 1. Promise 생성하기
function fetchUser(userId) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (userId > 0) {
                resolve({ id: userId, name: '홍길동', email: 'hong@example.com' });
            } else {
                reject(new Error('유효하지 않은 사용자 ID'));
            }
        }, 1000);
    });
}

// 2. Promise 체이닝 (then, catch, finally)
fetchUser(1)
    .then(user => {
        console.log('사용자 조회 성공:', user.name);
        return fetchUserPosts(user.id);  // 다음 Promise 반환
    })
    .then(posts => {
        console.log('게시물 수:', posts.length);
        return posts.map(p => p.title);
    })
    .then(titles => {
        console.log('게시물 제목들:', titles);
    })
    .catch(error => {
        // 체인 어디서든 발생한 에러를 여기서 처리
        console.error('오류 발생:', error.message);
    })
    .finally(() => {
        console.log('작업 완료 (성공/실패 무관)');
    });


// 3. Promise.all - 여러 Promise 병렬 실행
async function fetchDashboardData() {
    const [user, posts, notifications] = await Promise.all([
        fetchUser(1),
        fetchUserPosts(1),
        fetchNotifications(1)
    ]);
    // 세 요청이 동시에 시작되어 가장 느린 요청 시간만 소요
    console.log('대시보드 데이터:', { user, posts, notifications });
}


// 4. Promise.allSettled - 실패해도 모든 결과 받기
const promises = [
    fetchUser(1),      // 성공
    fetchUser(-1),     // 실패
    fetchUser(2)       // 성공
];

Promise.allSettled(promises).then(results => {
    results.forEach((result, i) => {
        if (result.status === 'fulfilled') {
            console.log(`Promise ${i}: 성공`, result.value);
        } else {
            console.log(`Promise ${i}: 실패`, result.reason.message);
        }
    });
});


// 5. async/await으로 동기 코드처럼 작성
async function processUser(userId) {
    try {
        const user = await fetchUser(userId);
        console.log('사용자:', user.name);

        const posts = await fetchUserPosts(user.id);
        console.log('게시물 수:', posts.length);

        // 병렬 실행이 필요하면 Promise.all 사용
        const [comments, likes] = await Promise.all([
            fetchComments(posts[0].id),
            fetchLikes(posts[0].id)
        ]);

        return { user, posts, comments, likes };
    } catch (error) {
        console.error('처리 중 오류:', error.message);
        throw error;  // 에러 재전파
    }
}

// async 함수 호출 (Promise 반환)
processUser(1).then(data => console.log('완료:', data));

🗣️ 실무에서 이렇게 말하세요

💬 면접에서 (Promise vs Callback)
"콜백은 함수를 인자로 전달해서 비동기 결과를 받지만, 중첩되면 콜백 지옥이 발생합니다. Promise는 then 체이닝으로 평탄하게 작성할 수 있고, catch로 에러를 한 곳에서 처리할 수 있습니다. 또한 Promise.all로 병렬 처리를 쉽게 구현할 수 있어 콜백보다 유연합니다."
💬 코드 리뷰에서 (에러 핸들링)
"이 async 함수에 try-catch가 없네요. await에서 reject되면 에러가 전파되어 unhandled rejection 경고가 발생합니다. try-catch로 감싸거나, 호출하는 쪽에서 .catch()를 추가해주세요. Node.js에서는 process.on('unhandledRejection')으로 전역 핸들러도 설정해두는 게 좋습니다."
💬 회의에서
"이 세 API 호출은 서로 의존성이 없으니까 순차 await 대신 Promise.all로 병렬 실행하면 응답 시간을 3초에서 1초로 줄일 수 있어요. 다만 하나라도 실패하면 전체가 reject되니까, 부분 실패를 허용하려면 Promise.allSettled를 사용해야 합니다."

⚠️ 흔한 실수 & 주의사항

Unhandled Promise Rejection

Promise에서 reject가 발생했는데 catch()나 try-catch로 처리하지 않으면 경고가 발생합니다. Node.js 15+에서는 프로세스가 종료될 수 있습니다. 모든 Promise 체인은 반드시 .catch()로 끝내거나, async 함수 내에서 try-catch를 사용하세요.

순차 await vs 병렬 실행

독립적인 비동기 작업을 순차적으로 await하면 불필요하게 느려집니다. `await a(); await b();` 대신 `await Promise.all([a(), b()]);`로 병렬 실행하세요. 단, 순서가 중요하거나 앞 결과가 뒤 호출에 필요한 경우는 순차 실행이 맞습니다.

async 함수의 반환값 주의

async 함수는 항상 Promise를 반환합니다. `return 값`은 `Promise.resolve(값)`과 동일합니다. forEach 콜백에서 async를 쓰면 반환된 Promise가 무시되므로, for...of 루프나 Promise.all(arr.map(...))을 사용하세요.

상황에 맞는 패턴 선택

간단한 비동기 흐름은 async/await이 가독성이 좋습니다. 병렬 처리에는 Promise.all, 부분 실패 허용에는 Promise.allSettled, 가장 빠른 결과만 필요하면 Promise.race를 사용하세요. 이벤트 기반 코드는 여전히 콜백이 적합합니다.

🔗 관련 용어

📚 더 배우기