동기 비동기 차이 — async/await 쉽게 이해하기


“동기랑 비동기가 뭔지는 아는데, 막상 설명하려니 말이 안 나와요.” 개발을 배우다 보면 반드시 넘어야 하는 개념의 벽이 있습니다. 동기 비동기 차이가 바로 그 대표적인 벽입니다. 면접에서 단골로 나오고, 실무에서 API를 호출할 때마다 등장하며, 버그의 상당수가 이 개념을 제대로 이해하지 못해서 발생합니다. 그러나 막상 설명하려 하면 “기다리냐 안 기다리냐의 차이”라는 말만 맴돌고 더 깊이 들어가지 못하는 경우가 많습니다. 오늘은 카페 주문 비유부터 콜백·Promise·async/await 실전 코드까지, 개발 입문자도 완전히 이해할 수 있도록 동기와 비동기를 처음부터 끝까지 분해해 드립니다.


목차

  1. 동기란 무엇인가 — 줄 서서 기다리는 단일 창구
  2. 비동기란 무엇인가 — 번호표를 받고 자유롭게 움직이는 구조
  3. 동기 비동기 차이 — 블로킹·논블로킹과 함께 완전 비교
  4. 자바스크립트의 비동기 처리 진화 — 콜백 → Promise → async/await
  5. 비동기의 함정과 에러 처리 — 실수하기 쉬운 패턴 완전 정리
  6. 동기·비동기 선택 기준과 면접 답변 가이드

1. 동기란 무엇인가 — 줄 서서 기다리는 단일 창구

동기(Synchronous)를 가장 쉽게 이해하는 방법은 은행 창구 줄 서기입니다. 창구가 하나이고, 앞 사람이 업무를 완전히 마쳐야 다음 사람이 창구에 설 수 있습니다. 내가 아무리 급해도 앞 사람의 업무가 끝날 때까지 기다려야 합니다. 그 시간 동안 나는 아무것도 할 수 없습니다. 그냥 서 있어야 합니다.

프로그래밍에서 동기란 정확히 이 구조입니다. 코드가 위에서 아래로 한 줄씩 순서대로 실행되며, 한 작업이 완전히 끝나야 다음 작업이 시작됩니다. 이전 작업의 결과를 받아야 다음 작업을 진행할 수 있기 때문에 순서가 보장됩니다.

동기 코드의 실제 모습

python

# Python 동기 코드 예시
import time

def make_coffee():
    print("☕ 커피 만들기 시작")
    time.sleep(3)          # 3초 대기 — 이 동안 아무것도 못 함
    print("☕ 커피 완성!")
    return "아메리카노"

def make_sandwich():
    print("🥪 샌드위치 만들기 시작")
    time.sleep(2)          # 2초 대기
    print("🥪 샌드위치 완성!")
    return "BLT 샌드위치"

def serve_customer():
    print("=== 주문 시작 ===")

    coffee = make_coffee()      # 커피가 완성될 때까지 3초 대기
    sandwich = make_sandwich()  # 커피 완성 후 샌드위치 시작 — 2초 대기

    print(f"✅ 제공: {coffee} + {sandwich}")
    print(f"⏱ 총 소요 시간: 약 5초 (3+2)")

serve_customer()

# 출력 순서 (항상 이 순서로 실행됨):
# === 주문 시작 ===
# ☕ 커피 만들기 시작
# (3초 대기)
# ☕ 커피 완성!
# 🥪 샌드위치 만들기 시작
# (2초 대기)
# 🥪 샌드위치 완성!
# ✅ 제공: 아메리카노 + BLT 샌드위치
# ⏱ 총 소요 시간: 약 5초 (3+2)

동기 코드의 핵심은 예측 가능성입니다. 순서가 항상 보장되기 때문에 코드 흐름을 머릿속에서 따라가기 쉽습니다. 그러나 한 작업이 오래 걸리면 그 시간 동안 프로그램 전체가 멈추는 블로킹(Blocking) 현상이 발생합니다. 3초짜리 커피를 기다리는 동안 다른 어떤 고객도 서비스받을 수 없는 것처럼요.

