icon안동민 개발노트

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의 자동 완성 기능을 최대한 활용할 수 있습니다.