JavaScript에서 비동기 처리는 효율적인 어플리케이션 개발의 핵심입니다. 과거의 콜백(Callback) 지옥에서 벗어나 코드의 가독성과 유지보수성을 높이기 위해 도입된 Promise, Async/Await, 그리고 Generator의 핵심 개념과 활용법을 정리합니다.
1. Promise: 비동기 상태의 객체화
Promise는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체입니다. 단순히 비동기 함수를 실행하는 것이 아니라, 비동기 작업의 결과 상태를 관리하는 구조를 제공합니다.
Promise의 기본 구조
// 일반적인 비동기 처리 (기존 방식)
setTimeout(() => {
console.log('3초 후 실행');
}, 3000);
// Promise를 활용한 구조 개선
const delayPrint = (ms) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve('작업 완료');
}, ms);
});
};
delayPrint(3000).then((message) => {
console.log(message);
});
Promise의 상태와 내부 속성
JavaScript 엔진 내부에서 Promise는 [[PromiseState]]와 [[PromiseResult]]라는 내부 속성을 가집니다. 상태는 오직 세 가지 중 하나이며, 한 번 결정되면 변경되지 않습니다.
- Pending: 초기 상태, 완료되지 않음.
- Fulfilled (Resolved): 작업이 성공적으로 완료됨.
- Rejected: 작업이 실패함.
주요 정적 메서드
- Promise.resolve(): 주어진 값을 담은 성공 상태의 Promise를 즉시 반환합니다.
- Promise.reject(): 에러 사유를 담은 실패 상태의 Promise를 반환하며, 반드시
.catch()로 처리해야 에러 로그 발생을 막을 수 있습니다. - Promise.all(): 여러 비동기 작업을 병렬로 처리하며, 모든 작업이 성공해야 결과를 배열로 반환합니다. 하나라도 실패하면 즉시 거부됩니다.
- Promise.race(): 가장 먼저 완료되는 비동기 작업의 결과만을 반환합니다.
2. 실행 순서와 마이크로태스크(Microtask)
Promise의 콜백은 마이크로태스크 큐에서 관리되어, setTimeout과 같은 매크로태스크(Macrotask)보다 우선순위가 높습니다.
console.log('시작');
setTimeout(() => console.log('매크로태스크 (setTimeout)'), 0);
Promise.resolve().then(() => {
console.log('마이크로태스크 (Promise)');
});
console.log('끝');
// 실행 결과: 시작 -> 끝 -> 마이크로태스크 -> 매크로태스크
3. Async/Await: 동기 코드처럼 작성하는 비동기
Async/Await는 Promise를 기반으로 하며, 비동기 코드를 마치 동기 코드인 것처럼 읽기 쉽게 작성할 수 있게 해주는 문법적 설탕(Syntactic Sugar)입니다.
특징과 동작 방식
async함수는 항상 Promise를 반환합니다. 명시적으로 값을 반환하면Promise.resolve()로 감싸집니다.await는 Promise가 처리될 때까지 함수의 실행을 일시 중단하지만, JavaScript 엔진의 메인 스레드를 차단하지는 않습니다.
const fetchData = async (id) => {
try {
// 비동기 요청이 순차적으로 진행됨
const response = await fetch(`https://api.example.com/user/${id}`);
const user = await response.json();
return user.name;
} catch (error) {
console.error('데이터 로드 실패:', error);
}
};
// 병렬 처리 최적화
const fetchMultipleUsers = async (ids) => {
const userPromises = ids.map(id => fetchData(id));
const names = await Promise.all(userPromises);
console.log(names);
};
4. Generator: 실행 제어의 주도권
Generator 함수는 실행을 중간에 멈췄다가 나중에 다시 시작할 수 있는 특수한 함수입니다. yield와 next()를 통해 외부와 데이터를 주고받으며 제어 흐름을 관리합니다.
function* numberGenerator(start) {
let current = start;
while (true) {
const increment = yield current;
if (increment) {
current += increment;
} else {
current++;
}
}
}
const gen = numberGenerator(10);
console.log(gen.next().value); // 10
console.log(gen.next(5).value); // 15 (yield의 반환값으로 5를 전달)
console.log(gen.next().value); // 16
Generator는 비동기 프로그래밍에서 반복자(Iterator)를 생성하거나, 라이브러리 수준(예: Redux-Saga)에서 복잡한 비동기 흐름을 제어하는 데 주로 활용됩니다.