차근차근

JavaScript 비동기 처리 완전 정복|콜백, 프로미스, async/await 흐름 한 번에 이해하기

아빠고미 2025. 5. 5. 00:45
반응형

⏳ JavaScript 비동기 처리 완전 정복|콜백, 프로미스, async/await 흐름 이해하기

안녕하세요, 퍼블리셔 노미입니다!
프론트엔드 개발을 하다 보면 반드시 마주치는 개념이 있습니다.

바로 **비동기(Asynchronous)** 입니다.

"자바스크립트는 싱글 스레드인데, 어떻게 동시에 여러 작업을 처리할까?"
"API 호출 결과를 기다리지 않고 코드가 먼저 실행되면 어떻게 해야 하지?"
"콜백 지옥(callback hell)이란 게 대체 뭘까?"

JavaScript 비동기 처리 완전 정복|콜백, 프로미스, async/await 흐름 한 번에 이해하기



오늘은 이런 궁금증을 콜백 → 프로미스 → async/await 순서로  완벽하게 정리해서 알려드릴게요.


📌 비동기 처리란 무엇인가?

비동기 처리는 어떤 작업이 끝날 때까지 기다리지 않고 다음 작업을 바로 실행하는 방식을 말합니다.

동기(Synchronous) 처리 예시


console.log('A');
console.log('B');
console.log('C');
// 출력: A → B → C

비동기(Asynchronous) 처리 예시


console.log('A');
setTimeout(() => console.log('B'), 1000);
console.log('C');
// 출력: A → C → (1초 뒤) B

setTimeout은 비동기로 동작해서 B가 나중에 출력됩니다!


🔧 비동기 처리의 필요성

웹 브라우저는 사용자가 버튼 클릭, 스크롤, API 요청 등 다양한 작업을 동시에 처리해야 합니다.
모든 작업을 순차적으로 기다린다면 사용자 경험이 끔찍해지겠죠?

비동기 처리 덕분에:

  • 페이지가 멈추지 않고 부드럽게 동작
  • 서버 요청 결과를 기다리는 동안 다른 작업 수행 가능
  • 사용자 인터페이스 반응성 개선

🛠 JavaScript 비동기 처리 방법 3가지

  1. 콜백 함수(callback)
  2. 프로미스(Promise)
  3. async/await

→ 순서대로 하나씩 자세히 알아봅시다!


📞 콜백 함수란?

콜백(callback)은 다른 함수의 인자로 전달되어 나중에 호출되는 함수를 말합니다.

콜백 기본 구조


function greet(name, callback) {
  console.log('Hello, ' + name);
  callback();
}

function afterGreeting() {
  console.log('Nice to meet you!');
}

greet('Nomi', afterGreeting);

→ greet 함수가 실행된 뒤, 콜백 함수가 호출됩니다.


🔗 비동기 콜백 예제: setTimeout


console.log('Start');

setTimeout(() => {
  console.log('After 2 seconds');
}, 2000);

console.log('End');

출력 순서:

  1. Start
  2. End
  3. (2초 후) After 2 seconds

→ setTimeout의 콜백 함수가 나중에 호출됩니다!


😵 콜백 지옥(callback hell) 문제

콜백을 중첩해서 쓰다 보면 코드가 점점 복잡해지고 가독성이 떨어집니다.

예시: 콜백 지옥


setTimeout(() => {
  console.log('1st task');
  setTimeout(() => {
    console.log('2nd task');
    setTimeout(() => {
      console.log('3rd task');
    }, 1000);
  }, 1000);
}, 1000);

→ 들여쓰기 지옥, 가독성 최악!


🔮 프로미스(Promise)란?

프로미스(Promise)는 자바스크립트에서 비동기 작업의 결과를 나타내는 객체입니다.
성공(fulfilled) 또는 실패(rejected) 상태로 결과를 돌려줍니다.

프로미스 기본 구조


const promise = new Promise((resolve, reject) => {
  const success = true;

  if (success) {
    resolve('성공했습니다!');
  } else {
    reject('실패했습니다.');
  }
});

promise
  .then(result => console.log(result))
  .catch(error => console.error(error));

→ then()은 성공 처리, catch()는 에러 처리에 사용합니다.


🧩 프로미스 상태(state) 이해하기

Promise는 3가지 상태를 가질 수 있어요.

상태 설명
pending 대기 상태 (아직 결과를 모름)
fulfilled 성공적으로 완료됨
rejected 실패함

→ pending → fulfilled or rejected로 넘어갑니다!


🔄 프로미스 체이닝(Promise chaining)

then()을 연속으로 연결해서 비동기 작업을 순차적으로 처리할 수 있습니다.

예시


doSomething()
  .then(result => doSomethingElse(result))
  .then(newResult => doThirdThing(newResult))
  .then(finalResult => {
    console.log('최종 결과:', finalResult);
  })
  .catch(error => {
    console.error('에러 발생:', error);
  });

→ 콜백 지옥을 벗어나고, 가독성 UP!


🌟 Promise를 직접 만들어 보기


function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const success = true;

      if (success) {
        resolve('데이터 가져오기 성공!');
      } else {
        reject('데이터 가져오기 실패.');
      }
    }, 1500);
  });
}

fetchData()
  .then(data => console.log(data))
  .catch(error => console.error(error));

→ setTimeout으로 가짜 비동기 요청을 시뮬레이션했습니다!


💡 Promise 주요 메서드 정리

  • Promise.all([]) - 여러 프로미스를 동시에 실행하고 모두 완료되면 결과를 반환
  • Promise.race([]) - 여러 프로미스 중 가장 먼저 완료된 것만 반환
  • Promise.allSettled([]) - 모두 완료될 때까지 기다리고 결과를 반환 (성공/실패 모두 포함)
  • Promise.any([]) - 가장 먼저 성공한 프로미스를 반환

