화면에 보이는 목록·글·상품은 대부분 서버(API)에서 가져온 데이터예요. 그런데 이 요청은 시간이 걸리는 비동기 작업이라, "기다리는 동안"과 "실패했을 때"까지 챙겨야 합니다. 이번 마지막 장에서 그 기본 패턴을 단계적으로 익혀봐요.
async/await·fetch·Promise·JSON·try/catch — 데이터를 가져오고 성공·실패를 다루는 문법 자체. 🟪 리액트: useEffect로 "언제 요청할지"를 정하고, useState로 로딩·에러·데이터 상태를 만들어 화면에 반영하는 것. 데이터를 가져오는 fetch·Promise는 순수 JS, 그걸 언제 실행하고 화면 상태로 만드는 게 리액트라고 보면 대체로 맞아요.데이터 페칭(Data Fetching)은 서버(API)에서 데이터를 가져와 화면에 보여주는 일이에요. 상품 목록, 게시글, 날씨, 사용자 정보 — 화면에 보이는 대부분의 진짜 데이터는 우리 코드 안에 하드코딩된 게 아니라, 서버에 요청해서 받아온 것입니다.
데이터 요청은 렌더링 중에 하면 안 돼요. 화면이 그려질 때마다 요청이 나가버리니까요. 그래서 화면이 처음 나타난 뒤 딱 한 번 요청하도록, useEffect 안에서 요청을 보냅니다. 받은 결과는 state에 저장하고요.
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이라는 글자 형식({ "name": "홍길동" }처럼 생긴 것)이라, res.json()으로 자바스크립트 객체로 바꿔서 씁니다..then()을 계속 잇는 것도 되지만, async/await를 쓰면 코드가 동기 코드처럼 위에서 아래로 읽혀서 더 편해요. 에러는 try/catch로, 성공이든 실패든 마지막에 로딩을 끄는 건 finally로 처리합니다.
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();
}, []);
이제 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>
);
}
id 값이 바뀔 때마다 다시 요청해요. 상세 페이지에서 보는 항목이 바뀌면 그에 맞는 데이터를 새로 가져와야 하니까요.
// 상세 페이지: id가 바뀌면 그 id의 데이터를 다시 요청
useEffect(() => {
fetch('https://api.example.com/users/' + id)
.then((res) => res.json())
.then(setData);
}, [id]); // ← id가 바뀔 때마다 재요청
AbortController로 요청을 취소하거나 "정리(cleanup)" 로직으로 이런 상황을 막습니다. 지금은 "이런 주의점이 있다" 정도만 기억해두세요.AbortController는 진행 중인 fetch 요청을 중간에 취소(abort)할 수 있게 해주는 브라우저 내장 도구예요. 레이스 컨디션은 요청 여러 개가 겹쳐 나갈 때 도착 순서가 보낸 순서와 달라져, 먼저 보낸 느린 응답이 나중에 도착해 최신 결과를 덮어써 화면에 엉뚱한 값이 남는 문제예요. (검색어가 빠르게 바뀔 때 자주 생겨요.) 그래서 이전 요청을 취소하거나 "무시 플래그"로 막아요. (문제풀이에서 다시 나와요.)지금 배운 useEffect + 3가지 상태는 데이터 페칭의 기본기예요. 하지만 실무에선 React Query(TanStack Query)나 SWR 같은 라이브러리를 많이 씁니다. 이들은 로딩·에러 상태, 캐싱, 재요청, 중복 요청 제거 같은 걸 알아서 처리해줘서 코드가 훨씬 간결해져요. 원리를 알면 이런 도구도 금방 익힐 수 있어요.