본문으로 건너뛰기

⏱️ 동기 vs 비동기

📖 정의​

**동기(Synchronous)**는 코드가 순서대로 하나씩 실행되는 방식이고, **비동기(Asynchronous)**는 시간이 걸리는 작업을 기다리지 않고 다음 코드를 실행하는 방식입니다. JavaScript는 단일 스레드 언어지만, 비동기 처리를 통해 여러 작업을 효율적으로 처리할 수 있습니다.

🎯 비유로 이해하기​

레스토랑 주문 비유​

동기와 비동기를 레스토랑에 비유하면:

동기 방식 (한 번에 한 명씩)
├─ 손님 A 주문 → 조리 완료 대기 → 서빙 → 계산
├─ 손님 B 주문 → 조리 완료 대기 → 서빙 → 계산
└─ 손님 C 주문 → 조리 완료 대기 → 서빙 → 계산

문제점:
- 손님 A가 음식 기다리는 동안 B, C는 주문도 못 함
- 비효율적
- 시간 낭비

비동기 방식 (여러 명 동시에)
├─ 손님 A 주문 → 주방으로 전달 (대기하지 않음)
├─ 손님 B 주문 → 주방으로 전달 (대기하지 않음)
├─ 손님 C 주문 → 주방으로 전달 (대기하지 않음)
└─ 조리 완료되면 각자에게 서빙

장점:
- 효율적
- 동시에 여러 주문 처리
- 시간 절약

세탁기 비유​

동기 방식 (한 가지만 집중)
1. 세탁기 돌리기 시작
2. 세탁기 앞에서 끝날 때까지 대기 (30분)
3. 세탁 끝나면 건조기에 넣기
4. 건조기 앞에서 끝날 때까지 대기 (60분)
5. 완료

→ 90분 동안 아무것도 못 함!

비동기 방식 (멀티태스킹)
1. 세탁기 돌리기 시작
2. 세탁하는 동안 청소하기
3. 청소하는 동안 요리하기
4. 요리하는 동안 책 읽기
5. 세탁 완료 알림 → 건조기에 넣기
6. 건조하는 동안 다른 일 하기
7. 건조 완료 알림 → 옷 개기

→ 90분 동안 여러 일 처리!

택배 배송 비유​

동기 방식 (직렬 처리)
배송기사가 택배를 하나씩만 처리:
1. A 집에 가서 배송 → 문 열 때까지 대기
2. B 집에 가서 배송 → 문 열 때까지 대기
3. C 집에 가서 배송 → 문 열 때까지 대기

→ 하루에 5개 배송

비동기 방식 (병렬 처리)
배송기사가 여러 택배를 동시에:
1. A, B, C 집 현관에 모두 배송
2. 각 집에서 확인할 때까지 기다리지 않음
3. 다음 동네로 이동
4. 확인 알림은 나중에 받음

→ 하루에 50개 배송

⚙️ 작동 원리​

1. 동기 코드​

// 동기 코드는 순서대로 실행
console.log('1. 시작');

function 무거운작업() {
console.log('2. 무거운 작업 시작');

// 3초 동안 블로킹 (다른 코드 실행 안 됨)
const start = Date.now();
while (Date.now() - start < 3000) {
// 3초 대기
}

console.log('3. 무거운 작업 완료');
return '결과';
}

const 결과 = 무거운작업();
console.log('4. 결과:', 결과);
console.log('5. 끝');

// 출력 순서 (예측 가능):
// 1. 시작
// 2. 무거운 작업 시작
// (3초 대기... 화면 멈춤)
// 3. 무거운 작업 완료
// 4. 결과: 결과
// 5. 끝

// 문제점:
// - 무거운 작업 동안 브라우저 멈춤
// - 사용자는 아무것도 할 수 없음
// - 버튼 클릭도 안 됨
// - 스크롤도 안 됨

2. 비동기 코드 (setTimeout)​

// 비동기 코드는 순서가 보장되지 않음
console.log('1. 시작');

setTimeout(() => {
console.log('2. 3초 후 실행');
}, 3000);

console.log('3. 끝');

// 출력 순서:
// 1. 시작
// 3. 끝
// (3초 후)
// 2. 3초 후 실행

// 설명:
// setTimeout은 비동기 함수
// → 3초를 기다리지 않고 다음 코드 실행
// → 3초 후 콜백 함수 실행

// 장점:
// - 브라우저 멈추지 않음
// - 다른 작업 계속 가능
// - 사용자 경험 좋음

