JavaScript Asynchronous Execution

"JavaScript는 오래 걸리는 작업을 기다리는 동안 UI를 멈추지 않으려고 어떤 수단을 쓰며, 콜백과 Promise와 async/await는 서로 어떤 관계인가?" 이 page는 그 질문에 답한다. 비동기 프로그래밍은 작업이 끝나기를 기다리지 않고, 오래 걸리는 작업을 시작해 둔 채 그동안 다른 이벤트에 응답하게 하는 기술이다.[1] JavaScript Language Model의 "함수는 값이다" 규칙이 콜백을 가능하게 하고, 같은 page의 ES6 문제 목록에 있는 "콜백 중첩"이 이 page의 Promise와 async/await로 이어진다. SPA and CSR이 말하는 initial load 이후의 data 요청과 부분 UI 갱신도 이 실행 흐름 위에서 일어난다고 볼 수 있다.

왜 비동기가 필요한가

콜백: 나중에 부를 함수를 넘긴다

// 콜백 지옥 — 중첩이 깊어질수록 가독성이 떨어짐
chooseToppings(function (toppings) {
  placeOrder(toppings, function (order) {
    collectOrder(order, function (pizza) {
      eatPizza(pizza);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);
const xhr = new XMLHttpRequest();
xhr.onload = function() { /* 완료 처리 */ };
xhr.onerror = function() { /* 에러 처리 */ };
xhr.open('GET', url);
xhr.send();

Promise: 비동기 결과를 표현하는 객체

// Promise 체이닝
chooseToppings()
  .then((toppings) => placeOrder(toppings))
  .then((order) => collectOrder(order))
  .then((pizza) => eatPizza(pizza))
  .catch(failureCallback);
fetch("/users")
  .then((response) => response.json())
  .then((users) => {
    console.log(users);
    return fetch(`/posts?userId=${users[0].id}`);
  })
  .then((response) => response.json())
  .then((posts) => {
    console.log(posts);
  })
  .catch((error) => {
    console.error(error);
  });

async/await: Promise 위의 동기식 표기

fetch("/users")
  .then((response) => response.json())
  .then((users) => console.log(users));
const response = await fetch("/users");
const users = await response.json();
console.log(users);
async function getUsers() {
  try {
    const response = await fetch("/users");
    const users = await response.json();

    console.log(users);
  } catch (error) {
    console.error(error);
  }
}

순차 처리와 동시 대기

const [users, posts] = await Promise.all([
  fetch("/users").then((response) => response.json()),
  fetch("/posts").then((response) => response.json()),
]);

이벤트 루프: 콜백이 실행되는 시점

flowchart LR
  Start["비동기 작업 시작"] -- "작업 완료 후" --> Queue["콜백이 이벤트 큐에 등록"]
  Queue --> Check{"콜 스택이 비었는가"}
  Check -- "예" --> Run["이벤트 루프가 콜백을 꺼내 실행"]

세 방식의 관계

방식 순서를 표현하는 방법 실패 처리 주의할 점
콜백 다음 단계를 콜백 안에 중첩한다.[9:2] 단계마다 실패 콜백을 넘긴다.[9:3] 중첩이 깊어질수록 가독성이 떨어진다.[9:4]
Promise then()을 이어 붙인다.[13:2][14:2] 체인 끝의 catch() 한 곳에서 처리한다.[13:3][18:1] 결과가 언제 돌아올지는 보장하지 않는다.[12:3]
async/await await를 동기 코드처럼 차례로 쓴다.[17:1] try-catch로 처리한다.[18:2] async 함수 안이나 module 최상위에서만 await를 쓸 수 있다.[19:1]

흔한 실패 모드

경계와 원문 공백

관련

테스트 질문

출처


  1. 비동기 프로그래밍.md 11줄 — "작업이 완료될 때까지 기다리지 않고, 오래 실행되는 작업을 시작한 뒤 그 동안 다른 이벤트에 응답할 수 있게 하는 기술." ↩︎

  2. ECMAScript.md (Study) 1027, 1039줄 — "JavaScript는 오래 걸리는 작업을 기다리는 동안 전체 실행이 멈추지 않도록 비동기 처리를 사용한다.", "비동기 처리는 오래 걸리는 작업을 요청해두고, 결과가 준비되면 이어서 처리하는 방식이다." ↩︎

  3. 비동기 프로그래밍.md 17-19줄 — "fetch() — HTTP 요청", "getUserMedia() — 카메라/마이크 접근", "showOpenFilePicker() — 파일 선택 요청" ↩︎

  4. ECMAScript.md (Study) 1031-1035줄 — "서버 API 요청", "파일 읽기", "타이머", "사용자 이벤트", "데이터베이스 요청" ↩︎

  5. 비동기 프로그래밍.md 21줄 — "이처럼 시간이 걸리는 작업을 동기로 처리하면 UI가 멈춘다." ↩︎ ↩︎

  6. ECMAScript.md (Study) 1037줄 — "동기 방식으로 네트워크 요청을 처리하면 응답이 올 때까지 다음 코드가 실행되지 않는다. 브라우저에서는 화면이 멈춘 것처럼 느껴질 수 있다." ↩︎ ↩︎

  7. 비동기 프로그래밍.md 29줄 — "이벤트 핸들러는 콜백의 특정 유형." 그리고 콜백 정의 "적절한 시점에 호출될 함수를 다른 함수에 전달" ↩︎

  8. ECMAScript.md (Study) 338-354줄 — "JavaScript에서 함수는 값이다.", "이 특성 때문에 콜백 함수, 고차 함수, 비동기 처리 같은 패턴이 가능하다." ↩︎

  9. 비동기 프로그래밍.md 31-40줄 — 코드 주석 "콜백 지옥 — 중첩이 깊어질수록 가독성이 떨어짐". 코드를 그대로 옮겼다. 단계마다 failureCallback을 넘기는 점은 코드에서 읽은 것이다. 원문은 참고 자료로 MDN 비동기 JS 소개 문서를 적는다(84줄). ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  10. 비동기 프로그래밍.md 62-70줄 — 절 제목 "XMLHttpRequest (초기 비동기 API)"와 onload·onerror 코드. 코드를 그대로 옮겼다. ↩︎

  11. ECMAScript.md (Study) 1043-1049줄 — "Promise는 비동기 작업의 결과를 표현하는 객체다.", "Pending: 아직 결과가 나오지 않은 상태", "Fulfilled: 작업이 성공한 상태", "Rejected: 작업이 실패한 상태" ↩︎

  12. 비동기 프로그래밍.md 44-49줄 — "이전 작업이 완료될 때까지 다음 작업을 연기하거나 에러를 깔끔하게 처리할 수 있는 객체.", "결과 반환 시간을 보장하지는 않지만, 반환 시 다음 코드로 진행시킴", "한 번에 성공/실패 중 하나만 가짐", "다음 코드 블록을 막지 않음 (Non-blocking)" ↩︎ ↩︎ ↩︎ ↩︎

  13. 비동기 프로그래밍.md 51-60줄 — Promise 체이닝 코드와 "then()도 Promise를 반환하기 때문에 체이닝이 가능하다." 코드를 그대로 옮겼다. ↩︎ ↩︎ ↩︎ ↩︎

  14. ECMAScript.md (Study) 1065-1085줄 — "Promise는 then()을 연결해서 비동기 작업을 순서대로 처리할 수 있다.", "각 then()은 이전 단계의 결과를 받아 다음 단계로 넘긴다.", "실패가 발생하면 catch()에서 처리할 수 있다." 코드 1067-1081줄을 그대로 옮겼다. ↩︎ ↩︎ ↩︎

  15. ECMAScript.md (Study) 1089, 1106줄 — "async/await는 Promise 기반 비동기 코드를 더 동기 코드처럼 작성하게 해주는 문법이다.", "중요한 점은 async/await가 Promise를 대체하는 별개의 기술이 아니라는 것이다. 내부적으로는 Promise를 기반으로 동작한다." ↩︎ ↩︎

  16. ECMAScript.md (Study) 1104줄 — "await는 Promise가 처리될 때까지 기다린 뒤 결과값을 반환한다." ↩︎

  17. ECMAScript.md (Study) 1108-1120줄 — "즉, 다음 두 방식은 표현 방식이 다를 뿐 같은 비동기 흐름을 다룬다." 두 코드를 그대로 옮겼다. ECMAScript.md (Computing) 75-88줄에도 같은 fetch 흐름을 Promise와 async-await로 나란히 쓴 예시가 있다. ↩︎ ↩︎

  18. ECMAScript.md (Study) 1124-1143줄 — "Promise chaining에서는 catch()를 사용한다.", "async/await에서는 일반적인 동기 코드처럼 try-catch를 사용할 수 있다.", "async/await의 장점은 비동기 에러를 동기 예외 처리 방식과 비슷하게 다룰 수 있다는 점이다." 코드는 같은 노트 1091-1102줄을 그대로 옮겼다. ↩︎ ↩︎ ↩︎

  19. ECMAScript.md (Study) 1147-1160줄 — "일반적으로 await는 async 함수 내부에서만 사용할 수 있다.", "하지만 ESM 환경에서는 모듈 최상위에서도 await를 사용할 수 있다.", "단, top-level await는 일반 script가 아니라 module 환경에서 사용할 수 있다." ↩︎ ↩︎ ↩︎

  20. ECMAScript.md (Study) 1164-1173줄 — "서로 의존하지 않는 여러 비동기 작업은 순서대로 기다릴 필요가 없다.", "Promise.all()을 사용하면 여러 Promise를 병렬로 실행하고, 모두 완료되었을 때 결과를 배열로 받을 수 있다." 코드 1168-1172줄을 그대로 옮겼다. ↩︎ ↩︎ ↩︎

  21. ECMAScript.md (Study) 1175줄 — "사용자 목록을 가져온 뒤 그 결과로 게시글 요청을 해야 한다면 순차 처리가 필요하다. 하지만 서로 관련 없는 요청이라면 Promise.all()로 병렬 처리하는 것이 효율적이다." ↩︎ ↩︎

  22. 비동기 프로그래밍.md 76줄 — "비동기 작업의 콜백은 **이벤트 큐(Event Queue)**에 등록되고, 이벤트 루프가 콜 스택이 비었을 때 꺼내 실행한다." ↩︎

  23. 출처 공백 — 비동기 프로그래밍.md 76줄은 큐를 하나로만 설명하고, Promise 콜백과 타이머 콜백을 구분하지 않는다. microtask와 task 큐의 차이는 이 batch의 어느 Raw에도 없다. MDN의 event loop 문서 같은 공식 자료가 들어오면 보강한다. ↩︎

  24. 출처 공백 — ECMAScript.md (Study) 1162-1175줄은 모든 작업이 성공하는 경우만 다룬다. 실패가 섞일 때의 Promise.all() 동작과 다른 조합 함수는 Raw로 확인하지 못했다. ↩︎

  25. 출처 공백 — 비동기 프로그래밍.md 78줄이 관련 노트로 "이벤트 루프와 Pub-Sub"를 링크하지만 그 노트는 Raw에 없다. 원문이 학습 출처로 적은 부스트캠프 챌린지 Day11 학습정리(85줄)와, 비동기 이벤트 매니저·Pub/Sub를 다룬 Day9 학습정리(Raw/courses/Naver BoostCamp/Challenge/Day9/학습정리.md 101줄부터)는 이번 batch에서 읽지 않았다. ↩︎