[JavaScript] Async

배창민·2025년 11월 20일
post-thumbnail

자바스크립트 비동기 처리 핵심 정리

동기 vs 비동기 · 콜백 · Promise · async/await · fetch · axios · 이벤트 루프


1. 동기 vs 비동기

1-1. 동기(synchronous)

  • 이전 코드가 끝난 다음에야 다음 코드가 실행됨
  • 한 작업이 오래 걸리면 그동안 다음 코드가 멈춰서 기다리는 상태
function sayHello() {
  console.log('Hello World!');
}

sayHello();
console.log('end!');
// 순서대로 Hello World! → end! 출력

1-2. 비동기(asynchronous)

  • 오래 걸리는 작업을 백그라운드에 맡겨두고, 다음 코드부터 먼저 실행
  • 작업이 끝났을 때 등록해 둔 콜백 함수를 나중에 실행
function sayHello() {
  console.log('Hello World!');
}

// 비동기 예제
setTimeout(sayHello, 3000);
console.log('end!');

// 바로 'end!' 가 찍히고, 3초 뒤에 'Hello World!' 출력

2. 콜백과 콜백 지옥

2-1. 콜백 함수

  • 비동기 작업이 끝난 뒤 실행할 함수를 콜백(callback) 으로 넘김
function increase(number, callback) {
  setTimeout(() => {
    const result = number + 10;
    if (callback) callback(result);
  }, 1000);
}

increase(0, result => {
  console.log(result);
});

2-2. 콜백 지옥(callback hell)

콜백 안에서 다시 콜백을 호출하는 구조가 계속 중첩되면서
가독성이 급격히 떨어지는 현상.

increase(0, r1 => {
  console.log(r1);
  increase(r1, r2 => {
    console.log(r2);
    increase(r2, r3 => {
      console.log(r3);
      // 계속 중첩...
    });
  });
});

문제점

  • 코드가 계단처럼 깊어짐
  • 에러 처리, 분기 처리, 재사용이 어려워짐

→ 이 문제를 해결하기 위해 Promise, async/await 사용


3. Promise

3-1. Promise 기본 개념

  • ES6 에서 도입된 비동기 상태 관리 객체

  • 세 가지 상태

    • pending (대기)
    • fulfilled (성공, resolve 호출)
    • rejected (실패, reject 호출)
  • 이후에 결과를 .then() / .catch() 로 처리

function increase(number) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const result = number + 10;
      if (result > 30) {
        return reject(new Error('NumberTooBig'));
      }
      resolve(result);
    }, 1000);
  });
}

3-2. then / catch 체이닝

increase(0)
  .then(n1 => {
    console.log(n1);
    return increase(n1);
  })
  .then(n2 => {
    console.log(n2);
    return increase(n2);
  })
  .then(n3 => {
    console.log(n3);
    return increase(n3);
  })
  .catch(e => {
    console.log(e); // 에러는 한 곳에서 처리
  });

장점

  • 콜백 지옥 구조를 수평으로 펴서 읽기 쉬워짐
  • 중간에 에러가 발생해도 .catch() 하나로 처리 가능

4. async / await

4-1. 기본 문법

  • async 키워드가 붙은 함수는 항상 Promise 반환
  • await 는 Promise 가 resolve 될 때까지 기다렸다가 그 값을 반환
  • awaitasync 함수 안에서만 사용 가능
function increase(number) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const result = number + 10;
      if (result > 30) {
        return reject(new Error('NumberTooBig'));
      }
      resolve(result);
    }, 1000);
  });
}

async function run() {
  try {
    let result = await increase(0);
    console.log(result);
    result = await increase(result);
    console.log(result);
    result = await increase(result);
    console.log(result);
    result = await increase(result); // 여기서 에러
    console.log(result);
  } catch (e) {
    console.log(e); // try/catch 로 동기 코드처럼 에러 처리
  }
}

run();

장점

  • 비동기 로직을 동기 코드처럼 위에서 아래로 읽을 수 있음
  • 복잡한 then 체인 대신 try / catch 로 에러 처리

5. API 호출 패턴

5-1. fetch + async/await

  • fetch(url) → Promise 반환
  • response.json() / response.text() 도 Promise 반환
    (응답 본문을 파싱하는 비동기 작업)