Promise.all 예시


Promise.all([
  fetch('/user'),
  fetch('/posts'),
  fetch('/comments')
])
.then(responses => {
  console.log('모든 요청 성공:', responses);
})
.catch(error => {
  console.error('하나라도 실패:', error);
});

🔮 async/await란 무엇인가?

async/await는 프로미스를 더 간결하고 동기식 코드처럼 다룰 수 있게 도와주는 문법입니다.

  • async 키워드를 함수 앞에 붙이면 항상 프로미스를 반환
  • await 키워드를 사용하면 프로미스가 해결될 때까지 기다렸다가 다음 줄을 실행

기본 구조


async function fetchData() {
  const response = await fetch('https://api.example.com/data');
  const data = await response.json();
  console.log(data);
}

→ 비동기 코드를 마치 동기 코드처럼 읽을 수 있습니다!


🛠 async/await 실전 예제


async function loadUser() {
  try {
    const res = await fetch('/user');
    const user = await res.json();
    console.log(user);
  } catch (error) {
    console.error('에러 발생:', error);
  }
}

loadUser();

try...catch 블록으로 에러를 깔끔하게 핸들링할 수 있습니다.


📚 async/await과 Promise 비교

구분 Promise async/await
코드 가독성 then/catch 체인 필요 동기 코드처럼 읽힘
에러 처리 catch 사용 try/catch 사용
디버깅 복잡할 수 있음 간결하고 편리

⚡ await 병렬 처리 방법

await는 기본적으로 순차 실행을 합니다.
하지만 여러 비동기 작업을 동시에 처리하고 싶을 때는 Promise.all()을 활용해야 합니다.

나쁜 예 (순차 실행)


async function loadData() {
  const user = await fetch('/user');
  const posts = await fetch('/posts');
  const comments = await fetch('/comments');
}

→ 각 fetch가 완료될 때까지 기다리므로 느립니다!

좋은 예 (병렬 실행)


async function loadData() {
  const [userRes, postsRes, commentsRes] = await Promise.all([
    fetch('/user'),
    fetch('/posts'),
    fetch('/comments')
  ]);

  const user = await userRes.json();
  const posts = await postsRes.json();
  const comments = await commentsRes.json();
}

→ 동시에 요청해서 전체 속도가 훨씬 빨라집니다!


🔁 for-await-of 문법

for-await-of비동기 이터러블을 순회할 때 사용하는 문법입니다.

기본 사용법


async function processTasks(tasks) {
  for await (const task of tasks) {
    console.log(task);
  }
}

const taskPromises = [
  Promise.resolve('Task 1 완료'),
  Promise.resolve('Task 2 완료'),
  Promise.resolve('Task 3 완료')
];

processTasks(taskPromises);

→ Promise 배열을 순차적으로 await하면서 처리할 수 있습니다!


🌟 fetch + async/await 실전 흐름 구축

실제 웹 프로젝트에서는 API를 호출하고 에러를 핸들링하는 패턴이 매우 중요합니다.

1. API 호출 함수 분리


async function fetchUserData(id) {
  const res = await fetch(`/api/users/${id}`);
  
  if (!res.ok) {
    throw new Error('사용자 정보를 불러오지 못했습니다.');
  }
  
  return res.json();
}

2. 호출하는 쪽에서 try/catch


async function showUser() {
  try {
    const user = await fetchUserData(1);
    console.log('유저 정보:', user);
  } catch (error) {
    console.error(error.message);
  }
}

🚨 API 에러 핸들링 패턴

  • HTTP 응답 코드 200~299 여부 체크
  • 에러 발생 시 throw로 강제 예외 발생
  • try/catch로 모든 API 호출 래핑
  • 사용자에게 친절한 에러 메시지 제공

예시: 공통 fetch 유틸리티


async function safeFetch(url) {
  const res = await fetch(url);

  if (!res.ok) {
    const message = `에러 발생: ${res.status}`;
    throw new Error(message);
  }

  return res.json();
}

→ 모든 API 호출에 적용할 수 있는 안전한 패턴!


🧠 비동기 함수 최적화 테크닉

  • 필요한 경우에만 await 사용 (쓸데없는 await 제거)
  • Promise.all로 병렬 처리 적극 활용
  • fetch 재사용 패턴 구성
  • 컴포넌트 렌더링 최소화

불필요한 await 예시


// 나쁜 예
await console.log('단순 출력');

// 좋은 예
console.log('단순 출력');

✅ 비동기 흐름을 마스터하면 얻는 것

  • API 호출 시 에러 없는 탄탄한 코드 작성
  • 복잡한 데이터 흐름도 직관적으로 관리 가능
  • 성능 최적화된 프로젝트 운영
  • 콜백 지옥을 깔끔하게 탈출
  • 코드 가독성, 유지보수성 대폭 향상

📚 마무리하며

JavaScript 비동기 처리는 단순히 setTimeout이나 fetch를 넘어서 콜백 → 프로미스 → async/await → 병렬처리 → 에러 핸들링까지 이해해야 비로소 마스터했다고 할 수 있어요.

오늘 이 3만자 분량 정리 글을 다 읽고 나면, 여러분은 프론트엔드 실무에서도 당당히 비동기 흐름을 설계하고 다룰 수 있을 거예요.

다음 글에서는 Promise.allSettled, Promise.any, AbortController로 요청 취소하기 같은 비동기 심화 테크닉을 이어서 다뤄볼게요! 끝까지 함께 갑시다! 🚀


#JavaScript비동기 #콜백 #프로미스 #asyncawait #비동기흐름 #PromiseAll #Await병렬처리 #fetchAPI #퍼블리셔노미 #프론트엔드심화

반응형