동기가 적합한 상황

동기 처리가 최선인 경우도 분명히 있습니다. 이전 작업의 결과물이 다음 작업의 입력으로 반드시 필요한 경우입니다. 파일을 읽고 → 그 내용을 파싱하고 → 파싱된 데이터를 가공하는 것처럼, 각 단계가 이전 단계에 의존하는 경우에는 순서 보장이 필수입니다. 또한 단순한 계산이나 소규모 스크립트처럼 비동기 처리의 복잡성을 도입할 필요가 없는 경우에도 동기가 더 깔끔한 선택입니다.


2. 비동기란 무엇인가 — 번호표를 받고 자유롭게 움직이는 구조

비동기(Asynchronous)를 이해하는 가장 좋은 비유는 패스트푸드 매장의 번호표 시스템입니다. 주문을 마치면 번호표를 받고 자리로 돌아갑니다. 음식이 준비되는 동안 나는 자리에서 스마트폰을 보거나 친구와 대화할 수 있습니다. 번호가 호출되면 그때 가서 음식을 받으면 됩니다. 음식이 나올 때까지 카운터 앞에서 멍하니 서 있지 않아도 됩니다.

프로그래밍에서 비동기란 이 구조입니다. 시간이 오래 걸리는 작업을 시작해 놓고, 그 작업이 완료되기를 기다리지 않고 다른 코드를 계속 실행합니다. 작업이 완료되면 미리 등록해둔 콜백(Callback) 함수나 Promise 등의 메커니즘을 통해 결과를 처리합니다.

비동기 코드의 실제 모습

javascript

// JavaScript 비동기 코드 예시 (setTimeout으로 지연 시뮬레이션)
console.log("=== 주문 시작 ===");

// 커피 주문 — 3초 후 완성, 기다리지 않고 바로 다음 줄 실행
setTimeout(() => {
    console.log("☕ 커피 완성!");
}, 3000);

// 샌드위치 주문 — 2초 후 완성, 커피를 기다리지 않고 동시 시작
setTimeout(() => {
    console.log("🥪 샌드위치 완성!");
}, 2000);

console.log("💺 자리에 앉아서 다른 일 중...");
console.log("⏱ 총 소요 시간: 약 3초 (병렬 처리)");

// 실제 출력 순서:
// === 주문 시작 ===
// 💺 자리에 앉아서 다른 일 중...
// ⏱ 총 소요 시간: 약 3초 (병렬 처리)
// (2초 후) 🥪 샌드위치 완성!
// (3초 후) ☕ 커피 완성!

동기라면 5초(3+2)가 걸렸을 작업이 비동기에서는 3초(더 오래 걸리는 작업의 시간)만에 끝납니다. 커피와 샌드위치를 동시에 만들기 시작했기 때문입니다. 이것이 비동기의 핵심 이점, 효율적인 시간 활용입니다.

자바스크립트는 왜 비동기가 필수인가

자바스크립트는 싱글 스레드(Single Thread) 언어입니다. 한 번에 하나의 작업만 처리할 수 있다는 의미입니다. 그렇다면 “어떻게 비동기 처리가 가능한가?”라는 의문이 생깁니다. 이 질문의 답이 바로 이벤트 루프(Event Loop) 입니다.

자바스크립트 비동기 동작 구조:

┌─────────────────┐    ┌──────────────┐    ┌─────────────────┐
│   Call Stack    │    │  Web APIs    │    │  Callback Queue │
│ (코드 실행 공간) │    │ (브라우저 제공)│    │  (완료된 콜백)  │
│                 │    │              │    │                 │
│  main()         │───▶│ setTimeout() │───▶│  () => "완료"   │
│  console.log()  │    │ fetch()      │    │                 │
└────────┬────────┘    │ DOM 이벤트   │    └────────┬────────┘
         │             └──────────────┘             │
         │                                          │
         └──────────── Event Loop ◀─────────────────┘
                      (Call Stack이 비면
                       Queue에서 하나씩 가져옴)

