← 전체 강의로 돌아가기

03 / EXECUTION FLOW

기다림을
다루는 방식

동기는 결과를 확인한 뒤 이어 가고, 비동기는 기다리는 동안 다른 일을 진행한다. 두 개념을 카페 주문과 브라우저의 흐름으로 연결해 보자.

ONE QUESTION

A의 결과를 기다리는 동안 B를 처리할 수 있을까?

같은 “기다림”이라도 현재 실행 흐름이 멈추는지, 나중에 결과를 이어 붙이는지에 따라 구조가 달라진다.

동기 · Sync “A가 끝났어? 그럼 B로 가자.”
비동기 · Async “A는 맡겨 둘게. 그동안 B를 하자.”

30초 핵심

단순히 “순차냐 아니냐”가 아니라, 결과를 다음 흐름과 언제 연결하는지에 초점을 둔다.

동기는 결과를 기다린 뒤 다음 작업을 이어 가고, 비동기는 작업을 맡겨 둔 채 다른 일을 하다가 완료 시점에 결과를 처리한다.

QUESTION → CHANGE → EXPLANATION

A의 음료를 기다리는 동안 B를 할 수 있을까?

카페 주문의 두 lane을 한 단계씩 진행해 보자. 색깔보다 각 문장의 “현재 상태”를 먼저 읽는다.

  1. 질문기다림 중에 다른 일을 할 수 있을까?
  2. 조작다음 단계·자동 재생을 눌러 본다.
  3. 변화대기와 다른 작업 진행을 비교한다.
  4. 해설시점·스레드·병렬성을 따로 판단한다.

자동 재생은 움직임을 보여 주고, 일시정지는 한 단계의 문장을 읽을 시간을 준다.

카페 주문 타임라인

동기·줄서기와 비동기·진동벨이 같은 요청을 어떻게 다루는지 비교한다.

1 / 4

현재 단계를 확인해 보세요.

다음 단계를 눌러 기다림의 차이를 관찰해 보세요.

  1. 주문을 넣는다
    동기 · 줄서기A 주문 처리 시작
    비동기 · 진동벨A 주문 맡기고 진동벨 받음
  2. A의 음료가 준비되는 중이다
    동기 · 줄서기A 완료까지 대기 · B는 아직
    비동기 · 진동벨B 작업 시작 · A는 준비 중
  3. 기다림을 다른 일과 겹친다
    동기 · 줄서기현재 흐름이 A에 붙잡힘
    비동기 · 진동벨B 작업 진행 · 자리에 앉아 기다림
  4. 완료 알림을 받고 결과를 처리한다
    동기 · 줄서기A 결과 확인 → 그제야 B 가능
    비동기 · 진동벨A 완료 알림 → A 결과 처리
지금 관찰할 것 비동기 lane이 먼저 B를 하는 것은 여러 CPU 코어가 동시에 계산한다는 뜻이 아니다. “기다리는 동안 흐름을 막지 않았는가?”를 보는 그림이다.
  • 동기·현재 흐름이 A 완료를 기다림
  • 비동기·완료 후 결과 처리
  • 외부 작업·음료 준비 또는 I/O

동기와 비동기의 정확한 뜻

“앞 작업이 끝나야 다음으로 간다”는 기억은 출발점으로 좋지만, 결과를 다음 흐름과 어떻게 연결하는지가 더 정확한 기준이다.

동기 · Sync

요청한 작업의 결과나 완료를 확인한 뒤 다음 흐름으로 진행한다. readFileSync()처럼 호출한 쪽이 결과를 바로 받아야 하는 API가 대표적이다.

비동기 · Async

작업을 시작해 둔 뒤 완료를 기다리는 흐름과 결과를 처리하는 흐름을 분리한다. fetch()와 타이머의 완료 콜백이 대표적이다.

중요: 비동기는 I/O 자체를 빠르게 만드는 마법이 아니다. 네트워크·DB·디스크가 응답하는 동안 현재 실행 흐름이 다른 일을 할 수 있게 만드는 방식이다.