3. 콜백 함수​

// 콜백: 나중에 실행될 함수

// 파일 읽기 예시 (Node.js)
const fs = require('fs');

console.log('1. 파일 읽기 시작');

fs.readFile('data.txt', 'utf8', (error, data) => {
// 이 함수는 파일 읽기가 완료되면 실행됨
if (error) {
console.error('에러:', error);
return;
}
console.log('3. 파일 내용:', data);
});

console.log('2. 파일 읽는 동안 다른 작업');

// 출력:
// 1. 파일 읽기 시작
// 2. 파일 읽는 동안 다른 작업
// (파일 읽기 완료 후)
// 3. 파일 내용: ...

// 콜백의 문제: 콜백 지옥 (Callback Hell)
getUserData(userId, (user) => {
getOrders(user.id, (orders) => {
getOrderDetails(orders[0].id, (details) => {
getPaymentInfo(details.paymentId, (payment) => {
console.log(payment);
// 너무 깊어짐!
});
});
});
});

// 문제점:
// - 코드 가독성 나쁨
// - 에러 처리 어려움
// - 유지보수 힘듦

4. Promise (약속)​

// Promise: 미래에 완료될 작업을 나타내는 객체

// Promise 생성
const promise = new Promise((resolve, reject) => {
// 비동기 작업
setTimeout(() => {
const success = true;

if (success) {
resolve('성공!'); // 성공 시
} else {
reject('실패!'); // 실패 시
}
}, 2000);
});

// Promise 사용
promise
.then((result) => {
console.log('결과:', result); // 성공!
})
.catch((error) => {
console.error('에러:', error);
})
.finally(() => {
console.log('항상 실행됨');
});

// Promise의 3가지 상태:
// 1. Pending (대기): 초기 상태
// 2. Fulfilled (이행): 성공
// 3. Rejected (거부): 실패

// Promise 체이닝
fetch('/api/user')
.then(response => response.json())
.then(user => fetch(`/api/orders/${user.id}`))
.then(response => response.json())
.then(orders => {
console.log('주문 목록:', orders);
})
.catch(error => {
console.error('에러:', error);
});

// 장점:
// - 콜백 지옥 해결
// - 에러 처리 간편
// - 체이닝 가능

5. async/await (최신 방식)​

// async/await: Promise를 더 쉽게 사용

// async 함수는 항상 Promise를 반환
async function fetchUserData() {
try {
// await: Promise가 완료될 때까지 대기
const response = await fetch('/api/user');
const user = await response.json();

const ordersResponse = await fetch(`/api/orders/${user.id}`);
const orders = await ordersResponse.json();

return orders;
} catch (error) {
console.error('에러:', error);
throw error;
}
}

// 사용
fetchUserData()
.then(orders => {
console.log('주문:', orders);
});

// 또는
async function main() {
const orders = await fetchUserData();
console.log('주문:', orders);
}

main();

// 장점:
// - 동기 코드처럼 읽기 쉬움
// - try-catch로 에러 처리
// - 가독성 최고

💡 실제 예시​

기본 비동기 패턴​

// 1. setTimeout - 시간 지연
console.log('지금');

setTimeout(() => {
console.log('1초 후');
}, 1000);

setTimeout(() => {
console.log('2초 후');
}, 2000);

console.log('즉시');

// 출력:
// 지금
// 즉시
// (1초 후)
// 1초 후
// (2초 후)
// 2초 후

// 2. setInterval - 반복 실행
let count = 0;

const timer = setInterval(() => {
count++;
console.log(`${count}초 경과`);

if (count === 5) {
clearInterval(timer); // 5초 후 중지
console.log('타이머 종료');
}
}, 1000);

// 3. 즉시 실행 비동기
Promise.resolve('즉시 완료')
.then(result => {
console.log(result);
});

console.log('이게 먼저');

// 출력:
// 이게 먼저
// 즉시 완료

Promise 상세 예시​

// Promise 기본 사용법
function 데이터가져오기(id) {
return new Promise((resolve, reject) => {
console.log(`${id} 데이터 요청 중...`);

// 서버 요청 시뮬레이션
setTimeout(() => {
if (id > 0) {
resolve({
id: id,
name: `사용자${id}`,
email: `user${id}@example.com`
});
} else {
reject(new Error('잘못된 ID'));
}
}, 1000);
});
}