비동기 작업(setTimeout, fetch, 이벤트 리스너 등)은 Web APIs에 위임됩니다. 자바스크립트 엔진은 이 작업을 기다리지 않고 다음 코드를 계속 실행합니다. Web APIs가 작업을 완료하면 콜백 함수를 Callback Queue에 넣습니다. 이벤트 루프는 Call Stack이 완전히 비어 있을 때만 Queue에서 콜백을 꺼내 실행합니다.


3. 동기 비동기 차이 — 블로킹·논블로킹과 함께 완전 비교

동기 비동기 차이를 정확히 이해하려면 자주 혼용되는 블로킹(Blocking)·논블로킹(Non-blocking)의 개념도 함께 파악해야 합니다.

핵심 개념 4가지 비교표

구분정의핵심 질문예시
동기(Sync)작업 완료를 기다린 후 다음 코드 실행“결과가 나올 때까지 기다리나?”은행 창구 대기
비동기(Async)작업 완료를 기다리지 않고 다음 코드 실행“완료 통보를 받아서 처리하나?”패스트푸드 번호표
블로킹(Blocking)호출한 함수가 제어권을 돌려줄 때까지 대기“다른 작업을 할 수 있나?”줄 서서 꼼짝 못함
논블로킹(Non-blocking)호출 즉시 제어권을 반환, 다른 작업 가능“바로 제어권을 돌려받나?”번호표 받고 자유롭게
조합으로 이해하는 4가지 패턴:

1. 동기 + 블로킹 (가장 일반적)
   → 결과 나올 때까지 기다리며 다른 일 못 함
   → 예: Python requests.get() 기본 동작

2. 동기 + 논블로킹 (드문 경우)
   → 완료됐는지 주기적으로 확인하며 기다림 (Polling)
   → 예: while 루프로 파일 I/O 상태 확인

3. 비동기 + 블로킹 (잘못된 패턴)
   → 비동기 작업을 시작하고 결과를 강제로 기다림
   → 예: async 함수 안에서 동기 대기 함수 호출

4. 비동기 + 논블로킹 (현대 웹 표준)
   → 작업 시작 후 즉시 제어권 반환, 완료 시 콜백 실행
   → 예: fetch(), addEventListener()

코드로 보는 동기 vs 비동기의 실행 순서 차이

javascript

// 실행 순서로 차이 이해하기

// ===== 동기 (예상 가능한 순서) =====
console.log("1. 시작");
console.log("2. 중간");
console.log("3. 끝");
// 출력: 1 → 2 → 3 (항상 이 순서)

// ===== 비동기 (예상과 다른 순서!) =====
console.log("1. 시작");

setTimeout(() => {
    console.log("2. 타임아웃 완료");   // 0초 후에도 마지막에 실행!
}, 0);

console.log("3. 끝");

// 출력: 1 → 3 → 2
// "0초" 타임아웃이어도 이벤트 루프 구조 때문에 마지막에 실행!
// 이것이 비동기의 핵심 — 타이밍이 예상과 다를 수 있다

이 예시에서 setTimeout(fn, 0)은 “0밀리초 후에 실행해줘”라고 요청하지만, 이벤트 루프 구조 때문에 현재 Call Stack이 완전히 비워진 후에야 실행됩니다. 이 동작을 이해하지 못하면 비동기 관련 버그를 잡기 매우 어렵습니다.


4. 자바스크립트의 비동기 처리 진화 — 콜백 → Promise → async/await

자바스크립트의 비동기 처리 방식은 세 단계로 진화해왔습니다. 각 단계가 왜 등장했는지, 무엇을 해결했는지를 이해하면 async/await를 단순 문법이 아니라 필연적 진화로 이해할 수 있습니다.

