[JavaScript] 비동기 처리 완벽 정리: 콜백, Promise, async/await 마스터하기 ([JavaScript] Mastering Asynchronous Operations: Callbacks, Promises, and async/await Explained)

안녕하세요! 현대 웹 개발에서 비동기 처리는 선택이 아닌 필수입니다. 사용자 경험을 해치지 않으면서 네트워크 요청, 파일 입출력 등 시간이 오래 걸리는 작업을 효율적으로 처리하기 위해 JavaScript는 다양한 비동기 처리 패턴을 제공합니다. 이번 포스팅에서는 JavaScript 비동기 처리의 핵심인 콜백(Callback), Promise, 그리고 async/await에 대해 심층적으로 다루고, 각 방식의 장단점과 올바른 사용법을 완벽하게 정리해 보겠습니다.

1. JavaScript 비동기 처리의 필요성

JavaScript는 기본적으로 싱글 스레드(Single-threaded) 언어입니다. 즉, 한 번에 하나의 작업만 처리할 수 있다는 의미입니다. 만약 시간이 오래 걸리는 작업(예: 서버에서 데이터 가져오기)이 동기적으로 실행된다면, 해당 작업이 완료될 때까지 UI는 멈추고 사용자는 아무것도 할 수 없게 됩니다. 이는 매우 나쁜 사용자 경험으로 이어집니다.

비동기 처리는 이러한 문제점을 해결하기 위해 도입되었습니다. 시간이 오래 걸리는 작업을 백그라운드에서 실행하고, 작업이 완료되면 미리 정의해 둔 특정 함수(콜백)를 호출하거나, 결과값을 반환하여 UI 스레드를 블로킹하지 않고 다른 작업을 계속할 수 있도록 합니다.

2. 비동기 처리의 진화: 콜백 → Promise → async/await

2.1. 콜백 함수 (Callback Function)

가장 기본적인 비동기 처리 패턴입니다. 특정 작업이 완료된 후 실행될 함수를 다른 함수의 인자로 전달하는 방식입니다. 이미 익명 함수 포스팅에서 콜백의 기본 개념을 다루었죠. setTimeout, addEventListener 등이 대표적인 콜백 기반 API입니다.

// 콜백 함수를 이용한 비동기 처리 예시
function fetchData(callback) {
  setTimeout(function() {
    const data = "서버에서 가져온 데이터입니다.";
    callback(data); // 데이터 수신 후 콜백 함수 호출
  }, 2000); // 2초 후 실행
}

console.log("데이터 요청 시작...");
fetchData(function(receivedData) {
  console.log("데이터 수신 완료:", receivedData);
});
console.log("다음 작업 진행..."); // 데이터 수신을 기다리지 않고 즉시 실행

콜백 지옥 (Callback Hell)

콜백은 간단한 비동기 처리에 유용하지만, 여러 비동기 작업이 순차적으로 중첩될 경우 코드의 들여쓰기가 깊어지고 가독성이 떨어지는 '콜백 지옥(Callback Hell)'에 빠지기 쉽습니다. 오류 처리도 복잡해집니다.

// 콜백 지옥 예시
getData(function(a) {
  processData(a, function(b) {
    saveData(b, function(c) {
      console.log('모든 작업 완료:', c);
    }, function(error) { /* 에러 처리 */ });
  }, function(error) { /* 에러 처리 */ });
}, function(error) { /* 에러 처리 */ });

2.2. Promise (프로미스)

콜백 지옥의 단점을 보완하기 위해 ES6(ECMAScript 2015)에서 도입된 비동기 처리 패턴입니다. Promise는 비동기 작업의 최종 완료 또는 실패를 나타내는 객체입니다.

  • State (상태): pending (대기) → fulfilled (이행) 또는 rejected (거부)
  • Producer (생산자): 비동기 작업을 수행하고 Promise 객체를 생성합니다.
  • Consumer (소비자): .then()으로 성공 콜백을, .catch()로 실패 콜백을 처리합니다.
// Promise를 이용한 비동기 처리 예시
function fetchDataPromise() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const success = true; // 가상으로 성공/실패 조건 설정
      if (success) {
        resolve("Promise로 가져온 데이터입니다."); // 성공 시 resolve 호출
      } else {
        reject("데이터를 가져오는 데 실패했습니다."); // 실패 시 reject 호출
      }
    }, 2000);
  });
}

