⏱️ 동기 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>