1세대: 콜백(Callback) — 비동기의 시작과 지옥

콜백은 “작업이 완료되면 이 함수를 호출해줘”라고 미리 등록해두는 방식입니다. 비동기의 가장 기본적인 해결책이지만, 중첩이 깊어지면 악명 높은 콜백 지옥(Callback Hell) 이 만들어집니다.

javascript

// 콜백 기본 패턴
function fetchUserData(userId, callback) {
    setTimeout(() => {
        const user = { id: userId, name: "김개발" };
        callback(null, user);     // (에러, 결과) 관례
    }, 1000);
}

fetchUserData(1, (err, user) => {
    if (err) {
        console.error("에러:", err);
        return;
    }
    console.log("유저:", user.name);
});

// ❌ 콜백 지옥 — 중첩이 깊어질수록 가독성 파괴
fetchUser(userId, (err, user) => {
    if (err) return handleError(err);
    fetchPosts(user.id, (err, posts) => {
        if (err) return handleError(err);
        fetchComments(posts[0].id, (err, comments) => {
            if (err) return handleError(err);
            fetchLikes(comments[0].id, (err, likes) => {
                if (err) return handleError(err);
                // 이 안에서 또 중첩...
                // 코드가 오른쪽으로 계속 밀려남 → "Pyramid of Doom"
                console.log("최종 데이터:", likes);
            });
        });
    });
});

콜백 지옥의 문제는 단순히 가독성만이 아닙니다. 에러 처리가 각 콜백 안에서 개별적으로 이루어져야 하고, 실행 흐름을 추적하기 어려우며, 공통 에러 처리 로직을 만들기가 매우 까다롭습니다.

2세대: Promise — 콜백 지옥의 탈출구

Promise는 “미래에 완료될 작업의 결과를 담는 약속 상자”입니다. 세 가지 상태를 가집니다. 대기(Pending), 이행(Fulfilled), 거부(Rejected)입니다. Promise는 .then()으로 체이닝하고 .catch()로 에러를 한 곳에서 처리할 수 있어 콜백 지옥을 구조적으로 해결합니다.

javascript

// Promise 기본 생성
function fetchUserData(userId) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (!userId) {
                reject(new Error("userId가 필요합니다"));  // 실패
                return;
            }
            const user = { id: userId, name: "김개발" };
            resolve(user);   // 성공
        }, 1000);
    });
}

// Promise 체이닝 — 콜백 지옥 없이 순차 처리
fetchUser(userId)
    .then(user => fetchPosts(user.id))       // 성공 시 다음 작업
    .then(posts => fetchComments(posts[0].id))
    .then(comments => fetchLikes(comments[0].id))
    .then(likes => {
        console.log("최종 데이터:", likes);
    })
    .catch(err => {
        console.error("에러:", err);         // 에러 한 곳에서 처리!
    })
    .finally(() => {
        console.log("완료 (성공/실패 무관)"); // 항상 실행
    });

// Promise.all — 여러 비동기 작업 병렬 실행
Promise.all([
    fetchUser(1),
    fetchUser(2),
    fetchUser(3)
])
.then(([user1, user2, user3]) => {
    console.log("3명 동시 조회 완료");     // 가장 느린 작업 기준으로 완료
})
.catch(err => {
    console.error("하나라도 실패하면 catch"); // 하나라도 실패 시 전체 reject
});

// Promise.allSettled — 실패해도 모든 결과 받기
Promise.allSettled([
    fetchUser(1),
    Promise.reject("에러 발생"),
    fetchUser(3)
])
.then(results => {
    results.forEach(result => {
        if (result.status === "fulfilled") {
            console.log("성공:", result.value);
        } else {
            console.log("실패:", result.reason);
        }
    });
});

// Promise.race — 가장 빠른 것 하나만
Promise.race([
    fetchFromServer1(),
    fetchFromServer2(),
    fetchFromServer3()
])
.then(fastest => {
    console.log("가장 빠른 서버 응답:", fastest);
});

