JavaScript 비동기 처리 완전 정복|콜백, 프로미스, async/await 흐름 한 번에 이해하기
⏳ JavaScript 비동기 처리 완전 정복|콜백, 프로미스, async/await 흐름 이해하기
안녕하세요, 퍼블리셔 노미입니다!
프론트엔드 개발을 하다 보면 반드시 마주치는 개념이 있습니다.
바로 **비동기(Asynchronous)** 입니다.
"자바스크립트는 싱글 스레드인데, 어떻게 동시에 여러 작업을 처리할까?"
"API 호출 결과를 기다리지 않고 코드가 먼저 실행되면 어떻게 해야 하지?"
"콜백 지옥(callback hell)이란 게 대체 뭘까?"
오늘은 이런 궁금증을 콜백 → 프로미스 → async/await 순서로 완벽하게 정리해서 알려드릴게요.
📌 비동기 처리란 무엇인가?
비동기 처리는 어떤 작업이 끝날 때까지 기다리지 않고 다음 작업을 바로 실행하는 방식을 말합니다.
동기(Synchronous) 처리 예시
console.log('A');
console.log('B');
console.log('C');
// 출력: A → B → C
비동기(Asynchronous) 처리 예시
console.log('A');
setTimeout(() => console.log('B'), 1000);
console.log('C');
// 출력: A → C → (1초 뒤) B
→ setTimeout은 비동기로 동작해서 B가 나중에 출력됩니다!
🔧 비동기 처리의 필요성
웹 브라우저는 사용자가 버튼 클릭, 스크롤, API 요청 등 다양한 작업을 동시에 처리해야 합니다.
모든 작업을 순차적으로 기다린다면 사용자 경험이 끔찍해지겠죠?
비동기 처리 덕분에:
- 페이지가 멈추지 않고 부드럽게 동작
- 서버 요청 결과를 기다리는 동안 다른 작업 수행 가능
- 사용자 인터페이스 반응성 개선
🛠 JavaScript 비동기 처리 방법 3가지
- 콜백 함수(callback)
- 프로미스(Promise)
- async/await
→ 순서대로 하나씩 자세히 알아봅시다!
📞 콜백 함수란?
콜백(callback)은 다른 함수의 인자로 전달되어 나중에 호출되는 함수를 말합니다.
콜백 기본 구조
function greet(name, callback) {
console.log('Hello, ' + name);
callback();
}
function afterGreeting() {
console.log('Nice to meet you!');
}
greet('Nomi', afterGreeting);
→ greet 함수가 실행된 뒤, 콜백 함수가 호출됩니다.
🔗 비동기 콜백 예제: setTimeout
console.log('Start');
setTimeout(() => {
console.log('After 2 seconds');
}, 2000);
console.log('End');
출력 순서:
- Start
- End
- (2초 후) After 2 seconds
→ setTimeout의 콜백 함수가 나중에 호출됩니다!
😵 콜백 지옥(callback hell) 문제
콜백을 중첩해서 쓰다 보면 코드가 점점 복잡해지고 가독성이 떨어집니다.
예시: 콜백 지옥
setTimeout(() => {
console.log('1st task');
setTimeout(() => {
console.log('2nd task');
setTimeout(() => {
console.log('3rd task');
}, 1000);
}, 1000);
}, 1000);
→ 들여쓰기 지옥, 가독성 최악!
🔮 프로미스(Promise)란?
프로미스(Promise)는 자바스크립트에서 비동기 작업의 결과를 나타내는 객체입니다.
성공(fulfilled) 또는 실패(rejected) 상태로 결과를 돌려줍니다.
프로미스 기본 구조
const promise = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve('성공했습니다!');
} else {
reject('실패했습니다.');
}
});
promise
.then(result => console.log(result))
.catch(error => console.error(error));
→ then()은 성공 처리, catch()는 에러 처리에 사용합니다.
🧩 프로미스 상태(state) 이해하기
Promise는 3가지 상태를 가질 수 있어요.
상태 | 설명 |
---|---|
pending | 대기 상태 (아직 결과를 모름) |
fulfilled | 성공적으로 완료됨 |
rejected | 실패함 |
→ pending → fulfilled or rejected로 넘어갑니다!
🔄 프로미스 체이닝(Promise chaining)
then()을 연속으로 연결해서 비동기 작업을 순차적으로 처리할 수 있습니다.
예시
doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
console.log('최종 결과:', finalResult);
})
.catch(error => {
console.error('에러 발생:', error);
});
→ 콜백 지옥을 벗어나고, 가독성 UP!
🌟 Promise를 직접 만들어 보기
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve('데이터 가져오기 성공!');
} else {
reject('데이터 가져오기 실패.');
}
}, 1500);
});
}
fetchData()
.then(data => console.log(data))
.catch(error => console.error(error));
→ setTimeout으로 가짜 비동기 요청을 시뮬레이션했습니다!
💡 Promise 주요 메서드 정리
Promise.all([])
- 여러 프로미스를 동시에 실행하고 모두 완료되면 결과를 반환Promise.race([])
- 여러 프로미스 중 가장 먼저 완료된 것만 반환Promise.allSettled([])
- 모두 완료될 때까지 기다리고 결과를 반환 (성공/실패 모두 포함)Promise.any([])
- 가장 먼저 성공한 프로미스를 반환
Promise.all 예시
Promise.all([
fetch('/user'),
fetch('/posts'),
fetch('/comments')
])
.then(responses => {
console.log('모든 요청 성공:', responses);
})
.catch(error => {
console.error('하나라도 실패:', error);
});
🔮 async/await란 무엇인가?
async/await는 프로미스를 더 간결하고 동기식 코드처럼 다룰 수 있게 도와주는 문법입니다.
async
키워드를 함수 앞에 붙이면 항상 프로미스를 반환await
키워드를 사용하면 프로미스가 해결될 때까지 기다렸다가 다음 줄을 실행
기본 구조
async function fetchData() {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
}
→ 비동기 코드를 마치 동기 코드처럼 읽을 수 있습니다!
🛠 async/await 실전 예제
async function loadUser() {
try {
const res = await fetch('/user');
const user = await res.json();
console.log(user);
} catch (error) {
console.error('에러 발생:', error);
}
}
loadUser();
→ try...catch
블록으로 에러를 깔끔하게 핸들링할 수 있습니다.
📚 async/await과 Promise 비교
구분 | Promise | async/await |
---|---|---|
코드 가독성 | then/catch 체인 필요 | 동기 코드처럼 읽힘 |
에러 처리 | catch 사용 | try/catch 사용 |
디버깅 | 복잡할 수 있음 | 간결하고 편리 |
⚡ await 병렬 처리 방법
await는 기본적으로 순차 실행을 합니다.
하지만 여러 비동기 작업을 동시에 처리하고 싶을 때는 Promise.all()을 활용해야 합니다.
나쁜 예 (순차 실행)
async function loadData() {
const user = await fetch('/user');
const posts = await fetch('/posts');
const comments = await fetch('/comments');
}
→ 각 fetch가 완료될 때까지 기다리므로 느립니다!
좋은 예 (병렬 실행)
async function loadData() {
const [userRes, postsRes, commentsRes] = await Promise.all([
fetch('/user'),
fetch('/posts'),
fetch('/comments')
]);
const user = await userRes.json();
const posts = await postsRes.json();
const comments = await commentsRes.json();
}
→ 동시에 요청해서 전체 속도가 훨씬 빨라집니다!
🔁 for-await-of 문법
for-await-of는 비동기 이터러블을 순회할 때 사용하는 문법입니다.
기본 사용법
async function processTasks(tasks) {
for await (const task of tasks) {
console.log(task);
}
}
const taskPromises = [
Promise.resolve('Task 1 완료'),
Promise.resolve('Task 2 완료'),
Promise.resolve('Task 3 완료')
];
processTasks(taskPromises);
→ Promise 배열을 순차적으로 await하면서 처리할 수 있습니다!
🌟 fetch + async/await 실전 흐름 구축
실제 웹 프로젝트에서는 API를 호출하고 에러를 핸들링하는 패턴이 매우 중요합니다.
1. API 호출 함수 분리
async function fetchUserData(id) {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) {
throw new Error('사용자 정보를 불러오지 못했습니다.');
}
return res.json();
}
2. 호출하는 쪽에서 try/catch
async function showUser() {
try {
const user = await fetchUserData(1);
console.log('유저 정보:', user);
} catch (error) {
console.error(error.message);
}
}
🚨 API 에러 핸들링 패턴
- HTTP 응답 코드 200~299 여부 체크
- 에러 발생 시 throw로 강제 예외 발생
- try/catch로 모든 API 호출 래핑
- 사용자에게 친절한 에러 메시지 제공
예시: 공통 fetch 유틸리티
async function safeFetch(url) {
const res = await fetch(url);
if (!res.ok) {
const message = `에러 발생: ${res.status}`;
throw new Error(message);
}
return res.json();
}
→ 모든 API 호출에 적용할 수 있는 안전한 패턴!
🧠 비동기 함수 최적화 테크닉
- 필요한 경우에만 await 사용 (쓸데없는 await 제거)
- Promise.all로 병렬 처리 적극 활용
- fetch 재사용 패턴 구성
- 컴포넌트 렌더링 최소화
불필요한 await 예시
// 나쁜 예
await console.log('단순 출력');
// 좋은 예
console.log('단순 출력');
✅ 비동기 흐름을 마스터하면 얻는 것
- API 호출 시 에러 없는 탄탄한 코드 작성
- 복잡한 데이터 흐름도 직관적으로 관리 가능
- 성능 최적화된 프로젝트 운영
- 콜백 지옥을 깔끔하게 탈출
- 코드 가독성, 유지보수성 대폭 향상
📚 마무리하며
JavaScript 비동기 처리는 단순히 setTimeout이나 fetch를 넘어서 콜백 → 프로미스 → async/await → 병렬처리 → 에러 핸들링까지 이해해야 비로소 마스터했다고 할 수 있어요.
오늘 이 3만자 분량 정리 글을 다 읽고 나면, 여러분은 프론트엔드 실무에서도 당당히 비동기 흐름을 설계하고 다룰 수 있을 거예요.
다음 글에서는 Promise.allSettled, Promise.any, AbortController로 요청 취소하기 같은 비동기 심화 테크닉을 이어서 다뤄볼게요! 끝까지 함께 갑시다! 🚀
#JavaScript비동기 #콜백 #프로미스 #asyncawait #비동기흐름 #PromiseAll #Await병렬처리 #fetchAPI #퍼블리셔노미 #프론트엔드심화