자바스크립트 비동기 처리 정복: 콜백 지옥에서 Async/Await까지
싱글 스레드 언어인 JavaScript가 서버 통신과 같은 무거운 작업을 멈춤 없이 처리하는 비결을 알아봅니다. Callback, Promise, 그리고 Async/Await로 이어지는 비동기 처리 패턴의 진화 과정을 실무 코드로 정리했습니다.
1. 싱글 스레드와 비동기(Asynchronous)의 필요성
자바스크립트는 기본적으로 싱글 스레드(Single Thread) 언어입니다. 즉, 한 번에 하나의 작업만 처리할 수 있습니다. 만약 서버에서 1GB짜리 데이터를 받아오는 작업을 동기(Synchronous)로 처리한다면, 다운로드가 끝날 때까지 브라우저는 멈춰버리고(Freezing) 사용자는 아무것도 할 수 없게 됩니다.
이러한 문제를 해결하기 위해 자바스크립트는 비동기(Asynchronous) 모델을 채택했습니다. 무거운 작업(서버 요청, 타이머 등)은 브라우저의 Web API에게 위임하고, 다음 코드를 즉시 실행합니다. 작업이 끝나면 이벤트 루프(Event Loop)가 결과를 다시 메인 스레드로 가져와 실행하는 방식입니다.
2. 과거의 유산: 콜백(Callback)과 콜백 지옥
ES6 이전에는 비동기 작업 후 특정 로직을 수행하기 위해 함수의 인자로 함수를 넘겨주는 콜백(Callback) 패턴을 사용했습니다. 하지만 비동기 작업이 연속적으로 필요할 경우, 코드가 옆으로 누운 피라미드 모양이 되며 가독성이 극도로 나빠지는 현상이 발생합니다.
// 가독성이 떨어지는 '콜백 지옥' 예시
loginUser(id, password, (user) => {
getUserRoles(user, (roles) => {
getAccessLevel(roles, (level) => {
// ... 계속되는 중첩 ...
console.log(level);
});
});
});
3. 모던 JS의 표준: 프로미스(Promise)의 등장
ES6(2015)에서 도입된 Promise는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체입니다. 콜백 지옥을 해결하고, 비동기 처리를 더 직관적으로 체이닝(Chaining)할 수 있게 해줍니다.
Promise는 다음 3가지 상태 중 하나를 가집니다.
- Pending (대기): 처리가 완료되지 않은 초기 상태
- Fulfilled (이행): 처리가 성공적으로 완료됨 (resolve 호출)
- Rejected (거부): 처리가 실패함 (reject 호출)
function loginUser(id, password) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id === 'admin') resolve({ name: '관리자' });
else reject(new Error('로그인 실패'));
}, 1000);
});
}
// .then()을 이용한 깔끔한 체이닝
loginUser('admin', '1234')
.then(user => {
console.log(`${user.name}님 환영합니다.`);
return getUserRoles(user); // 다음 Promise 반환
})
.then(roles => console.log(roles))
.catch(error => console.error(error)); // 에러 통합 처리
4. 궁극의 가독성: Async / Await 패턴
ES8(2017)에 도입된 async/await는 Promise를 기반으로 동작하지만, 문법적으로는 마치 동기 코드처럼 작성할 수 있게 해주는 문법적 설탕(Syntactic Sugar)입니다.
- 함수 앞에
async를 붙이면 해당 함수는 항상 Promise를 반환합니다. await키워드는 Promise가 처리될 때까지 기다렸다가 결과를 반환합니다. (async 함수 내부에서만 사용 가능)
async function processLogin() {
try {
const user = await loginUser('admin', '1234'); // 기다림
const roles = await getUserRoles(user); // 기다림
console.log('최종 권한:', roles);
} catch (error) {
// 동기 코드처럼 try-catch로 에러 처리 가능
console.error('에러 발생:', error);
}
}
5. [실전] Fetch API로 사용자 데이터 조회하기
실제 웹 개발에서 가장 많이 사용하는 패턴입니다. 외부 API에서 데이터를 가져와 화면에 표시하는 로직을 Async/Await를 사용해 구현해 보겠습니다.
async function fetchUserData(userId) {
const url = `https://jsonplaceholder.typicode.com/users/${userId}`;
try {
// 1. 서버에 데이터 요청 (비동기)
const response = await fetch(url);
// 2. HTTP 상태 코드 확인
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
// 3. JSON 파싱 (비동기)
const userData = await response.json();
console.log('사용자 정보:', userData);
return userData;
} catch (error) {
console.error('데이터 조회 실패:', error.message);
}
}
// 실행
fetchUserData(1);
💡 핵심 요약: 현대 자바스크립트 개발에서 비동기 처리는 선택이 아닌 필수입니다. Promise로 비동기의 흐름을 제어하고, Async/Await로 코드의 가독성을 높여 유지보수하기 쉬운 코드를 작성하세요.
댓글
댓글 쓰기