Callback vs. Promise vs. Async/Await#
JavaScript의 비동기 처리 방식은 프로그램의 실행 흐름을 막지 않고 다른 작업을 수행할 수 있게 해주는 중요한 기능이다.
주요 비동기 처리 방식에는 콜백(Callbacks), 프로미스(Promises), 그리고 async/await가 있다.
특성 | 콜백 (Callback) | Promise | Async/Await |
---|
정의 | 다른 함수의 인자로 전달되어 특정 시점에 실행되는 함수 | 비동기 작업의 최종 완료 또는 실패를 나타내는 객체 | Promise를 기반으로 비동기 코드를 동기 코드처럼 작성할 수 있게 해주는 문법 |
도입 시기 | JavaScript 초기부터 사용 | ES6 (2015) | ES8 (2017) |
문법 | function(err, result) { … } | new Promise((resolve, reject) => { … }) | async function() { await … } |
에러 처리 | 콜백 함수의 첫 번째 인자로 에러 객체 전달 | .catch() 메서드 사용 | try-catch 구문 사용 |
장점 | - 간단한 비동기 처리에 적합 - 모든 환경에서 지원 | - 체이닝 가능 - 에러 처리 용이 - 병렬 처리 가능 (Promise.all) | - 동기 코드와 유사한 구조 - 가독성 향상 - 직관적인 에러 처리 |
단점 | - 콜백 지옥 발생 가능 - 에러 처리 복잡 | - 약간의 학습 곡선 존재 - 브라우저 지원 고려 필요 | - 항상 Promise를 반환 - 오래된 환경에서 지원 안 됨 |
비동기 처리 방식 | 콜백 함수를 통해 결과 처리 | then() 메서드를 통해 결과 처리 | await 키워드로 결과를 기다림 |
중첩 처리 | 콜백 안에 콜백을 계속 넣어야 함 | .then() 체이닝으로 처리 | 일반적인 동기 코드처럼 작성 가능 |
병렬 처리 | 복잡한 로직 필요 | Promise.all() 사용 | Promise.all()과 함께 사용 |
순차적 처리 | 콜백 중첩으로 처리 | .then() 체이닝으로 처리 | 일반 동기 코드처럼 작성 |
타입스크립트 통합 | 타입 추론이 어려움 | 제네릭을 통해 타입 안정성 확보 | 가장 타입 안정적 |
테스트 용이성 | 테스트 작성이 복잡할 수 있음 | 테스트 작성이 비교적 쉬움 | 가장 테스트 작성이 쉬움 |
디버깅 | 콜백 중첩으로 인해 어려움 | 스택 트레이스가 깔끔함 | 동기 코드와 유사해 가장 쉬움 |
메모리 사용 | 콜백 중첩 시 메모리 사용량 증가 | 체이닝으로 인한 약간의 오버헤드 | 일반적으로 가장 효율적 |
취소 가능성 | 직접 구현 필요 | 취소 불가능 (별도 구현 필요) | 취소 불가능 (별도 구현 필요) |
구현 예시#
- 콜백 함수 (Callbacks)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // 콜백 함수를 사용한 비동기 처리 예시
function fetchData(callback) {
// 데이터를 가져오는 비동기 작업 시뮬레이션
setTimeout(() => {
const data = { id: 1, name: "John" };
callback(null, data); // 성공시 첫 번째 인자는 null
}, 1000);
}
fetchData((error, data) => {
if (error) {
console.error('에러 발생:', error);
return;
}
console.log('데이터:', data);
});
|
하지만 콜백 방식은 여러 비동기 작업을 연달아 처리해야 할 때 “콜백 지옥"이라는 문제가 발생합니다:
1
2
3
4
5
6
7
8
9
10
| fetchData((error, data1) => {
if (error) return handleError(error);
fetchMoreData(data1, (error, data2) => {
if (error) return handleError(error);
processData(data2, (error, data3) => {
if (error) return handleError(error);
// 이런 식으로 계속 중첩됨…
});
});
});
|
- Promise
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // Promise를 사용한 비동기 처리 예시
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const data = { id: 1, name: "John" };
resolve(data); // 성공시 resolve 호출
// 실패시: reject(new Error('에러 메시지'));
}, 1000);
});
}
fetchData()
.then(data => {
console.log('데이터:', data);
return processData(data); // 다른 Promise 반환
})
.then(result => {
console.log('처리된 결과:', result);
})
.catch(error => {
console.error('에러 발생:', error);
});
|
Promise는 여러 비동기 작업을 병렬로 처리하는 것도 가능하게 해줍니다:
1
2
3
4
5
6
7
8
9
10
11
12
| // 여러 Promise를 동시에 처리
Promise.all([
fetchData1(),
fetchData2(),
fetchData3()
])
.then(([result1, result2, result3]) => {
console.log('모든 데이터:', result1, result2, result3);
})
.catch(error => {
console.error('하나라도 실패하면 호출됨:', error);
});
|
- Async/Await
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| // Async/Await를 사용한 비동기 처리 예시
async function fetchAndProcessData() {
try {
// await는 Promise가 완료될 때까지 기다립니다
const data = await fetchData();
console.log('데이터:', data);
const processedData = await processData(data);
console.log('처리된 결과:', processedData);
return processedData;
} catch (error) {
console.error('에러 발생:', error);
throw error;
}
}
// async 함수는 항상 Promise를 반환합니다
fetchAndProcessData()
.then(result => {
console.log('최종 결과:', result);
})
.catch(error => {
console.error('최종 에러 처리:', error);
});
|
Async/Await도 여러 비동기 작업을 병렬로 처리할 수 있습니다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| async function fetchAllData() {
try {
// Promise.all과 함께 사용하여 병렬 처리
const [result1, result2, result3] = await Promise.all([
fetchData1(),
fetchData2(),
fetchData3()
]);
console.log('모든 결과:', result1, result2, result3);
} catch (error) {
console.error('에러 발생:', error);
}
}
|
참고 및 출처#