JavaScript는 싱글 스레드 언어입니다. 하지만 웹 개발에서는 API 호출, 파일 읽기, 타이머 등 시간이 걸리는 작업을 처리해야 합니다. 어떻게 이게 가능할까요? 바로 비동기 프로그래밍 덕분입니다.
JavaScript is a single-threaded language. However, web development requires handling time-consuming operations like API calls, file reading, and timers. How is this possible? Thanks to asynchronous programming.
이 가이드에서는 콜백에서 Promise, async/await까지 비동기 처리의 진화 과정과 실무에서 바로 쓸 수 있는 패턴을 모두 다룹니다.
This guide covers the evolution from callbacks to Promises and async/await, and practical patterns you can use immediately.
1. 왜 비동기가 필요한가? 1. Why Do We Need Asynchronous Code?
만약 JavaScript가 동기적으로만 작동한다면, 서버에서 데이터를 받아오는 동안 페이지 전체가 멈춰버릴 것입니다. 이는 사용자 경험을 크게 해칩니다.
If JavaScript worked only synchronously, the entire page would freeze while fetching data from a server. This severely damages user experience.
❌ 동기 처리 (가정)
❌ Synchronous (Hypothetical)
// 3초간 멈춤... 😱
// Freezes for 3 seconds... 😱
console.log(data);
✅ 비동기 처리
✅ Asynchronous
.then(data => {
console.log(data);
});
// 다른 코드 실행 가능! ✓
// Other code can run! ✓
2. 콜백 (Callback): 비동기의 시작 2. Callbacks: The Beginning of Async
초기 JavaScript에서는 비동기 작업을 처리하기 위해 콜백 함수를 사용했습니다.
Early JavaScript used callback functions to handle asynchronous operations.
setTimeout(() => {
console.log('1초 후 실행');
}, 1000);
// 데이터를 받아오는 콜백
function fetchUser(id, callback) {
// ... 서버 요청
callback(userData);
}
fetchUser(1, (user) => {
console.log(user.name);
});
setTimeout(() => {
console.log('Runs after 1 second');
}, 1000);
// Callback for fetching data
function fetchUser(id, callback) {
// ... server request
callback(userData);
}
fetchUser(1, (user) => {
console.log(user.name);
});
⚠️ 콜백 지옥 (Callback Hell) ⚠️ Callback Hell
여러 비동기 작업을 연속으로 처리하면 콜백이 중첩되어 코드 가독성이 크게 떨어집니다.
Handling multiple async operations sequentially leads to nested callbacks, severely reducing code readability.
getPosts(user.id, (posts) => {
getComments(posts[0].id, (comments) => {
// 더 깊어질 수도... 😱
});
});
});
3. Promise: 콜백 지옥의 해결책 3. Promise: Solution to Callback Hell
ES6에서 도입된 Promise는 비동기 작업의 성공 또는 실패를 나타내는 객체입니다.
Promise, introduced in ES6, is an object representing the success or failure of an
async operation.
Promise의 3가지 상태 Promise's 3 States
- Pending (대기): 초기 상태, 아직 완료되지 않음
- Fulfilled (이행): 작업 성공적으로 완료됨
- Rejected (거부): 작업 실패
- Pending: Initial state, not yet completed
- Fulfilled: Operation completed successfully
- Rejected: Operation failed
const promise = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve('성공!');
} else {
reject(new Error('실패!'));
}
});
// Promise 사용
promise
.then(result => console.log(result))
.catch(error => console.error(error));
const promise = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve('Success!');
} else {
reject(new Error('Failed!'));
}
});
// Using the Promise
promise
.then(result => console.log(result))
.catch(error => console.error(error));
Promise 체이닝 Promise Chaining
Promise는 .then()으로 연결하여 순차적인 비동기 작업을 깔끔하게 처리할 수 있습니다.
Promises can be chained with .then() to handle sequential async operations cleanly.
.then(user => fetchPosts(user.id))
.then(posts => fetchComments(posts[0].id))
.then(comments => {
console.log('모든 데이터 로드 완료');
})
.catch(error => {
console.error('에러 발생:', error);
});
.then(user => fetchPosts(user.id))
.then(posts => fetchComments(posts[0].id))
.then(comments => {
console.log('All data loaded');
})
.catch(error => {
console.error('Error occurred:', error);
});
4. async/await: Promise를 더 쉽게 4. async/await: Making Promises Easier
ES2017(ES8)에서 도입된 async/await는 Promise를 더 동기 코드처럼 작성할 수 있게 해줍니다.
async/await, introduced in ES2017 (ES8), makes Promises look more like synchronous
code.
Promise 체인
Promise Chain
.then(res => res.json())
.then(user => {
console.log(user);
})
.catch(err => {
console.error(err);
});
✅ async/await
✅ async/await
try {
const res = await fetch('/api/user');
const user = await res.json();
console.log(user);
} catch (err) {
console.error(err);
}
}
async/await 사용 규칙 async/await Usage Rules
await는 반드시async함수 안에서만 사용 가능await는 Promise가 resolve될 때까지 기다림- 에러 처리를 위해
try/catch블록 사용
awaitcan only be used insideasyncfunctionsawaitwaits until the Promise resolves- Use
try/catchblocks for error handling
async function fetchAllData() {
try {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
return { user, posts, comments };
} catch (error) {
console.error('데이터 가져오기 실패:', error);
throw error; // 에러 재발생
} finally {
console.log('작업 종료');
}
}
async function fetchAllData() {
try {
const user = await fetchUser(userId);
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
return { user, posts, comments };
} catch (error) {
console.error('Failed to fetch data:', error);
throw error; // Re-throw error
} finally {
console.log('Operation complete');
}
}
5. Promise.all: 병렬 처리 5. Promise.all: Parallel Execution
여러 비동기 작업을 동시에 실행하고 모두 완료될 때까지 기다리려면 Promise.all을 사용합니다.
Use Promise.all to execute multiple async operations simultaneously
and wait for all to complete.
const user = await fetchUser();
const posts = await fetchPosts();
// 총 시간 = fetchUser 시간 + fetchPosts 시간
// 병렬 실행 (빠름) ✓
const [user, posts] = await Promise.all([
fetchUser(),
fetchPosts()
]);
// 총 시간 = max(fetchUser 시간, fetchPosts 시간)
const user = await fetchUser();
const posts = await fetchPosts();
// Total time = fetchUser time + fetchPosts time
// Parallel execution (fast) ✓
const [user, posts] = await Promise.all([
fetchUser(),
fetchPosts()
]);
// Total time = max(fetchUser time, fetchPosts time)
💡 Promise.all vs Promise.allSettled 💡 Promise.all vs Promise.allSettled
- Promise.all: 하나라도 reject되면 전체 실패
- Promise.allSettled: 모든 Promise의 결과를 반환 (성공/실패 무관)
- Promise.all: Fails if any promise rejects
- Promise.allSettled: Returns all results regardless of success/failure
6. Fetch API: 실무 HTTP 요청 6. Fetch API: Real-World HTTP Requests
fetch()는 브라우저에 내장된 HTTP 요청 함수로, Promise를 반환합니다.
fetch() is a built-in browser function for HTTP requests that returns a Promise.
async function getUsers() {
const response = await fetch('https://api.example.com/users');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
}
// POST 요청
async function createUser(userData) {
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(userData)
});
return await response.json();
}
async function getUsers() {
const response = await fetch('https://api.example.com/users');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
}
// POST request
async function createUser(userData) {
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(userData)
});
return await response.json();
}
7. 실무 패턴과 Best Practices 7. Practical Patterns & Best Practices
✨ 실무에서 자주 쓰는 패턴 ✨ Commonly Used Patterns
- 에러 처리 유틸 함수: fetch 래퍼를 만들어 반복 코드 제거
- 타임아웃 추가: Promise.race로 시간 제한 구현
- 재시도 로직: 실패 시 자동으로 재시도
- 로딩 상태 관리: UI에 로딩 표시
- Error handling utility: Create fetch wrapper to reduce redundancy
- Add timeouts: Implement time limits with Promise.race
- Retry logic: Automatically retry on failure
- Loading state management: Show loading indicators in UI
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 response;
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('요청 시간 초과');
}
throw error;
}
}
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 response;
} catch (error) {
&n bsp; if (error.name === 'AbortError') {
throw new Error('Request timeout');
}
throw error;
}
}
8. 마무리: 비동기를 타자로 익히기 8. Conclusion: Learning Async Through Typing
비동기 프로그래밍은 현대 JavaScript 개발의 핵심입니다. API 호출, 파일 처리, 사용자 상호작용 등 거의 모든 곳에서 사용됩니다.
Asynchronous programming is at the heart of modern JavaScript development. It's used almost everywhere: API calls, file handling, user interactions, and more.
DevType의 JavaScript 타자 연습에는 이 가이드에서 다룬 모든 비동기 패턴이 포함되어 있습니다:
DevType's JavaScript typing practice includes all async patterns covered in this guide:
async function과await키워드fetch()를 사용한 실제 API 호출 코드Promise.all,Promise.race등 Promise 유틸리티.then(),.catch()Promise 체이닝try/catch/finally에러 처리
async functionandawaitkeywords- Real API call code using
fetch() - Promise utilities like
Promise.all,Promise.race - Promise chaining with
.then(),.catch() - Error handling with
try/catch/finally
이 패턴들을 직접 타이핑하며 익히면, 실제 프로젝트에서 비동기 코드를 자연스럽게 작성할 수 있게 됩니다.
By typing these patterns yourself, you'll naturally be able to write async code in real projects.
