데이터 페칭이란 기본 패턴 async/await 3가지 화면 정리·주의 📝 문제풀이
◀ 이전 🎉 완주 · 목차
🌊 CHAPTER 14 · 고급

서버에서 데이터 가져오기 — 비동기의 세계

화면에 보이는 목록·글·상품은 대부분 서버(API)에서 가져온 데이터예요. 그런데 이 요청은 시간이 걸리는 비동기 작업이라, "기다리는 동안"과 "실패했을 때"까지 챙겨야 합니다. 이번 마지막 장에서 그 기본 패턴을 단계적으로 익혀봐요.

🎯 이 장을 끝내면
🧩
이 장에서 뭐가 JS고 뭐가 리액트? 🟦 순수 JS: async/await·fetch·Promise·JSON·try/catch — 데이터를 가져오고 성공·실패를 다루는 문법 자체. 🟪 리액트: useEffect로 "언제 요청할지"를 정하고, useState로딩·에러·데이터 상태를 만들어 화면에 반영하는 것. 데이터를 가져오는 fetch·Promise는 순수 JS, 그걸 언제 실행하고 화면 상태로 만드는 게 리액트라고 보면 대체로 맞아요.
📡
데이터 페칭이란
서버에서 데이터를 가져온다는 것
지금까지의 데이터는 우리가 직접 만들었지만, 실무는 대부분 서버에서 받아와요.

데이터 페칭(Data Fetching)서버(API)에서 데이터를 가져와 화면에 보여주는 일이에요. 상품 목록, 게시글, 날씨, 사용자 정보 — 화면에 보이는 대부분의 진짜 데이터는 우리 코드 안에 하드코딩된 게 아니라, 서버에 요청해서 받아온 것입니다.

⏳ 왜 "비동기(asynchronous)"가 중요할까🟦 순수 JS 서버에 요청을 보내면 답이 즉시 오지 않아요. 네트워크를 타고 갔다가 오는 데 몇 백 밀리초에서 몇 초가 걸릴 수 있어요. 이렇게 "보내놓고 결과를 나중에 받는" 작업을 비동기라고 해요. 자바스크립트는 이 시간 동안 멈추지 않고 다른 일을 계속하다가, 답이 오면 그때 처리합니다. 그래서 데이터 페칭에는 항상 "기다리는 중"과 "혹시 실패했을 때"라는 상황이 따라와요. 이 둘을 화면에서 처리하는 게 이번 장의 핵심이에요.
🧍
동기 (synchronous)
주문하고 음식이 나올 때까지 카운터에서 그대로 기다려요. 그 사이엔 아무것도 못 해요.
🔔
비동기 (asynchronous)
진동벨을 받고 자리에서 다른 일을 해요. 음식이 다 되면 벨이 울려 그때 받으러 가요.
데이터 페칭 API 비동기 fetch useEffect 로딩 에러 async/await
🧭
큰 그림. "요청을 언제·어디서 보내고(useEffect), 받은 걸 어디에 담고(state), 기다리는 동안·실패했을 때 무엇을 보여줄지(로딩·에러)" — 이 흐름만 잡으면 데이터 페칭은 어렵지 않아요.
🪝
기본 패턴
useEffect 안에서 요청하고, state에 저장
데이터 페칭의 가장 기본이 되는 뼈대예요.

데이터 요청은 렌더링 중에 하면 안 돼요. 화면이 그려질 때마다 요청이 나가버리니까요. 그래서 화면이 처음 나타난 뒤 딱 한 번 요청하도록, useEffect 안에서 요청을 보냅니다. 받은 결과는 state에 저장하고요.

🔤
렌더링 — React가 컴포넌트 함수를 실행해서 화면(UI)을 그려내는 일이에요. state가 바뀌면 React는 그 컴포넌트를 다시 렌더링해요. "렌더링 중"은 이 그리는 과정이 진행되는 순간을 말해요.
📦 함께 관리하는 3가지 상태🟪 리액트 데이터 하나만으로는 부족해요. 요청은 시간이 걸리고 실패할 수도 있으니, 세 가지 상태를 함께 둡니다.
① 데이터(data) — 성공하면 받은 값을 담아요.
② 로딩(loading) — 요청 중인지 여부(기다리는 중 화면용).
③ 에러(error) — 실패하면 그 내용을 담아요(실패 화면용).
import { useState, useEffect } from 'react';

