CS Fundamentals
동기와 비동기에 대해서 설명해주세요
실행 흐름 시뮬레이션으로 직접 체험하는 Sync vs Async. 이벤트 루프부터 Promise.all 병렬화까지, 면접에서 자주 묻는 비동기 개념을 완벽히 준비합니다.
2026년 3월 7일 · 약 12분 읽기
Q. "동기(Synchronous)와 비동기(Asynchronous)의 차이를 설명하고, JavaScript에서 비동기를 어떻게 처리하는지 말씀해주세요."
예상 꼬리질문
답변 가이드
"동기는 작업이 끝날 때까지 다음 코드가 실행되지 않는 블로킹 방식이고, 비동기는 완료를 기다리지 않고 즉시 다음으로 넘어가는 논블로킹 방식입니다."
"JavaScript는 단일 스레드이지만 이벤트 루프(Event Loop)를 통해 비동기를 구현합니다. Call Stack이 비는 순간 Callback Queue의 작업을 실행하는 방식입니다."
"비동기 코드 작성 방식은 콜백 → Promise → async/await 순서로 진화했고, 독립적인 여러 비동기 작업은 Promise.all()로 병렬화해야 성능을 최대로 낼 수 있습니다."
서버에 데이터를 요청할 때, 우리는 보통 두 가지 방식 중 하나를 선택합니다. "데이터가 올 때까지 기다렸다가 다음 작업을 한다" — 아니면 "일단 요청을 보내고, 기다리는 동안 다른 일을 한다". 이 차이가 바로 동기(Synchronous)와 비동기(Asynchronous)의 본질입니다.
처음에는 단순한 개념처럼 보이지만, 이 차이를 제대로 이해하지 못하면 비동기로 코드를 짰는데 왜 느리냐는 질문을 하게 됩니다. async/await를 쓰는데도 순차적으로 실행되고, JavaScript가 단일 스레드라는데 어떻게 동시에 여러 요청을 처리하는지 헷갈립니다.
이 아티클에서는 실행 흐름 시뮬레이션부터 이벤트 루프 다이어그램, 비동기 패턴의 역사까지 — 직접 체험하며 이해해 보겠습니다.
1. 동기 vs 비동기 — 실행 흐름의 차이
꼬리질문: "동기와 비동기의 실행 흐름이 실제로 얼마나 차이나나요?"
동기(Synchronous) 방식은 커피숍에서 카운터 앞에 서서 커피가 나올 때까지 꼼짝도 않고 기다리는 것과 같습니다. 코드가 한 줄씩 순서대로 실행되고, 이전 작업이 완료되어야 다음 줄로 넘어갑니다.
비동기(Asynchronous) 방식은 진동벨을 받고 자리에 앉아 다른 일을 하다가 벨이 울리면 커피를 가져오는 것입니다. 작업을 시작하고 완료를 기다리지 않고 즉시 다음 코드를 실행합니다.
아래에서 API 호출(300ms), DB 조회(200ms), 파일 읽기(150ms) 세 작업을 동기와 비동기로 각각 실행해 보세요.
동기: 작업 A가 끝나야 B가 시작됩니다. 총 650ms 소요.
동기는 650ms, 비동기는 300ms. 2.2배의 성능 차이가 납니다. 물론 이 차이는 I/O 작업(네트워크, 파일, DB)에서 두드러집니다. CPU 연산만 하는 작업에서는 비동기가 아무런 이점을 주지 않습니다.
2. 이벤트 루프 — 단일 스레드로 비동기를 처리하는 방법
꼬리질문: "JavaScript는 단일 스레드인데 어떻게 비동기를 처리하나요?"
"JavaScript는 단일 스레드(single-thread)라고 했는데, 어떻게 여러 작업을 동시에 처리하죠?" — 이 질문의 답이 바로 이벤트 루프(Event Loop)입니다.
- Call Stack: 현재 실행 중인 함수들이 쌓이는 공간. 함수가 호출되면 올라가고(push), 완료되면 내려갑니다(pop).
- Web APIs: 브라우저나 Node.js가 제공하는 비동기 기능 실행 공간. setTimeout, fetch 등이 여기서 처리됩니다.
- Callback Queue: Web APIs에서 완료된 작업의 콜백이 순서대로 대기하는 줄.
- Event Loop: Call Stack이 비어있을 때 Callback Queue에서 콜백을 꺼내 실행시키는 무한 루프.
아래 다이어그램에서 "다음 단계" 버튼을 눌러 단계별로 확인해 보세요.
console.log('1. 시작');
setTimeout(() => {
console.log('3. setTimeout 콜백');
}, 0);
console.log('2. 끝');
// 출력: 1. 시작 → 2. 끝 → 3. setTimeout 콜백Call Stack
Web APIs
비어있음
Callback Queue
비어있음
Event Loop
대기 중
console.log('1. 시작')이 Call Stack에 추가되고 즉시 실행됩니다.
핵심은 Call Stack이 완전히 비어야만 Event Loop가 다음 콜백을 실행할 수 있다는 것입니다. 동기 코드가 오래 걸리면 비동기 콜백이 아무리 완료되어 있어도 실행되지 않고 대기합니다 — 이것이 UI가 멈추는 현상(UI freeze)의 원인입니다.
3. 콜백 지옥에서 async/await까지 — 비동기 패턴의 진화
꼬리질문: "콜백, Promise, async/await의 차이점을 설명해주세요"
비동기를 코드로 표현하는 방법은 JavaScript의 역사와 함께 발전해 왔습니다.
콜백(Callback, ES5): 함수의 인자로 다른 함수를 전달해 완료 시 호출하는 방식입니다. 중첩이 깊어질수록 코드가 오른쪽으로 계속 들여쓰기되는 "콜백 지옥(Callback Hell)"이 발생합니다.
Promise(ES2015): .then() 체이닝으로 콜백 중첩을 해소했습니다. .catch() 하나로 전체 체인의 오류를 처리할 수 있어 훨씬 편리합니다.
async/await(ES2017): 비동기 코드가 동기처럼 읽히고, try/catch로 익숙한 방식의 오류 처리가 가능합니다. 내부적으로는 Promise를 사용합니다.
세 가지 패턴을 전환하며 같은 로직이 어떻게 다르게 표현되는지 비교해 보세요.
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);
render(user, posts, comments);
});
});
});오류 처리
각 콜백마다 err 파라미터로 개별 처리
학습 곡선
문법은 단순하지만 중첩 구조 이해 필요
장점
- ✓추가 문법 없이 바로 사용 가능
- ✓모든 환경에서 동작 (ES5)
단점
- ✗콜백 지옥(Callback Hell) — 중첩이 깊어질수록 가독성 급락
- ✗오류 처리를 매번 반복해야 함
- ✗실행 순서 추적이 어려움
4. await를 잘못 쓰면 왜 느릴까 — Promise.all 병렬화
꼬리질문: "async/await를 써도 느릴 수 있는 경우가 있나요?"
async/await를 사용한다고 자동으로 빨라지는 것이 아닙니다. 독립적인 작업에 await를 직렬로 사용하면 비동기임에도 느려집니다.
세 요청이 서로 의존하지 않음에도 불구하고 하나씩 순서대로 기다리면 총 2400ms가 소요됩니다. Promise.all()을 사용하면 여러 Promise를 동시에 시작하고, 모두 완료될 때까지 한 번만 기다려 1000ms로 줄일 수 있습니다.
아래 데모에서 두 방식을 직접 실행하고 시간 차이를 확인해 보세요.
느린 방식 (직렬 await)
// 각각 순서대로 기다림
const user = await fetchUser();
const posts = await fetchPosts();
const comments = await fetchComments();
// 총 2400ms빠른 방식 (Promise.all)
// 동시에 시작, 한 번만 기다림
const [user, posts, comments] =
await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments(),
]);
// 총 ~1000ms아래 버튼을 눌러 실행해 보세요.
면접 체크리스트
이 항목들을 자신 있게 설명할 수 있다면 동기/비동기 질문은 준비 완료입니다.
- - 동기 vs 비동기: 블로킹/논블로킹의 차이, I/O 바운드에서 비동기가 유리한 이유
- - 이벤트 루프: Call Stack이 비어야만 Callback Queue의 콜백이 실행됨
- - 콜백 → Promise → async/await: 각 패턴의 등장 배경과 장단점
- - Promise.all(): 독립적인 비동기 작업은 병렬 처리로 성능 최적화
- - UI freeze: 동기 코드가 오래 걸릴 때 이벤트 루프가 막히는 이유
참고 자료
- JavaScript 비동기 처리와 콜백 함수 — 캡틴 판교 — 콜백부터 Promise까지 한국어로 명확하게 정리된 입문 글
- JavaScript Visualized: Event Loop — Lydia Hallie — GIF 애니메이션으로 이벤트 루프를 시각적으로 설명
- Jake Archibald: In The Loop (JSConf 2018) — 이벤트 루프와 태스크 큐를 가장 명쾌하게 설명하는 강연 (35분)
의견을 들려주세요
서비스 개선에 큰 도움이 됩니다. 익명으로 자유롭게 남겨주세요.