JavaScript는 싱글 스레드 언어입니다. 하나의 작업이 끝나야 다음 작업을 시작할 수 있습니다. 그런데 네트워크 요청처럼 시간이 오래 걸리는 작업을 동기적으로 처리하면 브라우저가 완전히 멈춰버립니다. 이 문제를 해결하기 위해 비동기 프로그래밍이 존재합니다.
1단계: 콜백(Callback)
가장 초기 방식은 콜백 함수입니다. 작업이 완료됐을 때 호출할 함수를 미리 넘기는 방식입니다.
function fetchUser(id, callback) {
setTimeout(() => {
callback(null, { id, name: '홍길동' });
}, 1000);
}
fetchUser(1, (err, user) => {
if (err) return console.error(err);
console.log(user.name);
});
문제는 중첩이 깊어질수록 "콜백 지옥"이 발생한다는 점입니다.
// 콜백 지옥 — 읽기도, 관리하기도 어렵습니다
getUser(1, (err, user) => {
getPosts(user.id, (err, posts) => {
getComments(posts[0].id, (err, comments) => {
getLikes(comments[0].id, (err, likes) => {
console.log(likes); // 4단계 중첩...
});
});
});
});
2단계: Promise
Promise는 비동기 작업의 미래 결과를 나타내는 객체입니다. pending(대기), fulfilled(이행), rejected(거부) 세 가지 상태를 가집니다.
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({ id, name: '홍길동' });
} else {
reject(new Error('잘못된 ID'));
}
}, 1000);
});
}
fetchUser(1)
.then(user => console.log(user.name)) // 성공
.catch(err => console.error(err)) // 실패
.finally(() => console.log('완료')); // 항상 실행
Promise를 체이닝하면 중첩 없이 순차 처리가 가능합니다.
getUser(1)
.then(user => getPosts(user.id))
.then(posts => getComments(posts[0].id))
.then(comments => getLikes(comments[0].id))
.then(likes => console.log(likes))
.catch(err => console.error(err));
병렬 처리: Promise.all & Promise.allSettled
// 모두 성공해야 함 — 하나라도 실패하면 catch
const [user, posts] = await Promise.all([
fetchUser(1),
fetchPosts(1)
]);
// 각각의 성공/실패를 모두 받음
const results = await Promise.allSettled([
fetchUser(1),
fetchUser(999) // 실패해도 OK
]);
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log(result.value);
} else {
console.error(result.reason);
}
});
3단계: async/await
ES2017에서 도입된 async/await는 Promise를 동기 코드처럼 읽기 좋게 작성할 수 있게 해줍니다.
async function loadUserDashboard(userId) {
try {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
return { user, posts, comments };
} catch (err) {
console.error('데이터 로드 실패:', err);
throw err;
}
}
// 최상위에서 async 함수 호출
loadUserDashboard(1).then(dashboard => {
renderDashboard(dashboard);
});
실전 패턴: API 요청 유틸리티
async function api(url, options = {}) {
const response = await fetch(url, {
headers: { 'Content-Type': 'application/json' },
...options
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.message || `HTTP ${response.status}`);
}
return response.json();
}
// 사용
const user = await api('/api/users/1');
const newPost = await api('/api/posts', {
method: 'POST',
body: JSON.stringify({ title: '새 글', content: '...' })
});
핵심 정리
콜백 → Promise → async/await 순서로 발전했습니다. 현대 JavaScript에서는
콜백 → Promise → async/await 순서로 발전했습니다. 현대 JavaScript에서는
async/await를 기본으로 사용하고, 병렬 처리가 필요할 땐 Promise.all을 활용하세요. 에러 처리는 반드시 try/catch로 감싸세요.