C
JavaScript/비동기/Lesson 17

async / await — *비동기를 동기처럼*

45분·theory
이 챕터
3/3

async / await — *비동기를 동기처럼*

🎯 이 lesson 을 읽고 나면

이 lesson 을 다 읽고 나면 아래 3가지를 자신 있게 할 수 있습니다.

  • ✅ async / await 가 Promise 의 문법 설탕 인 이유
  • ✅ forEach 의 await 가 동작하지 않는 이유 + for-of 해결
  • ✅ AbortController 로 fetch 타임아웃 구현

학습 목표를 체크리스트로 두고 다 답할 수 있게 되면 lesson 을 닫으세요.

async / await

핵심 한 줄

async/await (ES2017) = Promise 를 동기 코드처럼 작성 하는 문법. then 체이닝보다 훨씬 읽기 좋음. 모던 JS 의 표준.

비교

javascript
// 🧪 가짜 API — 50ms 후 응답 (복붙 가능하게 mock)
const wait = (ms) => new Promise(r => setTimeout(r, ms));
const fetchUser   = (id) => wait(50).then(() => ({ id, name: '홍' }));
const fetchOrders = (uid) => wait(50).then(() => [{ orderId: 1, userId: uid }]);

// 👴 옛 방식 — then 체이닝 (중첩 들여쓰기)
function getUserDataOld(id) {
    return fetchUser(id)
        .then(user => {
            return fetchOrders(user.id)        // ← user 가 있어야 호출 가능
                .then(orders => ({ user, orders }));
        });
}

// 🆕 모던 방식 — async / await (동기처럼 평면적)
async function getUserData(id) {
    const user   = await fetchUser(id);        // ① user 받을 때까지 대기
    const orders = await fetchOrders(user.id); // ② 그 다음 orders 받기
    return { user, orders };
}

// ▶️ 호출 — 두 방식 결과 동일 (IIFE 로 top-level await 회피)
(async () => {
    const data = await getUserData(42);
    console.log(data);   // { user: { id: 42, name: '홍' }, orders: [{ orderId: 1, userId: 42 }] }
})();

동기 코드와 거의 동일한 모양. 직선적으로 읽힙니다.

규칙 2가지

1. awaitasync 함수 안에서만:

javascript
async function fn() {
    const x = await promise;   // ✅
}

const x = await promise;       // ❌ Top-level 에선 일부 환경만

2. async 함수는 항상 Promise 리턴:

javascript
async function fn() {
    return 42;       // 평범한 숫자 리턴인데...
}

const result = fn();           // Promise { 42 }   ← Promise 로 "감싸짐"!
console.log(result);           // Promise { 42 }

const v = await fn();          // 42   ← await 으로 "풀면" 진짜 값
console.log(v);                // 42

// 💡 async function 의 return X 는 모두 Promise 로 감싸짐
//    → 받는 쪽에서 .then() 또는 await 로 풀어야 함

에러 처리 — try-catch

javascript
async function getUserData(id) {
    try {
        const user = await fetchUser(id);
        const orders = await fetchOrders(user.id);
        return { user, orders };
    } catch (err) {
        console.error("실패:", err);
        throw err;          // 상위로 전파
    }
}

then-catch 보다 친숙한 try-catch 사용. 예외 흐름이 일반 코드와 동일.

병렬 — await 직렬 의 함정

javascript
// 🧪 가짜 API — 100ms 걸린다고 가정 (실제 fetch 대신 setTimeout 으로 흉내)
const wait = (ms) => new Promise(r => setTimeout(r, ms));
const fetchUser    = (id) => wait(100).then(() => ({ id, name: 'A' }));
const fetchOrders  = (id) => wait(100).then(() => [{ orderId: 1 }]);
const fetchProfile = (id) => wait(100).then(() => ({ bio: '개발자' }));

