[JS] 비동기 프로그래밍의 진화: Callback → Promise → async/await

minimanimo·2024년 11월 29일
0

Next.js

목록 보기
8/8

JavaScript는 비동기 프로그래밍을 통해 네트워크 요청, 파일 읽기, 타이머와 같은 시간이 소요되는 작업을 효율적으로 처리할 수 있습니다. 이러한 비동기 프로그래밍 방식은 콜백 함수에서 시작해 Promise, 그리고 async/await로 진화해왔습니다.

🎁 콜백 함수(Callback)

콜백 함수는 다른 함수의 인자로 전달되어 특정 작업이 완료된 후 호출되는 함수입니다. 비동기 작업이 완료되면 호출되기 때문에 결과를 기다리지 않고 다음 코드가 실행될 수 있습니다.

📍 콜백 함수 코드

🎉 앞으로 이 코드가 어떻게 변환되는지 살펴보도록 하겠습니다.
setTimeout(() => {
  console.log("1단계 완료");
  setTimeout(() => {
    console.log("2단계 완료");
    setTimeout(() => {
      console.log("3단계 완료");
    }, 1000);
  }, 1000);
}, 1000);

🎀 장점

  1. 간단한 구현
    초기에 비동기 작업을 처리하기 위한 간단한 방식.

  2. 즉각적 호출
    특정 작업이 끝났을 때 즉시 후속 작업을 수행 가능.

🎀 단점

  1. 콜백 지옥(Callback Hell)
    콜백 함수가 중첩될수록 코드의 구조가 깊어지고 복잡해지는 문제가 발생합니다. 위 코드의 setTimeout과 같은 비동기 함수가 여러 개 중첩되면 코드의 시작과 끝을 명확히 파악하기 어려워지며, 가독성이 떨어집니다.

  2. 에러 처리의 복잡성
    비동기 작업 중 에러가 발생했을 때, 각 콜백 함수에서 개별적으로 에러를 처리해야 합니다. 위 코드의 세 개의 setTimeout은 각각의 콜백 함수 3개에 에러 처리를 해야합니다.
    즉, 콜백이 중첩될수록 에러 처리 로직도 여러 단계에 분산되어 일관된 에러 관리가 어려워집니다. 하나의 콜백에서 에러를 처리하지 못하면 상위 단계로 전파되지 않고 무시될 가능성도 있습니다.

  3. 비동기 흐름 제어의 어려움
    여러 비동기 작업을 순차적 또는 병렬로 실행할 때 복잡한 흐름 제어가 필요합니다. 이로 인해 코드가 복잡해지고 오류 발생 가능성이 높아집니다.




🎁 Promise

Promise는 비동기 코드의 복잡성을 줄이고 가독성과 에러 관리를 개선하기 위해 도입된 ES6(ECMAScript 2015) 의 기능으로, 작업의 결과 값을 나중에 사용할 수 있도록 처리합니다.

📍 Promise의 비동기 작업 결과를 나타내는 객체

  • resolve(value): Promise를 성공(fulfilled) 상태로 전환하고, 결과 값(value)을 전달합니다. 이후 .then() 메서드에서 결과를 처리합니다.
  • reject(reason): Promise를 실패(rejected) 상태로 전환하고, 실패 이유(reason)를 전달합니다. 이후 .catch() 메서드에서 에러를 처리합니다.

📍 Promise의 상태

  • Pending(대기 중): 초기 상태, 결과가 없는 상태.
  • Fulfilled(이행됨): 작업이 성공적으로 완료된 상태.
  • Rejected(거부됨): 작업이 실패한 상태.

📍 콜백 함수 → Promise 코드

function delayLog(message, delay) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(message);
      resolve(); // 작업이 완료되면 resolve 호출
    }, delay);
  });
}

// 1단계 -> 2단계 -> 3단계 순차 실행
delayLog("1단계 완료", 1000)
  .then(() => delayLog("2단계 완료", 1000))
  .then(() => delayLog("3단계 완료", 1000))
  .catch((error) => {
    console.error("에러 발생:", error);
  });

🎀 장점

  1. 가독성 개선
    then, catch, finally 구문을 통해 코드 흐름이 명확해지고 읽기 쉬워집니다. 기존 콜백 방식의 중첩된 구조와 달리, 비동기 작업을 선형적으로 표현할 수 있어 가독성이 크게 향상됩니다. 특히, 후속 작업을 체인 형태로 연결할 수 있어 코드가 깔끔하게 정리됩니다.

  2. 일관된 에러 처리
    catch 구문을 통해 여러 단계에서 발생하는 에러를 한 곳에서 일괄적으로 처리할 수 있습니다. 콜백 방식에서는 각 단계마다 개별적으로 에러를 처리해야 했던 반면, Promise는 에러가 발생한 단계와 상관없이 최종 에러 핸들러에서 처리할 수 있어 에러 관리가 간소화됩니다.

  3. 비동기 작업 흐름 제어
    Promise.allPromise.race 메서드를 제공하여 효율적인 흐름 제어가 가능합니다. Promise.all은 모든 비동기 작업이 완료될 때까지 기다린 후 결과를 반환하며, Promise.race는 가장 먼저 완료된 작업의 결과를 반환합니다. 이러한 기능 덕분에 병렬 작업 제어가 간단하고 직관적으로 이루어집니다.

