FullStack FrontEnd

React 비동기 처리 기초 정리 - Promise, async/await, fetch

SBDevv 2026. 9. 29. 08:48

React 비동기 처리 기초 정리 - Promise, async/await, fetch

서버와 통신하는 코드를 짜려면 비동기 처리를 이해해야 한다. 동기/비동기의 차이부터 Promise, async/await 문법, 그리고 fetch API로 실제 데이터를 요청하는 방법까지 정리한다.

동기(sync) vs 비동기(async) 처리

지금까지 작성한 코드는 위에서 아래로, 한 줄이 끝나야 다음 줄이 실행되는 방식이었다. 이런 방식을 동기(sync) 처리라고 한다. 하지만 서버에서 데이터를 받아오는 작업처럼 시간이 오래 걸리는 작업을 동기 방식으로 처리하면, 데이터가 도착할 때까지 화면 전체가 멈춰버린다.

function App() {
  const handleClick = () => {
    console.log("1. 시작");
    setTimeout(() => {
      console.log("2. 3초 후 실행됨");
    }, 3000);
    console.log("3. 끝");
  };

  return <button onClick={handleClick}>실행</button>;
}

버튼을 클릭하면 콘솔에 1 → 3 → 2 순서로 출력된다. setTimeout처럼 시간이 걸리는 작업은 결과를 기다리지 않고 일단 다음 코드("3. 끝")를 먼저 실행한 뒤, 나중에 준비가 되면 그때 실행된다. 이렇게 "기다리지 않고 다음 코드를 먼저 진행하는 방식"을 비동기(async) 처리라고 한다.

서버에서 데이터를 받아오는 데는 짧게는 몇 백 밀리초, 길게는 몇 초가 걸릴 수 있다. 만약 이 시간 동안 동기적으로 코드가 멈춰버리면, 사용자는 그동안 버튼도 못 누르고 스크롤도 안 되는 "먹통" 화면을 보게 된다. 비동기 처리를 사용하면 데이터를 기다리는 동안에도 사용자는 다른 버튼을 누르거나 화면을 스크롤할 수 있다.

Promise 기본 개념

자바스크립트는 비동기 작업의 결과를 다루기 위해 Promise라는 객체를 사용한다. Promise는 "언젠가 끝날 작업의 결과를 담아두는 상자"라고 생각하면 된다.

function App() {
  const handleClick = () => {
    const myPromise = new Promise((resolve, reject) => {
      setTimeout(() => {
        const success = true;
        if (success) {
          resolve("데이터 가져오기 성공!");
        } else {
          reject("데이터 가져오기 실패...");
        }
      }, 2000);
    });

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

  return <button onClick={handleClick}>실행</button>;
}

new Promise(...) 안에서 작업이 성공하면 resolve(값)을 호출하고, 실패하면 reject(값)을 호출한다. .then()은 resolve로 넘어온 값을 받아서 처리하고, .catch()는 reject로 넘어온 값을 받아서 처리한다.

async/await 문법

.then()을 계속 이어 붙이는 방식은 처리할 작업이 많아질수록 코드가 옆으로 길게 늘어져 읽기 어려워진다. async/await는 Promise를 마치 동기 코드처럼 순서대로 읽히게 만들어주는 문법이다.

function App() {
  const getData = () => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve("데이터 가져오기 성공!");
      }, 2000);
    });
  };

  const handleClick = async () => {
    console.log("1. 요청 시작");
    const result = await getData();
    console.log("2. 결과:", result);
  };

  return <button onClick={handleClick}>실행</button>;
}

함수 앞에 async를 붙이면 그 함수 안에서 await를 사용할 수 있다. await getData()는 "getData()가 resolve될 때까지 여기서 기다렸다가, 결과값을 result에 담아라"는 뜻이다. .then()을 쓰지 않아도 마치 동기 코드처럼 위에서 아래로 순서대로 읽을 수 있다는 점이 가장 큰 장점이다.

