⏱️ 동기 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('모두 실패');
});