JavaScript에서 Promise, await, setTimeout은 어떤 순서로 실행되나요?
아래 예제는 동기 코드가 끝난 뒤 Promise 콜백과 await 다음 코드가 실행되고, setTimeout 콜백이 이어집니다. 코드를 적은 순서보다 대기 작업을 등록한 위치가 중요합니다.
적용 조건·범위: 실행 검증: Node.js에서 위 출력과 일치했습니다. 실제 fetch 응답 시점과 렌더링 결과는 이 최소 예제로 확인할 수 없으므로 미확인입니다.
공식 문서 참고: MDN: 실행 컨텍스트·태스크·마이크로태스크
혼자 실행해 보는 이벤트 루프 출력 순서
JavaScript 이벤트 루프: Promise와 await 실행 순서 추적하기 · 독립 실습
동기 코드·Promise·타이머의 출력 순서를 확인하세요.
예제 크게 보기 (새 탭)독자 질문: 동기 코드와 Promise, await, setTimeout은 어떤 순서로 출력되는가?
js-13-example.mjs로 저장하고 node js-13-example.mjs을 실행하세요. 외부 패키지는 필요하지 않습니다.
console.log('A 동기');
setTimeout(() => console.log('E 타이머'), 0);
Promise.resolve().then(() => console.log('C Promise'));
async function run() {
console.log('B async 시작');
await Promise.resolve();
console.log('D await 이후');
}
run();
console.log('끝 동기');
예상 출력:
A 동기
B async 시작
끝 동기
C Promise
D await 이후
E 타이머
실행 검증: Node.js에서 위 출력과 일치했습니다. 실제 fetch 응답 시점과 렌더링 결과는 이 최소 예제로 확인할 수 없으므로 미확인입니다.
직접 풀어볼 문제
Promise.resolve().then을 run() 아래로 옮겨 C와 D의 순서를 예측하고 실행 결과로 확인하세요. 등록 위치를 바꾸면 어느 마이크로태스크가 먼저 대기하게 되는지 설명하고, 타이머 E가 그 뒤에 남는지도 확인합니다.
먼저 동기 코드가 실행됩니다
console.log('A');
console.log('B');
결과는 A, B 순서입니다.
Promise callback은 마이크로태스크로 이어집니다
console.log('A');
Promise.resolve().then(() => {
console.log('B');
});
console.log('C');
결과:
A
C
B
현재 실행 중인 동기 코드가 끝난 뒤 Promise callback이 실행됩니다.
setTimeout(..., 0)도 즉시 실행이 아닙니다
console.log('A');
setTimeout(() => {
console.log('timeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise');
});
console.log('B');
일반적인 브라우저 환경에서 관찰하면:
A
B
promise
timeout
현재 태스크의 동기 코드가 끝난 뒤 마이크로태스크를 처리하고, 그 이후 다음 태스크로 넘어갑니다.
await 이후 코드도 비동기 경계를 만듭니다
async function run() {
console.log('run:start');
await Promise.resolve();
console.log('run:after await');
}
console.log('A');
run();
console.log('B');
출력:
A
run:start
B
run:after await
await 이전은 즉시 실행되고 Promise가 처리된 뒤의 나머지 코드는 이후 마이크로태스크 흐름에서 계속됩니다.
이벤트 루프를 하나의 큐라고 부르지 않습니다
브라우저에는 여러 종류의 태스크 소스와 렌더링 단계가 있고 Promise microtask도 별도 규칙을 가집니다. 학습 단계에서 단순화는 필요하지만 ‘모든 비동기는 한 큐에서 순서대로 실행된다’고 설명하면 실제 동작을 이해하기 어렵습니다.