3세대: async/await — 비동기를 동기처럼 읽게

async/await는 Promise를 기반으로 동작하지만, 마치 동기 코드처럼 읽히도록 문법적 설탕(Syntactic Sugar)을 제공합니다. async 키워드가 붙은 함수는 항상 Promise를 반환하고, await는 Promise가 완료될 때까지 해당 줄에서 대기합니다. 단, await는 반드시 async 함수 안에서만 사용할 수 있습니다.

javascript

// async/await 기본 패턴
async function getUserData(userId) {
    try {
        // await — Promise가 완료될 때까지 대기 (블로킹처럼 보이지만 내부는 비동기!)
        const user = await fetchUser(userId);        // 1초 대기
        const posts = await fetchPosts(user.id);    // 1초 대기
        const comments = await fetchComments(posts[0].id);  // 1초 대기

        return comments;
    } catch (err) {
        // try/catch로 동기 코드처럼 에러 처리
        console.error("에러 발생:", err);
        throw err;  // 에러 재발생으로 호출자에게 전파
    } finally {
        console.log("항상 실행");
    }
}

// async 함수 호출
getUserData(1)
    .then(data => console.log("결과:", data))
    .catch(err => console.error("최종 에러:", err));

// 또는 최상위에서 async/await 사용 (Top-level await — ES2022+)
// const data = await getUserData(1);

// ✅ 병렬 처리가 필요할 때 — await를 잘못 쓰면 순차 실행이 됨!
async function goodParallel() {
    // ❌ 잘못된 패턴 — 순차 실행 (3초 소요)
    const user1 = await fetchUser(1);   // 1초
    const user2 = await fetchUser(2);   // 1초
    const user3 = await fetchUser(3);   // 1초

    // ✅ 올바른 병렬 패턴 — Promise.all 활용 (1초 소요)
    const [u1, u2, u3] = await Promise.all([
        fetchUser(1),
        fetchUser(2),
        fetchUser(3)
    ]);

    console.log(u1, u2, u3);
}

3세대 진화 한눈에 비교

javascript

// 같은 로직을 세 가지 방식으로 구현

const userId = 1;

// 1️⃣ 콜백 방식
fetchUser(userId, (err, user) => {
    if (err) return console.error(err);
    fetchPosts(user.id, (err, posts) => {
        if (err) return console.error(err);
        console.log(posts);
    });
});

// 2️⃣ Promise 방식
fetchUser(userId)
    .then(user => fetchPosts(user.id))
    .then(posts => console.log(posts))
    .catch(err => console.error(err));

// 3️⃣ async/await 방식 (현재 표준)
async function main() {
    try {
        const user = await fetchUser(userId);
        const posts = await fetchPosts(user.id);
        console.log(posts);
    } catch (err) {
        console.error(err);
    }
}
main();

// 세 가지 모두 같은 동작 — 가독성과 에러 처리 편의성이 다름

5. 비동기의 함정과 에러 처리 — 실수하기 쉬운 패턴 완전 정리

비동기를 이해했다고 해도 실무에서는 특정 패턴에서 반복적으로 실수가 발생합니다. 가장 자주 마주치는 함정들을 미리 파악해두면 버그를 예방할 수 있습니다.

함정 1. await 없이 async 함수를 호출하는 실수

javascript

async function fetchData() {
    const response = await fetch("https://api.example.com/data");
    return response.json();
}

// ❌ 잘못된 사용 — await 없이 호출하면 Promise 객체 그대로 반환
const data = fetchData();
console.log(data);  // Promise { <pending> } ← 데이터가 아님!

// ✅ 올바른 사용
const data = await fetchData();  // async 함수 안에서
// 또는
fetchData().then(data => console.log(data));

함정 2. forEach 안에서 await 사용 불가

javascript

const userIds = [1, 2, 3];

