fetch와 async/await로 실제 API 데이터를 가져오고, 로딩·에러·성공 상태를 처리하는 커스텀 훅 useFetch를 직접 만들어봅니다.
🌐 외부 데이터 패칭의 3가지 상태
API에서 데이터를 가져올 때는 항상 세 가지 상태를 처리해야 합니다: 로딩 중 / 성공 / 실패. 이 세 상태를 올바르게 처리하는 것이 좋은 UX의 기본입니다.
실제 API(JSONPlaceholder)에서 사용자 목록을 가져오는 예제로 시작합니다:
interface User {
id: number
name: string
email: string
phone: string
}
function UserList() {
const [users, setUsers] = useState<User[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
async function fetchUsers() {
try {
setLoading(true)
const res = await fetch('https://jsonplaceholder.typicode.com/users')
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data: User[] = await res.json()
setUsers(data)
} catch (e) {
setError(e instanceof Error ? e.message : '알 수 없는 오류')
} finally {
setLoading(false)
}
}
fetchUsers()
}, [])
if (loading) return <div className="text-center p-8">불러오는 중...</div>
if (error) return <div className="text-red-500 p-8">오류: {error}</div>
return (
<ul className="divide-y">
{users.map((user) => (
<li key={user.id} className="py-3">
<p className="font-bold">{user.name}</p>
<p className="text-sm text-gray-500">{user.email}</p>
</li>
))}
</ul>
)
}
useEffect 안에서 async/await를 사용할 때는 이펙트 함수 자체를 async로 만들 수 없습니다. 내부에 async 함수를 선언하고 즉시 호출하는 패턴을 사용합니다.
♻️ 커스텀 훅 useFetch — 로직 재사용
위의 로딩/에러/데이터 패턴은 여러 컴포넌트에서 반복됩니다. 이를 커스텀 훅으로 추출하면 재사용할 수 있습니다. 커스텀 훅은 이름이 use로 시작하는 함수입니다.
// hooks/useFetch.ts
import { useState, useEffect } from 'react'
interface FetchState<T> {
data: T | null
loading: boolean
error: string | null
}
export function useFetch<T>(url: string): FetchState<T> {
const [state, setState] = useState<FetchState<T>>({
data: null,
loading: true,
error: null,
})
useEffect(() => {
let cancelled = false // 컴포넌트 언마운트 시 상태 업데이트 방지
async function fetchData() {
setState((prev) => ({ ...prev, loading: true, error: null }))
try {
const res = await fetch(url)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data: T = await res.json()
if (!cancelled) setState({ data, loading: false, error: null })
} catch (e) {
if (!cancelled) setState({
data: null,
loading: false,
error: e instanceof Error ? e.message : '오류 발생',
})
}
}
fetchData()
return () => { cancelled = true } // 클린업: 언마운트 시 응답 무시
}, [url])
return state
}
이제 어느 컴포넌트에서든 한 줄로 데이터를 가져올 수 있습니다:
function UserList() {
const { data: users, loading, error } = useFetch<User[]>(
'https://jsonplaceholder.typicode.com/users'
)
if (loading) return <div>로딩 중...</div>
if (error) return <div>오류: {error}</div>
if (!users) return null
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name} — {user.email}</li>
))}
</ul>
)
}
🔁 URL이 바뀌면 자동 재패치
function PostViewer() {
const [selectedId, setSelectedId] = useState(1)
const { data: post, loading } = useFetch<Post>(
`https://jsonplaceholder.typicode.com/posts/${selectedId}`
)
return (
<div>
<div className="flex gap-2 mb-4">
{[1, 2, 3, 4, 5].map((id) => (
<button
key={id}
onClick={() => setSelectedId(id)}
className={selectedId === id ? 'text-indigo-600 font-bold' : ''}
>
{id}번
</button>
))}
</div>
{loading ? <div>로딩...</div> : <p>{post?.title}</p>}
</div>
)
}
버튼을 클릭하면 URL이 바뀌고, useFetch가 새 URL을 감지해 자동으로 재패치합니다. 이 패턴이 커스텀 훅의 핵심 가치입니다.
⚠️ fetch의 한계 — 나중에 TanStack Query로
직접 만든 useFetch는 학습 목적으로는 훌륭하지만 실무에서는 한계가 있습니다:
- 캐싱이 없어 동일 URL을 여러 컴포넌트가 중복 요청합니다.
- 재시도 로직이 없습니다.
- 낙관적 업데이트(optimistic update)가 어렵습니다.
실무에서는 TanStack Query(React Query)를 사용합니다. 뒤 강의에서 다룹니다.
✅ 정리
- API 패칭 시 항상 로딩/에러/성공 세 상태를 관리합니다.
useEffect안 async: 이펙트 함수 내부에 async 함수 선언 후 호출.- 클린업에서
cancelled플래그로 언마운트 후 상태 업데이트를 방지합니다. useFetch커스텀 훅으로 패칭 로직을 한 곳에 캡슐화해 재사용합니다.
관련 주제
- fetch API
- async await
- useEffect 데이터 요청
- 로딩 에러 상태 처리
- 개발·프로그래밍
- 개발·프로그래밍 강의
- 리액트 입문 — 컴포넌트로 만드는 웹
- 무료강의
- 무료 온라인 강의
- NUGUNA
- 누구나
댓글
0/1000
불러오는 중...
