Taeyoung Kim

Engineering

JS 비동기 핵심 패턴

JS 비동기 핵심 패턴 학습 내용을 정리한 백필 노트입니다.

이 글은 2025년 학습 기록을 블로그 형식으로 정리한 백필 노트입니다.


1. 동기와 비동기의 개념

  • 동기(Synchronous): 하나의 작업이 완료될 때까지 다음 작업이 대기하는 직렬적 실행 방식입니다. 코드가 작성된 순서대로 동작합니다.

    console.log(1);
    console.log(2);
    console.log(3);
    // 출력: 1, 2, 3
    
  • 비동기(Asynchronous): 하나의 작업이 완료되지 않아도 다음 작업을 실행할 수 있는 병렬적 실행 방식입니다. 주로 네트워크 통신과 같이 시간이 걸리는 작업에 사용되어 시스템 멈춤 현상을 방지합니다.

    console.log(1);
    setTimeout(() => console.log(2), 0);
    console.log(3);
    // 출력: 1, 3, 2
    

2. 비동기 코드 처리 패턴

비동기 코드의 실행 순서를 보장하기 위한 주요 패턴은 다음과 같습니다.

콜백 (Callback) 패턴

  • 특정 비동기 작업이 완료된 후 실행될 함수(콜백)를 인자로 전달하는 방식입니다.

  • 문제점: 비동기 작업을 순차적으로 여러 번 처리해야 할 경우, 콜백 함수가 계속 중첩되어 코드의 가독성이 떨어지고 복잡해지는 **콜백 지옥(Callback Hell)**이 발생할 수 있습니다.

    a(() => {
      b(() => {
        c(() => {
          // ...
        });
      });
    });
    

Promise.then() 패턴

  • ES2015에서 도입된 Promise 객체를 사용하여 콜백 지옥 문제를 해결합니다.

  • 비동기 작업의 완료 시점을 .then() 메소드로 보장받을 수 있습니다.

  • .then() 메소드에서 다시 Promise 객체를 반환함으로써 중첩 없이 체인 형태로 코드를 작성할 수 있어 가독성이 향상됩니다.

    a()
      .then(() => b())
      .then(() => c());
    

async/await 패턴

  • ES2017에서 도입된 가장 최신 방식으로, 비동기 코드를 동기 코드처럼 직관적으로 작성할 수 있게 합니다.

  • async 키워드가 붙은 함수 안에서만 await 키워드를 사용할 수 있습니다.

  • awaitPromise가 완료될 때까지 함수의 실행을 기다립니다.

    (async () => {
      await a();
      await b();
      await c();
    })();
    

3. Promise 심화

  • Promise는 비동기 작업의 상태를 나타내는 객체로, 세 가지 상태를 가집니다.
    1. 대기 (Pending): 작업이 시작되었지만 아직 완료(이행 또는 거부)되지 않은 상태.
    2. 이행 (Fulfilled): 작업이 성공적으로 완료된 상태. resolve(결과)를 통해 값을 전달합니다.
    3. 거부 (Rejected): 작업이 실패한 상태. reject(이유)를 통해 에러를 전달합니다.

4. 에러 핸들링 (Error Handling)

  • 콜백 방식: 성공, 실패, 최종 실행 콜백을 각각 전달하여 처리합니다.

  • .then() 방식: .catch() 메소드로 실패(거부) 상태를 처리하고, .finally() 메소드로 성공/실패 여부와 관계없이 항상 실행되어야 하는 코드를 처리합니다.

    promise
      .then(res => console.log(res))
      .catch(err => console.error(err))
      .finally(() => console.log('종료!'));
    
  • async/await 방식: try...catch...finally 구문을 사용하여 동기 코드와 유사한 방식으로 에러를 처리합니다.

    try {
      const res = await promise;
    } catch (err) {
      console.error(err);
    } finally {
      console.log('종료!');
    }
    

5. 반복문에서의 비동기 처리

  • .forEach(), .map()과 같은 배열 메소드는 await을 기다리지 않고 모든 반복을 동시에 실행하므로, 순차적인 비동기 처리에 적합하지 않습니다.

  • 반복문 안에서 비동기 작업을 순서대로 실행하려면 for 또는 for...of 반복문을 사용해야 합니다.

    // 올바른 방법
    (async () => {
      for (const title of titles) {
        const movies = await getMovies(title);
        console.log(title, movies);
      }
    })();
    

6. Promise 정적 메소드

  • Promise.all(promises): 모든 Promise가 이행될 때까지 기다립니다. 하나라도 거부되면 즉시 전체가 거부됩니다.
  • Promise.allSettled(promises): 모든 Promise가 이행되거나 거부될 때까지 기다린 후, 각 Promise의 상태와 결과를 배열로 반환합니다.
  • Promise.race(promises): 가장 먼저 이행되거나 거부된 Promise의 결과를 반환합니다.
  • Promise.any(promises): 가장 먼저 '이행'된 Promise를 반환합니다. 모든 Promise가 거부될 경우에만 거부됩니다.
  • Promise.resolve(value): 무조건 이행되는 Promise를 생성합니다.
  • Promise.reject(reason): 무조건 거부되는 Promise를 생성합니다.

7. 활용 예제: 이미지 로드

  • Image 객체의 loaderror 이벤트를 Promise로 감싸 비동기적으로 이미지 로딩을 처리할 수 있습니다.
  • await을 사용하여 이미지 로드가 완료될 때까지 기다린 후, 로딩 애니메이션을 제거하고 이미지를 화면에 표시하는 로직을 구현할 수 있습니다.