멋사 20일차 TIL - JavaScript 비동기 프로그래밍 완전 정리 📚¶
날짜: 2025년 8월 26일 (화)
과정: 멋쟁이 사자처럼 클라우드 엔지니어링 20일차
주제: JavaScript 비동기 프로그래밍 & Fetch API
🎯 오늘 배운 핵심 개념¶
📋 학습 목표¶
- [x] 비동기 프로그래밍의 개념과 필요성 이해
- [x] 콜백, Promise, Async/Await 차이점 학습
- [x] Fetch API 사용법 완전 마스터
- [x] Promise 병렬 처리 방법 습득
- [x] 경쟁 상태 문제와 해결책 이해
🌟 1. 비동기 프로그래밍이란?¶
🤔 왜 비동기가 필요할까?¶
동기 처리는 코드가 위에서 아래로 순서대로 실행되는 방식입니다. 하지만 웹 개발에서는 서버 통신, 파일 읽기 등 시간이 오래 걸리는 작업들이 많습니다.
graph TD
A[사용자 클릭] --> B{처리 방식}
B -->|동기 처리| C[서버 요청]
C --> D[대기... 화면 멈춤 😵]
D --> E[응답 받음]
E --> F[화면 업데이트]
B -->|비동기 처리| G[서버 요청 시작]
G --> H[다른 작업 계속 진행 😊]
H --> I[응답 도착 시 화면 업데이트]
style D fill:#ffcdd2
style H fill:#c8e6c9
💡 비동기의 장점¶
- ✅ 사용자 경험 개선: 화면이 멈추지 않음
- ✅ 효율성: 여러 작업을 동시에 처리 가능
- ✅ 응답성: 빠른 반응으로 더 나은 앱 성능
🔄 2. 비동기 처리 방법의 진화¶
📈 발전 과정 시각화¶
timeline
title 비동기 처리 방법의 진화
section 초기
콜백 함수 : 함수를 인자로 전달
: 콜백 지옥 문제 발생
section ES6 (2015)
Promise : .then(), .catch() 도입
: 체인 형태로 가독성 개선
section ES2017
Async/Await : 동기 코드처럼 작성
: 가장 직관적인 문법
🔥 콜백 지옥 체험¶
// ❌ 콜백 지옥 - 읽기 어려운 코드
setTimeout(() => {
console.log('1. 사용자 조회');
setTimeout(() => {
console.log('2. 게시물 조회');
setTimeout(() => {
console.log('3. 댓글 조회');
// 계속 중첩되면서 코드가 복잡해짐...
}, 1000);
}, 1000);
}, 1000);
⛓️ Promise로 개선¶
// ✅ Promise 체인 - 더 깔끔한 코드
fetchUser()
.then(() => fetchPosts())
.then(() => fetchComments())
.then(() => console.log('모든 작업 완료!'))
.catch(error => console.error('에러 발생:', error));
🎯 Async/Await로 완성¶
// ✅ Async/Await - 가장 읽기 쉬운 코드
async function loadAllData() {
try {
await fetchUser();
await fetchPosts();
await fetchComments();
console.log('모든 작업 완료!');
} catch (error) {
console.error('에러 발생:', error);
}
}
📡 3. Fetch API 완전 정리¶
🏗️ 기본 구조¶
Fetch API는 네트워크 요청을 보내는 현대적인 방법입니다.
flowchart TD
A[👨💻 클라이언트: fetch 호출] --> B[📡 네트워크 요청]
B --> C[⏳ Promise 대기 상태]
C --> D{🌐 서버 응답}
D -->|성공| E[📦 Response 객체 반환]
D -->|실패| F[❌ 네트워크 에러]
E --> G{✅ response.ok 체크}
G -->|true| H[🎁 response.json 호출]
G -->|false| I[🚨 HTTP 에러]
H --> J[📄 JSON 파싱 완료]
J --> K[🎉 최종 데이터 사용]
F --> L[🔧 .catch 에러 처리]
I --> L
style A fill:#e1f5fe
style K fill:#c8e6c9
style L fill:#ffcdd2
📝 기본 사용법¶
// 기본 템플릿 - 복사해서 사용하세요!
fetch('https://api.example.com/data')
.then(response => {
// 1단계: 응답 상태 확인
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json(); // JSON 변환
})
.then(data => {
// 2단계: 데이터 활용
console.log('성공:', data);
})
.catch(error => {
// 3단계: 에러 처리
console.error('실패:', error);
});
🎯 HTTP 메서드별 활용¶
// GET - 데이터 조회
const getTodos = async () => {
const response = await fetch('/api/todos');
return await response.json();
};
// POST - 데이터 생성
const createTodo = async (todoData) => {
const response = await fetch('/api/todos', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(todoData)
});
return await response.json();
};
// PUT - 데이터 전체 수정
const updateTodo = async (id, todoData) => {
const response = await fetch(`/api/todos/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(todoData)
});
return await response.json();
};
// DELETE - 데이터 삭제
const deleteTodo = async (id) => {
await fetch(`/api/todos/${id}`, {
method: 'DELETE'
});
};
🚀 4. Promise 병렬 처리¶
⚡ Promise.all() - 모든 작업 완료 대기¶
gantt
title Promise.all() vs 순차 실행 비교
dateFormat X
axisFormat %s초
section 순차 실행 (4.5초)
작업A (1.5초) :seq1, 0, 1500
작업B (2초) :seq2, after seq1, 2000
작업C (1초) :seq3, after seq2, 1000
section Promise.all 병렬 실행 (2초)
작업A (1.5초) :par1, 0, 1500
작업B (2초) :par2, 0, 2000
작업C (1초) :par3, 0, 1000
// 동시에 여러 API 호출하기
async function loadDashboard() {
try {
console.log('대시보드 데이터 로딩 시작...');
// 3개 작업을 동시에 실행 (병렬 처리)
const [weather, news, stocks] = await Promise.all([
fetch('/api/weather').then(r => r.json()), // 날씨 (1.5초)
fetch('/api/news').then(r => r.json()), // 뉴스 (2초)
fetch('/api/stocks').then(r => r.json()) // 주식 (1초)
]);
console.log('모든 데이터 로드 완료! (2초 소요)');
return { weather, news, stocks };
// 순차 실행했다면 4.5초 걸렸을 것을 2초만에 완성!
} catch (error) {
console.error('데이터 로드 실패:', error);
}
}
🏆 Promise.race() - 가장 빠른 응답만¶
// 여러 서버 중 가장 빠른 응답 받기
async function getFastestServer() {
const servers = [
fetch('https://server1.com/api/data'),
fetch('https://server2.com/api/data'),
fetch('https://server3.com/api/data')
];
try {
// 가장 먼저 응답하는 서버의 결과만 사용
const fastestResponse = await Promise.race(servers);
const data = await fastestResponse.json();
console.log('가장 빠른 서버 응답:', data);
return data;
} catch (error) {
console.error('모든 서버 요청 실패:', error);
}
}
⚔️ 5. 경쟁 상태(Race Condition) 해결¶
🚨 문제 상황¶
경쟁 상태는 여러 비동기 작업이 동시에 실행될 때, 실행 순서에 따라 결과가 달라지는 문제입니다.
sequenceDiagram
participant 사용자
participant 앱
participant 서버
Note over 사용자,서버: 문제 상황: 응답 순서가 뒤바뀜
사용자->>앱: 요청1 (검색: "사과")
앱->>서버: API 호출1
사용자->>앱: 요청2 (검색: "바나나")
앱->>서버: API 호출2
사용자->>앱: 요청3 (검색: "오렌지")
앱->>서버: API 호출3
Note over 서버: 서버 응답 시간이 다름
서버->>앱: 응답3 도착 (빠름)
앱->>사용자: 화면에 "오렌지" 표시 ✅
서버->>앱: 응답1 도착 (늦음)
앱->>사용자: 화면에 "사과" 표시 ❌ (잘못됨!)
✅ 해결 방법¶
let latestRequestId = 0; // 최신 요청 ID 추적
function searchWithSolution(keyword) {
// 새로운 요청마다 ID 증가
const currentRequestId = ++latestRequestId;
console.log(`요청 ${currentRequestId}: "${keyword}" 검색 시작`);
// API 호출 시뮬레이션
setTimeout(() => {
// 응답 도착 시 최신 요청인지 확인
if (currentRequestId === latestRequestId) {
console.log(`✅ 최신 요청 - 화면 업데이트: ${keyword}`);
updateUI(keyword);
} else {
console.log(`❌ 오래된 요청 - 무시: ${keyword}`);
}
}, Math.random() * 2000);
}
// 사용 예시
searchWithSolution('사과'); // 요청 1
searchWithSolution('바나나'); // 요청 2
searchWithSolution('오렌지'); // 요청 3 (최종 검색어)
// 결과: '오렌지'만 화면에 표시됨 ✅
🛠️ 6. 실전 활용 팁¶
🎯 상황별 최적 선택¶
flowchart TD
A[비동기 작업 필요];
A --> B{작업들의 관계는?};
B -->|순차 의존| C[Async/Await 사용];
B -->|독립적| D{결과 활용은?};
D -->|모든 결과 필요| E[Promise.all 사용];
D -->|가장 빠른 것만| F[Promise.race 사용];
C --> G[await task1 -> await task2 -> await task3];
E --> H[Promise.all with task1, task2, task3];
F --> I[Promise.race with server1, server2, server3];
🧰 실용적인 유틸리티 함수¶
// 1. 지연 함수 (테스트용)
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
// 2. 재시도 함수
async function retry(fn, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxAttempts) throw error;
console.log(`시도 ${attempt} 실패, 재시도 중...`);
await delay(1000 * attempt); // 점진적 지연
}
}
}
// 3. 타임아웃 함수
function withTimeout(promise, ms) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('타임아웃')), ms)
)
]);
}
// 사용 예시
async function robustApiCall() {
try {
const result = await retry(async () => {
return await withTimeout(
fetch('/api/data').then(r => r.json()),
5000 // 5초 타임아웃
);
}, 3); // 최대 3회 재시도
return result;
} catch (error) {
console.error('API 호출 최종 실패:', error);
return null;
}
}
⚠️ 자주하는 실수들¶
// ❌ 실수 1: async 함수에서 forEach 사용
async function wrongWay() {
const items = [1, 2, 3, 4, 5];
items.forEach(async (item) => {
await processItem(item); // 순서 보장 안됨!
});
console.log('완료'); // 처리 전에 실행됨!
}
// ✅ 올바른 방법: for...of 사용
async function correctWay() {
const items = [1, 2, 3, 4, 5];
for (const item of items) {
await processItem(item); // 순서대로 처리
}
console.log('완료'); // 모든 처리 후 실행
}
// ❌ 실수 2: 에러 처리 누락
async function withoutErrorHandling() {
const data = await riskyApiCall(); // 에러시 앱 멈춤!
return data;
}
// ✅ 올바른 방법: try-catch 사용
async function withErrorHandling() {
try {
const data = await riskyApiCall();
return data;
} catch (error) {
console.error('API 호출 실패:', error);
return null; // 안전한 기본값
}
}
📊 7. 오늘의 실습 결과¶
🔄 실습 코드 주요 기능¶
flowchart TB
A[실습 프로젝트];
A --> B[콜백 지옥];
B --> B1[3초 순차 실행];
B --> B2[가독성 문제 체험];
A --> C[Promise 체인];
C --> C1[동일한 3초 실행];
C --> C2[then 체인 구조];
A --> D[Async/Await];
D --> D1[동일한 3초 실행];
D --> D2[가장 읽기 쉬운 코드];
A --> E[Promise.all];
E --> E1[2초 병렬 실행];
E --> E2[약 55% 성능 개선];
A --> F[Promise.race];
F --> F1[0.8초 최고 속도];
F --> F2[가장 빠른 서버 선택];
A --> G[경쟁 상태];
G --> G1[문제 상황 재현];
G --> G2[해결책 구현];
📈 성능 비교 결과¶
방식 | 실행 시간 | 가독성 | 성능 |
---|---|---|---|
콜백 지옥 | 3초 | ⭐ | ⭐⭐ |
Promise 체인 | 3초 | ⭐⭐⭐ | ⭐⭐ |
Async/Await | 3초 | ⭐⭐⭐⭐⭐ | ⭐⭐ |
Promise.all | 2초 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
Promise.race | 0.8초 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
🎯 8. 핵심 요약¶
✨ 오늘 배운 핵심 포인트¶
- 📝 비동기 프로그래밍은 필수
- 사용자 경험 향상의 핵심
-
현대 웹 개발의 기본기
-
🔄 3가지 처리 방식의 특징
- 콜백: 기본이지만 지옥 위험
- Promise: 체인 구조로 가독성 개선
-
Async/Await: 가장 직관적이고 실용적
-
⚡ 성능 최적화 방법
- 순차 처리: 의존성이 있을 때
- 병렬 처리: 독립적인 작업들
-
경쟁 처리: 빠른 응답이 중요할 때
-
🛡️ 안전한 코드 작성
- 항상 에러 처리 포함
- 경쟁 상태 문제 고려
- 타임아웃과 재시도 구현
🔥 실전 활용 가이드¶
// 🎯 완벽한 비동기 함수 템플릿
async function perfectAsyncFunction(data) {
try {
// 1. 입력 검증
if (!data) throw new Error('데이터가 필요합니다');
// 2. 병렬로 처리 가능한 작업들
const [result1, result2] = await Promise.all([
apiCall1(data),
apiCall2(data)
]);
// 3. 순차 처리가 필요한 작업
const finalResult = await processResults(result1, result2);
return finalResult;
} catch (error) {
// 4. 에러 로깅 및 안전한 처리
console.error('함수 실행 실패:', error);
throw error; // 또는 기본값 반환
}
}
🚀 9. 다음 단계 학습 계획¶
📚 심화 학습 목표¶
- [ ] 웹 워커(Web Workers) - 메인 스레드 차단 없는 처리
- [ ] Service Worker - 오프라인 지원 및 캐싱
- [ ] 실시간 통신 - WebSocket, Server-Sent Events
- [ ] 상태 관리 - React/Vue에서의 비동기 처리
- [ ] 테스팅 - 비동기 코드 테스트 방법
🛠️ 실습 프로젝트 아이디어¶
- 실시간 검색 기능 구현 (디바운싱 + API 호출)
- 이미지 갤러리 만들기 (병렬 이미지 로딩)
- 채팅 앱 기초 (WebSocket 활용)
- 날씨 대시보드 (여러 API 통합)
📖 참고 자료¶
🔗 유용한 링크¶
🎥 추천 영상¶
- 드림코딩 - 자바스크립트 비동기 처리
- 코딩애플 - Promise, async/await 완벽정리
- 제로초 - 자바스크립트 이벤트 루프
💭 오늘의 소감¶
🎉 성취 포인트 - 복잡했던 비동기 개념이 명확해졌다! - Fetch API 사용법을 완전히 익혔다! - Promise.all과 Promise.race의 차이를 실습으로 체험했다! - 경쟁 상태 문제와 해결책을 배워서 실무에 바로 적용할 수 있겠다!
🤔 어려웠던 부분 - 콜백 지옥이 왜 문제인지 처음에는 이해하기 어려웠음 - Promise의 상태 변화 개념이 추상적이었음 - 경쟁 상태 문제는 실제로 겪어봐야 이해될 것 같음
🎯 내일 할 일 - 오늘 배운 내용을 실제 미니 프로젝트에 적용해보기 - API 호출하는 간단한 웹 앱 만들어보기 - 에러 처리를 더 견고하게 하는 방법 연구하기
작성자: 멋사 20기 수강생
작성일: 2025년 8월 26일
태그: #JavaScript
#비동기
#Promise
#AsyncAwait
#FetchAPI
#멋사20기