자바스크립트의 비동기 처리
동기 실행과 이벤트 기반 비동기 실행을 구분하고 콜백에서 Promise와 async·await로 발전한 제어 흐름을 익힙니다.
실제 웹 애플리케이션 개발에서 필수적인 요소인 비동기 처리(Asynchronous Processing)와 데이터 페칭(Data Fetching)을 다루겠습니다.
웹 애플리케이션은 서버에서 데이터를 가져오거나(API 호출), 파일 시스템에 접근하거나, 타이머를 사용하는 등 시간이 오래 걸리는 작업을 자주 수행합니다.
이 작업들을 동기적으로 처리하면 애플리케이션이 멈추거나 먹통이 될 수 있습니다.
이 문제를 방지하고 사용자 경험을 높이기 위해 비동기 처리가 필요합니다.
이 장에서는 자바스크립트의 비동기 처리 기본 메커니즘과 핵심 도구인 콜백, Promise, async/await를 정리합니다.
async/await로 갈수록 가독성이 개선됩니다.
- 1자바스크립트의 비동기 처리 비동기 처리
기다림을 이벤트 루프와 Promise 상태로 분리한다
- 2API 호출, 타이머, 파일 처리처럼 오래 걸리
작업은 결과가 돌아올 때 다시 이어 붙이고, 그 사이 UI 응답성을 유지한다.
- 3읽는 순서 동기 대기
비동기 위임을 먼저 나누고, 콜백의 중첩 문제를 Promise와 async/await가 어떻게 정리하는지 본다.
- 4비동기 처리 책임표 방식 기다림
처리 대표 사용 확인 지점 동기 실행 앞 줄 완료까지 다음 줄이 멈춘다.
동기 vs 비동기
개념을 명확히 하기 위해 동기(Synchronous) 처리와 비동기(Asynchronous) 처리를 비교해 봅시다.
-
동기(Synchronous) 처리
- 코드가 위에서 아래로 순서대로 실행됩니다.
- 한 작업이 완료될 때까지 다음 작업이 대기합니다.
- 장점: 코드의 흐름을 파악하기 쉽고 예측 가능합니다.
- 단점: 시간이 오래 걸리는 작업이 있으면 전체 애플리케이션이 블로킹(blocking) 되어 사용자 인터페이스가 멈추거나 응답하지 않게 됩니다.
console.log("작업 1 시작"); // 매우 오래 걸리는 동기 작업이라고 가정 for (let i = 0; i < 1000000000; i++) {} // CPU를 많이 사용하는 작업 console.log("작업 2 완료"); // 작업 1이 끝나야 실행 console.log("작업 3 완료");위 코드에서 작업 2 완료는 작업 1 시작 뒤의 루프가 모두 실행된 후에야 출력됩니다.
-
비동기(Asynchronous) 처리
- 특정 작업이 완료되기를 기다리지 않고 다음 코드를 즉시 실행합니다.
- 시간이 오래 걸리는 작업은 백그라운드에서 실행되고, 완료되면 미리 정해진 로직(콜백, Promise 후속 처리)을 실행합니다.
- 장점: 애플리케이션이 블로킹되지 않아 사용자 경험이 부드럽습니다. (논블로킹 Non-blocking)
- 단점: 코드의 흐름이 동기적이지 않아 때로는 이해하거나 디버깅하기 어려울 수 있습니다.
console.log("작업 A 시작"); setTimeout(() => { // 비동기 작업: 1초 후 실행될 콜백 함수 등록 console.log("작업 B 완료 (1초 지연)"); }, 1000); console.log("작업 C 완료"); // 작업 B가 끝나기를 기다리지 않고 즉시 실행위 코드의 출력 순서는 작업 A 시작 -> 작업 C 완료 -> 작업 B 완료 (1초 지연) 가 될 것입니다.
콜백(Callback) 함수
자바스크립트에서 비동기 처리를 구현하는 가장 기본적인 방법은 콜백 함수를 사용하는 것입니다.
콜백 함수는 다른 함수의 인자로 전달되어, 특정 작업이 완료된 후 호출되는 함수를 말합니다.
예시:setTimeout
function greet(name, callback) {
setTimeout(() => {
const message = `Hello, ${name}!`;
callback(message); // 작업 완료 후 콜백 함수 호출
}, 1000);
}
function displayMessage(msg) {
console.log(msg);
}
console.log("인사 시작...");
greet("Alice", displayMessage); // 콜백으로 displayMessage 함수 전달
console.log("인사 요청 완료.");출력
인사 시작...
인사 요청 완료.
Hello, Alice!여기서 displayMessage가 greet 함수의 콜백으로 전달되어, setTimeout 내부의 비동기 작업이 완료된 후에 실행됩니다.
콜백 헬(Callback Hell) 문제: 여러 개의 비동기 작업이 순차적으로 실행되어야 할 때, 콜백 함수들이 중첩되어 코드의 가독성이 떨어지고 유지보수가 어려워지는 현상을 콜백 헬(Callback Hell) 또는 피라미드 오브 둠(Pyramid of Doom)이라고 부릅니다.
// 콜백 헬 예시
getData(function(a) {
processData(a, function(b) {
displayResult(b, function(c) {
// ... 계속 중첩
});
});
});Promise (프로미스)
콜백 헬의 단점을 극복하고 비동기 코드를 보다 깔끔하게 작성하기 위해 ES6에서 Promise(프로미스)가 도입되었습니다.
Promise는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체입니다.
Promise의 세 가지 상태Pending (대기): 비동기 작업이 아직 완료되지 않은 초기 상태.
Fulfilled (이행/성공): 비동기 작업이 성공적으로 완료된 상태.
Rejected (거부/실패): 비동기 작업이 실패한 상태.
- Promise 생성:
new Promise((resolve, reject) => { ... })resolve: 비동기 작업이 성공했을 때 호출하는 함수.reject: 비동기 작업이 실패했을 때 호출하는 함수.
- Promise 체이닝:
.then(),.catch(),.finally().then(onFulfilled, onRejected): Promise가 성공(fulfilled) 또는 실패(rejected)했을 때 실행할 콜백 함수를 등록합니다. 일반적으로 성공 시만 처리합니다..catch(onRejected): Promise가 실패(rejected)했을 때만 실행되는 콜백을 등록하여 에러를 처리합니다..then(null, onRejected)와 동일합니다..finally(): Promise의 성공/실패 여부와 상관없이 항상 실행되는 콜백을 등록합니다.
function fetchDataPromise() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true; // 가상으로 성공/실패 여부 결정
if (success) {
resolve("데이터를 성공적으로 가져왔습니다!"); // 성공 시 resolve 호출
} else {
reject("데이터를 가져오는 데 실패했습니다."); // 실패 시 reject 호출
}
}, 2000);
});
}
console.log("데이터 요청 시작...");
fetchDataPromise()
.then(response => { // 성공적으로 데이터를 가져왔을 때
console.log("성공:", response);
return "다음 작업으로 전달할 값"; // 다음 .then()으로 값 전달
})
.then(nextValue => { // 이전 .then()에서 반환된 값 받음
console.log("다음 체인:", nextValue);
})
.catch(error => { // Promise가 실패했을 때
console.error("실패:", error);
})
.finally(() => { // 성공/실패와 상관없이 항상 실행
console.log("데이터 요청 작업 완료.");
});
console.log("Promise 요청은 즉시 반환되었습니다.");출력
데이터 요청 시작...
Promise 요청은 즉시 반환되었습니다.
(2초 후)
성공: 데이터를 성공적으로 가져왔습니다!
다음 체인: 다음 작업으로 전달할 값
데이터 요청 작업 완료.Promise는 콜백 헬을 벗어나 비동기 코드를 순차적으로 연결(Promise Chain)하여 가독성을 높여줍니다.
비동기 문법은 콜백에서 Promise, async/await로 발전했지만 해결하려는 문제는 같습니다.
중첩을 줄이고, 성공/실패 흐름을 읽기 쉬운 형태로 펼치는 것입니다.
callback은 성공/실패가 안쪽으로 들어가고, Promise는 체인으로 분리하며, async/await는 동기 코드처럼 순서를 읽게 한다.
- 1중첩 함수
Callback 중첩 함수 오류 흐름 흩어짐
- 2then/catch
Promise then/catch 성공/실패 체인
- 3try/catch
async/await try/catch 순서가 평평함
- 4loading/error/data
UI state loading/error/data 결과를 화면 상태로 매핑
async/await (어싱크/어웨이트)
ES8(ECMAScript 2017)에서 도입된 async/await 문법은 Promise를 기반으로 하며, 비동기 코드를 마치 동기 코드처럼 보이게 작성할 수 있도록 도와주어 가독성을 극대화합니다.
-
async키워드- 함수 앞에
async를 붙이면, 해당 함수는 항상 Promise를 반환합니다. - 함수 내에서
await키워드를 사용할 수 있게 됩니다.
- 함수 앞에
-
await키워드- Promise 앞에만 사용할 수 있습니다.
await키워드가 붙은 Promise가 이행될 때까지 함수의 실행을 일시 중지합니다.- Promise가 이행되면, Promise의 결과값(
resolve된 값)을 반환합니다. - Promise가 거부되면, 에러를 던집니다.
async/await 사용
function fetchDataAsync() {
return new Promise(resolve => {
setTimeout(() => {
resolve("비동기 데이터 획득!");
}, 1500);
});
}
async function processData() { // async 함수로 정의
console.log("데이터 처리 시작 (async/await)...");
try {
// await은 Promise가 완료될 때까지 기다립니다.
const data = await fetchDataAsync(); // fetchDataAsync Promise가 이행될 때까지 대기
console.log("받은 데이터:", data);
const processedData = data + " -> 처리 완료!";
console.log("처리된 데이터:", processedData);
// 다음 비동기 작업도 await으로 순차적으로 처리 가능
const anotherResult = await new Promise(resolve => setTimeout(() => resolve("추가 작업 완료!"), 500));
console.log(anotherResult);
} catch (error) { // Promise가 rejected되면 catch 블록으로 이동
console.error("오류 발생:", error);
} finally {
console.log("데이터 처리 종료.");
}
}
// async 함수는 Promise를 반환하므로 .then()으로 처리 완료를 알 수 있습니다.
processData().then(() => {
console.log("processData 함수 전체 실행 완료.");
});
console.log("async 함수는 호출 즉시 반환됩니다.");출력
async 함수는 호출 즉시 반환됩니다.
데이터 처리 시작 (async/await)...
(1.5초 후)
받은 데이터: 비동기 데이터 획득!
처리된 데이터: 비동기 데이터 획득! -> 처리 완료!
(0.5초 후)
추가 작업 완료!
데이터 처리 종료.
processData 함수 전체 실행 완료.async/await는 비동기 코드의 가독성을 비약적으로 향상시켜줍니다.
특히 여러 비동기 작업이 순차적으로 이루어져야 할 때 Promise Chain보다 훨씬 직관적입니다.
비동기 문법을 UI 상태로 옮길 때는 성공 경로만이 아니라 대기, 실패, 정리 단계를 함께 모델링해야 합니다.
요청 시작과 완료, 빈 결과, 실패를 하나의 state로 섞지 않아야 화면 분기가 예측 가능해진다.
- 101 Idle
아직 요청 전
- 202 Loading
요청 중
- 303 Success
data 표시
- 404 Error
실패 안내
코드는 위에서 아래로 읽히지만, 실제 실행은 콜 스택, 브라우저 API, 큐를 오가며 진행됩니다.
아래 다이어그램은 콜백과 Promise가 어떤 실행 장소를 거쳐 다시 화면 갱신으로 이어지는지 정리합니다.
코드에 적힌 순서와 실제 실행 순서를 call stack·microtask·task로 나눠 읽는다.
- 동기Call stack
현재 함수와 console.log를 끝까지 실행
- 예약Promise.then
microtask queue에 callback 등록
- 예약setTimeout
task queue에 callback 등록
- 비움Microtasks
현재 stack 뒤 Promise 작업을 모두 처리
- 다음 turnTask
timer callback을 한 차례 실행
비동기 처리의 주요 개념 요약
- 이벤트 루프(Event Loop): 자바스크립트의 단일 스레드(Single-threaded) 환경에서 비동기 작업을 어떻게 처리하는지 설명하는 핵심 메커니즘입니다. 콜 스택, 힙, 태스크 큐(콜백 큐), 마이크로태스크 큐 등으로 구성됩니다. (자세한 내용은 별도의 심화 학습 필요)
- Non-blocking I/O: 입력/출력 작업(파일 읽기, 네트워크 요청)이 완료되기를 기다리지 않고 다른 작업을 계속 수행하는 방식입니다. 비동기 처리의 핵심 원리입니다.
자바스크립트의 비동기 처리는 여기까지입니다.
이 장에서는 동기 처리와 비동기 처리의 차이를 이해하고, 자바스크립트 비동기 처리의 발전 과정인 콜백, Promise, 그리고 async/await의 기본적인 사용법과 특징을 알아보았습니다.
특히 async/await는 현대 리액트 애플리케이션에서 서버와의 통신 등 데이터 페칭 시 가장 널리 사용되는 방식이므로 개념을 확실히 익히는 것이 중요합니다.
이제 자바스크립트가 비동기 작업을 어떻게 다루는지 이해했으므로, 다음 절에서는 이러한 비동기 처리 기술을 활용하여 리액트 컴포넌트 내부에서 실제로 데이터를 가져오는 방법인 데이터 페칭(Data Fetching)에 대해 구체적으로 알아보겠습니다.
비동기 처리는 문법별 차이보다 시작, 성공, 실패, 마무리 상태를 분리해 보는 것이 중요합니다.
callback, Promise, async await는 표현 방식은 달라도 대기 중인 작업과 결과 처리, 에러 경로를 분리해야 한다는 점은 같습니다.
- 1callback
함수를 인자로 넘겨 나중에 실행하지만 중첩이 깊어지면 흐름 추적이 어렵습니다.
- 2Promise
pending, fulfilled, rejected 상태로 비동기 결과를 체인으로 연결합니다.
- 3async await
Promise를 동기 코드처럼 읽게 하지만 try catch로 실패 경로를 명시해야 합니다.
- 4start
요청 시작 시 loading 상태를 먼저 켭니다.
- 5success
성공 데이터는 필요한 형태로 저장합니다.
동기 vs 비동기와 콜백(Callback) 함수 중심으로 정리한 보조 다이어그램입니다.
동기 코드가 먼저 끝나고 Promise callback은 microtask queue에서 실행되므로 화면 업데이트 순서를 이해해야 한다.
- 1동기 코드 실행
Call stack 동기 코드 실행 현재 함수 처리
- 2microtask 등록
Promise resolve microtask 등록 then 대기
- 3then/catch 실행
Microtask then/catch 실행 state update 예약
- 4React 반영
Render React 반영 화면 갱신