데이터 연동 페이지와 검증
데이터 연동 페이지와 라우터를 조합하고 CRUD 실패·지연·재시도를 검증합니다.
데이터 연동 페이지 구성
게시판 CRUD 페이지는 목록 조회, 상세 조회, 추가, 삭제가 같은 요청 계층을 지나 화면 상태로 돌아오는 구조로 읽습니다.
아래 다이어그램은 각 화면이 어떤 상태 경계를 거쳐 사용자 반응으로 이어지는지 정리합니다.
PostsPage.jsx
게시물 목록과 게시물 추가 폼을 함께 표시합니다.
import { useState } from 'react';
import useAxios from '../hooks/useAxios';
import PostList from '../components/PostList';
import AddPostForm from '../components/AddPostForm';
import mockApi from '../api/mockApi'; // Axios 인스턴스
function PostsPage() {
// 게시물 목록 조회
const { data, loading, error, fetchData: refetchPosts } = useAxios('/posts?_limit=20'); // useAxios 훅 사용!
const posts = data || [];
// 새 게시물 추가를 위한 상태
const [addLoading, setAddLoading] = useState(false);
const [addError, setAddError] = useState(null);
const handleAddPost = async (newPost) => {
setAddLoading(true);
setAddError(null);
try {
await mockApi.post('/posts', newPost);
alert('게시물이 성공적으로 추가되었습니다!');
refetchPosts(); // 게시물 추가 후 목록 새로고침
} catch (err) {
setAddError(err);
alert('게시물 추가에 실패했습니다!');
} finally {
setAddLoading(false);
}
};
const handleDeletePost = async (id) => {
if (!window.confirm(`${id}번 게시물을 정말 삭제하시겠습니까?`)) {
return;
}
setAddLoading(true); // 로딩 상태 재활용
setAddError(null);
try {
await mockApi.delete(`/posts/${id}`);
alert(`${id}번 게시물이 삭제되었습니다.`);
refetchPosts(); // 게시물 삭제 후 목록 새로고침
} catch (err) {
setAddError(err);
alert(`게시물 삭제에 실패했습니다. ${err.message}`);
} finally {
setAddLoading(false);
}
};
return (
<div style={{ padding: '20px' }}>
<h1 style={{ textAlign: 'center', marginBottom: '40px', color: 'var(--header-color)' }}>게시판</h1>
<AddPostForm onAddPost={handleAddPost} loading={addLoading} error={addError} />
<PostList
posts={posts}
loading={loading}
error={error}
onDelete={handleDeletePost}
onRetry={refetchPosts}
/>
</div>
);
}
export default PostsPage;SinglePostPage.jsx
단일 게시물 상세 페이지입니다.
import { useParams } from 'react-router-dom';
import useAxios from '../hooks/useAxios';
import PostDetail from '../components/PostDetail';
function SinglePostPage() {
const { postId } = useParams(); // URL 파라미터에서 postId 가져오기
// useAxios 훅을 사용하여 특정 게시물 조회
const { data: post, loading, error, fetchData: refetchPost } = useAxios(
postId ? `/posts/${postId}` : null // postId가 있을 때만 요청 (null이면 요청X)
);
return (
<div style={{ padding: '20px' }}>
<PostDetail post={post} loading={loading} error={error} onRetry={refetchPost} />
</div>
);
}
export default SinglePostPage;App.jsx (라우팅 + Axios 최종 통합)
라우팅과 함께 모든 컴포넌트를 통합합니다.
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
import { AppProvider } from './contexts/AppContext'; // 7장 실습의 AppContext 재사용 (테마, 로그인)
import Header from './components/Header'; // AppContext를 사용하는 Header
import PostsPage from './pages/PostsPage';
import SinglePostPage from './pages/SinglePostPage';
import NotFoundPage from './pages/NotFoundPage'; // 7장 실습에서 만든 404 페이지
import HomePage from './pages/HomePage'; // 간단한 홈페이지 (Optional)
// 7장 AppContext가 필요하다면 재활용 (이번 장 실습에서는 필수는 아님)
// AppContext.js 파일이 있다면 Header 컴포넌트가 정상 작동할 것입니다.
// 간단하게 404 페이지는 만들지 않고 아래에 직접 넣어두겠습니다.
const DefaultNotFoundPage = () => (
<div style={{ textAlign: 'center', padding: '50px' }}>
<h1>404 - 페이지를 찾을 수 없습니다.</h1>
<p>요청하신 페이지가 존재하지 않습니다.</p>
<Link to="/" className="button">홈으로 돌아가기</Link>
</div>
);
function App() {
return (
<AppProvider> {/* 7장 AppContext를 사용한다면 AppProvider로 감쌉니다. */}
<BrowserRouter>
<Header /> {/* Header는 AppContext에 의존하므로, AppProvider 안에 있어야 합니다. */}
<div className="main-content">
<Routes>
<Route path="/" element={<HomePage />} /> {/* Optional: 간단한 홈페이지 추가 */}
<Route path="/posts" element={<PostsPage />} />
<Route path="/posts/:postId" element={<SinglePostPage />} />
<Route path="*" element={<DefaultNotFoundPage />} /> {/* 404 페이지 */}
</Routes>
</div>
</BrowserRouter>
</AppProvider>
);
}
export default App;- 주의:
App.jsx에서HomePage,DashboardPage,NotFoundPage등은 7장 실습에서 만든 파일들을 재활용하거나 간단하게 대체할 수 있습니다. 여기서는HomePage와DefaultNotFoundPage를 포함했습니다.
구현이 끝난 뒤에는 CRUD 동작을 요청, 화면 상태, 실패 복구 기준으로 함께 검증해야 합니다.
검증 순서 및 확인 사항: CRUD 경로 점검
실습 검증은 목록 조회 -> 상세 조회 -> 추가 -> 삭제 -> 실패 응답 순서로 고정합니다.
이 순서로 진행하면 CRUD 경로별 회귀를 단계적으로 확인할 수 있습니다.
npm create vite@latest axios-data-fetching-app -- --template reactcd axios-data-fetching-appnpm install axios react-router-dom(또는yarn add axios react-router-dom)
파일 구조 생성: 위에 제시된 디렉토리 구조에 따라 파일들을 생성합니다.
코드 복사/붙여넣기: 각 파일에 해당하는 코드를 정확히 복사하여 붙여넣습니다. (index.css, api/mockApi.js, hooks/useAxios.js, components 폴더 안의 모든 .jsx 파일, pages 폴더 안의 모든 .jsx 파일, App.jsx까지)
애플리케이션 실행: npm run dev (또는 yarn dev) 명령어를 실행하여 개발 서버를 시작합니다.
- 홈 (
/): 간단한 소개 페이지가 보입니다. -
게시판 (
/posts)- 로딩 스피너가 잠시 보인 후, 게시물 목록이 나타나는지 확인합니다.
- 새 게시물 추가 폼에 제목과 내용을 입력하고 게시물 추가 버튼을 클릭합니다. 성공 메시지(alert)와 함께 목록에 새 게시물(
json-server의db.json기준)이 추가되는 것을 확인합니다. - 각 게시물 옆의 삭제 버튼을 클릭하여 게시물이 목록에서 사라지고 삭제 메시지(alert)가 뜨는지 확인합니다. (실제 서버에는 삭제되지 않음)
- 네트워크 탭에서 GET, POST, DELETE 요청이 올바르게 전송되는지 확인합니다.
-
게시물 상세 (
/posts/1또는 다른 ID)- 목록에서 게시물 제목을 클릭하거나 URL에 직접
/posts/게시물ID를 입력하여 특정 게시물의 상세 내용이 로딩 스피너 후 나타나는지 확인합니다. - 존재하지 않는 게시물 ID (예:
/posts/99999)로 접근하여 에러 메시지가 잘 표시되는지 확인합니다.
- 목록에서 게시물 제목을 클릭하거나 URL에 직접
- 로딩 및 에러 처리: 네트워크 속도를 늦춰가며(크롬 개발자 도구 Network 탭에서
Slow 3G등으로 설정) 로딩 스피너가 잘 표시되는지 확인합니다. 의도적으로 API URL을 잘못 입력하여 에러 메시지가 잘 나타나는지도 확인해 보세요. - 테마 및 로그인 (Optional): 7장에서 만든
AppContext.js와Header.jsx가 있다면, 로그인/로그아웃 및 테마 전환 기능도 여전히 작동하는지 확인합니다.
Axios, useEffect, 커스텀 훅을 조합하면 데이터 페칭 로직을 컴포넌트 밖으로 분리하고 재사용할 수 있습니다.
이 실습에서는 다음 개발 패턴을 확인했습니다.
- 비동기 요청을 위한
Axios인스턴스 설정 useAxios와 같은 범용적인 데이터 페칭 커스텀 훅의 설계 및 구현- 로딩, 에러, 데이터 상태를 컴포넌트에 통합하여 사용자에게 시각적인 피드백 제공
- CRUD 작업 중 C(reate)와 D(elete)를 위한 POST 및 DELETE 요청 처리
이 실습의 핵심은 요청 함수를 외우는 것이 아니라, 조회·상세·추가·삭제·실패 응답을 같은 순서로 반복 검증하는 습관입니다.
화면이 보이는지만 확인하지 말고 로딩, 에러, 재시도, 목록 갱신까지 함께 확인해야 외부 API 연동의 회귀를 줄일 수 있습니다.