🎀 단점

  1. 체인 복잡성
    then 체인이 길어지면 코드가 다시 복잡해질 수 있습니다. 특히, 체인이 여러 단계에 걸쳐 중첩될 경우 각 작업의 흐름을 따라가기가 어려워지며, 코드 관리가 어려워질 수 있습니다.

  2. 디버깅 어려움
    여러 비동기 작업이 있을 경우, 각 단계의 에러를 추적하기 위해서는 catch를 적절히 사용해야 하며, 에러가 어디서 발생했는지 정확하게 파악하기 어려운 경우가 많습니다.




🎁 async/await

async/await는 Promise 기반 코드를 동기 코드처럼 작성할 수 있게 해주는 문법입니다. async 함수는 항상 Promise를 반환하며, await 키워드는 Promise가 해결될 때까지 대기합니다.

📍 Promise → async/await 코드

function delayLog(message, delay) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(message);
      resolve(); // 작업 완료 시 resolve 호출
    }, delay);
  });
}

async function runSequentialLogs() {
  try {
    await delayLog("1단계 완료", 1000); // 1초 대기 후 메시지 출력
    await delayLog("2단계 완료", 1000); // 1초 대기 후 메시지 출력
    await delayLog("3단계 완료", 1000); // 1초 대기 후 메시지 출력
  } catch (error) {
    console.error("에러 발생:", error);
  } finally {
    console.log("작업 완료"); // 성공, 실패와 관계없이 실행
  }
}

runSequentialLogs();

🎀 장점

  1. 가독성 극대화
    async/await를 사용하면 비동기 코드가 동기 코드처럼 보이게 되어, 코드의 흐름을 쉽게 이해할 수 있습니다. await를 사용해 비동기 작업이 완료될 때까지 기다리는 방식은 자연스럽게 코드가 순차적으로 실행되는 것처럼 보이게 하여, 복잡한 비동기 처리를 간결하고 직관적으로 작성할 수 있습니다.

  2. 에러 처리 간소화
    async/awaittry-catch 블록을 사용하여 에러를 처리할 수 있기 때문에, 여러 단계의 비동기 작업에서 발생할 수 있는 에러를 일관성 있게 관리할 수 있습니다.

  3. 디버깅 용이
    async/await는 코드 흐름이 동기적처럼 보이기 때문에, 디버깅할 때 코드의 순서를 쉽게 파악할 수 있습니다. 각 비동기 작업이 명확히 어떤 순서로 실행되는지 알 수 있어, 디버깅 과정에서 문제가 발생한 지점을 빠르게 찾을 수 있습니다.

🎀 단점

  1. 전체 흐름 중단
    async/await에서 await는 비동기 작업이 완료될 때까지 함수의 실행을 중단시킵니다. 즉, 하나의 await가 오래 걸리면, 그 이후의 코드 실행이 완료될 때까지 기다려야 하기 때문에 전체 함수가 지연될 수 있습니다. 이로 인해 성능 문제가 발생할 수 있으며, 특히 많은 비동기 작업이 있을 때 처리 시간이 길어질 수 있습니다.

  2. 최신 브라우저/환경 요구
    async/awaitES2017(ES8)에서 도입된 기능이기 때문에, 이를 사용하려면 최신 브라우저나 Node.js 버전이 필요합니다.




🎁 결론

비동기 작업 처리 방법으로는 콜백 함수, Promise, async/await가 있으며, 각각의 방식은 가독성, 에러 처리, 흐름 제어 측면에서 장단점이 있습니다.

콜백 함수는 간단하지만 중첩되면 가독성이 떨어지고, Promise는 더 나은 가독성과 에러 처리 기능을 제공하지만 체인이 길어지면 복잡해집니다. async/await는 가장 직관적이고 간단한 코드 작성이 가능하지만, 전체 흐름을 중단시키는 단점이 있습니다.

앞으로는 병렬 처리나 리소스 관리가 더 효율적이고 직관적으로 이루어지는 새로운 비동기 처리 방식이 등장할 수 있기를 기대합니다.

profile
Java, Spring, React, Next.js 3년차 개발자 입니다😾

0개의 댓글