// ❌ 잘못된 패턴 — forEach는 async 콜백을 기다리지 않음
userIds.forEach(async (id) => {
    const user = await fetchUser(id);
    console.log(user);  // 순서 보장 안 됨, 에러 처리 불가
});
console.log("완료?");  // 실제로는 위 작업들이 아직 진행 중

// ✅ 순차 처리 — for...of 사용
async function processSequential() {
    for (const id of userIds) {
        const user = await fetchUser(id);  // 하나씩 순서대로
        console.log(user);
    }
}

// ✅ 병렬 처리 — Promise.all 사용
async function processParallel() {
    const users = await Promise.all(
        userIds.map(id => fetchUser(id))  // 동시에 시작
    );
    console.log(users);  // 모두 완료 후 한 번에
}

함정 3. 에러를 잡지 못하는 비동기 코드

javascript

// ❌ 에러 처리 없는 Promise — Unhandled Promise Rejection
fetchUser(999)
    .then(user => console.log(user));
// 에러 발생 시 프로그램이 경고 없이 조용히 실패

// ❌ async 함수 에러를 밖에서 못 잡는 실수
async function riskyOperation() {
    throw new Error("실패!");
}

try {
    riskyOperation();   // await 없이 호출 → catch로 못 잡힘!
} catch (err) {
    console.log("잡혔나요?");  // 실행 안 됨
}

// ✅ 올바른 에러 처리
// 방법 1. async/await + try/catch
async function safeOperation() {
    try {
        const user = await fetchUser(999);
        return user;
    } catch (err) {
        console.error("처리된 에러:", err.message);
        return null;  // 기본값 반환
    }
}

// 방법 2. Promise 체인 + .catch()
fetchUser(999)
    .then(user => processUser(user))
    .catch(err => {
        console.error("에러:", err.message);
        return null;
    });

// 방법 3. 전역 에러 핸들러 (최후 수단)
window.addEventListener("unhandledrejection", (event) => {
    console.error("잡히지 않은 Promise 에러:", event.reason);
    event.preventDefault();
});

함정 4. 실전 API 호출 완전 패턴

javascript

// 실무에서 사용하는 완성형 API 호출 패턴
async function apiCall(url, options = {}) {
    const controller = new AbortController();  // 요청 취소용
    const timeoutId = setTimeout(() => controller.abort(), 5000);  // 5초 타임아웃

    try {
        const response = await fetch(url, {
            ...options,
            signal: controller.signal,
            headers: {
                "Content-Type": "application/json",
                ...options.headers,
            },
        });

        clearTimeout(timeoutId);

        // HTTP 에러 처리 (fetch는 4xx, 5xx를 에러로 던지지 않음!)
        if (!response.ok) {
            throw new Error(`HTTP 에러: ${response.status} ${response.statusText}`);
        }

        return await response.json();

    } catch (err) {
        clearTimeout(timeoutId);

        if (err.name === "AbortError") {
            throw new Error("요청 시간 초과 (5초)");
        }
        throw err;  // 에러 재발생
    }
}

// 사용 예시
async function loadUserProfile(userId) {
    try {
        const [user, posts, followers] = await Promise.all([
            apiCall(`/api/users/${userId}`),
            apiCall(`/api/users/${userId}/posts`),
            apiCall(`/api/users/${userId}/followers`),
        ]);

        return { user, posts, followers };

    } catch (err) {
        console.error("프로필 로드 실패:", err.message);
        return null;
    }
}

6. 동기·비동기 선택 기준과 면접 답변 가이드

동기 vs 비동기 선택 기준 체크리스트

동기를 선택해야 할 때:
□ 이전 작업 결과가 다음 작업의 입력으로 반드시 필요할 때
□ 실행 순서가 반드시 보장되어야 할 때
□ 단순 계산·메모리 내 연산처럼 대기 시간이 없을 때
□ 코드 복잡도를 최소화해야 할 때 (소규모 스크립트)