동기와 Blocking은 같은 개념인가요?

동기 ≠ Blocking. 두 개념은 자주 같이 나타나지만, 서로 다른 질문을 한다.

동기·비동기

결과와 다음 흐름을 언제 연결하는가?
결과를 확인한 뒤 이어 가는지, 완료 시점에 후속 처리를 하는지를 본다.

Blocking·non-blocking

현재 스레드가 멈추는가?
호출한 스레드가 다른 일을 못 하도록 붙잡는지, 호출이 돌아와 계속할 수 있는지를 본다.

조합모습
동기 + BlockingreadFileSync()처럼 결과를 받을 때까지 호출한 스레드가 멈춘다.
비동기 + non-blockingfetch()처럼 요청 후 흐름이 돌아오고, 완료 알림에서 결과를 처리한다.
동기 + non-blocking호출은 바로 돌아오지만 호출자가 완료 여부를 직접 확인하는 API도 생각할 수 있다.
비동기 + Blocking비동기 작업을 시작한 뒤 별도의 wait()로 결과까지 붙잡고 기다리면 가능하다.

JavaScript 주의: await는 현재 async 함수의 이어지는 부분을 잠시 멈추지만, 브라우저 메인 스레드 전체를 Blocking하는 것은 아니다.

I/O 대기 중에 실행 흐름을 살려 두기

I/O는 네트워크·DB·디스크처럼 프로그램 바깥과 데이터를 주고받는 일이다. CPU는 빠르게 다음 명령을 처리할 수 있지만, 외부 응답은 시간이 걸릴 수 있다.

동기 + Blocking

요청 A가 DB 응답을 기다리는 동안 A를 맡은 작업자는 멈춰 있다. 작업자가 하나뿐이면 요청 B도 늦어진다.

작업자 1: A 요청 → DB 요청 → [응답 대기] → A 응답

비동기 + non-blocking

DB 요청을 시작한 뒤 작업자는 B를 처리할 수 있다. A가 끝나면 완료 알림을 받아 A의 결과를 처리한다.

작업자 1: A 요청 → DB 요청 시작
작업자 1:             B 요청 → B 응답
작업자 1: DB 완료 알림 → A 응답

JavaScript는 싱글 스레드인데 어떻게 비동기 처리를 하나요?

JavaScript 메인 스레드는 한 번에 하나의 JS 작업을 실행한다. 대신 브라우저 런타임이 I/O 대기와 완료 알림을 맡는다.

1 · Call Stack

JS 엔진이 현재 JavaScript 코드를 한 번에 하나씩 실행하는 무대다.

2 · Web API

브라우저가 fetch·타이머·DOM 이벤트 같은 대기 작업을 맡는 공간이다.

3 · Task Queue

응답이나 이벤트가 준비되면 나중에 실행할 콜백이 줄을 선다.

4 · Event Loop

Call Stack이 비었을 때 큐의 다음 작업을 실행할 차례로 올린다.

console.log('요청 시작');

fetch('/api/user')
  .then((response) => response.json())
  .then((user) => console.log('데이터 도착', user));

console.log('다음 코드');

콘솔은 보통 요청 시작 → 다음 코드 → 데이터 도착 순서가 된다. 브라우저가 네트워크 대기를 맡고, 응답이 도착하면 완료 처리를 큐에 넣기 때문이다. 이 장면은 비동기이지, JS 코드가 여러 줄 동시에 실행되는 병렬성은 아니다.

비동기 처리를 동기처럼 보이게 하는 문법

Promise와 async/await는 완료 후 이어질 코드를 위에서 아래로 읽기 쉽게 적도록 도와준다.

async function loadUser() {
  const response = await fetch('/api/user');
  const user = await response.json();
  return user;
}