렌더링 시점도 지나치게 단순화하지 않습니다
DOM을 변경했다고 각 JavaScript 줄 사이에서 즉시 화면 픽셀이 그려지는 것은 아닙니다. 브라우저는 이벤트 루프와 렌더링 기회를 기준으로 화면을 갱신합니다.
이 글의 목표는 정확한 브라우저 렌더링 파이프라인 전체가 아니라 Promise·await·task 사이의 코드 실행 순서를 구분하는 것입니다.
자주 발생하는 오류
setTimeout(0)을 ‘즉시 다음 줄’로 생각합니다
최소 지연 시간이 0이라는 의미에 가깝고 현재 코드와 microtask보다 먼저 실행된다는 의미가 아닙니다.
비동기 순서를 맞추려고 임의의 timeout을 넣습니다
필요한 완료 조건을 Promise로 표현하거나 실제 비동기 작업을 await하는 편이 명확합니다.
학습 완료 체크리스트
- 동기 코드가 먼저 실행되는 흐름을 설명할 수 있습니다.
- Promise callback이 microtask라는 점을 이해했습니다.
setTimeout과 Promise의 일반적인 순서를 추적할 수 있습니다.await전후 실행 위치를 구분할 수 있습니다.- 임의의 timeout으로 순서를 맞추지 않을 수 있습니다.
독립 실습은 여기서 마칠 수 있습니다. 아래 선택 실습은 TaskFlow 프로젝트 파일로 진행합니다.
선택 실습: 할 일 앱에 적용하기
TaskFlow는 작업을 추가·완료·삭제하는 할 일 앱입니다. 이 선택 실습에서는 JavaScript로 화면에 동작을 붙입니다.
기능 코드는 12회차를 유지하고 이번 체크포인트에서는 실행 순서를 관찰하기 위한 app 코드만 추가합니다.
선택: 연재 프로젝트와 전체 자료
연재 프로젝트를 이어 만드는 분을 위한 자료입니다. 이 글의 개념과 예제만 찾으셨다면 필요한 파일과 실행 방법만 펼쳐보세요.
TaskFlow 프로젝트 적용
JavaScript 이벤트 루프: Promise와 await 실행 순서 추적하기 · 단계별 TaskFlow
작업 추가·완료 전환·삭제와 현재 단계의 필터/출력을 확인하세요. 검색 조건을 바꿔 가상 URL을 확인하세요.
예제 크게 보기 (새 탭)변경 설명과 부분 예제를 읽은 뒤, 시작·완료 파일과 적용 순서에서 필요한 선언·import·화면 연결을 포함한 전체 파일을 확인하세요. 이번 단계의 변경 이유와 적용 위치도 함께 확인합니다.
시작본과 이번 변경
JavaScript fetch 오류 처리: Promise부터 404까지의 완료 체크포인트를 시작본으로 사용합니다.
js/app.js의 initializeTasks에 관찰 로그를 넣고 호출 전후 순서를 기록합니다. 기존 store·render·loadTasks import를 유지하고 초기화 함수를 두 번 호출하지 마세요. 로그 0·1·2·3·4·5·6을 확인한 뒤 DEBUG를 false로 바꾸고 같은 기능 결과를 확인합니다.
연속 프로젝트 선수 조건
- Promise와 async/await
- TaskFlow의
loadTasks()초기 로딩 흐름 javascript-step-12-fetch체크포인트
이전 체크포인트
javascript-step-12-fetch
이번 회차 변경 코드와 전체본 연결
기능 코드는 12회차를 유지하고 이번 체크포인트에서는 실행 순서를 관찰하기 위한 app 코드만 추가합니다.
관찰용 로그 추가
app.js의 초기 로딩 함수에 임시 로그를 추가합니다.
const DEBUG = true;
function debug(...values) {
if (DEBUG) {
console.log(...values);
}
}
async function initializeTasks() {
debug('1. initialize:start');
renderLoadState('작업을 불러오는 중입니다.');
try {
debug('2. before loadTasks');
const tasks = await loadTasks();
debug('4. after await loadTasks');
store.replaceTasks(tasks);
debug('5. before render');
render();
debug('6. after render');
renderLoadState('작업을 불러왔습니다.');
} catch (error) {
console.error(error);
renderLoadState('작업을 불러오지 못했습니다.');
}
}
debug('0. before initialize');
initializeTasks();
debug('3. after initialize call');
실제 fetch 완료 시점에 따라 네트워크 대기 시간은 달라지지만 await 이후 코드가 초기 동기 코드보다 나중에 이어지는 것은 확인할 수 있습니다.
독립 실행 순서 검증
console.log('sync:1');
setTimeout(() => {
console.log('task');
}, 0);
Promise.resolve().then(() => {
console.log('microtask');
});
console.log('sync:2');
예상:
sync:1
sync:2
microtask
task
체크포인트 정리
관찰이 끝나면 DEBUG를 false로 바꾸거나 학습용 로그를 제거합니다. 로그 때문에 기능 동작이 달라져서는 안 됩니다.
단일 요청 기준으로 다음 기능이 모두 정상이어야 합니다.
- 초기 fetch
- render
- 추가
- 완료 변경
- 삭제
- 필터
- 검색
- URL 상태
- 날짜 표시
중간부터 시작하는 전체 파일과 적용 순서
시작 ZIP은 직전 프로젝트 단계 javascript-step-12-fetch의 실제 완료 파일입니다. 프로젝트 순서를 이어가는 기준이며 사이트의 기술 읽기 순서와는 구분합니다.
- 시작 ZIP을 새 폴더에 풉니다. 이 폴더가 작업 루트이며 기존 체크포인트 파일을 별도로 찾을 필요가 없습니다.
- app의 초기 로더를 관찰 로그가 있는 전체 흐름으로 교체합니다.
- 아래 변경 파일을 같은 상대 경로에 추가하거나 전체 교체합니다. 표시하지 않은 파일과 images는 시작본 그대로 유지합니다. package-lock.json은 완료 ZIP에 있는 파일을 함께 적용합니다.
- 작업 루트에서 명령을 실행하고 예상 결과를 확인합니다. 완료 ZIP은 직접 적용한 결과를 비교하는 기준입니다.
추가·교체 파일
CHANGES.md— 전체 교체README.md— 전체 교체js/app.js— 전체 교체
실행과 확인
npm start
npm test
개발 서버는 실행 상태로 두고 다른 터미널에서 검사 명령을 실행하세요. Node.js 24.18.0에서 검증했습니다. JavaScript는 http://127.0.0.1:4173, TypeScript는 Vite 실행 출력의 주소로 접속합니다.
예상 결과: 로그 0→1→2→3→4→5→6. 3은 요청을 기다리는 동안 먼저 출력됩니다.
실제 검증: JavaScript 모듈·DOM·경계값 실행 검사 통과. 시작 ZIP에 이 글의 변경 파일만 적용한 결과와 완료 ZIP의 모든 파일 내용이 일치했습니다. 브라우저 화면 배치·실제 기기·외부 CDN 가용성은 이 검사에 포함하지 않았습니다.
이 절의 전체 파일은 ZIP과 일치합니다. 위쪽 원문의 짧은 예제나 생략 표시가 있는 부분 코드는 개념·변경 위치 설명용입니다. 서버 저장과 인증은 제공하지 않으며 JSON fixture는 로컬 읽기 실습용입니다.
복사할 변경 파일 전체 코드
js/app.js — 전체 파일
import { loadTasks } from './task-service.js';
import { renderLoadState } from './task-view.js';
import { createTaskStore } from './task-store.js';
const store = createTaskStore();
import {
bindTaskControls,
bindTaskForm,
bindTaskList,
getTaskControls,
renderTasks,
setTaskControls,
} from './task-view.js';
import { readUrlState, writeUrlState } from './url-state.js';
function render() {
const controls = getTaskControls();
const tasks = store.filterTasks(controls.status, controls.query);
renderTasks(tasks, store.getTaskSummary());
}
const initialUrlState = readUrlState();
setTaskControls(initialUrlState);
bindTaskForm(({ title, dueAt }) => {
store.addTask(title, dueAt);
render();
});
bindTaskControls(() => {
const controls = getTaskControls();
writeUrlState(controls);
render();
});
bindTaskList(({ id, action }) => {
if (action === 'toggle') store.toggleTask(id);
if (action === 'delete') store.deleteTask(id);
render();
});
render();
const DEBUG = true;
function debug(...values) {
if (DEBUG) {
console.log(...values);
}
}
async function initializeTasks() {
debug('1. initialize:start');
renderLoadState('작업을 불러오는 중입니다.');
try {
debug('2. before loadTasks');
const tasks = await loadTasks();
debug('4. after await loadTasks');
store.replaceTasks(tasks);
debug('5. before render');
render();
debug('6. after render');
renderLoadState('작업을 불러왔습니다.');
} catch (error) {
console.error(error);
renderLoadState('작업을 불러오지 못했습니다.');
}
}
debug('0. before initialize');
initializeTasks();
debug('3. after initialize call');
완료 체크포인트
javascript-step-13-event-loop
다음 학습
현재는 요청이 하나라는 가정에서는 정상입니다. 다음 글에서는 검색·필터 요청이 빠르게 연속 발생할 때 오래된 응답이 최신 화면을 덮는 race condition을 재현하고 해결합니다.
다음 글: JavaScript 고급 비동기: AbortController와 최신 요청 경쟁 제어
글 소개·학습 준비·전체 목차

JavaScript · 13 / 15 · 필수 원리 · 이벤트 루프와 실행 순서
아래 예제에서는 현재 실행 중인 동기 코드가 먼저 끝나고, 등록된 Promise 콜백과 await 이후 코드가 이어진 뒤 setTimeout 콜백이 실행됩니다. 소스에 적힌 순서보다 대기 작업의 등록 시점을 비교해 출력 순서를 예측합니다.
이번 글에서는 콜 스택, 태스크, 마이크로태스크를 이용해 실행 순서를 추적하고 TaskFlow의 fetch 흐름에서 await 전후를 관찰합니다.
이번 글에서 배우는 내용
- 콜 스택
- 태스크 큐
- 마이크로태스크 큐
- Promise callback
await이후 코드setTimeout- 실행 순서 추적
먼저 알아야 할 내용
함수 호출과 Promise·async/await 문법을 알면 됩니다. loadTasks나 TaskFlow 파일은 상단 실행 순서 실습에 필요하지 않습니다.
목차
이 글이 도움이 되었나요?
읽음 기록
읽음 — / 전체 15개 · 필수 14개
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.