function UserList() {
  const [data, setData] = useState(null);      // ① 데이터
  const [loading, setLoading] = useState(true); // ② 로딩
  const [error, setError] = useState(null);     // ③ 에러

  useEffect(() => {
    // 화면이 처음 뜬 뒤 한 번만 요청
    fetch('https://api.example.com/users')
      .then((res) => res.json())   // 응답을 JSON으로 변환
      .then((json) => setData(json)) // 데이터 state에 저장
      .catch((err) => setError(err)) // 실패하면 에러 저장
      .finally(() => setLoading(false)); // 성공이든 실패든 로딩 끝
  }, []); // 빈 배열 = 최초 1회만

  // ... 아래에서 loading / error / data 로 화면을 나눔
}
🔤
fetch · API · JSON — fetch는 브라우저가 서버에 요청을 보내는 내장 함수예요. 요청을 받는 서버 쪽 창구가 API고요. 서버가 돌려주는 데이터는 보통 JSON이라는 글자 형식({ "name": "홍길동" }처럼 생긴 것)이라, res.json()으로 자바스크립트 객체로 바꿔서 씁니다.
💡
핵심 한 줄. "데이터 페칭은 useEffect 안에서 하고, 데이터·로딩·에러 세 상태로 관리한다." 이 문장이 이번 장의 뼈대예요.
async/await
더 읽기 쉬운 async/await 버전
.then 체인 대신 위에서 아래로 읽히는 방식이에요.

.then()을 계속 잇는 것도 되지만, async/await를 쓰면 코드가 동기 코드처럼 위에서 아래로 읽혀서 더 편해요. 에러는 try/catch로, 성공이든 실패든 마지막에 로딩을 끄는 건 finally로 처리합니다.

🔤
프로미스(Promise) · await — fetch는 결과를 바로 주지 않고, "곧 결과가 올 거라는 약속표"인 프로미스를 돌려줘요. 그 앞에 await를 붙이면 "이 약속의 결과가 올 때까지 기다렸다가 값을 받아라"라는 뜻이에요. 그래서 아래처럼 위에서 아래로 자연스럽게 읽혀요.
useEffect(() => {
  async function loadUsers() {
    try {
      setLoading(true);            // 요청 시작 → 로딩 켜기
      const res = await fetch('https://api.example.com/users');
      if (!res.ok) throw new Error('요청 실패: ' + res.status);
      const json = await res.json();
      setData(json);               // 성공 → 데이터 저장
    } catch (err) {
      setError(err);               // 실패 → 에러 저장
    } finally {
      setLoading(false);           // 끝나면 항상 로딩 끄기
    }
  }
  loadUsers();
}, []);
🧯 try / catch / finally 역할🟦 순수 JS try — 요청 같은 "실패할 수 있는" 코드를 담아요.
catch — 그 안에서 에러가 나면 여기로 와서 에러 상태를 채워요.
finally — 성공하든 실패하든 무조건 실행돼요. 그래서 로딩 끄기에 딱 좋아요. useEffect 콜백 자체는 async로 만들 수 없어서, 안에 async 함수를 따로 정의하고 바로 호출하는 형태를 자주 써요.
🖼️
3가지 화면
로딩 · 에러 · 성공을 조건부로 렌더링
세 상태를 나눠서 각각 다른 화면을 보여줘요.

이제 loading, error, data 세 상태에 따라 서로 다른 화면을 보여주면 돼요. 흔히 이른 반환(early return)으로 위에서부터 순서대로 걸러냅니다.