// 사용 예시 1: then/catch
데이터가져오기(1)
.then(user => {
console.log('성공:', user);
return 데이터가져오기(2); // 체이닝
})
.then(user => {
console.log('성공:', user);
})
.catch(error => {
console.error('에러:', error.message);
})
.finally(() => {
console.log('완료!');
});

// Promise 여러 개 동시 실행
const promise1 = 데이터가져오기(1);
const promise2 = 데이터가져오기(2);
const promise3 = 데이터가져오기(3);

// Promise.all - 모두 완료될 때까지 대기
Promise.all([promise1, promise2, promise3])
.then(results => {
console.log('모두 완료:', results);
// [user1, user2, user3]
})
.catch(error => {
console.error('하나라도 실패하면:', error);
});

// Promise.race - 가장 먼저 완료되는 것
Promise.race([promise1, promise2, promise3])
.then(result => {
console.log('가장 빠른 것:', result);
});

// Promise.allSettled - 모두 완료 (실패 포함)
Promise.allSettled([
데이터가져오기(1),
데이터가져오기(-1), // 실패
데이터가져오기(3)
])
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`${index}: 성공`, result.value);
} else {
console.log(`${index}: 실패`, result.reason);
}
});
});

// Promise.any - 하나라도 성공하면
Promise.any([
데이터가져오기(-1),
데이터가져오기(-2),
데이터가져오기(3)
])
.then(result => {
console.log('첫 번째 성공:', result);
})
.catch(() => {
console.log('모두 실패');
});

async/await 상세 예시​

// async 함수는 항상 Promise 반환
async function 간단한함수() {
return '결과';
}

간단한함수().then(result => {
console.log(result); // 결과
});

// await은 async 함수 안에서만 사용 가능
async function 사용자정보가져오기(userId) {
try {
console.log('사용자 정보 로딩...');

// API 호출 (await로 대기)
const userResponse = await fetch(`/api/users/${userId}`);
const user = await userResponse.json();
console.log('사용자:', user.name);

// 사용자의 게시글 가져오기
const postsResponse = await fetch(`/api/users/${userId}/posts`);
const posts = await postsResponse.json();
console.log('게시글 수:', posts.length);

// 각 게시글의 댓글 가져오기
const commentsPromises = posts.map(post =>
fetch(`/api/posts/${post.id}/comments`)
.then(res => res.json())
);

const allComments = await Promise.all(commentsPromises);
console.log('총 댓글 수:', allComments.flat().length);

return {
user,
posts,
comments: allComments
};

} catch (error) {
console.error('에러 발생:', error.message);
throw error;
}
}

// 사용
async function main() {
const data = await 사용자정보가져오기(1);
console.log('완료:', data);
}

main();

// 병렬 실행 (더 빠름)
async function 병렬실행() {
// ❌ 느린 방법 (순차 실행)
const user1 = await 데이터가져오기(1); // 1초
const user2 = await 데이터가져오기(2); // 1초
const user3 = await 데이터가져오기(3); // 1초
// 총 3초

// ✅ 빠른 방법 (병렬 실행)
const [user1, user2, user3] = await Promise.all([
데이터가져오기(1),
데이터가져오기(2),
데이터가져오기(3)
]);
// 총 1초 (동시 실행)

return [user1, user2, user3];
}

실전 API 호출 예시​

// 1. 기본 API 호출
async function 유저목록가져오기() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users');

if (!response.ok) {
throw new Error(`HTTP 에러! 상태: ${response.status}`);
}

const users = await response.json();
return users;

} catch (error) {
console.error('API 호출 실패:', error);
throw error;
}
}

// 2. POST 요청
async function 사용자생성(userData) {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData)
});

const newUser = await response.json();
console.log('생성된 사용자:', newUser);
return newUser;

} catch (error) {
console.error('사용자 생성 실패:', error);
throw error;
}
}

// 사용
사용자생성({
name: '김철수',
email: 'kim@example.com'
});

// 3. 타임아웃 추가
async function fetchWithTimeout(url, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);

try {
const response = await fetch(url, {
signal: controller.signal
});
clearTimeout(timeoutId);
return await response.json();

} catch (error) {
if (error.name === 'AbortError') {
throw new Error('요청 시간 초과');
}
throw error;
}
}

// 사용
fetchWithTimeout('https://api.example.com/data', 3000)
.then(data => console.log(data))
.catch(error => console.error(error.message));

