11장: 비동기 프로그래밍 – 프로미스, async/await, 그리고 타입스크립트
비동기 프로그래밍의 세계: TypeScript에서 약속과 async/await 이해하기
웹 개발이 발전하면서 데이터 통신, API 호출, 사용자 상호작용 등 작업을 동시에 부드럽게 처리하는 비동기 프로그래밍의 필요성은 더욱 커졌습니다. TypeScript는 자바스크립트의 동시성 모델을 고스란히 계승하면서, 여기에 강력한 타입 시스템을 결합해 더욱 안정적이고 효율적인 비동기 코드를 작성할 수 있게 해줍니다.
프로미스(Promise): 미래의 값을 다루는 방법
가장 기본이 되는 비동기 도구는 Promise입니다. 프로미스는 '아직 결정되지 않은 값'을 다루는 객체로, 실행 결과가 준비되길 기다렸다가 성공(fulfilled) 또는 실패(rejected) 여부에 따라 콜백을 실행합니다. 예를 들어 서버에서 데이터를 요청할 때, Promise를 사용하면 "언젠가는 값이 올 것"임을 명확히 표현할 수 있습니다.
function fetchData(): Promise<string> {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('데이터 전송 완료');
}, 1000);
});
}
TypeScript에서는 제네릭으로 반환 타입을 정하게 되어, 예기치 못한 값이 사용되는 실수를 줄여줍니다.
async와 await: 읽기 쉬운 비동기 코드
비동기 코드가 많아질수록 콜백지옥이나 지나친 then 체인이 발생하기 쉽습니다. 이를 해결한 문법이 async/await입니다. async 함수는 Promise를 반환하며, await는 비동기 작업이 끝날 때까지 기다립니다. 이 문법으로 인해 비동기 코드가 마치 동기식 코드처럼 읽히고 유지보수가 쉬워집니다.
async function showData() {
try {
const data = await fetchData();
console.log(data);
} catch (e) {
console.error('에러 발생:', e);
}
}
복잡한 조건문이나 반복문 안에서도 동기식 흐름에 최대한 가깝게 코드를 설계할 수 있습니다.
병렬 처리와 고급 패턴
여러 비동기 작업이 상호 의존적이지 않다면, Promise.all로 병렬 처리가 가능합니다. 이렇게 하면 각각의 작업이 완료될 때까지 '모두' 대기하고, 효율적인 자원 활용이 이루어집니다.
const result = await Promise.all([fetchA(), fetchB(), fetchC()]);
특정 작업만 먼저 처리해야 한다면 Promise.race, 여러 작업 중 일부만 실패해도 전체 취소가 필요한 경우 AbortController API를 활용하는 식으로 다양한 비동기 전략을 설계할 수 있습니다.
타입스크립트에서의 타입 안정성과 실전 조언
TypeScript는 비동기 함수의 반환값에도 타입을 적용하여, 데이터 흐름을 확실하게 추론할 수 있습니다. API에서 실패하거나 데이터 형식이 예고와 다를 때도, 타입 정의만으로 코드를 단단하게 지킬 수 있죠. 또한, try-catch 문법과 결합해, 예상치 못한 에러나 예외 상황 역시 안전하게 다룰 수 있습니다.
비동기 프로그래밍에서 중요한 점은 명확한 데이터 흐름과 에러 처리, 그리고 타입의 일관성입니다. 복잡한 비동기 로직도 TypeScript와 함께라면 한층 더 예측 가능하고 유지보수하기 쉬운 코드로 짤 수 있습니다.