React 컴포넌트에서 API 연동하기
2025. 4. 1. 00:55ㆍFrameworks/React
React 컴포넌트에서 API 연동하기
1. useState와 useEffect를 사용한 기본 연동
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function PostList() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// 컴포넌트 마운트 시 데이터 가져오기
const fetchPosts = async () => {
try {
setLoading(true);
const response = await axios.get('https://jsonplaceholder.typicode.com/posts');
setPosts(response.data);
setError(null);
} catch (err) {
setError('데이터를 불러오는데 실패했습니다.');
setPosts([]);
} finally {
setLoading(false);
}
};
fetchPosts();
}, []); // 빈 의존성 배열은 컴포넌트 마운트 시 한 번만 실행
if (loading) return <div>로딩 중...</div>;
if (error) return <div>에러: {error}</div>;
return (
<div>
<h1>포스트 목록</h1>
<ul>
{posts.map(post => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</li>
))}
</ul>
</div>
);
}
export default PostList;
2. 사용자 입력에 따른 API 요청
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function UserSearch() {
const [query, setQuery] = useState('');
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// 쿼리가 변경될 때마다 API 요청
useEffect(() => {
// 쿼리가 비어있으면 API 호출 안 함
if (!query) {
setUsers([]);
return;
}
// 디바운스 처리 (타이핑 중 연속적인 API 호출 방지)
const timer = setTimeout(() => {
const fetchUsers = async () => {
try {
setLoading(true);
const response = await axios.get(
`https://api.github.com/search/users?q=${query}`
);
setUsers(response.data.items);
setError(null);
} catch (err) {
setError('사용자를 검색하는데 실패했습니다.');
setUsers([]);
} finally {
setLoading(false);
}
};
fetchUsers();
}, 500); // 500ms 지연
// 클린업 함수: 타이머 취소
return () => clearTimeout(timer);
}, [query]);
return (
<div>
<h1>GitHub 사용자 검색</h1>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="사용자 이름 입력"
/>
{loading && <div>검색 중...</div>}
{error && <div>에러: {error}</div>}
<ul>
{users.map(user => (
<li key={user.id}>
<img src={user.avatar_url} alt={user.login} width="50" />
<a href={user.html_url} target="_blank" rel="noopener noreferrer">
{user.login}
</a>
</li>
))}
</ul>
</div>
);
}
export default UserSearch;
3. CRUD 작업 구현
import React, { useState, useEffect } from 'react';
import axios from 'axios';
function TodoApp() {
const [todos, setTodos] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [newTodoText, setNewTodoText] = useState('');
// 할 일 목록 가져오기
const fetchTodos = async () => {
try {
setLoading(true);
const response = await axios.get('https://jsonplaceholder.typicode.com/todos?_limit=10');
setTodos(response.data);
setError(null);
} catch (err) {
setError('할 일 목록을 불러오는데 실패했습니다.');
} finally {
setLoading(false);
}
};
// 컴포넌트 마운트 시 할 일 목록 가져오기
useEffect(() => {
fetchTodos();
}, []);
// 새 할 일 추가
const addTodo = async (e) => {
e.preventDefault();
if (!newTodoText.trim()) return;
try {
const response = await axios.post('https://jsonplaceholder.typicode.com/todos', {
title: newTodoText,
completed: false,
userId: 1, // 임의의 사용자 ID
});
// 응답으로 받은 새 할 일을 목록에 추가 (ID가 할당된 데이터)
setTodos([...todos, response.data]);
setNewTodoText('');
} catch (err) {
setError('할 일을 추가하는데 실패했습니다.');
}
};
// 할 일 완료 상태 토글
const toggleTodo = async (id) => {
try {
const todoToUpdate = todos.find(todo => todo.id === id);
const updatedTodo = { ...todoToUpdate, completed: !todoToUpdate.completed };
// API 요청으로 상태 업데이트
await axios.put(`https://jsonplaceholder.typicode.com/todos/${id}`, updatedTodo);
// 로컬 상태 업데이트
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
} catch (err) {
setError('할 일 상태를 변경하는데 실패했습니다.');
}
};
// 할 일 삭제
const deleteTodo = async (id) => {
try {
// API 요청으로 삭제
await axios.delete(`https://jsonplaceholder.typicode.com/todos/${id}`);
// 로컬 상태에서 삭제된 할 일 제거
setTodos(todos.filter(todo => todo.id !== id));
} catch (err) {
setError('할 일을 삭제하는데 실패했습니다.');
}
};
if (loading && todos.length === 0) return <div>로딩 중...</div>;
return (
<div>
<h1>할 일 목록</h1>
{error && <div style={{ color: 'red' }}>{error}</div>}
<form onSubmit={addTodo}>
<input
type="text"
value={newTodoText}
onChange={(e) => setNewTodoText(e.target.value)}
placeholder="새 할 일 입력"
/>
<button type="submit">추가</button>
</form>
<ul>
{todos.map(todo => (
<li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
{todo.title}
<button onClick={() => deleteTodo(todo.id)}>삭제</button>
</li>
))}
</ul>
</div>
);
}
export default TodoApp;
커스텀 훅으로 API 로직 추상화
API 호출 로직을 재사용 가능한 커스텀 훅으로 분리하면 코드 중복을 줄이고 관심사를 분리할 수 있습니다.
기본 데이터 가져오기 훅
// hooks/useFetch.js
import { useState, useEffect } from 'react';
import axios from 'axios';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await axios.get(url);
setData(response.data);
setError(null);
} catch (err) {
setError('데이터를 불러오는데 실패했습니다.');
setData(null);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
export default useFetch;
훅 사용 예:
import React from 'react';
import useFetch from '../hooks/useFetch';
function PostList() {
const { data: posts, loading, error } = useFetch('https://jsonplaceholder.typicode.com/posts');
if (loading) return <div>로딩 중...</div>;
if (error) return <div>에러: {error}</div>;
if (!posts) return <div>데이터가 없습니다.</div>;
return (
<div>
<h1>포스트 목록</h1>
<ul>
{posts.map(post => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</li>
))}
</ul>
</div>
);
}
export default PostList;
더 복잡한 API 훅 (CRUD 기능 포함)
// hooks/useAPI.js
import { useState } from 'react';
import axios from 'axios';
function useAPI(baseURL) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// GET 요청
const get = async (endpoint = '') => {
try {
setLoading(true);
const response = await axios.get(`${baseURL}${endpoint}`);
setData(response.data);
setError(null);
return response.data;
} catch (err) {
setError('데이터를 불러오는데 실패했습니다.');
throw err;
} finally {
setLoading(false);
}
};
// POST 요청
const post = async (endpoint = '', payload) => {
try {
setLoading(true);
const response = await axios.post(`${baseURL}${endpoint}`, payload);
setError(null);
return response.data;
} catch (err) {
setError('데이터를 생성하는데 실패했습니다.');
throw err;
} finally {
setLoading(false);
}
};
// PUT 요청
const put = async (endpoint = '', payload) => {
try {
setLoading(true);
const response = await axios.put(`${baseURL}${endpoint}`, payload);
setError(null);
return response.data;
} catch (err) {
setError('데이터를 업데이트하는데 실패했습니다.');
throw err;
} finally {
setLoading(false);
}
};
// DELETE 요청
const del = async (endpoint = '') => {
try {
setLoading(true);
const response = await axios.delete(`${baseURL}${endpoint}`);
setError(null);
return response.data;
} catch (err) {
setError('데이터를 삭제하는데 실패했습니다.');
throw err;
} finally {
setLoading(false);
}
};
return { data, loading, error, get, post, put, del };
}
export default useAPI;
훅 사용 예:
import React, { useState, useEffect } from 'react';
import useAPI from '../hooks/useAPI';
function TodoApp() {
const [todos, setTodos] = useState([]);
const [newTodoText, setNewTodoText] = useState('');
const api = useAPI('https://jsonplaceholder.typicode.com');
// 할 일 목록 가져오기
useEffect(() => {
const fetchTodos = async () => {
try {
const data = await api.get('/todos?_limit=10');
setTodos(data);
} catch (error) {
console.error('할 일 목록을 불러오는데 실패했습니다:', error);
}
};
fetchTodos();
}, []);
// 새 할 일 추가
const addTodo = async (e) => {
e.preventDefault();
if (!newTodoText.trim()) return;
try {
const newTodo = await api.post('/todos', {
title: newTodoText,
completed: false,
userId: 1
});
setTodos([...todos, newTodo]);
setNewTodoText('');
} catch (error) {
console.error('할 일을 추가하는데 실패했습니다:', error);
}
};
// 할 일 완료 상태 토글
const toggleTodo = async (id) => {
try {
const todoToUpdate = todos.find(todo => todo.id === id);
const updatedTodo = { ...todoToUpdate, completed: !todoToUpdate.completed };
await api.put(`/todos/${id}`, updatedTodo);
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
} catch (error) {
console.error('할 일 상태를 변경하는데 실패했습니다:', error);
}
};
// 할 일 삭제
const deleteTodo = async (id) => {
try {
await api.del(`/todos/${id}`);
setTodos(todos.filter(todo => todo.id !== id));
} catch (error) {
console.error('할 일을 삭제하는데 실패했습니다:', error);
}
};
return (
<div>
<h1>할 일 목록</h1>
{api.error && <div style={{ color: 'red' }}>{api.error}</div>}
{api.loading && <div>로딩 중...</div>}
<form onSubmit={addTodo}>
<input
type="text"
value={newTodoText}
onChange={(e) => setNewTodoText(e.target.value)}
placeholder="새 할 일 입력"
/>
<button type="submit" disabled={api.loading}>추가</button>
</form>
<ul>
{todos.map(todo => (
<li key={todo.id} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
disabled={api.loading}
/>
{todo.title}
<button onClick={() => deleteTodo(todo.id)} disabled={api.loading}>삭제</button>
</li>
))}
</ul>
</div>
);
}
export default TodoApp;
'Frameworks > React' 카테고리의 다른 글
에러 처리와 로딩 상태 관리 (0) | 2025.04.01 |
---|---|
고급 데이터 가져오기 라이브러리 (0) | 2025.04.01 |
React와 API 연동하기 (0) | 2025.04.01 |
Context API vs Redux (0) | 2025.04.01 |
Redux Toolkit (0) | 2025.04.01 |