// 4. 재시도 로직
async function fetchWithRetry(url, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
console.log(`시도 ${i + 1}/${maxRetries}`);
const response = await fetch(url);
return await response.json();

} catch (error) {
if (i === maxRetries - 1) {
throw new Error(`${maxRetries}번 시도 후 실패`);
}

// 지수 백오프 (1초, 2초, 4초...)
const delay = Math.pow(2, i) * 1000;
console.log(`${delay}ms 후 재시도...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}

// 5. 로딩 상태 관리
async function 데이터로딩(url) {
let loading = true;
let error = null;
let data = null;

try {
console.log('로딩 시작...');
data = await fetch(url).then(res => res.json());
console.log('로딩 완료!');

} catch (err) {
error = err;
console.error('에러 발생:', err);

} finally {
loading = false;
}

return { data, loading, error };
}

React에서 비동기 처리​

import React, { useState, useEffect } from 'react';

function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

// 컴포넌트 마운트 시 데이터 가져오기
useEffect(() => {
async function fetchUsers() {
try {
setLoading(true);
setError(null);

const response = await fetch('https://jsonplaceholder.typicode.com/users');

if (!response.ok) {
throw new Error('데이터를 불러올 수 없습니다');
}

const data = await response.json();
setUsers(data);

} catch (err) {
setError(err.message);

} finally {
setLoading(false);
}
}

fetchUsers();
}, []); // 빈 배열: 마운트 시 한 번만 실행

// 로딩 중
if (loading) {
return <div>로딩 중...</div>;
}

// 에러 발생
if (error) {
return <div>에러: {error}</div>;
}

// 데이터 표시
return (
<div>
<h1>사용자 목록</h1>
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} ({user.email})
</li>
))}
</ul>
</div>
);
}

// 커스텀 Hook으로 재사용
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
let cancelled = false;

async function fetchData() {
try {
setLoading(true);
const response = await fetch(url);
const json = await response.json();

if (!cancelled) {
setData(json);
}
} catch (err) {
if (!cancelled) {
setError(err);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}

fetchData();

// 클린업: 컴포넌트 언마운트 시
return () => {
cancelled = true;
};
}, [url]);

return { data, loading, error };
}

// 사용
function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);

if (loading) return <div>로딩 중...</div>;
if (error) return <div>에러!</div>;

return <div>{user.name}</div>;
}

에러 처리 패턴​

// 1. try-catch-finally
async function 에러처리예시1() {
try {
const data = await fetch('/api/data').then(r => r.json());
console.log('성공:', data);

} catch (error) {
console.error('에러 발생:', error);

// 에러 타입별 처리
if (error instanceof TypeError) {
console.error('네트워크 에러');
} else if (error.message.includes('404')) {
console.error('찾을 수 없음');
} else {
console.error('알 수 없는 에러');
}

} finally {
console.log('항상 실행');
// 로딩 상태 해제 등
}
}

// 2. Promise catch
fetch('/api/data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.then(data => {
console.log(data);
})
.catch(error => {
console.error('에러:', error);
});

// 3. 커스텀 에러
class APIError extends Error {
constructor(message, statusCode) {
super(message);
this.name = 'APIError';
this.statusCode = statusCode;
}
}

async function fetchData(url) {
try {
const response = await fetch(url);

if (!response.ok) {
throw new APIError(
'데이터를 가져올 수 없습니다',
response.status
);
}

return await response.json();

} catch (error) {
if (error instanceof APIError) {
console.error(`API 에러 (${error.statusCode}):`, error.message);

if (error.statusCode === 404) {
console.log('데이터가 없습니다');
} else if (error.statusCode >= 500) {
console.log('서버 오류입니다');
}
} else {
console.error('네트워크 에러:', error);
}

throw error; // 에러를 다시 던짐
}
}

// 4. 에러 바운더리 패턴
async function safeAsync(asyncFn, fallbackValue = null) {
try {
return await asyncFn();
} catch (error) {
console.error('에러 발생, 기본값 반환:', error);
return fallbackValue;
}
}

// 사용
const users = await safeAsync(
() => fetch('/api/users').then(r => r.json()),
[] // 에러 시 빈 배열 반환
);

실전 복합 예시​

// 여러 API를 조합하여 데이터 가져오기
async function getUserWithDetails(userId) {
try {
console.log('데이터 로딩 시작...');

// 1. 사용자 기본 정보
const userResponse = await fetch(`/api/users/${userId}`);
const user = await userResponse.json();
console.log('✓ 사용자 정보 로드');

// 2. 병렬로 여러 데이터 가져오기
const [posts, followers, following] = await Promise.all([
fetch(`/api/users/${userId}/posts`).then(r => r.json()),
fetch(`/api/users/${userId}/followers`).then(r => r.json()),
fetch(`/api/users/${userId}/following`).then(r => r.json())
]);
console.log('✓ 게시글, 팔로워, 팔로잉 로드');

// 3. 각 게시글의 좋아요 수 가져오기
const postsWithLikes = await Promise.all(
posts.map(async (post) => {
const likes = await fetch(`/api/posts/${post.id}/likes`)
.then(r => r.json());
return {
...post,
likesCount: likes.length
};
})
);
console.log('✓ 좋아요 정보 로드');

// 4. 최종 데이터 구성
return {
user: {
...user,
stats: {
posts: posts.length,
followers: followers.length,
following: following.length
}
},
posts: postsWithLikes,
followers,
following
};

} catch (error) {
console.error('데이터 로딩 실패:', error);
throw error;
}
}

// 사용
async function displayUserProfile(userId) {
const loadingEl = document.getElementById('loading');
const errorEl = document.getElementById('error');
const contentEl = document.getElementById('content');

try {
// 로딩 상태
loadingEl.style.display = 'block';
errorEl.style.display = 'none';
contentEl.style.display = 'none';

// 데이터 로드
const data = await getUserWithDetails(userId);

// 성공: 데이터 표시
loadingEl.style.display = 'none';
contentEl.style.display = 'block';
contentEl.innerHTML = `
<h1>${data.user.name}</h1>
<p>게시글: ${data.user.stats.posts}</p>
<p>팔로워: ${data.user.stats.followers}</p>
<p>팔로잉: ${data.user.stats.following}</p>
`;

} catch (error) {
// 에러 상태
loadingEl.style.display = 'none';
errorEl.style.display = 'block';
errorEl.textContent = `에러: ${error.message}`;
}
}

// 버튼 클릭 시 실행
document.getElementById('loadBtn').addEventListener('click', () => {
displayUserProfile(1);
});

디바운스와 쓰로틀​

// 디바운스: 마지막 호출 후 일정 시간 대기
function debounce(func, delay) {
let timeoutId;

return function (...args) {
clearTimeout(timeoutId);

timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}

// 사용: 검색 입력
const searchInput = document.getElementById('search');

const handleSearch = debounce(async (query) => {
console.log('검색 중:', query);

const results = await fetch(`/api/search?q=${query}`)
.then(r => r.json());

console.log('결과:', results);
}, 500); // 500ms 대기

searchInput.addEventListener('input', (e) => {
handleSearch(e.target.value);
});

// 쓰로틀: 일정 시간마다 한 번만 실행
function throttle(func, limit) {
let inThrottle;

return function (...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;

setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}

// 사용: 스크롤 이벤트
const handleScroll = throttle(() => {
console.log('스크롤 위치:', window.scrollY);
}, 200); // 200ms마다 최대 1번

window.addEventListener('scroll', handleScroll);

// 비동기 디바운스
function asyncDebounce(func, delay) {
let timeoutId;
let latestResolve;

return function (...args) {
clearTimeout(timeoutId);

return new Promise((resolve) => {
latestResolve = resolve;

timeoutId = setTimeout(async () => {
const result = await func.apply(this, args);
if (latestResolve === resolve) {
resolve(result);
}
}, delay);
});
};
}

// 사용
const debouncedFetch = asyncDebounce(async (query) => {
const response = await fetch(`/api/search?q=${query}`);
return await response.json();
}, 500);

searchInput.addEventListener('input', async (e) => {
const results = await debouncedFetch(e.target.value);
console.log('결과:', results);
});

🤔 자주 묻는 질문​

Q1. 동기와 비동기는 언제 사용하나요?​

A: 작업의 특성에 따라 선택합니다:

// 동기 사용 (순서가 중요한 경우)
const useSynchronous = {
적합한 경우: [
'간단한 계산',
'즉시 완료되는 작업',
'순서가 중요한 로직',
'데이터 의존성이 있는 경우'
],
예시: `
// 계산
const sum = 1 + 2 + 3;

// 배열 처리
const doubled = numbers.map(n => n * 2);

// 조건 분기
if (user.isAdmin) {
console.log('관리자');
}
`
};

// 비동기 사용 (시간이 걸리는 경우)
const useAsynchronous = {
적합한 경우: [
'API 호출',
'파일 읽기/쓰기',
'데이터베이스 쿼리',
'타이머',
'사용자 입력 대기',
'애니메이션'
],
예시: `
// API 호출
const data = await fetch('/api/data');

// 파일 읽기
const file = await fs.readFile('data.txt');

// 타이머
await new Promise(resolve => setTimeout(resolve, 1000));
`
};

// 판단 기준
function shouldUseAsync(task) {
return (
task.takesTime || // 시간이 걸리는가?
task.waitForExternal || // 외부 리소스를 기다리는가?
task.canBeParallel // 병렬 처리 가능한가?
);
}

// 예시: 음식 주문
// 동기 방식 (비효율)
function orderSync() {
const pizza = makePizza(); // 30분
const salad = makeSalad(); // 10분
const drink = getDrink(); // 1분
return { pizza, salad, drink }; // 총 41분
}

// 비동기 방식 (효율)
async function orderAsync() {
const [pizza, salad, drink] = await Promise.all([
makePizza(), // 30분
makeSalad(), // 10분
getDrink() // 1분
]);
return { pizza, salad, drink }; // 총 30분 (병렬)
}

Q2. Promise와 async/await 중 무엇을 사용해야 하나요?​

A: async/await를 권장하지만, 상황에 따라 Promise도 유용합니다:

// Promise 체이닝
function promiseStyle() {
return fetch('/api/user')
.then(res => res.json())
.then(user => fetch(`/api/orders/${user.id}`))
.then(res => res.json())
.then(orders => {
console.log(orders);
return orders;
})
.catch(error => {
console.error(error);
throw error;
});
}

// async/await (더 읽기 쉬움)
async function asyncStyle() {
try {
const userRes = await fetch('/api/user');
const user = await userRes.json();

const ordersRes = await fetch(`/api/orders/${user.id}`);
const orders = await ordersRes.json();

console.log(orders);
return orders;

} catch (error) {
console.error(error);
throw error;
}
}

// Promise가 더 나은 경우
// 1. 여러 Promise를 병렬 처리
Promise.all([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/comments')
])
.then(([users, posts, comments]) => {
console.log({ users, posts, comments });
});

// async/await로는 더 길어짐
async function parallelAsync() {
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 });
}

// 2. 조건부 체이닝
getUser()
.then(user => {
if (user.isAdmin) {
return getAdminData();
}
return getUserData();
})
.then(data => {
console.log(data);
});

// async/await가 더 나은 경우
// 1. 순차적인 비동기 작업
async function sequential() {
const user = await getUser();
const profile = await getProfile(user.id);
const posts = await getPosts(profile.id);
return { user, profile, posts };
}

// 2. 복잡한 에러 처리
async function complexErrorHandling() {
try {
const data = await fetchData();

if (!data.isValid) {
throw new Error('Invalid data');
}

const processed = await processData(data);
return processed;

} catch (error) {
if (error.code === 'NETWORK_ERROR') {
console.log('네트워크 에러, 재시도...');
return await fetchData(); // 재시도
}
throw error;
}
}

// 권장 사항
const recommendation = {
기본: 'async/await 사용 (가독성)',
병렬처리: 'Promise.all 사용',
레이스조건: 'Promise.race 사용',
복잡한로직: 'async/await + Promise 조합'
};

Q3. 콜백 지옥을 어떻게 피하나요?​

A: Promise나 async/await를 사용하여 해결합니다:

// ❌ 콜백 지옥 (Callback Hell)
function callbackHell() {
getUser(userId, (error, user) => {
if (error) {
handleError(error);
return;
}

getProfile(user.id, (error, profile) => {
if (error) {
handleError(error);
return;
}

getPosts(profile.id, (error, posts) => {
if (error) {
handleError(error);
return;
}

getComments(posts[0].id, (error, comments) => {
if (error) {
handleError(error);
return;
}

console.log('완료:', comments);
});
});
});
});
}

// ✅ 해결 1: Promise 체이닝
function promiseSolution() {
getUser(userId)
.then(user => getProfile(user.id))
.then(profile => getPosts(profile.id))
.then(posts => getComments(posts[0].id))
.then(comments => {
console.log('완료:', comments);
})
.catch(error => {
handleError(error);
});
}

// ✅ 해결 2: async/await (최고!)
async function asyncSolution() {
try {
const user = await getUser(userId);
const profile = await getProfile(user.id);
const posts = await getPosts(profile.id);
const comments = await getComments(posts[0].id);

console.log('완료:', comments);

} catch (error) {
handleError(error);
}
}

// ✅ 해결 3: 함수 분리
async function getUserData(userId) {
const user = await getUser(userId);
return user;
}

async function getUserProfile(user) {
const profile = await getProfile(user.id);
return profile;
}

async function getUserPosts(profile) {
const posts = await getPosts(profile.id);
return posts;
}

async function getPostComments(posts) {
const comments = await getComments(posts[0].id);
return comments;
}

async function mainFlow() {
try {
const user = await getUserData(userId);
const profile = await getUserProfile(user);
const posts = await getUserPosts(profile);
const comments = await getPostComments(posts);

console.log('완료:', comments);

} catch (error) {
handleError(error);
}
}

// ✅ 해결 4: Promise 래퍼 함수
function promisify(fn) {
return function (...args) {
return new Promise((resolve, reject) => {
fn(...args, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
};
}

// 콜백 함수를 Promise로 변환
const getUserAsync = promisify(getUser);
const getProfileAsync = promisify(getProfile);
const getPostsAsync = promisify(getPosts);
const getCommentsAsync = promisify(getComments);

// 이제 async/await 사용 가능
async function modernFlow() {
const user = await getUserAsync(userId);
const profile = await getProfileAsync(user.id);
const posts = await getPostsAsync(profile.id);
const comments = await getCommentsAsync(posts[0].id);

return comments;
}

Q4. 이벤트 루프란 무엇인가요?​

A: JavaScript의 비동기 처리를 가능하게 하는 메커니즘입니다:

// JavaScript 실행 모델

/*
┌─────────────────────────────┐
│ Call Stack │ 동기 코드 실행
│ (현재 실행 중인 함수) │
└─────────────────────────────┘
↓
┌─────────────────────────────┐
│ Web APIs │ 비동기 작업 처리
│ (setTimeout, fetch 등) │ (브라우저/Node.js 제공)
└─────────────────────────────┘
↓
┌─────────────────────────────┐
│ Task Queue (Macro) │ 콜백 대기
│ (setTimeout, I/O) │
└─────────────────────────────┘
↓
┌─────────────────────────────┐
│ Microtask Queue │ Promise 대기
│ (Promise, async/await) │ (우선순위 높음)
└─────────────────────────────┘
↓
Event Loop
(작업을 Call Stack으로)
*/

// 예시 1: 실행 순서
console.log('1. 시작');

setTimeout(() => {
console.log('3. setTimeout (Macro Task)');
}, 0);

Promise.resolve().then(() => {
console.log('2. Promise (Micro Task)');
});

console.log('1. 끝');

// 출력 순서:
// 1. 시작
// 1. 끝
// 2. Promise (Micro Task)
// 3. setTimeout (Macro Task)

// 설명:
// 1. 동기 코드 먼저 (console.log)
// 2. Microtask Queue (Promise)
// 3. Macrotask Queue (setTimeout)

// 예시 2: 복잡한 순서
console.log('A');

setTimeout(() => {
console.log('B');

Promise.resolve().then(() => {
console.log('C');
});
}, 0);

Promise.resolve()
.then(() => {
console.log('D');
})
.then(() => {
console.log('E');
});

console.log('F');

// 출력:
// A (동기)
// F (동기)
// D (Microtask)
// E (Microtask)
// B (Macrotask)
// C (Microtask - B 실행 후)

// 예시 3: async/await와 이벤트 루프
async function asyncFunc() {
console.log('1');

await Promise.resolve();

console.log('2');
}

console.log('3');
asyncFunc();
console.log('4');

// 출력:
// 3
// 1
// 4
// 2

// 설명:
// - await 이전 코드는 동기 실행
// - await는 Promise를 기다림
// - await 이후 코드는 Microtask로

// Task 우선순위:
// 1. 동기 코드 (Call Stack)
// 2. Microtask (Promise, async/await)
// 3. Macrotask (setTimeout, setInterval)
// 4. requestAnimationFrame (렌더링 전)

// 실전 예시: 무거운 작업 분할
async function heavyTask() {
const items = Array.from({ length: 1000000 }, (_, i) => i);

// ❌ 블로킹 (UI 멈춤)
for (let item of items) {
processItem(item);
}

// ✅ 비블로킹 (UI 반응)
for (let i = 0; i < items.length; i += 1000) {
// 1000개씩 처리
for (let j = 0; j < 1000 && i + j < items.length; j++) {
processItem(items[i + j]);
}

// 다른 작업이 실행될 기회
await new Promise(resolve => setTimeout(resolve, 0));
}
}

Q5. fetch와 axios의 차이는 무엇인가요?​

A: 둘 다 HTTP 요청을 하지만 사용법과 기능이 다릅니다:

// fetch (브라우저 내장)
// 장점: 추가 설치 불필요, 표준 API
// 단점: 에러 처리 복잡, JSON 변환 수동

async function fetchExample() {
try {
// GET 요청
const response = await fetch('https://api.example.com/users');

// ❌ fetch는 HTTP 에러를 throw하지 않음!
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}

// JSON 변환 필요
const users = await response.json();
console.log(users);

// POST 요청
const newUser = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: '김철수',
email: 'kim@example.com'
})
});

const result = await newUser.json();
return result;

} catch (error) {
console.error('Fetch 에러:', error);
throw error;
}
}

// axios (라이브러리)
// 장점: 편리한 API, 자동 JSON 변환, 에러 처리 쉬움
// 단점: 추가 설치 필요 (npm install axios)

import axios from 'axios';

async function axiosExample() {
try {
// GET 요청 (자동 JSON 변환)
const response = await axios.get('https://api.example.com/users');
const users = response.data; // 자동으로 JSON
console.log(users);

// POST 요청 (더 간단)
const result = await axios.post('https://api.example.com/users', {
name: '김철수',
email: 'kim@example.com'
});

return result.data;

} catch (error) {
// axios는 자동으로 HTTP 에러를 throw
if (error.response) {
// 서버 응답 (4xx, 5xx)
console.error('서버 에러:', error.response.status);
console.error('에러 데이터:', error.response.data);
} else if (error.request) {
// 요청은 보냈지만 응답 없음
console.error('응답 없음:', error.request);
} else {
// 요청 설정 중 에러
console.error('설정 에러:', error.message);
}
throw error;
}
}

// 기능 비교
const comparison = {
JSON변환: {
fetch: 'response.json() 수동 호출',
axios: '자동 (response.data)'
},

에러처리: {
fetch: '수동으로 response.ok 체크',
axios: '자동으로 throw'
},

타임아웃: {
fetch: 'AbortController 사용',
axios: '{ timeout: 5000 } 옵션'
},

인터셉터: {
fetch: '없음 (직접 구현)',
axios: '내장 (요청/응답 가로채기)'
},

취소: {
fetch: 'AbortController',
axios: 'CancelToken'
}
};

// axios 인터셉터 (편리!)
axios.interceptors.request.use(
config => {
// 모든 요청에 토큰 추가
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
error => {
return Promise.reject(error);
}
);

axios.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
// 인증 에러 시 로그인 페이지로
window.location.href = '/login';
}
return Promise.reject(error);
}
);

// fetch로 동일한 기능 구현 (복잡함)
async function fetchWithToken(url, options = {}) {
const token = localStorage.getItem('token');

const response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
}
});

if (response.status === 401) {
window.location.href = '/login';
throw new Error('Unauthorized');
}

if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}

return await response.json();
}

// 결론: 선택 가이드
const guide = {
fetch사용: [
'간단한 프로젝트',
'번들 크기 중요',
'최신 브라우저만',
'기본 기능만 필요'
],

axios사용: [
'복잡한 프로젝트',
'편의 기능 필요',
'에러 처리 중요',
'인터셉터 필요',
'구형 브라우저 지원'
]
};

🎓 다음 단계​

동기/비동기를 이해했다면, 다음을 학습해보세요:

  1. 이벤트 루프 - JavaScript 실행 모델 깊이 이해하기
  2. 클로저란? - 비동기 코드에서 자주 사용되는 클로저
  3. 프로토타입이란? - JavaScript 객체 이해하기

🎬 마무리​

동기와 비동기는 JavaScript의 핵심 개념입니다:

  • 동기: 순차 실행, 간단하지만 블로킹
  • 비동기: 병렬 처리, 복잡하지만 효율적
  • 콜백: 전통적 방식, 콜백 지옥 위험
  • Promise: 체이닝 가능, 에러 처리 개선
  • async/await: 최신 방식, 가독성 최고

비동기를 마스터하면 빠르고 반응성 좋은 웹 애플리케이션을 만들 수 있습니다!