React 컴포넌트에서 외부 API와 통신하여 데이터를 가져오고, 생성하고, 수정하고, 삭제할 수 있습니다. API 호출은 useEffect Hook과 함께 사용합니다.
- 컴포넌트가 마운트될 때 (화면에 처음 나타날 때)
- 특정 상태가 변경될 때
- 사용자 액션(버튼 클릭 등)에 반응할 때
import { useState, useEffect } from "react";
interface User {
id: number;
name: string;
email: string;
}
function UserList() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// API 호출 함수
const fetchUsers = async () => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
if (!response.ok) {
throw new Error("데이터를 불러오는데 실패했습니다.");
}
const data = await response.json();
setUsers(data);
} catch (err) {
setError(err instanceof Error ? err.message : "알 수 없는 오류");
} finally {
setLoading(false);
}
};
fetchUsers();
}, []); // 빈 배열: 컴포넌트 마운트 시 1번만 실행
if (loading) return <div>로딩 중...</div>;
if (error) return <div>에러: {error}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
);
}function DataFetchingPattern() {
const [data, setData] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true); // 로딩 시작
setError(null); // 이전 에러 초기화
const response = await fetch("https://api.example.com/data");
if (!response.ok) throw new Error("Failed to fetch");
const result = await response.json();
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error");
} finally {
setLoading(false); // 로딩 종료 (성공/실패 무관)
}
};
fetchData();
}, []);
// 로딩 UI
if (loading) {
return <div className="spinner">로딩 중...</div>;
}
// 에러 UI
if (error) {
return (
<div className="error">
<p>에러가 발생했습니다: {error}</p>
<button onClick={() => window.location.reload()}>다시 시도</button>
</div>
);
}
// 데이터 UI
return (
<div>
{data.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}interface Post {
id?: number;
title: string;
body: string;
userId: number;
}
function CreatePost() {
const [title, setTitle] = useState("");
const [body, setBody] = useState("");
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<Post | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
setLoading(true);
const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
title,
body,
userId: 1
})
});
if (!response.ok) throw new Error("Failed to create post");
const data = await response.json();
setResult(data);
setTitle("");
setBody("");
} catch (error) {
console.error("Error:", error);
} finally {
setLoading(false);
}
};
return (
<div>
<h2>새 게시글 작성</h2>
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="제목"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
<textarea
placeholder="내용"
value={body}
onChange={(e) => setBody(e.target.value)}
required
/>
<button type="submit" disabled={loading}>
{loading ? "작성 중..." : "작성"}
</button>
</form>
{result && (
<div className="success">
<h3>작성 완료!</h3>
<p>제목: {result.title}</p>
<p>ID: {result.id}</p>
</div>
)}
</div>
);
}function UpdatePost() {
const [postId, setPostId] = useState(1);
const [title, setTitle] = useState("");
const [loading, setLoading] = useState(false);
// 게시글 불러오기
useEffect(() => {
const fetchPost = async () => {
try {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts/${postId}`
);
const data = await response.json();
setTitle(data.title);
} catch (error) {
console.error("Error:", error);
}
};
fetchPost();
}, [postId]);
// 게시글 수정
const handleUpdate = async () => {
try {
setLoading(true);
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts/${postId}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
id: postId,
title,
body: "Updated body",
userId: 1
})
}
);
if (!response.ok) throw new Error("Failed to update");
const data = await response.json();
console.log("수정 완료:", data);
alert("게시글이 수정되었습니다!");
} catch (error) {
console.error("Error:", error);
} finally {
setLoading(false);
}
};
return (
<div>
<h2>게시글 수정</h2>
<input
type="number"
value={postId}
onChange={(e) => setPostId(Number(e.target.value))}
min="1"
/>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<button onClick={handleUpdate} disabled={loading}>
{loading ? "수정 중..." : "수정"}
</button>
</div>
);
}function DeletePost() {
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
// 게시글 목록 불러오기
const fetchPosts = async () => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
const data = await response.json();
setPosts(data.slice(0, 5)); // 처음 5개만
} catch (error) {
console.error("Error:", error);
}
};
fetchPosts();
}, []);
const handleDelete = async (id: number) => {
if (!confirm("정말 삭제하시겠습니까?")) return;
try {
setLoading(true);
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts/${id}`,
{
method: "DELETE"
}
);
if (!response.ok) throw new Error("Failed to delete");
// UI에서 제거
setPosts(posts.filter(post => post.id !== id));
alert("삭제되었습니다!");
} catch (error) {
console.error("Error:", error);
} finally {
setLoading(false);
}
};
return (
<div>
<h2>게시글 목록</h2>
{posts.map(post => (
<div key={post.id} style={{ border: "1px solid #ccc", padding: "10px", margin: "10px" }}>
<h3>{post.title}</h3>
<button
onClick={() => handleDelete(post.id!)}
disabled={loading}
>
삭제
</button>
</div>
))}
</div>
);
}axios는 fetch보다 더 간편한 API 통신 라이브러리입니다.
npm install axiosimport axios from "axios";
function AxiosExample() {
const [users, setUsers] = useState<User[]>([]);
useEffect(() => {
// fetch 방식
const fetchWithFetch = async () => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
if (!response.ok) throw new Error("Failed");
const data = await response.json(); // JSON 파싱 필요
setUsers(data);
} catch (error) {
console.error(error);
}
};
// axios 방식 (더 간결!)
const fetchWithAxios = async () => {
try {
const response = await axios.get("https://jsonplaceholder.typicode.com/users");
setUsers(response.data); // 자동으로 JSON 파싱
} catch (error) {
console.error(error);
}
};
fetchWithAxios();
}, []);
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}import axios from "axios";
const API_BASE = "https://jsonplaceholder.typicode.com";
// GET
const getUsers = async () => {
const response = await axios.get(`${API_BASE}/users`);
return response.data;
};
// POST
const createPost = async (post: { title: string; body: string; userId: number }) => {
const response = await axios.post(`${API_BASE}/posts`, post);
return response.data;
};
// PUT
const updatePost = async (id: number, post: any) => {
const response = await axios.put(`${API_BASE}/posts/${id}`, post);
return response.data;
};
// DELETE
const deletePost = async (id: number) => {
await axios.delete(`${API_BASE}/posts/${id}`);
};
// 사용 예제
function AxiosCRUD() {
const [posts, setPosts] = useState<Post[]>([]);
useEffect(() => {
const fetchPosts = async () => {
try {
const response = await axios.get(`${API_BASE}/posts`);
setPosts(response.data.slice(0, 5));
} catch (error) {
console.error("Error:", error);
}
};
fetchPosts();
}, []);
const handleCreate = async () => {
try {
const newPost = await createPost({
title: "새 게시글",
body: "내용",
userId: 1
});
setPosts([newPost, ...posts]);
} catch (error) {
console.error("Error:", error);
}
};
const handleDelete = async (id: number) => {
try {
await deletePost(id);
setPosts(posts.filter(post => post.id !== id));
} catch (error) {
console.error("Error:", error);
}
};
return (
<div>
<button onClick={handleCreate}>새 게시글 추가</button>
{posts.map(post => (
<div key={post.id}>
<h3>{post.title}</h3>
<button onClick={() => handleDelete(post.id!)}>삭제</button>
</div>
))}
</div>
);
}반복되는 API 로직을 커스텀 Hook으로 만들어 재사용할 수 있습니다.
import { useState, useEffect } from "react";
interface UseFetchResult<T> {
data: T | null;
loading: boolean;
error: string | null;
refetch: () => void;
}
function useFetch<T>(url: string): UseFetchResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [trigger, setTrigger] = useState(0);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url);
if (!response.ok) throw new Error("Failed to fetch");
const result = await response.json();
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error");
} finally {
setLoading(false);
}
};
fetchData();
}, [url, trigger]);
const refetch = () => setTrigger(prev => prev + 1);
return { data, loading, error, refetch };
}
// 사용 예제
function UserList() {
const { data: users, loading, error, refetch } = useFetch<User[]>(
"https://jsonplaceholder.typicode.com/users"
);
if (loading) return <div>로딩 중...</div>;
if (error) return <div>에러: {error}</div>;
if (!users) return <div>데이터 없음</div>;
return (
<div>
<button onClick={refetch}>새로고침</button>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
);
}interface UseAsyncResult<T> {
execute: (...args: any[]) => Promise<void>;
data: T | null;
loading: boolean;
error: string | null;
}
function useAsync<T>(asyncFunction: (...args: any[]) => Promise<T>): UseAsyncResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const execute = async (...args: any[]) => {
try {
setLoading(true);
setError(null);
const result = await asyncFunction(...args);
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error");
} finally {
setLoading(false);
}
};
return { execute, data, loading, error };
}
// 사용 예제
function CreatePostForm() {
const createPost = async (title: string, body: string) => {
const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, body, userId: 1 })
});
return response.json();
};
const { execute, data, loading, error } = useAsync(createPost);
const [title, setTitle] = useState("");
const [body, setBody] = useState("");
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
execute(title, body);
};
return (
<form onSubmit={handleSubmit}>
<input value={title} onChange={(e) => setTitle(e.target.value)} />
<textarea value={body} onChange={(e) => setBody(e.target.value)} />
<button type="submit" disabled={loading}>
{loading ? "작성 중..." : "작성"}
</button>
{error && <p className="error">{error}</p>}
{data && <p className="success">작성 완료! ID: {data.id}</p>}
</form>
);
}interface SearchResult {
id: number;
title: string;
body: string;
}
function SearchPosts() {
const [query, setQuery] = useState("");
const [results, setResults] = useState<SearchResult[]>([]);
const [loading, setLoading] = useState(false);
// 디바운싱: 입력 후 500ms 후에 검색
useEffect(() => {
if (!query.trim()) {
setResults([]);
return;
}
const timeoutId = setTimeout(async () => {
try {
setLoading(true);
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
const posts = await response.json();
// 클라이언트 사이드 필터링
const filtered = posts.filter((post: SearchResult) =>
post.title.toLowerCase().includes(query.toLowerCase())
);
setResults(filtered);
} catch (error) {
console.error("Error:", error);
} finally {
setLoading(false);
}
}, 500); // 500ms 디바운싱
return () => clearTimeout(timeoutId); // 클린업
}, [query]);
return (
<div>
<h2>게시글 검색</h2>
<input
type="text"
placeholder="검색어를 입력하세요..."
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
{loading && <p>검색 중...</p>}
{!loading && results.length > 0 && (
<div>
<p>{results.length}개의 결과</p>
<ul>
{results.map(post => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</li>
))}
</ul>
</div>
)}
{!loading && query && results.length === 0 && (
<p>검색 결과가 없습니다.</p>
)}
</div>
);
}function PaginatedPosts() {
const [posts, setPosts] = useState<Post[]>([]);
const [currentPage, setCurrentPage] = useState(1);
const [loading, setLoading] = useState(false);
const postsPerPage = 10;
useEffect(() => {
const fetchPosts = async () => {
try {
setLoading(true);
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts?_page=${currentPage}&_limit=${postsPerPage}`
);
const data = await response.json();
setPosts(data);
} catch (error) {
console.error("Error:", error);
} finally {
setLoading(false);
}
};
fetchPosts();
}, [currentPage]);
const handleNext = () => setCurrentPage(prev => prev + 1);
const handlePrev = () => setCurrentPage(prev => Math.max(1, prev - 1));
return (
<div>
<h2>게시글 목록</h2>
{loading ? (
<p>로딩 중...</p>
) : (
<>
<ul>
{posts.map(post => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</li>
))}
</ul>
<div className="pagination">
<button onClick={handlePrev} disabled={currentPage === 1}>
이전
</button>
<span>페이지 {currentPage}</span>
<button onClick={handleNext}>다음</button>
</div>
</>
)}
</div>
);
}// 에러 타입 정의
interface ApiError {
message: string;
status?: number;
}
// 에러 처리 유틸리티
const handleApiError = (error: unknown): ApiError => {
if (error instanceof Error) {
return { message: error.message };
}
return { message: "알 수 없는 오류가 발생했습니다." };
};
// 재시도 로직
async function fetchWithRetry<T>(
url: string,
options?: RequestInit,
maxRetries = 3
): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, options);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
}
}
throw new Error("Max retries reached");
}
// 사용 예제
function RobustDataFetching() {
const [data, setData] = useState<any[]>([]);
const [error, setError] = useState<ApiError | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const result = await fetchWithRetry(
"https://jsonplaceholder.typicode.com/users"
);
setData(result);
} catch (err) {
setError(handleApiError(err));
} finally {
setLoading(false);
}
};
fetchData();
}, []);
if (loading) return <div>로딩 중...</div>;
if (error) {
return (
<div className="error-container">
<h2>오류가 발생했습니다</h2>
<p>{error.message}</p>
{error.status && <p>상태 코드: {error.status}</p>}
<button onClick={() => window.location.reload()}>
다시 시도
</button>
</div>
);
}
return (
<div>
{data.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}다음 컴포넌트들을 직접 만들어보세요:
// TODO: JSONPlaceholder API를 사용하여 사용자 프로필 표시
// - 사용자 ID를 입력받아 해당 사용자 정보 표시
// - 로딩, 에러 상태 처리
// - 사용자의 게시글도 함께 표시// TODO: 댓글 시스템 구현
// - 댓글 목록 표시 (GET)
// - 새 댓글 작성 (POST)
// - 댓글 삭제 (DELETE)
// - 로딩 스피너 추가// TODO: 실시간 검색 기능
// - 입력 시마다 API 호출 (디바운싱 적용)
// - 검색 결과를 카드 형태로 표시
// - 검색 중 상태 표시// TODO: 무한 스크롤 구현
// - 스크롤이 바닥에 도달하면 다음 페이지 로드
// - IntersectionObserver 사용
// - 로딩 인디케이터 표시다음 섹션에서는 지금까지 배운 모든 내용을 활용하여 완전한 Todo 앱을 만들어봅니다!