Promise의 탄생 배경
초기 자바스크립트에서는 비동기 처리를 위해 콜백 패턴을 사용했는데, 심각한 문제들이 발생하며 이를 해결하기 위한 과정에서 Promise가 등장하게 되었다.
1. 콜백 지옥 (Callback Hell)
// 콜백 지옥의 예시
getUserData(userId, function(user) {
getPosts(user.id, function(posts) {
getComments(posts[0].id, function(comments) {
getAuthor(comments[0].authorId, function(author) {
// 들여쓰기가 계속 깊어지고 코드가 복잡해짐
});
});
});
});
// Promise를 사용한 개선된 코드
getUserData(userId)
.then(user => getPosts(user.id))
.then(posts => getComments(posts[0].id))
.then(comments => getAuthor(comments[0].authorId))
.then(author => {... })
.catch(error => {
// 모든 에러를 한 곳에서 처리
});
2. 에러 처리의 어려움
// 콜백에서의 에러 처리
getData(function(error, data) {
if (error) {
// 에러 처리
return;
}
processData(data, function(error, processedData) {
if (error) {
// 또 다른 에러 처리
return;
}
// 계속되는 에러 처리 중복
});
});
// Promise를 사용한 개선된 에러 처리
getData()
.then(data => processData(data))
.then(processedData => {
// 데이터 처리
})
.catch(error => {
// 모든 에러를 한 곳에서 처리
});
3. 신뢰성 문제
- 콜백이 여러 번 호출될 수 있음
- 콜백이 호출되지 않을 수 있음
- 콜백이 동기적으로 또는 비동기적으로 호출될 수 있음
이러한 문제들을 해결하기 위해 Promise가 도입되었으며, 다음과 같은 장점을 제공한다
- 체이닝 가능:
.then()
을 사용한 명확한 흐름 제어 - 에러 처리 통합:
.catch()
를 통한 중앙화된 에러 처리 - 상태 보장: 한 번 완료된 Promise는 상태가 변경되지 않음
- 비동기 작업의 동기적 표현: 비동기 코드를 동기 코드처럼 작성 가능
Promise란?
Promise는 자바스크립트/타입스크립트에서 비동기 작업을 처리하기 위한 객체다. Promise는 생성 시점에 정확히 알 수 없는 값에 대한 프록시로서, 비동기 작업의 결과를 나타내며, 작업이 완료되면 결과를 반환하거나 에러를 처리할 수 있다. 다만 최종 결과를 반환하는 것이 아니고, 미래의 어떤 시점에 결과를 제공하겠다는 Promise
를 반환하게 된다.
특히 중요한 점은
- Promise는 비동기 메서드가 동기 메서드처럼 값을 반환할 수 있다.
- Promise는 'settled'(확정됨) 상태가 되면 더 이상 상태가 변경되지 않는다.
- Promise가 fulfilled나 rejected 상태가 된 후에 핸들러를 연결해도 정상적으로 실행된다.
비동기
비동기는 특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드를 먼저 실행하는 방식을 의미합니다. 예를 들어 서버에서 데이터를 가져오거나, 대용량 파일을 읽는 등 시간이 오래 걸리는 작업을 수행할 때, 해당 작업이 완료될 때까지 다른 코드의 실행을 멈추지 않고 계속 실행합니다.
Promise의 상태와 용어
Promise가 갖을 수 있는 상태
- Pending (대기): 초기 상태로, 이행되거나 거부되지 않은 상태
- Fulfilled (이행): 작업이 성공적으로 완료된 상태
- Rejected (거부): 작업이 실패한 상태
프로미스가 이행되거나 거부되었지만 보류 중이 아닌 경우, 프로미스가 확정된 것으로 간주한다.
기본 문법
const promise: Promise<string> = new Promise((resolve, reject) => {
// 비동기 작업 수행
if (/* 성공 조건 */) {
resolve('성공 결과');
} else {
reject(new Error('실패 사유'));
}
});
핵심 메서드
- then()
promise
.then((result: string) => {
console.log('성공:', result);
return nextOperation(result);
})
.then((nextResult: string) => {
console.log('다음 작업:', nextResult);
});
- catch()
promise
.then((result: string) => {
// 작업 수행
})
.catch((error: Error) => {
console.error('에러 발생:', error);
});
- finally()
promise
.then((result: string) => {
// 작업 수행
})
.catch((error: Error) => {
console.error('에러 발생:', error);
})
.finally(() => {
console.log('비동기 작업 완료');
});
유용한 Promise 메서드
Promise.all()
여러 Promise를 병렬로 실행하고 모든 Promise가 완료될 때까지 기다린다.
모든 Promise가 성공적으로 이행되면 결과값 배열을 반환하고, 하나라도 실패하면 즉시 에러를 반환하게 된다.
const promises: [Promise<User[]>, Promise<Post[]>, Promise<Comment[]>] = [
fetch('/api/users').then(res => {...}),
fetch('/api/posts').then(res => {...}),
fetch('/api/comments').then(res => {...})
];
Promise.all(promises)
.then(([users, posts, comments]) => {...})
.catch(error => {...});
Promise.race()
여러 Promise 중 가장 먼저(성공, 실패 구분 없이) 완료된 결과를 반환한다.
다른 Promise들은 자동으로 취소되지 않고 계속 실행된다.
const promises: [Promise<User[]>, Promise<Post[]>, Promise<Comment[]>] = [
fetch('/api/users').then(res => {...}),
fetch('/api/posts').then(res => {...}),
fetch('/api/comments').then(res => {...}),
];
const timeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error('시간 초과!')), 5000);
});
Promise.race([...promises, timeout])
.then(result => {...})
.catch(error => {...});
Promise.allSettled()
모든 Promise가 완료(성공 또는 실패)될 때까지 기다리며, 실패가 있어도 에러를 던지지 않는다.
const promises: [Promise<User[]>, Promise<Post[]>, Promise<Comment[]>] = [
fetch('/api/users').then(res => {...}),
fetch('/api/posts').then(res => {...}),
fetch('/api/comments').then(res => {...})
];
Promise.allSettled(promises)
.then((results) => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`성공 ${index}:`, result.value);
} else {
console.log(`실패 ${index}:`, result.reason);
}
});
// 성공한 결과만 필터링
const successfulResults = results
.filter((r): r is PromiseFulfilledResult<User[] | Post[] | Comment[]> =>
r.status === 'fulfilled'
)
.map(r => r.value);
console.log('성공한 결과만:', successfulResults);
});
예제
API 호출 처리
interface User {
id: number;
name: string;
email: string;
}
async function fetchUserData(userId: number): Promise<User> {
try {
const response: Response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('사용자를 찾을 수 없습니다');
}
return await response.json() as User;
} catch (error) {
console.error('사용자 데이터 조회 실패:', error);
throw error;
}
}
병렬처리
async function fetchMultipleUsers(userIds: number[]): Promise<User[]> {
const promises: Promise<User>[] = userIds.map(id => fetchUserData(id));
const users: User[] = await Promise.all(promises);
return users;
}
주의사항
- Promise 체인에서는 항상 값을 반환하기
- 적절한 에러 처리 구현하기
- async 함수는 항상 Promise를 반환함을 기억하기
- 불필요한 중첩 피하기
- then() 체인 대신 가능한 async/await 사용하기
- 타입 정의를 명확하게 하여 타입 안정성 확보하기
고급 Promise 패턴과 실전 예제
재시도 로직 구현
비동기 작업이 실패했을 때 자동으로 재시도하는 패턴
async function retryOperation<T>(
operation: () => Promise<T>,
maxAttempts: number = 3
): Promise<T> {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await operation();
} catch (error) {
if (attempt === maxAttempts) throw error;
// 지수 백오프: 재시도 간격을 점점 늘림
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
console.log(`재시도 ${attempt}/${maxAttempts}`);
}
}
throw new Error('재시도 실패');
}
// 사용 예시
async function fetchWithRetry(url: string) {
return retryOperation(async () => {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return response.json();
});
}
Promise 타임아웃 처리
특정 시간 이후에는 Promise를 취소하는 패턴
function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('작업 시간 초과')), timeoutMs);
});
return Promise.race([promise, timeoutPromise]);
}
// 사용 예시
async function fetchWithTimeout(url: string) {
try {
const response = await withTimeout(fetch(url), 5000); // 5초 타임아웃
return await response.json();
} catch (error) {
if (error.message === '작업 시간 초과') {
console.log('요청이 시간 초과되었습니다');
}
throw error;
}
}
Promise 풀링
동시에 실행할 수 있는 Promise의 개수를 제한하는 패턴
class PromisePool {
private running = 0;
private queue: (() => void)[] = [];
constructor(private concurrency: number) {}
async add<T>(promise: () => Promise<T>): Promise<T> {
if (this.running >= this.concurrency) {
await new Promise(resolve => this.queue.push(resolve));
}
this.running++;
try {
return await promise();
} finally {
this.running--;
if (this.queue.length > 0) {
const next = this.queue.shift();
next?.();
}
}
}
}
// 사용 예시
async function downloadFiles(urls: string[]) {
const pool = new PromisePool(3); // 최대 3개의 동시 다운로드
const downloads = urls.map(url =>
pool.add(() => fetch(url))
);
return Promise.all(downloads);
}
주의해야 할 안티패턴
1. Promise 중첩 피하기
// 잘못된 예
fetchUser(userId).then(user => {
fetchPosts(user.id).then(posts => {
fetchComments(posts[0].id).then(comments => {
// 콜백 지옥과 비슷한 상황
});
});
});
// 올바른 예
fetchUser(userId)
.then(user => fetchPosts(user.id))
.then(posts => fetchComments(posts[0].id))
.then(comments => {
// 깔끔한 체이닝
});
꼭 기억해야 할 포인트
- Promise는 항상 비동기적으로 실행됩니다.
.then()
은 항상 새로운 Promise를 반환합니다.- 에러 처리는 반드시 구현해야 합니다.
- async/await는 Promise를 더 읽기 쉽게 만들어주지만, 내부적으로는 여전히 Promise입니다.
- Promise.all()과 Promise.race()의 차이점을 이해해야 합니다.
- Promise 체인에서는 항상 값을 반환해야 합니다.
출처
Promise - JavaScript | MDN
Promise 객체는 비동기 작업이 맞이할 미래의 완료 또는 실패와 그 결과 값을 나타냅니다.
developer.mozilla.org
프라미스
ko.javascript.info
📚 자바스크립트 Promise 개념 & 문법 정복하기
콜백 지옥을 탈출하는 새로운 문법 자바스크립트에서 '비동기 처리' 란 현재 실행중인 작업과는 별도로 다른 작업을 수행하는 것을 말한다. 예를 들어 서버에서 데이터를 받아오는 작업은 시간
inpa.tistory.com
자바스크립트 Promise 쉽게 이해하기
(중급) 자바스크립트 입문자를 위한 Promise 설명. 쉽게 알아보는 자바스크립트 Promise 개념, 사용법, 예제 코드. 예제로 알아보는 then(), catch() 활용법
joshua1988.github.io
'Server > Node' 카테고리의 다른 글
자바스크립트 이벤트 루프의 이해 (JavaScript Eventloop) (1) | 2025.02.02 |
---|---|
tRPC에서의 쿠키 설정 및 안전한 클라이언트-서버 간 쿠키 관리 방법 (2) | 2024.11.10 |
RPC의 이해와 tRPC 예제 소개 (10) | 2024.10.13 |
[Nest.js] TypeOrm, Postgresql 적용 (0) | 2023.08.15 |