API 호출이나 파일 처리 같은 비동기 작업을 다루다 보면 콜백 함수를 중첩해서 사용하게 되는 경우가 많습니다. 이런 상황에서 발생하는 '콜백 지옥'을 Promise와 async/await를 활용해 어떻게 효과적으로 해결할 수 있는지 알아보겠습니다.
1. 콜백 지옥의 문제점
// 😱 콜백 지옥의 예시
const getUserData = (userId, callback) => {
setTimeout(() => {
const user = { id: userId, name: 'John' };
callback(user);
}, 1000);
};
const getUserPosts = (user, callback) => {
setTimeout(() => {
const posts = [
{ id: 1, title: 'Post 1' },
{ id: 2, title: 'Post 2' }
];
callback(posts);
}, 1000);
};
const getPostComments = (post, callback) => {
setTimeout(() => {
const comments = [
{ id: 1, text: 'Nice post!' },
{ id: 2, text: 'Thanks for sharing' }
];
callback(comments);
}, 1000);
};
// 콜백 지옥 발생
getUserData(1, (user) => {
console.log('User:', user);
getUserPosts(user, (posts) => {
console.log('Posts:', posts);
getPostComments(posts[0], (comments) => {
console.log('Comments:', comments);
});
});
});
// ✨ Promise를 사용한 개선 코드
const getUserDataPromise = (userId) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const user = { id: userId, name: 'John' };
resolve(user);
}, 1000);
});
};
const getUserPostsPromise = (user) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const posts = [
{ id: 1, title: 'Post 1' },
{ id: 2, title: 'Post 2' }
];
resolve(posts);
}, 1000);
});
};
const getPostCommentsPromise = (post) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const comments = [
{ id: 1, text: 'Nice post!' },
{ id: 2, text: 'Thanks for sharing' }
];
resolve(comments);
}, 1000);
});
};
// Promise 체이닝
getUserDataPromise(1)
.then(user => {
console.log('User:', user);
return getUserPostsPromise(user);
})
.then(posts => {
console.log('Posts:', posts);
return getPostCommentsPromise(posts[0]);
})
.then(comments => {
console.log('Comments:', comments);
})
.catch(error => {
console.error('Error:', error);
});
// 🌟 async/await를 사용한 최종 개선 코드
const fetchUserData = async (userId) => {
try {
const user = await getUserDataPromise(userId);
console.log('User:', user);
const posts = await getUserPostsPromise(user);
console.log('Posts:', posts);
const comments = await getPostCommentsPromise(posts[0]);
console.log('Comments:', comments);
return comments;
} catch (error) {
console.error('Error:', error);
}
};
// 실행
fetchUserData(1);
// ⚡ 병렬 실행이 필요한 경우
const fetchMultipleData = async (userIds) => {
try {
const userPromises = userIds.map(id => getUserDataPromise(id));
const users = await Promise.all(userPromises);
console.log('All users:', users);
return users;
} catch (error) {
console.error('Error fetching multiple users:', error);
}
};
// 실행
fetchMultipleData([1, 2, 3]);
위 코드 예시에서 볼 수 있듯이, 콜백 함수가 중첩되면서 다음과 같은 문제가 발생합니다:
- 코드의 가독성이 떨어짐
- 에러 처리가 복잡해짐
- 비동기 작업의 순서 제어가 어려움
2. Promise를 활용한 개선
Promise를 사용하면 다음과 같은 장점이 있습니다:
- 체이닝을 통한 가독성 향상
- .catch()를 통한 통합 에러 처리
- .then() 메서드로 비동기 작업의 순서 제어 용이
3. async/await로 더 나은 코드 작성
async/await를 사용하면 다음과 같은 이점이 있습니다:
- 동기 코드처럼 직관적인 코드 작성 가능
- try-catch 문으로 에러 처리 가능
- 비동기 작업의 순서를 명확하게 표현
4. 실전 팁
- Promise.all()을 활용한 병렬 처리
const results = await Promise.all([ fetchUser(1), fetchUser(2), fetchUser(3) ]);
- 에러 처리는 항상 try-catch로 감싸기
try { await asyncFunction(); } catch (error) { console.error(error); }
- async 함수는 항상 Promise를 반환한다는 점 기억하기
마치며
비동기 처리는 현대 웹 개발에서 피할 수 없는 부분입니다. Promise와 async/await를 적절히 활용하면 더 깔끔하고 유지보수하기 좋은 코드를 작성할 수 있습니다. 특히 실무에서는 API 호출이나 데이터베이스 작업과 같은 비동기 처리가 매우 빈번하게 발생하므로, 이러한 패턴을 잘 이해하고 활용하는 것이 중요합니다.
다음 글에서는 두 번째 주제인 "불변성을 지키며 상태 관리하기"에 대해 다루도록 하겠습니다.
'개발일지' 카테고리의 다른 글
자바스크립트 불변성(Immutability)을 지키며 상태 관리하기 📘 (0) | 2024.12.12 |
---|---|
Java Reflection API 개발일지: 동적 프로그래밍의 실전 여행 🔍 (0) | 2024.12.11 |
Java 개발일지: CompletableFuture로 구현하는 비동기 프로그래밍 여정 📘 (0) | 2024.12.10 |
Java Records: 현대적 데이터 클래스의 새로운 패러다임 📝 (0) | 2024.12.10 |
Java Optional: 우아한 null 처리의 완벽 가이드 ✨ (0) | 2024.12.10 |