자바스크립트에서 비동기 처리는 필수 개념입니다. 특히 네트워크 요청, 파일 읽기 등 시간이 오래 걸리는 작업을 메인 스레드를 멈추지 않고 효율적으로 처리하기 위해 사용되죠. 이 글에서는 비동기 처리의 핵심인 Promise와 async/await의 동작 원리를 누구나 이해할 수 있도록 쉽게, 하지만 깊이 있게 설명해 드릴게요!
⏳ 1. 비동기 처리가 필요한 이유: 동기 vs. 비동기
자바스크립트는 기본적으로 싱글 스레드(Single Thread) 언어입니다. 즉, 작업을 한 번에 하나씩 순서대로 처리하죠 (동기 처리).
- 동기 처리 (Synchronous): A 작업이 완료되어야 B 작업이 시작됩니다. 만약 A 작업이 10초가 걸린다면, B 작업은 10초 동안 대기해야 하므로 화면이 멈추는 블로킹(Blocking) 현상이 발생합니다.
- 비동기 처리 (Asynchronous): 시간이 오래 걸리는 A 작업을 백그라운드에 맡기고, 다음 작업인 B를 즉시 실행합니다. A 작업이 완료되면 결과만 전달받아 처리하죠. 메인 스레드가 멈추지 않아 논블로킹(Non-Blocking) 방식이 가능해집니다.
🏠 비유:
- 동기: 식당에서 주문하고 음식이 나올 때까지 카운터 앞에서 꼼짝 않고 서서 기다리는 것입니다. (다른 손님 주문 불가)
- 비동기: 식당에서 주문하고 자리에 앉아 다른 일을 하거나 기다리는 것입니다. (다른 손님 주문 가능)
📜 2. 비동기 처리를 위한 약속, Promise의 원리
Promise는 비동기 작업의 최종 완료(성공) 또는 실패(오류)를 나타내는 객체입니다. 비동기 작업의 결과를 마치 동기 작업의 결과처럼 사용할 수 있게 해주는 ‘미래의 값에 대한 약속’이죠.
1) Promise의 3가지 상태 (State)
Promise 객체는 생성 순간부터 결과가 확정될 때까지 다음 3가지 상태 중 하나를 가집니다.
| 상태 (State) | 의미 |
| Pending (대기) | 비동기 작업이 진행 중인 초기 상태입니다. |
| Fulfilled (이행/성공) | 비동기 작업이 성공적으로 끝났고, 결과를 반환합니다. |
| Rejected (거부/실패) | 비동기 작업이 실패했고, 오류(에러)를 반환합니다. |
2) 동작 흐름
- Promise 생성 (Pending):
new Promise()로 객체를 생성하면 대기 상태가 됩니다. - 비동기 작업 실행: 내부의
executor함수가 실행되며, 비동기 작업이 시작됩니다. - 결과 처리:
- 성공 시:
resolve함수 호출 $\rightarrow$ Fulfilled 상태로 전환. 결과 값은.then()으로 받습니다. - 실패 시:
reject함수 호출 $\rightarrow$ Rejected 상태로 전환. 오류는.catch()로 받습니다.
- 성공 시:
3) Promise 체이닝 (.then(), .catch())
.then() 메서드를 통해 여러 개의 비동기 작업을 순차적으로 연결할 수 있습니다. 이를 Promise 체이닝(Chaining)이라고 합니다.
JavaScript
// A 작업 -> B 작업 -> C 작업 순으로 실행
fetchUser()
.then(getUserData) // A 성공 -> B 실행
.then(formatData) // B 성공 -> C 실행
.catch(handleError); // 중간에 실패하면 catch로 이동
✨ 3. Promise를 더욱 쉽게, async/await의 원리
async/await는 ES8(ECMAScript 2017)에 도입된 문법으로, Promise를 더욱 동기 코드처럼 보이게 만들어 가독성을 높여줍니다.
1) async 함수
함수 앞에 async 키워드를 붙이면, 이 함수는 항상 Promise를 반환하도록 만듭니다.
async function fetchData() { return '결과'; }- 이 함수는 자동으로
Promise.resolve('결과')를 반환합니다.
2) await 키워드
await 키워드는 반드시 async 함수 안에서만 사용할 수 있습니다.
await은 Promise 앞에 붙여서 사용하며, 해당 Promise가 Fulfilled 또는 Rejected 상태가 될 때까지 기다립니다.- Promise가 성공적으로 이행되면,
await은 Promise의 결과 값(resolve 값)을 반환합니다.
3) 동작 원리 (제너레이터와 이벤트 루프)
async/await가 동기처럼 보이지만 비동기로 동작하는 핵심은 자바스크립트 내부의 이벤트 루프(Event Loop)와 제너레이터(Generator) 함수 원리에 기반합니다.
async함수가 실행되면, 내부적으로 제너레이터처럼 동작하는 함수로 변환됩니다.await somePromise()를 만나면, 해당 Promise의 처리 결과를 기다립니다.- 이때,
async함수는 잠시 실행을 일시 중지(Yield)하고, 메인 스레드를 막지 않습니다. - Promise가 Fulfilled 상태가 되어 결과를 반환하면, 이벤트 루프를 통해 태스크 큐(Task Queue)에 콜백이 등록되고, 메인 스레드가 비어있을 때 재개(Resume)되어 다음 코드를 실행합니다.
JavaScript
async function process() {
const resultA = await taskA(); // 1. taskA 시작, async 함수 일시 중지
// (중지되는 동안 메인 스레드는 다른 작업 처리 가능)
const resultB = await taskB(resultA); // 2. taskA 완료 후 재개, taskB 시작
return resultB;
}
🛠️ 4. 결론: 언제 무엇을 사용해야 할까?
| 구분 | Promise | async/await |
| 문법 | .then(), .catch() 체이닝 방식 | try...catch를 사용한 동기 코드 형태 |
| 가독성 | 다소 복잡해질 수 있음 (콜백 지옥 방지) | 압도적으로 높음 (동기 코드처럼 보임) |
| 오류 처리 | .catch() 사용 | try...catch 블록 사용 |
| 사용처 | Promise 기반 API를 사용할 때, 저수준(Low-level) 처리 시 | 대부분의 비동기 코드 (가독성 최우선) |
✅ 추천 가이드
- 현재는:
async/await를 사용하는 것이 압도적으로 권장됩니다. 코드의 흐름을 이해하기 가장 쉽고 오류 처리(try...catch)도 간편합니다. - Promise의 이해는 필수:
async/await는 Promise 위에 덧씌워진 문법적 설탕(Syntactic Sugar)일 뿐이므로, 근본 원리인 Promise의 상태와 동작 원리를 이해하는 것이 중요합니다.