function UserList() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => { /* ...위에서 본 요청 코드... */ }, []);

  if (loading) return <p>⏳ 불러오는 중...</p>;   // ① 로딩 화면
  if (error) return <p>⚠️ 에러: {error.message}</p>; // ② 에러 화면

  // ③ 성공 화면
  return (
    <ul>
      {data.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}
로딩 중
loading === true
"불러오는 중..."
⚠️
에러
error 있음
"에러 메시지"
성공
data 있음
목록 렌더링
🤔
왜 세 상태가 필요할까? 데이터만 있으면 "아직 안 온 건지, 실패한 건지, 진짜 비어 있는 건지" 구분할 수 없어요. 로딩이 있어야 "기다려" 화면을, 에러가 있어야 "실패" 화면을 보여줄 수 있죠. 이 셋이 사용자 경험을 완성합니다.
🧹
정리·주의
의존성 배열과 실무 이야기
언제 다시 요청하는지, 실무에선 무엇을 쓰는지 짚어요.
📌 의존성 배열이 "언제 요청할지"를 정해요🟪 리액트 [] (빈 배열) — 화면이 처음 나타날 때 딱 한 번만 요청해요. 목록을 처음 불러올 때 흔한 형태예요.
[id]id 값이 바뀔 때마다 다시 요청해요. 상세 페이지에서 보는 항목이 바뀌면 그에 맞는 데이터를 새로 가져와야 하니까요.
// 상세 페이지: id가 바뀌면 그 id의 데이터를 다시 요청
useEffect(() => {
  fetch('https://api.example.com/users/' + id)
    .then((res) => res.json())
    .then(setData);
}, [id]); // ← id가 바뀔 때마다 재요청
⚠️
언마운트 주의. 요청이 끝나기 전에 컴포넌트가 사라지면(언마운트), 없어진 컴포넌트의 state를 바꾸려다 문제가 생길 수 있어요. 실무에선 AbortController로 요청을 취소하거나 "정리(cleanup)" 로직으로 이런 상황을 막습니다. 지금은 "이런 주의점이 있다" 정도만 기억해두세요.
🔤
AbortController · 레이스 컨디션(race condition) — AbortController는 진행 중인 fetch 요청을 중간에 취소(abort)할 수 있게 해주는 브라우저 내장 도구예요. 레이스 컨디션은 요청 여러 개가 겹쳐 나갈 때 도착 순서가 보낸 순서와 달라져, 먼저 보낸 느린 응답이 나중에 도착해 최신 결과를 덮어써 화면에 엉뚱한 값이 남는 문제예요. (검색어가 빠르게 바뀔 때 자주 생겨요.) 그래서 이전 요청을 취소하거나 "무시 플래그"로 막아요. (문제풀이에서 다시 나와요.)
🚀 실무에선 라이브러리를 많이 써요

지금 배운 useEffect + 3가지 상태는 데이터 페칭의 기본기예요. 하지만 실무에선 React Query(TanStack Query)SWR 같은 라이브러리를 많이 씁니다. 이들은 로딩·에러 상태, 캐싱, 재요청, 중복 요청 제거 같은 걸 알아서 처리해줘서 코드가 훨씬 간결해져요. 원리를 알면 이런 도구도 금방 익힐 수 있어요.

🧠 이 장 핵심 요약
🎉 축하해요 — 리액트 코스 완주! 개요와 역사부터 JSX, 컴포넌트, state, props, 이벤트, 훅, 라우팅, 그리고 이번 데이터 페칭까지 — 리액트의 큰 그림을 전부 한 바퀴 돌았어요. 여기까지 온 것만으로 이미 "화면을 상태로 그리는" 리액트의 사고방식이 몸에 붙기 시작했을 거예요. 이제 작은 프로젝트를 직접 만들어보며 배운 걸 이어붙여 보세요. 막히면 언제든 로드맵으로 돌아와 복습하면 됩니다. 정말 수고 많았어요! 👏
📝
문제풀이 · 점검
배운 걸 가볍게 점검해봐요
시험이 아니라 "내가 이해했나" 확인용이에요. 틀려도 바로 해설이 나와요.
🧪
문제를 풀면 즉시 정답과 해설이 나오고, 위쪽 바에 점수가 쌓여요. 네 가지 유형(객관식 · O/X · 빈칸 · 개념결과)을 섞어 두었어요.
🎉 완주 🗺️ 전체 로드맵으로 돌아가 배운 걸 한눈에 정리하기