console.log("Promise 데이터 요청 시작...");
fetchDataPromise()
  .then(data => { // 성공 시 실행
    console.log("Promise 데이터 수신 완료:", data);
    return data + " (처리됨)"; // 다음 then으로 전달
  })
  .then(processedData => { // 체이닝
    console.log("2차 처리 완료:", processedData);
  })
  .catch(error => { // 실패 시 실행
    console.error("Promise 오류:", error);
  })
  .finally(() => { // 성공/실패 여부와 상관없이 항상 실행
    console.log("Promise 작업 종료.");
  });
console.log("Promise 다음 작업 진행...");

.then() 메소드를 통해 비동기 작업을 체이닝하여 콜백 지옥을 해결하고, .catch()를 통해 중앙집중식 오류 처리가 가능해집니다.

2.3. async/await

ES8(ECMAScript 2017)에서 도입되었으며, Promise를 기반으로 비동기 코드를 마치 동기 코드처럼 보이게 작성할 수 있게 해주는 문법입니다. 가독성이 매우 뛰어나고 오류 처리가 간결해집니다.

  • async 키워드는 함수 앞에 붙어 해당 함수가 항상 Promise를 반환함을 나타냅니다.
  • await 키워드는 async 함수 내에서만 사용 가능하며, Promise가 이행될 때까지 함수의 실행을 일시 중지합니다.
// async/await를 이용한 비동기 처리 예시
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function processDataAsync() {
  try {
    console.log("async/await 데이터 요청 시작...");
    await delay(1000); // 1초 대기
    const data1 = await fetchDataPromise(); // Promise가 이행될 때까지 대기
    console.log("async/await 첫 번째 데이터:", data1);

    await delay(1000); // 또 1초 대기
    const data2 = data1 + " (async/await 처리됨)";
    console.log("async/await 두 번째 데이터:", data2);
    
    // 비동기 작업 중 오류 발생 시 throw
    // if (Math.random() < 0.5) throw new Error("가상 오류 발생");

    return "모든 async/await 작업 완료";
  } catch (error) {
    console.error("async/await 오류 발생:", error.message);
    return "async/await 작업 실패";
  } finally {
    console.log("async/await 작업 종료.");
  }
}

// async 함수는 Promise를 반환하므로 .then/.catch로 처리 가능
processDataAsync()
  .then(result => console.log("최종 결과:", result))
  .catch(error => console.error("최종 에러:", error));

console.log("async/await 다음 작업 진행...");

async/awaittry...catch 블록을 사용하여 동기 코드처럼 오류를 쉽게 처리할 수 있게 해줍니다. 가장 현대적이고 선호되는 비동기 처리 방식입니다.

결론: 올바른 비동기 처리 선택

JavaScript의 비동기 처리 방식은 콜백에서 Promise, 그리고 async/await로 진화하며 코드의 가독성과 유지보수성을 크게 향상시켰습니다. 각 방식은 고유의 장단점을 가지고 있으며, 상황에 따라 적절히 선택하여 사용해야 합니다.

  • 콜백: 간단한 비동기 작업에 사용되지만, 중첩 시 콜백 지옥 발생.
  • Promise: 콜백 지옥을 해결하고 비동기 작업의 성공/실패를 명확히 표현. 체이닝과 오류 처리에 용이.
  • async/await: Promise를 기반으로 가장 직관적이고 동기적인 코드 흐름처럼 비동기 코드를 작성 가능. 가독성과 오류 처리에 탁월.

대부분의 현대 JavaScript 프로젝트에서는 가독성 및 유지보수성 측면에서 async/await를 가장 선호하며, 복잡한 비동기 흐름을 제어하는 데 최적의 솔루션으로 자리 잡았습니다. 이 세 가지 비동기 처리 방식을 숙지하는 것은 JavaScript 개발자로서의 역량을 한 단계 끌어올리는 데 필수적입니다.

이 포스팅이 JavaScript 비동기 처리에 대한 깊이 있는 이해를 돕는 데 유용했기를 바랍니다. 궁금한 점이나 추가하고 싶은 내용이 있다면 언제든지 댓글로 남겨주세요!

댓글 없음:

댓글 쓰기

댓글 폭탄 해결! 자바스크립트 댓글 접기/펼치기로 가독성 200% 높이는 법(Solve Comment Chaos: Elevate Readability 200% with JS Comment Folding/Unfolding)

내 웹사이트에 적용! 초간단 자바스크립트 댓글 펼치기/숨기기 튜토리얼 내 웹사이트에 적용! 초간단 자바스크립트 댓글 펼치기/숨기기 튜토...