// ❌ 직렬 — 한 줄 끝나야 다음 줄 시작 (느림)
async function bad() {
    console.time('bad');
    const user    = await fetchUser(42);      // ⏱️ 0 → 100ms (대기)
    const orders  = await fetchOrders(42);    // ⏱️ 100 → 200ms
    const profile = await fetchProfile(42);   // ⏱️ 200 → 300ms
    console.timeEnd('bad');                   // bad: ~300ms
}

// ✅ 병렬 — 세 요청 "동시" 시작 (빠름)
async function good() {
    console.time('good');
    const [user, orders, profile] = await Promise.all([
        fetchUser(42),      // ← 동시 출발
        fetchOrders(42),    // ← 동시 출발
        fetchProfile(42)    // ← 동시 출발
    ]);
    console.timeEnd('good');                  // good: ~100ms (가장 느린 것만큼만)
}

// ▶️ 실행 (IIFE 로 감싸 top-level await 회피 → 어디서든 복붙 OK)
(async () => {
    await bad();    // 📤 bad: ~300ms
    await good();   // 📤 good: ~100ms   ← 3배 빠름!
})();

// 💡 직선적 await: "앞이 끝나야 뒤 시작" → 의존성 없는데도 줄줄이 기다림
//    Promise.all : "모두 동시 시작" → 가장 느린 것만큼만 걸림

요청들이 서로 독립적 이면 반드시 병렬.

실전 패턴 — 재시도

javascript
async function fetchWithRetry(url, retries = 3) {
    for (let i = 0; i < retries; i++) {
        try {
            const res = await fetch(url);
            if (res.ok) return res.json();
            throw new Error(`HTTP ${res.status}`);
        } catch (err) {
            if (i === retries - 1) throw err;
            await new Promise(r => setTimeout(r, 1000 * (i + 1)));   // 1·2·3초
        }
    }
}

Top-level await — 모듈에서

ES2022 부터 모듈 최상위에서 await 가능:

javascript
// module.mjs
const config = await fetch('/config.json').then(r => r.json());
export { config };

Node.js·모던 브라우저 (모듈) 에서 지원. 코드 진입점을 깔끔하게.

React 컴포넌트에서

jsx
function UserProfile({ id }) {
    const [user, setUser] = useState(null);

    useEffect(() => {
        // useEffect 콜백은 async 불가 (Promise 리턴하면 안 됨)
        // → 안에서 async 함수 정의 후 호출
        async function load() {
            const data = await fetch(`/api/users/${id}`).then(r => r.json());
            setUser(data);
        }
        load();
    }, [id]);

    if (!user) return <p>로딩 중...</p>;
    return <h1>{user.name}</h1>;
}

한 번 정리

  • async function + await Promise = 동기 같은 비동기
  • 에러는 try-catch
  • 독립 요청은 Promise.all 로 병렬
  • 모던 JS 의 표준 — then 체이닝은 거의 안 씀

⚡ 직접 해보기 — async / await + 직렬 vs 병렬

직렬 await 가 왜 느린지, Promise.all 이 왜 빠른지 시간 비교.
✏️ JS 코드
📟 콘솔 출력
▶ 실행 버튼을 눌러보세요
⚠️ 브라우저 샌드박스에서 실행 — console.log()만 지원, alert/fetch 불가

🤖 AI 에게 이렇게 요청해보세요

이 lesson 의 개념을 알면 AI 에게 구체적으로 지시할 수 있습니다. 막연한 "고쳐줘" 가 아니라 어휘를 가진 요청 — 그게 토큰 절약의 출발점입니다.

  • "이 forEach 안 await 가 동작 안 하는데 for...of 또는 Promise.all 로 바꿔줘"
  • "이 fetch 에 AbortController 타임아웃 추가해줘"
  • "이 함수에 try-catch + 친절한 에러 메시지 추가해줘"

왜 이게 토큰을 줄이나

개념을 모를 땐 AI 답변을 받고도 "그게 뭐예요?" 를 다시 물어야 합니다. 그 "다시 물음" 이 토큰을 잡아먹습니다. 개념 한 번 익혀두면 대화가 한 번에 끝납니다.

Async/Await - JavaScript