Ver3.5 ⚡ JavaScript 비동기 처리와 Promise, async/await 제대로 파헤치기

⚡ JavaScript 비동기 처리와 Promise, async/await 제대로 파헤치기
콜백 지옥부터 모던 비동기까지 — 진짜 실전에서 쓰는 방법만 모았어요 🔥
야 솔직히 말해서 JavaScript 처음 배울 때 비동기 처리 개념 나오면 멘탈 나가지 않았음?? ㅋㅋㅋ 나만 그런 거 아니지. 콜백(Callback) 쓰다가 콜백 안에 콜백 안에 콜백... 이게 무슨 러시아 마트료시카도 아니고 😅
근데 이거 제대로 이해하면 진짜 개발 레벨이 확 올라가거든. 오늘은 JavaScript 비동기 처리의 모든 것을 처음부터 끝까지 싹 다 정리해볼게. 콜백부터 Promise, 그리고 async/await까지 — 실전 코드 예제랑 같이 보면서 가자고 🚀
일단 기초부터 잡고 가야지. 동기(Synchronous)랑 비동기(Asynchronous)의 차이를 모르면 뒤에 내용이 다 안 들어오거든 ㅋㅋ
동기 처리는 코드가 위에서 아래로 순서대로 실행돼. 앞에 있는 작업이 끝나야 다음 작업이 시작됨. 마치 편의점 계산대에서 한 명씩 줄 서서 계산하는 것처럼!
// 동기 처리 예시
console.log("1번 작업 시작");
console.log("2번 작업 시작");
console.log("3번 작업 시작");
// 출력 순서: 1번 → 2번 → 3번 (무조건 이 순서)
비동기 처리는 시간이 걸리는 작업을 기다리지 않고 다음 코드를 먼저 실행해버려. 서버에서 데이터 받아오는 동안 다른 거 먼저 처리하는 거지. 이게 JavaScript의 핵심 특성이야!
console.log("1번 시작");
setTimeout(() => {
console.log("2번 - 1초 후 실행");
}, 1000);
console.log("3번 시작");
// 실제 출력 순서: 1번 → 3번 → 2번 (!!)
// 2번은 1초 기다리는 동안 3번이 먼저 실행됨 ㅋㅋ
이벤트 루프는 JavaScript 비동기의 핵심 메커니즘이야. 구조를 간단히 보면:
콜백은 "나중에 실행할 함수를 미리 넘겨주는 것"이야. 비동기 처리의 가장 기본적인 방법이지. 근데 이게 중첩되면... 진짜 지옥이 펼쳐짐 ㅋㅋㅋ
// 콜백 함수 기본 예시
function fetchData(callback) {
setTimeout(() => {
const data = { name: "재능넷 유저", skill: "JavaScript" };
callback(null, data); // 첫 번째 인자: 에러, 두 번째: 데이터
}, 1000);
}
fetchData((error, data) => {
if (error) {
console.error("에러 발생:", error);
return;
}
console.log("데이터 받음:", data);
});
여러 비동기 작업을 순서대로 처리해야 할 때 콜백을 중첩하면 이런 괴물이 탄생함:
// 콜백 지옥 예시 😱
getUser(userId, (err, user) => {
if (err) throw err;
getProfile(user.id, (err, profile) => {
if (err) throw err;
getPosts(profile.id, (err, posts) => {
if (err) throw err;
getComments(posts[0].id, (err, comments) => {
if (err) throw err;
getLikes(comments[0].id, (err, likes) => {
if (err) throw err;
// 이 시점에서 멘탈 붕괴 ㅋㅋㅋ
console.log(likes);
});
});
});
});
});
2. 에러 처리가 각 단계마다 필요해서 코드 중복 심각
3. 디버깅이 너무 힘들어
4. 코드 재사용이 거의 불가능
이 문제를 해결하기 위해 등장한 게 바로 Promise야! 🎉
Promise는 ES6(2015)에서 도입된 비동기 처리 패턴이야. 이름 그대로 "나중에 결과를 줄게!"라는 약속을 나타내는 객체지. 콜백 지옥을 탈출할 수 있는 첫 번째 열쇠 🗝️
Promise가 생성되고 아직 결과가 없는 상태. 비동기 작업이 진행 중이야.
비동기 작업이 성공적으로 완료된 상태. resolve()가 호출됨.
// Promise 기본 구조
const myPromise = new Promise((resolve, reject) => {
// 비동기 작업 수행
const success = true;
if (success) {
resolve("작업 성공! 🎉"); // Fulfilled 상태로 전환
} else {
reject(new Error("작업 실패 😭")); // Rejected 상태로 전환
}
});
// Promise 사용
myPromise
.then(result => console.log(result)) // 성공 시
.catch(error => console.error(error)) // 실패 시
.finally(() => console.log("무조건 실행")); // 항상 실행
Promise의 진짜 강점은 .then()을 체이닝해서 순차적인 비동기 작업을 깔끔하게 처리할 수 있다는 거야!
// 아까 그 콜백 지옥을 Promise로 바꾸면?
getUser(userId)
.then(user => getProfile(user.id))
.then(profile => getPosts(profile.id))
.then(posts => getComments(posts[0].id))
.then(comments => getLikes(comments[0].id))
.then(likes => console.log(likes))
.catch(error => console.error("에러 발생:", error));
// 훨씬 읽기 쉽지 않음?? ㅋㅋ 에러 처리도 한 곳에서!
하나라도 실패하면 전체 실패! (All or Nothing)
// 3개 API를 동시에 호출 — 순서대로 기다리지 않아서 빠름!
const [users, posts, comments] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
]);
console.log(users, posts, comments);
각 결과의 status가 'fulfilled' 또는 'rejected'로 옴
const results = await Promise.allSettled([
fetch('/api/data1'),
fetch('/api/data2'), // 이게 실패해도
fetch('/api/data3') // 나머지 결과는 다 받을 수 있어
]);
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('성공:', result.value);
} else {
console.log('실패:', result.reason);
}
});
🔹 Promise.any() — 가장 먼저 성공하는 Promise의 결과 사용 (모두 실패하면 AggregateError)
// 타임아웃 구현에 race() 활용!
const timeout = (ms) => new Promise((_, reject) =>
setTimeout(() => reject(new Error('타임아웃!')), ms)
);
const result = await Promise.race([
fetch('/api/slow-endpoint'),
timeout(5000) // 5초 안에 응답 없으면 에러!
]);
드디어 나왔다! ES8(2017)에서 도입된 async/await은 Promise를 더 쉽고 직관적으로 쓸 수 있게 해주는 문법이야. 비동기 코드를 마치 동기 코드처럼 작성할 수 있어서 가독성이 엄청 좋아짐 ㅋㅋ
// async 키워드: 이 함수는 비동기 함수야!
async function fetchUserData() {
// await 키워드: Promise가 완료될 때까지 여기서 기다려!
const response = await fetch('https://api.example.com/user');
const data = await response.json();
return data;
}
// 화살표 함수로도 가능!
const fetchUserData = async () => {
const response = await fetch('https://api.example.com/user');
return await response.json();
};
// async 함수는 항상 Promise를 반환해!
fetchUserData().then(data => console.log(data));
async/await의 에러 처리는 일반 동기 코드처럼 try/catch를 쓰면 돼. Promise의 .catch()보다 훨씬 직관적이야!
async function getUserPosts(userId) {
try {
const userRes = await fetch(`/api/users/${userId}`);
// HTTP 에러 체크
if (!userRes.ok) {
throw new Error(`HTTP 에러! 상태: ${userRes.status}`);
}
const user = await userRes.json();
const postsRes = await fetch(`/api/posts?userId=${user.id}`);
const posts = await postsRes.json();
return posts;
} catch (error) {
console.error("에러 발생:", error.message);
throw error; // 필요하면 상위로 에러 전파
} finally {
console.log("요청 완료 (성공이든 실패든)");
}
}
이거 진짜 중요한 포인트야. await을 순서대로 쓰면 순차 실행이 돼서 느릴 수 있어!
// ❌ 나쁜 예시 — 순차 실행 (총 3초 걸림)
async function slowWay() {
const users = await fetchUsers(); // 1초
const posts = await fetchPosts(); // 1초
const tags = await fetchTags(); // 1초
// 합계: 3초 😭
}
// ✅ 좋은 예시 — 병렬 실행 (총 1초 걸림)
async function fastWay() {
// Promise.all로 동시에 실행!
const [users, posts, tags] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchTags()
]);
// 합계: 1초 🚀 (가장 오래 걸리는 것 기준)
}
// 배열의 각 항목을 순차적으로 처리할 때
async function processItems(items) {
for (const item of items) {
await processItem(item); // 순서 보장됨
console.log(`${item} 처리 완료`);
}
}
// ⚠️ forEach는 async/await과 잘 안 맞아!
// forEach는 await을 기다려주지 않음 — 이거 함정임 ㅋㅋ
items.forEach(async (item) => {
await processItem(item); // 이렇게 하면 안 됨!
});
이론은 충분히 봤으니까 이제 실전에서 자주 쓰는 패턴들 보자고! 재능넷 같은 플랫폼 개발할 때도 이런 패턴들 엄청 많이 쓰거든 ㅋㅋ
// 재사용 가능한 API 호출 유틸리티
async function apiCall(url, options = {}) {
const defaultOptions = {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
};
try {
const response = await fetch(url, { ...defaultOptions, ...options });
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || `HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === 'TypeError') {
throw new Error('네트워크 연결을 확인해주세요');
}
throw error;
}
}
// 사용 예시
const userData = await apiCall('/api/users/me');
const newPost = await apiCall('/api/posts', {
method: 'POST',
body: JSON.stringify({ title: '새 글', content: '내용' })
});
// 실패 시 자동 재시도 함수
async function withRetry(fn, maxRetries = 3, delay = 1000) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
console.log(`시도 ${attempt}/${maxRetries} 실패`);
if (attempt === maxRetries) throw error;
// 지수 백오프: 1초, 2초, 4초... 점점 늘어남
await new Promise(resolve =>
setTimeout(resolve, delay * Math.pow(2, attempt - 1))
);
}
}
}
// 사용 예시
const data = await withRetry(
() => fetch('/api/unstable-endpoint').then(r => r.json()),
3, // 최대 3번 재시도
1000 // 1초 기본 딜레이
);
// 간단한 메모이제이션(캐싱) 구현
const cache = new Map();
async function fetchWithCache(url, ttl = 60000) {
const cached = cache.get(url);
if (cached && Date.now() - cached.timestamp < ttl) {
console.log('캐시에서 반환 ⚡');
return cached.data;
}
const response = await fetch(url);
const data = await response.json();
cache.set(url, { data, timestamp: Date.now() });
console.log('서버에서 새로 받아옴 🌐');
return data;
}
// React에서 비동기 데이터 fetching 패턴
import { useState, useEffect } from 'react';
function useAsyncData(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false; // 클린업을 위한 플래그
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
const result = await response.json();
if (!cancelled) { // 컴포넌트가 언마운트되지 않았을 때만
setData(result);
}
} catch (err) {
if (!cancelled) setError(err);
} finally {
if (!cancelled) setLoading(false);
}
};
fetchData();
return () => { cancelled = true; }; // 클린업
}, [url]);
return { data, loading, error };
}
비동기 처리하다가 진짜 많이 하는 실수들 정리해봤어. 이거 알면 디버깅 시간 엄청 줄어들 거야 ㅋㅋ
// ❌ 잘못된 코드
async function wrong() {
const data = fetch('/api/data'); // await 빠뜨림!
console.log(data); // Promise 객체가 출력됨 😅
}
// ✅ 올바른 코드
async function correct() {
const response = await fetch('/api/data');
const data = await response.json();
console.log(data); // 실제 데이터 출력 ✅
}
// ❌ 잘못된 코드 (일반 함수에서 await 사용)
function notAsync() {
const data = await fetch('/api/data'); // SyntaxError!
}
// ✅ 해결책 1: async 함수로 만들기
async function isAsync() {
const data = await fetch('/api/data'); // OK!
}
// ✅ 해결책 2: 최상위 레벨 await (ES2022, 모듈에서만)
const data = await fetch('/api/data'); // .mjs 파일에서 가능
// ❌ 에러 처리 없음 — UnhandledPromiseRejection 발생!
async function noErrorHandling() {
const data = await fetch('/api/might-fail');
return data.json();
}
// ✅ 에러 처리 포함
async function withErrorHandling() {
try {
const response = await fetch('/api/might-fail');
return await response.json();
} catch (error) {
console.error('에러:', error);
return null; // 기본값 반환
}
}
// Promise.all은 하나라도 실패하면 전체 실패!
// 개별 에러 처리가 필요하면 이렇게
const results = await Promise.all([
fetchUsers().catch(err => ({ error: err, data: [] })),
fetchPosts().catch(err => ({ error: err, data: [] })),
fetchTags().catch(err => ({ error: err, data: [] }))
]);
// 또는 Promise.allSettled 사용!
const settled = await Promise.allSettled([
fetchUsers(),
fetchPosts(),
fetchTags()
]);
// ❌ var 사용 시 클로저 문제
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
// 출력: 3, 3, 3 (예상: 0, 1, 2) 😱
}
// ✅ let 사용으로 해결
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
// 출력: 0, 1, 2 ✅
}
async/await이 나오기 전에는 Generator 함수로 비동기를 처리하기도 했어. async/await의 내부 동작 원리를 이해하는 데 도움이 돼!
// Generator 기반 비동기 (async/await의 원형)
function* asyncGenerator() {
const user = yield fetchUser();
const posts = yield fetchPosts(user.id);
return posts;
}
// async/await은 이걸 자동으로 처리해주는 것!
async function asyncAwaitVersion() {
const user = await fetchUser();
const posts = await fetchPosts(user.id);
return posts;
}
// 비동기 이터레이터 — 대용량 데이터 스트리밍에 유용!
async function* asyncDataStream() {
const pages = [1, 2, 3];
for (const page of pages) {
const data = await fetch(`/api/data?page=${page}`)
.then(r => r.json());
yield data; // 한 페이지씩 yield
}
}
// for await...of로 사용
for await (const pageData of asyncDataStream()) {
console.log('페이지 데이터:', pageData);
// 메모리 효율적으로 대용량 데이터 처리 가능!
}
// 진행 중인 fetch 요청 취소!
const controller = new AbortController();
const { signal } = controller;
async function fetchWithCancel() {
try {
const response = await fetch('/api/large-data', { signal });
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.log('요청이 취소됐어!');
} else {
throw error;
}
}
}
// 5초 후 자동 취소
setTimeout(() => controller.abort(), 5000);
fetchWithCancel();
마지막으로 두 방식을 깔끔하게 비교해볼게. 어떤 상황에서 뭘 써야 하는지 감이 잡힐 거야!
✔ 여러 Promise를 병렬로 처리할 때
✔ Promise.all, race 등 정적 메서드 활용
✔ 함수형 프로그래밍 스타일 선호
✔ 간단한 변환 작업 (.then 하나)
✔ 순차적인 비동기 작업이 많을 때
✔ 복잡한 에러 처리 로직
✔ 조건문/반복문과 함께 사용
✔ 코드 가독성이 중요할 때
재능넷처럼 다양한 API를 동시에 호출해야 하는 플랫폼에서는 특히 Promise.all()과 async/await의 조합이 성능과 가독성 두 마리 토끼를 잡을 수 있어!
비동기 처리는 JavaScript 개발의 핵심 중의 핵심이야. 처음엔 어렵게 느껴지지만 계속 쓰다 보면 자연스럽게 손에 익어. 오늘 배운 내용을 직접 코드로 작성해보면서 연습하는 게 제일 빠른 방법이야! 💪
더 다양한 JavaScript 관련 지식이나 개발 스킬을 배우고 싶다면 재능넷에서 JavaScript 전문가들의 강의나 멘토링을 찾아보는 것도 좋은 방법이야. 실무 경험 있는 개발자한테 직접 배우면 훨씬 빠르게 성장할 수 있거든 🚀
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.