🚀 자바스크립트 비동기 처리 완전 정복! (Promise와 async/await 동작 원리 쉽게 딥하게) 🖥️

자바스크립트에서 비동기 처리는 필수 개념입니다. 특히 네트워크 요청, 파일 읽기 등 시간이 오래 걸리는 작업을 메인 스레드를 멈추지 않고 효율적으로 처리하기 위해 사용되죠. 이 글에서는 비동기 처리의 핵심인 Promiseasync/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) 동작 흐름

  1. Promise 생성 (Pending): new Promise()로 객체를 생성하면 대기 상태가 됩니다.
  2. 비동기 작업 실행: 내부의 executor 함수가 실행되며, 비동기 작업이 시작됩니다.
  3. 결과 처리:
    • 성공 시: 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) 함수 원리에 기반합니다.

  1. async 함수가 실행되면, 내부적으로 제너레이터처럼 동작하는 함수로 변환됩니다.
  2. await somePromise()를 만나면, 해당 Promise의 처리 결과를 기다립니다.
  3. 이때, async 함수는 잠시 실행을 일시 중지(Yield)하고, 메인 스레드를 막지 않습니다.
  4. 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. 결론: 언제 무엇을 사용해야 할까?

구분Promiseasync/await
문법.then(), .catch() 체이닝 방식try...catch를 사용한 동기 코드 형태
가독성다소 복잡해질 수 있음 (콜백 지옥 방지)압도적으로 높음 (동기 코드처럼 보임)
오류 처리.catch() 사용try...catch 블록 사용
사용처Promise 기반 API를 사용할 때, 저수준(Low-level) 처리 시대부분의 비동기 코드 (가독성 최우선)

✅ 추천 가이드

  • 현재는: async/await를 사용하는 것이 압도적으로 권장됩니다. 코드의 흐름을 이해하기 가장 쉽고 오류 처리(try...catch)도 간편합니다.
  • Promise의 이해는 필수: async/await는 Promise 위에 덧씌워진 문법적 설탕(Syntactic Sugar)일 뿐이므로, 근본 원리인 Promise의 상태와 동작 원리를 이해하는 것이 중요합니다.