console.log('A');
loadUser().then((user) => console.log('결과', user));
console.log('B');
  1. async 함수는 Promise를 반환한다. 호출자는 나중에 완료될 값을 받을 약속을 얻는다.
  2. await는 현재 함수의 이어지는 부분을 보류한다. 네트워크 결과가 오면 같은 함수의 다음 줄을 이어서 실행한다.
  3. 메인 스레드 전체를 붙잡는 것은 아니다. 기다리는 동안 이벤트 루프는 다른 작업을 처리할 수 있다.

비동기라고 언제나 좋은 것은 아니다

비동기는 I/O 대기와 다른 작업을 겹칠 때 유리하다. 작업의 성격과 순서를 함께 봐야 한다.

잘못된 생각

async만 붙이면 CPU 계산도 백그라운드에서 실행되고 화면이 빨라진다.”

정확한 생각

async는 비동기 결과를 다루는 문법이다. CPU 집중 작업을 다른 스레드로 옮기려면 Web Worker나 worker_threads가 필요할 수 있다.”

잘못된 생각

“비동기 = 병렬성. 비동기 요청은 항상 여러 CPU 코어에서 동시에 계산된다.”

정확한 생각

비동기 ≠ 병렬성. 한 이벤트 루프에서도 I/O 대기와 다른 이벤트 처리를 겹칠 수 있다. 실제 병렬 계산은 별도 실행 자원이 필요하다.

비동기가 불리할 수 있는 때: CPU 계산이 무거울 때, 앞 결과가 꼭 필요할 때, 요청을 너무 많이 동시에 보내 자원을 압박할 때, 작은 작업에 Promise 복잡성만 늘어날 때.

정답을 보기 전에 내 말로 답해 보기

퀴즈를 먼저 풀고, 헷갈린 문항은 위의 타임라인으로 돌아가 다시 확인하자.

  1. 참/거짓: 동기와 Blocking은 같은 개념이므로 동기 작업은 항상 현재 스레드를 멈춘다.
  2. 객관식: 비동기 처리의 설명으로 가장 알맞은 것은 무엇일까?
    A. 네트워크 요청 시간이 반드시 짧아진다. B. 결과를 기다리는 동안 가능한 다른 작업을 하고 완료 시점에 결과를 처리한다. C. 서로 다른 CPU 코어가 항상 같은 순간에 계산한다. D. JavaScript 코드가 여러 줄 동시에 실행된다.
  3. 순서 판단: console.log('A'); fetch('/api/data').then(() => console.log('C')); console.log('B');의 출력 순서를 적어 보자.
  4. 짧은 설명: JavaScript가 싱글 스레드인데도 fetch()를 기다리는 동안 버튼 클릭을 처리할 수 있는 이유는 무엇일까?
  5. 상황 판단: async function 안의 CPU 계산 때문에 화면이 멈췄다. async만으로 해결되지 않은 이유와 다음에 검토할 방법은 무엇일까?
정답과 해설 보기

1. 거짓. 동기/비동기는 결과와 다음 흐름을 언제 연결할지, Blocking/non-blocking은 현재 스레드가 멈추는지를 뜻한다. 자주 함께 나타나지만 같은 말은 아니다.

2. B. 비동기는 기다림을 다른 작업과 겹쳐 처리하고, 완료 알림이나 Promise 후속 코드에서 결과를 처리한다. 자동으로 요청 시간이 짧아지거나 CPU 병렬성이 생기는 것은 아니다.

3. A → B → C. fetch()의 완료 처리는 나중에 실행되므로 현재 코드는 먼저 B까지 실행한다.

4. 브라우저 Web API가 네트워크 대기를 맡고, 응답이 오면 완료 콜백을 큐에 넣는다. 이벤트 루프는 현재 JS 작업이 끝난 뒤 큐의 콜백을 실행한다.

5. async는 결과 전달 방식을 바꿀 뿐 CPU 계산을 다른 스레드로 옮기지 않는다. 계산량을 줄이거나 Web Worker·worker_threads 같은 별도 실행 흐름을 검토한다.