Skip to main content

⏱️ 동기 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: 최신 방식, 가독성 최고

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