동기 vs 비동기 — 콜백, Promise, async/await
이 토픽을 마치면
동기와 비동기의 차이를 설명할 수 있고, 콜백 → Promise → async/await의 발전 과정을 이해합니다.
동기 — 한 줄씩 순서대로
동기(Synchronous) 는 코드가 위에서 아래로 한 줄씩 실행되는 방식입니다. 앞의 작업이 끝나야 다음 작업이 시작됩니다.
console.log("1. 주문 접수");
console.log("2. 커피 제조 (3분 소요)");
console.log("3. 커피 전달");
// 1 → 2 → 3 순서대로 실행직관적이지만 문제가 있습니다. 커피를 만드는 동안 아무것도 못 합니다. 파일 읽기에 5초가 걸리면 그동안 서버가 멈추고, DB 조회에 2초 걸리면 그동안 다른 사용자 요청을 처리하지 못합니다.
비동기 — 기다리지 않고 다음으로
비동기(Asynchronous) 는 시간이 오래 걸리는 작업을 시작해놓고, 완료를 기다리지 않고 다음 코드를 먼저 실행합니다. 작업이 끝나면 나중에 결과를 받습니다.
const fs = require("fs");
console.log("1. 파일 읽기 시작");
// 비동기 — 파일 읽기를 시작하고 바로 다음으로
fs.readFile("data.txt", "utf8", (err, data) => {
console.log("3. 파일 읽기 완료:", data);
});
console.log("2. 다음 작업 진행");
// 출력 순서:
// 1. 파일 읽기 시작
// 2. 다음 작업 진행 ← 기다리지 않고 먼저 실행!
// 3. 파일 읽기 완료: (파일 내용)출력 순서가 1 → 2 → 3이 아니라 1 → 2 → 3입니다. 숫자만 보면 같아 보이지만, 2번이 3번보다 먼저 실행됩니다. 파일 읽기가 끝나기를 기다리지 않았기 때문입니다.
콜백 — 비동기의 첫 번째 방법
위 코드에서 (err, data) => { ... } 부분이 콜백(callback) 함수입니다. "작업이 끝나면 이 함수를 불러줘"라는 뜻입니다.
문제는 비동기 작업이 연달아 필요할 때 발생합니다:
// 콜백 지옥 (callback hell)
fs.readFile("a.txt", "utf8", (err, a) => {
fs.readFile("b.txt", "utf8", (err, b) => {
fs.readFile("c.txt", "utf8", (err, c) => {
console.log(a + b + c);
// 들여쓰기가 계속 깊어짐...
});
});
});이렇게 콜백 안에 콜백이 중첩되면 코드가 읽기 어려워집니다. 이것을 콜백 지옥이라고 부릅니다.
Promise — 콜백 지옥의 해결
Promise는 "나중에 결과를 주겠다는 약속"입니다. .then()으로 연결하면 콜백 지옥 없이 순서대로 처리할 수 있습니다.
const fs = require("fs").promises;
fs.readFile("a.txt", "utf8")
.then(a => {
console.log("a 읽기 완료");
return fs.readFile("b.txt", "utf8");
})
.then(b => {
console.log("b 읽기 완료");
return fs.readFile("c.txt", "utf8");
})
.then(c => {
console.log("c 읽기 완료");
})
.catch(err => {
console.error("에러 발생:", err.message);
});들여쓰기가 깊어지지 않고, .catch()로 에러 처리도 한 곳에서 합니다.
async/await — 비동기를 동기처럼
async/await는 Promise를 더 읽기 쉽게 쓰는 문법입니다. 비동기 코드가 동기 코드처럼 위에서 아래로 읽힙니다.
const fs = require("fs").promises;
async function readAll() {
try {
const a = await fs.readFile("a.txt", "utf8");
const b = await fs.readFile("b.txt", "utf8");
const c = await fs.readFile("c.txt", "utf8");
console.log(a + b + c);
} catch (err) {
console.error("에러:", err.message);
}
}
readAll();await는 "이 작업이 끝날 때까지 기다려"라는 뜻이지만, 전체 프로그램을 멈추는 것이 아니라 이 함수만 잠시 멈추고 다른 작업은 계속 진행됩니다. await를 쓰려면 함수에 async를 붙여야 합니다.
현대 JavaScript에서는 거의 대부분 async/await를 사용합니다. AI에게 비동기 코드를 요청할 때도 "async/await로 작성해줘"라고 하면 가장 읽기 쉬운 코드가 나옵니다.
→ 바이오에 적용: DevBench — 비동기 패턴