비동기를 선택해야 할 때:
□ 네트워크 요청 (API 호출, 데이터 fetch)
□ 파일 I/O (읽기·쓰기)
□ 데이터베이스 쿼리
□ 타이머·지연 실행
□ 사용자 이벤트 처리 (클릭, 입력)
□ 여러 독립적인 작업을 병렬로 처리해야 할 때
□ UI가 멈추면 안 되는 브라우저 환경

면접에서 자주 나오는 질문 모범 답변

Q: “동기와 비동기의 차이를 설명해 주세요”

“동기는 작업이 완전히 끝날 때까지 다음 코드 실행을 기다리는 방식입니다. 실행 순서가 보장되어 예측 가능하지만, 오래 걸리는 작업이 있으면 그 동안 프로그램 전체가 멈추는 블로킹이 발생합니다. 비동기는 작업을 시작해 놓고 완료를 기다리지 않고 다음 코드를 즉시 실행하는 방식입니다. 작업이 완료되면 콜백·Promise·async/await를 통해 결과를 처리합니다. 네트워크 요청이나 파일 I/O처럼 대기 시간이 긴 작업에서 비동기를 사용하면 그 시간 동안 다른 작업을 처리할 수 있어 효율적입니다.”

Q: “자바스크립트는 싱글 스레드인데 어떻게 비동기가 가능한가요?”

“자바스크립트 엔진 자체는 싱글 스레드이지만, 브라우저나 Node.js가 제공하는 Web APIs와 이벤트 루프 덕분에 비동기가 가능합니다. setTimeout이나 fetch 같은 비동기 작업은 Web APIs에 위임되어 별도로 처리됩니다. 작업이 완료되면 콜백 함수가 Callback Queue에 들어가고, 이벤트 루프가 Call Stack이 비어있을 때 Queue에서 꺼내 실행합니다. 자바스크립트 엔진 자체가 멀티스레드가 아니라 런타임 환경이 비동기를 지원하는 구조입니다.”

Q: “Promise와 async/await의 차이는 무엇인가요?”

“async/await는 Promise를 기반으로 동작하는 문법적 설탕입니다. 기능적으로 동일하지만, async/await는 비동기 코드를 마치 동기 코드처럼 순서대로 읽을 수 있게 해줍니다. Promise 체인(.then().catch())보다 가독성이 높고, try/catch로 동기 코드와 동일한 방식으로 에러를 처리할 수 있습니다. 그러나 Promise.all 같은 병렬 처리가 필요할 때는 await와 Promise를 함께 활용합니다.”

추천 학습 도구

도구·자료활용 목적
Loupe (latentflip.com)이벤트 루프 시각화 — Call Stack·Web API·Queue 실시간 확인
JavaScript Visualizer 9000코드 실행 흐름과 Promise 상태 시각화
MDN Web Docs — PromisePromise API 공식 레퍼런스·예제
Node.js 공식 문서서버 사이드 비동기 I/O 패턴
《You Don’t Know JS》카일 심슨 저, 비동기 챕터 — JS 비동기의 바이블
《자바스크립트 딥다이브》이웅모 저, 국내 최고의 JS 기초 교재

결론

동기 비동기 차이의 핵심은 단 하나입니다. 동기는 작업이 끝날 때까지 기다리고, 비동기는 기다리지 않고 다음을 실행하다가 완료 통보를 받습니다. 자바스크립트에서 비동기는 콜백이라는 원시적 방식에서 시작해 Promise의 체이닝, async/await의 동기 스타일 문법으로 진화해왔습니다. 세 가지 모두 결국 같은 목표, 즉 오래 걸리는 작업 중에도 프로그램이 멈추지 않도록 하는 것을 추구합니다. 오늘 바로 Loupe 사이트에서 setTimeout 코드를 입력하고 이벤트 루프가 어떻게 동작하는지 눈으로 확인해 보세요. 그 순간 동기와 비동기가 완전히 이해될 것입니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다