fetch API로 데이터 요청하기

fetch는 브라우저에 기본으로 내장된 함수로, 서버에 데이터를 요청할 때 사용한다. fetch가 돌려주는 값도 Promise이기 때문에, .then()이나 async/await로 결과를 받을 수 있다.

function App() {
  const handleClick = () => {
    fetch("https://jsonplaceholder.typicode.com/todos/1")
      .then((response) => response.json())
      .then((data) => console.log(data));
  };

  return <button onClick={handleClick}>데이터 요청</button>;
}

fetch(주소)는 요청을 보내고 응답(response)을 돌려주는 Promise다. 응답 자체에는 아직 실제 데이터가 아니라 여러 정보가 섞여 있기 때문에, response.json()을 한 번 더 호출해서 우리가 사용할 수 있는 자바스크립트 객체로 변환해야 한다. 이 과정도 시간이 걸리는 비동기 작업이라 .then()이 한 번 더 필요하다.

같은 코드를 async/await로 바꾸면 다음과 같다.

function App() {
  const handleClick = async () => {
    const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
    const data = await response.json();
    console.log(data);
  };

  return <button onClick={handleClick}>데이터 요청</button>;
}

await가 두 번 쓰인 것을 볼 수 있다. 첫 번째는 서버로부터 응답이 올 때까지, 두 번째는 그 응답을 자바스크립트 객체로 변환할 때까지 기다리는 것이다. 두 코드는 동작이 완전히 같지만, async/await 방식이 순서대로 읽히기 때문에 훨씬 이해하기 쉽다.

Promise 객체를 사용하지 않을 때 생기는 문제

Promise 객체를 반환하지 않는 getData() 함수를 실행해보자. 형태만 보면 2초 뒤에 완료가 될 것 같지만, setTimeout()을 기다리지 않고 함수를 종료하기 때문에 undefined만 출력되고 "완료!"라는 메시지는 무시된다.

const getData = () => {
  let data;
  setTimeout(() => {
    data = "완료!"; // 2초 뒤에 실행됨
  }, 2000);
  return data; // 기다리지 않고 '바로' 실행됨 (결과는 undefined)
};

const handleClick = async () => {
  const result = await getData();
  console.log(result); // 2초 뒤가 아니라 즉시 undefined 출력됨
};

await는 Promise 앞에서만 의미가 있다. Promise를 반환하지 않는 함수 앞에 await를 붙여도, 함수는 그냥 즉시 실행되고 끝나버린다. 진짜로 "기다리게" 하려면 그 함수 자체가 Promise를 반환해야 한다.

실습 정리

실습 1) setTimeout으로 비동기 실행 순서 확인하기 — 버튼을 클릭하면 "1. 시작", "3. 끝"을 순서대로 출력하고, setTimeout으로 2초 뒤에 "2. 2초 후 실행됨"을 출력하는 코드를 작성해서 콘솔 순서를 확인한다.

실습 2) Promise를 async/await로 바꿔보기 — 2초 뒤에 "완료!"를 resolve하는 getData 함수를 만들고, async/await로 결과를 콘솔에 출력한다.

마무리

  • 동기는 "한 줄씩 순서대로, 끝나야 다음 줄", 비동기는 "오래 걸리는 작업은 기다리지 않고 다음 코드를 먼저 실행"하는 방식이다.
  • Promise는 비동기 작업의 결과를 담는 상자이며, 성공은 resolve, 실패는 reject로 표현한다.
  • async/await는 .then() 체인을 동기 코드처럼 순서대로 읽히게 만들어주는 문법일 뿐, Promise 자체를 대체하는 건 아니다.
  • fetch는 응답(response)과 실제 데이터(json) 두 단계 모두 비동기이기 때문에 await가 두 번 필요하다는 걸 기억하자.
  • await는 반드시 Promise를 반환하는 함수 앞에서만 의도한 대로 동작한다.