JavaScript 비동기 처리의 이해: Promise, Async/Await 그리고 Generator

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 함수는 실행을 중간에 멈췄다가 나중에 다시 시작할 수 있는 특수한 함수입니다. yieldnext()를 통해 외부와 데이터를 주고받으며 제어 흐름을 관리합니다.

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)에서 복잡한 비동기 흐름을 제어하는 데 주로 활용됩니다.

태그: JavaScript ES6 Promise AsyncAwait generator

8월 12일 06:26에 게시됨