async function callAPI() {
  // HTTP 요청
  const response = await fetch(
    'https://jsonplaceholder.typicode.com/users'
  );
  console.log(response);
  console.log(`bodyUsed: ${response.bodyUsed}`);

  // JSON 파싱
  const data = await response.json();
  console.log(data);
  console.log(`bodyUsed: ${response.bodyUsed}`);
}

callAPI();

흐름

  1. fetch 로 요청 전송
  2. 응답 도착 → response 객체 반환
  3. response.json() 으로 JSON 파싱 (또 하나의 비동기 작업)
  4. 파싱 완료 후 실제 데이터 사용

5-2. fetch + then 체이닝

같은 작업을 then 체이닝으로 작성할 수도 있다.

function callAPI() {
  fetch('https://jsonplaceholder.typicode.com/users')
    .then(response => {
      console.log(response);
      console.log(`bodyUsed: ${response.bodyUsed}`);
      return response.json();
    })
    .then(data => {
      console.log(data);
    });
}

callAPI();
  • 패턴 자체는 Promise 기본 형태라 여전히 많이 사용됨
  • 코드가 길어질수록 async/await 쪽이 더 읽기 편한 경우가 많다

5-3. axios

  • 별도의 라이브러리
  • 내부에서 JSON 파싱까지 처리해 주므로 코드가 더 간단해짐
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
  axios
    .get('https://jsonplaceholder.typicode.com/users')
    .then(response => {
      console.log('axios response:', response);
      console.log('axios data:', response.data); // 바로 data 사용
    });
</script>

특징

  • response.data 에 바로 파싱된 데이터가 들어감
  • 인터셉터, 기본 설정, 에러 핸들링 등 기능이 풍부해서 프로젝트에서 자주 사용

6. 자바스크립트 비동기 동작 원리

6-1. 태스크 큐(task queue)

  • 비동기 콜백, 이벤트 핸들러가 실제로 실행되기 전 대기하는 공간
  • 타이머, HTTP 요청, DOM 이벤트 등 Web API 에서 처리된 콜백이
    작업이 끝난 뒤 이 큐에 들어와서 순서를 기다리는 상태

6-2. 이벤트 루프(event loop)

  • 이벤트 루프의 역할

    • 콜 스택(call stack)에 실행 중인 코드가 있는지 확인
    • 태스크 큐에 대기 중인 콜백이 있는지 확인
  • 스택이 비어 있고 큐에 콜백이 있다면

    • 큐에서 하나를 꺼내 콜 스택으로 이동
    • 스택에 올라간 콜백 함수가 실행

setTimeout 예시에서 일어나는 흐름

  1. setTimeout(callback, 1000) 호출
  2. 브라우저 타이머(Web API)가 1초를 센 뒤 콜백을 태스크 큐로 이동
  3. 콜 스택이 비는 시점에 이벤트 루프가 큐에서 콜백을 꺼내 실행

핵심 포인트

  • 자바스크립트는 싱글 스레드
  • 비동기는 스레드를 여러 개 쓰는 게 아니라
    이벤트 루프 + 태스크 큐 구조로 비동기처럼 보이게 만드는 것

6-3. 블로킹 vs 논블로킹

  • 블로킹(blocking)

    • 현재 작업이 끝날 때까지 다음 작업이 진행되지 않는 형태
    • 예: 동기 파일 읽기, CPU 많이 쓰는 연산 루프 등
      → 그동안 다른 이벤트 처리도 멈춤
  • 논블로킹(non-blocking)

    • 요청을 던져놓고 바로 다음 코드로 넘어간 뒤
      결과는 콜백/Promise/이벤트로 나중에 받는 형태
    • 예: 비동기 I/O, fetch, setTimeout 콜백 등

마무리 요약

  • 콜백 → Promise → async/await 순서로 발전해 왔고,
    지금은 async/await 이 가장 읽기 좋은 패턴
  • fetch / axios 같은 API 호출 도구는 모두 Promise 기반
  • 실제 동작은 콜 스택 + 태스크 큐 + 이벤트 루프 구조로 돌아가며
    이 구조를 이해하면 "왜 setTimeout 이 바로 실행 안 되는지",
    "왜 비동기 콜백이 나중에 실행되는지" 를 명확하게 이해할 수 있다.
profile
개발자 희망자

0개의 댓글