Promise와 async/await
Promise와 async/await는 자바스크립트와 타입스크립트에서 비동기 프로그래밍을 위한 핵심 기능입니다.
이들은 콜백 지옥을 피하고 더 읽기 쉬운 비동기 코드를 작성할 수 있게 해줍니다.
Promise의 개념과 타입스크립트에 사용
Promise는 비동기 연산의 최종 완료(또는 실패)와 그 결과값을 나타내는 객체입니다.
타입스크립트에서 Promise 타입 사용
function fetchData(): Promise<string> {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Data fetched successfully");
}, 1000);
});
}
fetchData().then(data => console.log(data));
async/await 문법
async/await는 Promise를 기반으로 동작하는 더 직관적인 문법입니다.
async function getData(): Promise<string> {
const result = await fetchData();
return `Processed: ${result}`;
}
getData().then(console.log);
async 함수는 항상 Promise를 반환합니다. await 키워드는 Promise가 이행될 때까지 실행을 일시 중지합니다.
Promise, async/await의 에러 처리
Promise 체이닝을 사용한 에러 처리
fetchData()
.then(data => processData(data))
.then(result => console.log(result))
.catch(error => console.error("Error:", error));
async/await를 사용한 에러 처리
async function handleData() {
try {
const data = await fetchData();
const result = await processData(data);
console.log(result);
} catch (error) {
console.error("Error:", error);
}
}
async/await는 동기 코드와 유사한 구조로 에러 처리가 가능하여 가독성이 높습니다.
반면, Promise 체이닝은 여러 단계의 에러 처리를 한 곳에서 할 수 있어 유연합니다.
제네릭을 활용한 타입 안전한 Promise
제네릭을 사용하여 다양한 타입의 Promise를 다룰 수 있습니다.
async function fetchJson<T>(url: string): Promise<T> {
const response = await fetch(url);
return response.json() as Promise<T>;
}
interface User {
id: number;
name: string;
}
const user = await fetchJson<User>('/api/user');
console.log(user.name); // 타입 안전성 보장
Promise 정적 메서드
1. Promise.all
- 여러 Promise를 병렬로 실행하고 모든 결과를 기다립니다.
async function fetchAllData() {
const [users, posts] = await Promise.all([
fetchJson<User[]>('/api/users'),
fetchJson<Post[]>('/api/posts')
]);
// users와 posts 모두 타입 추론됨
}
2. Promise.race
- 여러 Promise 중 가장 먼저 완료되는 것의 결과를 반환합니다.
async function fetchWithTimeout<T>(
promise: Promise<T>,
timeout: number
): Promise<T> {
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('Timeout')), timeout);
});
return Promise.race([promise, timeoutPromise]);
}
3. Promise.allSettled
- 모든 Promise의 완료를 기다리며, 각각의 결과 상태를 반환합니다.
const results = await Promise.allSettled([
fetchJson<User>('/api/user/1'),
fetchJson<User>('/api/user/2'),
fetchJson<User>('/api/user/3')
]);
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('Success:', result.value);
} else {
console.error('Error:', result.reason);
}
});
async 함수의 반환 타입 추론
타입스크립트는 async 함수의 반환 타입을 자동으로 Promise로 감싸 추론합니다.
async function getUser(id: number) {
// 함수의 반환 타입은 Promise<User>로 추론됨
return { id, name: 'John Doe' };
}
명시적 타입 지정도 가능합니다.
async function getUser(id: number): Promise<User> {
// ...
}
Best Practices와 주의사항
1. async 함수 내에서는 항상 await 사용하기
- 불필요한 Promise 체이닝을 방지하고 코드 가독성을 높입니다.
2. 병렬 실행 활용
- 독립적인 비동기 작업은 Promise.all을 사용하여 병렬로 실행합니다.
3. 적절한 에러 처리
- async 함수 내에서 try-catch 구문을 사용하여 에러를 처리합니다.
4. Promise 반환 타입 명시
- 복잡한 비동기 로직에서는 반환 타입을 명시적으로 지정하여 타입 안정성을 높입니다.
5. 불필요한 async 피하기
- 단순히 Promise를 반환하는 경우 async를 사용하지 않습니다.
6. 타입 가드 활용
- 비동기 결과의 타입을 좁히기 위해 타입 가드를 사용합니다.
7. 취소 가능한 비동기 작업 구현
- AbortController를 사용하여 필요한 경우 비동기 작업을 취소할 수 있게 합니다.
async function fetchWithCancel<T>(url: string, signal: AbortSignal): Promise<T> {
const response = await fetch(url, { signal });
return response.json();
}
const controller = new AbortController();
const promise = fetchWithCancel<User>('/api/user', controller.signal);
// 필요시 취소
controller.abort();
8. 비동기 함수의 문서화
- 복잡한 비동기 함수의 경우, JSDoc 주석을 사용하여 동작과 예외 상황을 문서화합니다.
9. 테스트 용이성 고려
- 비동기 코드 작성 시 단위 테스트 작성의 용이성을 고려합니다.
Promise와 async/await는 타입스크립트에서 비동기 프로그래밍의 핵심 요소입니다.
이들을 효과적으로 사용하면 복잡한 비동기 로직을 더 간결하고 이해하기 쉬운 코드로 작성할 수 있습니다.
타입스크립트의 정적 타입 시스템과 결합하면, 컴파일 타임에 많은 잠재적 오류를 잡아낼 수 있어 더욱 안정적인 비동기 코드를 작성할 수 있습니다.
제네릭을 활용한 Promise 처리는 타입 안정성을 크게 높여줍니다.
특히 API 호출과 같은 외부 데이터 처리 시 매우 유용합니다. 이를 통해 런타임 오류를 줄이고 IDE의 자동 완성 기능을 최대한 활용할 수 있습니다.