자바스크립트의 비동기 처리
실제 웹 애플리케이션 개발에서 필수적인 요소인 비동기 처리(Asynchronous Processing) 와 데이터 페칭(Data Fetching) 에 대해 다루겠습니다.
웹 애플리케이션은 서버로부터 데이터를 가져오거나(API 호출), 파일 시스템에 접근하거나, 타이머를 사용하는 등 시간이 오래 걸리는 작업을 수행해야 할 때가 많습니다. 이러한 작업들은 동기적으로 처리될 경우 애플리케이션이 멈추거나 '먹통'이 될 수 있습니다. 이를 방지하고 사용자 경험을 향상시키기 위해 비동기 처리가 필요합니다.
이 장에서는 자바스크립트가 어떻게 비동기 작업을 처리하는지 그 기본적인 메커니즘과, 이를 구현하기 위한 핵심 개념들인 콜백, Promise, 그리고 async/await
에 대해 알아보겠습니다.
동기 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 사용법
- 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의 성공/실패 여부와 상관없이 항상 실행되는 콜백을 등록합니다.
예시: 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
)하여 가독성을 높여줍니다.
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
보다 훨씬 직관적입니다.
비동기 처리의 주요 개념 요약
- 이벤트 루프(Event Loop): 자바스크립트의 단일 스레드(Single-threaded) 환경에서 비동기 작업을 어떻게 처리하는지 설명하는 핵심 메커니즘입니다. 콜 스택, 힙, 태스크 큐(콜백 큐), 마이크로태스크 큐 등으로 구성됩니다. (자세한 내용은 별도의 심화 학습 필요)
- Non-blocking I/O: 입력/출력 작업(파일 읽기, 네트워크 요청)이 완료되기를 기다리지 않고 다른 작업을 계속 수행하는 방식입니다. 비동기 처리의 핵심 원리입니다.
"자바스크립트의 비동기 처리"는 여기까지입니다. 이 장에서는 동기 처리와 비동기 처리의 차이를 이해하고, 자바스크립트 비동기 처리의 발전 과정인 콜백, Promise, 그리고 async/await
의 기본적인 사용법과 특징을 알아보았습니다. 특히 async/await
는 현대 리액트 애플리케이션에서 서버와의 통신 등 데이터 페칭 시 가장 널리 사용되는 방식이므로 개념을 확실히 익히는 것이 중요합니다.
이제 자바스크립트가 비동기 작업을 어떻게 다루는지 이해했으므로, 다음 장에서는 이러한 비동기 처리 기술을 활용하여 리액트 컴포넌트 내부에서 실제로 데이터를 가져오는 방법인 데이터 페칭(Data Fetching) 에 대해 구체적으로 알아보겠습니다.