데이터 연동 페이지와 검증
데이터 연동 페이지와 라우터를 조합하고 조회·추가·삭제의 실패·지연·재시도를 검증합니다.
데이터 연동 페이지 구성
게시판의 목록·상세 조회와 추가·삭제는 같은 Axios 인스턴스를 사용하지만, 조회 상태와 mutation 상태는 서로 독립적으로 관리합니다.
PostsPage.jsx
게시물 목록과 게시물 추가 폼을 함께 표시합니다.
import { useEffect, useRef, useState } from 'react';
import useAxios from '../hooks/useAxios';
import PostList from '../components/PostList';
import AddPostForm from '../components/AddPostForm';
import ErrorDisplay from '../components/ErrorDisplay';
import mockApi from '../api/mockApi';
function PostsPage() {
const { data, loading, error, fetchData: refetchPosts } = useAxios('/posts');
const posts = Array.isArray(data) ? data : [];
const responseShapeError =
data !== null && !Array.isArray(data)
? new Error('GET /posts 응답이 배열이 아닙니다.')
: null;
const [createLoading, setCreateLoading] = useState(false);
const [createError, setCreateError] = useState(null);
const [deletingId, setDeletingId] = useState(null);
const [deleteError, setDeleteError] = useState(null);
const [refreshError, setRefreshError] = useState(null);
const pageActiveRef = useRef(false);
useEffect(() => {
pageActiveRef.current = true;
return () => {
pageActiveRef.current = false;
};
}, []);
const retryPosts = () => {
void refetchPosts().catch(() => {});
};
const retryRefresh = () => {
setRefreshError(null);
void refetchPosts().catch((retryError) => {
if (retryError.code !== 'ERR_CANCELED') {
setRefreshError(retryError);
}
});
};
const refreshAfterMutation = async () => {
if (!pageActiveRef.current) return;
setRefreshError(null);
try {
await refetchPosts();
} catch (refreshRequestError) {
if (
pageActiveRef.current &&
refreshRequestError.code !== 'ERR_CANCELED'
) {
setRefreshError(refreshRequestError);
}
}
};
const handleAddPost = async (newPost) => {
setCreateLoading(true);
setCreateError(null);
try {
await mockApi.post('/posts', newPost);
if (!pageActiveRef.current) return false;
await refreshAfterMutation();
return pageActiveRef.current;
} catch (mutationError) {
if (pageActiveRef.current) {
setCreateError(mutationError);
}
return false;
} finally {
if (pageActiveRef.current) {
setCreateLoading(false);
}
}
};
const handleDeletePost = async (id) => {
if (deletingId !== null) {
return;
}
if (!window.confirm(`${id}번 게시물을 정말 삭제하시겠습니까?`)) {
return;
}
setDeletingId(id);
setDeleteError(null);
try {
await mockApi.delete(`/posts/${id}`);
if (!pageActiveRef.current) return;
await refreshAfterMutation();
} catch (mutationError) {
if (pageActiveRef.current) {
setDeleteError(mutationError);
}
} finally {
if (pageActiveRef.current) {
setDeletingId(null);
}
}
};
return (
<div style={{ padding: '20px' }}>
<h1 style={{ textAlign: 'center', marginBottom: '40px', color: 'var(--header-color)' }}>게시판</h1>
<AddPostForm onAddPost={handleAddPost} loading={createLoading} error={createError} />
{deletingId && <p>{deletingId}번 게시물을 삭제하는 중입니다.</p>}
<ErrorDisplay error={deleteError} />
<ErrorDisplay error={refreshError} onRetry={retryRefresh} />
<PostList
posts={posts}
loading={loading}
error={refreshError ? null : error ?? responseShapeError}
onDelete={handleDeletePost}
onRetry={retryPosts}
deletingId={deletingId}
/>
</div>
);
}
export default PostsPage;POST 또는 DELETE 자체가 실패하면 각 mutation error를 표시합니다. 저장은 성공했지만 뒤이은 GET이 실패하면 기존 posts를 유지하고 refreshError를 비차단 안내로 표시하므로, 사용자가 같은 mutation을 중복 실행하지 않고 목록 조회만 다시 시도할 수 있습니다. 페이지가 unmount된 뒤 mutation이 끝난 경우에는 pageActiveRef가 후속 GET 시작과 상태 쓰기를 막습니다.
원 페이지의 responseShapeError는 null이 아닌 비배열을 오류로 표시합니다. useAxios의 초기 data와 성공 응답 body가 모두 null일 수 있으므로, 이 조건만으로는 200·JSON null을 빈 목록과 구별하지 못합니다. 아래 성공 응답 검증을 추가하면 null까지 포함한 비배열을 오류로 처리할 수 있습니다. 삭제는 이 예제에서 한 번에 하나만 실행하며, deletingId가 있는 동안 다른 삭제 버튼을 잠가 앞선 요청의 finally가 뒤 요청의 표시를 지우는 경쟁을 막습니다.
저장 성공 뒤 갱신이 실패하는 경우
refreshAfterMutation은 GET 실패를 refreshError로 기록하고 내부에서 소비합니다. 따라서 활성 페이지의 POST가 성공했다면 목록 갱신이 실패해도 handleAddPost는 true를 반환합니다. 폼은 비워지고 기존 목록은 남으며, 사용자는 목록 조회만 다시 시도합니다. POST를 다시 보내면 같은 제목의 게시물을 하나 더 만들 수 있으므로 저장 실패라고 안내하지 않는 것이 중요합니다.
이 구현은 부모 상태가 다음 렌더에 반영되어 버튼을 비활성화하는 일반 사용자 클릭을 전제로 합니다. 같은 렌더의 콜백을 외부 코드가 동시에 여러 번 호출하는 범용 mutation 큐·서버의 중복 방지 키를 구현한 예제는 아닙니다. 조회 요청 취소도 이미 끝난 POST·DELETE를 서버에서 되돌리는 rollback을 뜻하지 않습니다.
예시: 성공 응답의 null과 초기 상태 구별하기
배열 검증은 HTTP 응답이 도착한 자리에서 수행하면 초기 state를 성공 값으로 오인하지 않습니다. 앞 문서의 src/api/mockApi.js에서 인스턴스를 만든 다음, 기존 export default mockApi 직전에 아래 코드를 추가하세요. 모듈이 초기화될 때 한 번 등록하며, 컴포넌트 렌더나 Effect마다 등록하지 않습니다. 원 useAxios와 페이지 코드는 그대로 두고 검증 단계만 보완하는 별도 예제입니다.
mockApi.interceptors.response.use((response) => {
const isPostsList =
response.config.method === 'get' && response.config.url === '/posts';
if (isPostsList && !Array.isArray(response.data)) {
const shapeError = new Error('GET /posts 응답이 배열이 아닙니다.');
shapeError.name = 'ResponseShapeError';
throw shapeError;
}
return response;
});이 조건은 이 실습에서 정확히 mockApi.get('/posts')로 호출하는 목록 조회에만 적용합니다. 상세 GET, POST와 DELETE 응답은 검사 대상이 아닙니다. 쿼리·URL 표현을 바꾸거나 다른 목록 API를 추가하면 대상 판정도 해당 요청 계약에 맞춰 바꿔야 합니다.
아직 응답이 오지 않았다면 인터셉터는 실행되지 않으므로 초기 null은 오류가 아닙니다. 성공 응답이 빈 배열이면 검사를 통과해 empty로 표시합니다. 200·null이나 객체이면 ResponseShapeError를 던져 원 훅의 catch가 error를 기록하며, 초기 목록에서는 error 분기가 empty보다 먼저 선택됩니다. 이 오류는 HTTP 실패가 아니라 성공 body의 계약 위반이므로 error.response.status가 있다고 가정하지 않습니다. ErrorDisplay는 일반 Error의 message도 표시할 수 있습니다.
저장 뒤 재조회에서 계약 위반이 발견되어도 이미 끝난 POST·DELETE를 되돌리지는 않습니다. 원 refreshAfterMutation은 이를 refreshError로 구분하고 기존 목록을 유지합니다. 처음부터 잘못된 body가 온 경우와 기존 목록을 가진 채 갱신이 실패한 경우를 따로 확인하세요.
혼자 확인하기
초기 조회이며 HTTP 상태는 모두 200이라고 가정합니다. 성공 응답 검증을 추가한 뒤 body가 빈 배열, null, 게시물 한 건의 배열일 때 empty·error·목록 중 무엇이 선택되는지 적으세요. 요청 전 초기 null은 언제 검사될까요?
응답 사건과 값 검증을 나누어 풀이
빈 배열은 배열 검증을 통과해 empty, null은 응답이 도착한 뒤 계약 오류, 한 건의 배열은 목록입니다. 초기 null에는 아직 응답 사건이 없으므로 검사가 실행되지 않습니다. 원 페이지의 null 제외 조건만 사용할 때와 보완 검증을 등록했을 때의 차이를 설명할 수 있어야 합니다.
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)
);
const retryPost = () => {
void refetchPost().catch(() => {});
};
if (error?.response?.status === 404) {
return <p>요청한 게시물을 찾을 수 없습니다.</p>;
}
return (
<div style={{ padding: '20px' }}>
<PostDetail post={post} loading={loading} error={error} onRetry={retryPost} />
</div>
);
}
export default SinglePostPage;App.jsx (라우팅 + Axios 최종 통합)
라우팅과 함께 모든 컴포넌트를 통합합니다.
import { BrowserRouter, Routes, Route, Link, Navigate } from 'react-router-dom';
import PostsPage from './pages/PostsPage';
import SinglePostPage from './pages/SinglePostPage';
const DefaultNotFoundPage = () => (
<div style={{ textAlign: 'center', padding: '50px' }}>
<h1>404 - 페이지를 찾을 수 없습니다.</h1>
<p>요청하신 페이지가 존재하지 않습니다.</p>
<Link to="/" className="button">홈으로 돌아가기</Link>
</div>
);
function App() {
return (
<BrowserRouter>
<div className="main-content">
<Routes>
<Route path="/" element={<Navigate to="/posts" replace />} />
<Route path="/posts" element={<PostsPage />} />
<Route path="/posts/:postId" element={<SinglePostPage />} />
<Route path="*" element={<DefaultNotFoundPage />} />
</Routes>
</div>
</BrowserRouter>
);
}
export default App;구현이 끝난 뒤에는 조회·추가·삭제 동작을 요청, 화면 상태, 실패 복구 기준으로 함께 검증해야 합니다. 아래 검증표의 비배열 성공 응답 검사는 앞서 보완한 성공 응답 검증을 등록한 상태를 기준으로 합니다.
게시판 조회·추가·삭제 통합과 검증 순서 — 상태 추적
POST/DELETE pending은 조회와 독립이며 저장 성공 뒤 await refetch 성공/실패는 다른 결과다. 서버 변경/조회 data/화면 안내를 추적해 성공 저장을 재실행하지 않도록 보강할 가치가 크다.
React · C/R/D integration
조회·추가·삭제의 독립 상태와 검증 기준
조회 상태와 mutation 상태를 분리하고, mutation 성공 뒤 목록 refetch까지 기다려야 화면 계약이 닫힙니다. POST·DELETE 실패와 저장은 성공했지만 갱신이 실패한 경우를 같은 오류로 합치지 않습니다.
서버 변경 성공과 목록 갱신 성공은 다르다
조회·create·대상 ID delete의 pending은 독립입니다. 저장 성공 뒤 refresh 실패는 POST/DELETE 재실행 허가가 아니며 조회만 안전하게 retry합니다. 전체 검증 기준은 원표에 남깁니다.
원래 설명과 상세 기준
목록과 상세는 URL이 request key다
/posts→ GET 목록 →Array.isArray로 확인하고 비배열 응답은 계약 오류로 처리/posts/:postId→ 문자열 ID 또는undefined→ 유효할 때만 상세 GET- 목록은 loading, error, empty, retry를 구분하고 상세는 not found를 404 화면으로 구분
- ID가 빠르게 바뀌면 이전 요청을 abort하고 최신 request ID만 화면 상태를 commit
POST pending과 error는 조회와 분리한다
폼 제출을 잠그고 create error를 비운다
활성 페이지에서만 POST 뒤 목록 refetch를 await한다
POST 실패와 refresh 실패를 서로 다른 안내로 기록한다
DELETE pending은 대상 ID를 가진다
삭제할 ID를 pending으로 표시하고 다른 삭제 실행을 잠근다
활성 페이지에서만 DELETE 뒤 목록 refetch를 await한다
로컬 서버의 삭제 persistence를 새로고침으로 확인한다
BrowserRouter와 Axios의 책임을 코드에서 잇는다
이 실습은 route component가 request key를 만들고 custom hook과 mutation handler가 상태를 관리하는 수동 구조입니다. React Router Data Router의 loader·action을 쓰는 설계도 가능하지만 여기서는 별도 대안이며 두 모델을 섞지 않습니다.
| 검증 | 조작 | 통과 기준과 실패 시 회귀 |
|---|---|---|
| Loading | 네트워크 지연 | 통과: 초기 요청과 mutation 진행이 독립적으로 보임 미충족: 전체 화면 잠금 또는 중복 제출 |
| Error · retry | API 중단 후 복구 | 통과: 조회·refresh만 안전하게 retry하고 mutation 오류는 자동 재실행하지 않음 미충족: 저장 성공을 실패로 오인하거나 mutation 중복 실행 |
| Empty | 빈 posts 배열 | 통과: 빈 배열만 empty, 비배열 성공 응답은 계약 error 미충족: 깨진 응답을 빈 목록으로 오인 |
| 404 | 없는 상세 ID | 통과: not found와 네트워크 장애를 구분 미충족: 빈 상세 또는 일반 오류로 합침 |
| Stale · cancel | ID를 빠르게 변경 | 통과: 이전 요청이 현재 상세를 덮어쓰지 않음 미충족: URL과 다른 게시물 표시 |
| List · detail | 추가·삭제 뒤 이동 | 통과: 목록과 상세가 같은 서버 상태를 가리킴 미충족: 한 화면만 오래된 data 유지 |
| Persistence | 브라우저 새로고침 | 통과: 미충족: 메모리 UI만 성공한 것으로 착각 |
| Seed reset | 초기 seed 복원 | 통과: 다음 실행이 같은 데이터에서 시작 미충족: 반복 실행마다 결과 변화 |
지연 중 상태를 분리한다
초기 조회, create, 대상 ID의 delete가 서로 다른 진행 상태로 보이는지 확인합니다.
실패 위치를 보존한다
조회·refresh만 안전하게 retry하며 mutation 오류는 구분해 표시하고 자동 재실행하지 않습니다.
값 없음의 의미를 나눈다
빈 배열만 success empty이며 비배열 응답은 계약 error, 없는 상세는 404로 렌더링합니다.
빠른 route 변경을 시험한다
이전 요청을 abort하고 늦은 응답이 현재 URL의 상세를 덮어쓰지 않는지 확인합니다.
화면 간 서버 상태를 맞춘다
추가·삭제 뒤 awaited refetch를 거쳐 목록과 상세가 같은 데이터를 가리키는지 확인합니다.
새로고침과 seed 복원을 확인한다
변경이 db.json에 남는지 확인하고 다음 반복 전에 보관한 seed를 복원합니다.
검증 순서는 목록 조회 → 상세 조회 → 추가 → 삭제 → 실패·지연 → 빠른 route 변경 → 새로고침 → seed 복원으로 고정합니다. 각 단계에서 네트워크 응답과 화면 state를 함께 기록합니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 원도표 조건·주석 | /posts → GET 목록 → Array.isArray로 확인하고 비배열 응답은 계약 오류로 처리 |
| 원도표 표 행 | 검증 | 조작 | 통과 기준과 실패 시 회귀 |
| 원도표 표 행 | Loading | 네트워크 지연 | 통과: 초기 요청과 mutation 진행이 독립적으로 보임; 미충족: 전체 화면 잠금 또는 중복 제출 |
| 원도표 표 행 | Error · retry | API 중단 후 복구 | 통과: 조회·refresh만 안전하게 retry하고 mutation 오류는 자동 재실행하지 않음; 미충족: 저장 성공을 실패로 오인하거나 mutation 중복 실행 |
| 원도표 표 행 | Empty | 빈 posts 배열 | 통과: 빈 배열만 empty, 비배열 성공 응답은 계약 error; 미충족: 깨진 응답을 빈 목록으로 오인 |
| 원도표 표 행 | 404 | 없는 상세 ID | 통과: not found와 네트워크 장애를 구분; 미충족: 빈 상세 또는 일반 오류로 합침 |
| 원도표 표 행 | Stale · cancel | ID를 빠르게 변경 | 통과: 이전 요청이 현재 상세를 덮어쓰지 않음; 미충족: URL과 다른 게시물 표시 |
| 원도표 표 행 | List · detail | 추가·삭제 뒤 이동 | 통과: 목록과 상세가 같은 서버 상태를 가리킴; 미충족: 한 화면만 오래된 data 유지 |
| 원도표 설명 | 통과: db.json의 추가·삭제 결과가 유지됨 |
| 원도표 표 행 | Persistence | 브라우저 새로고침 | 통과: db.json의 추가·삭제 결과가 유지됨; 미충족: 메모리 UI만 성공한 것으로 착각 |
| 원도표 표 행 | Seed reset | 초기 seed 복원 | 통과: 다음 실행이 같은 데이터에서 시작; 미충족: 반복 실행마다 결과 변화 |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| mutation 시작 | read: 별도 조회 상태 / mutation: 진행 중 | POST 또는 대상 ID DELETE의 독립 pending을 켠다 | 중복 제출과 다른 삭제의 허용 정책을 분리한다 |
| 서버 성공 | server: 변경 완료 / list: 이전 결과 가능 | 서버 변경 뒤 목록 refetch를 await한다 | 저장 성공이 새 목록의 반영 완료와 같지는 않다 |
| refresh 결과 | 저장: 성공 유지 / refresh: 성공/실패별 안내 | 갱신 성공은 현재 목록, 실패는 저장 성공과 별도 refresh 오류를 표시한다 | 갱신 실패를 POST나 DELETE의 재실행 허가로 해석하지 않는다 |
게시판 조회·추가·삭제 통합과 검증 순서 — 상태 추적
POST/DELETE pending은 조회와 독립이며 저장 성공 뒤 await refetch 성공/실패는 다른 결과다. 서버 변경/조회 data/화면 안내를 추적해 성공 저장을 재실행하지 않도록 보강할 가치가 크다.
React · C/R/D integration
조회·추가·삭제의 독립 상태와 검증 기준
조회 상태와 mutation 상태를 분리하고, mutation 성공 뒤 목록 refetch까지 기다려야 화면 계약이 닫힙니다. POST·DELETE 실패와 저장은 성공했지만 갱신이 실패한 경우를 같은 오류로 합치지 않습니다.
서버 변경 성공과 목록 갱신 성공은 다르다
조회·create·대상 ID delete의 pending은 독립입니다. 저장 성공 뒤 refresh 실패는 POST/DELETE 재실행 허가가 아니며 조회만 안전하게 retry합니다. 전체 검증 기준은 원표에 남깁니다.
원래 설명과 상세 기준
목록과 상세는 URL이 request key다
/posts→ GET 목록 →Array.isArray로 확인하고 비배열 응답은 계약 오류로 처리/posts/:postId→ 문자열 ID 또는undefined→ 유효할 때만 상세 GET- 목록은 loading, error, empty, retry를 구분하고 상세는 not found를 404 화면으로 구분
- ID가 빠르게 바뀌면 이전 요청을 abort하고 최신 request ID만 화면 상태를 commit
POST pending과 error는 조회와 분리한다
폼 제출을 잠그고 create error를 비운다
활성 페이지에서만 POST 뒤 목록 refetch를 await한다
POST 실패와 refresh 실패를 서로 다른 안내로 기록한다
DELETE pending은 대상 ID를 가진다
삭제할 ID를 pending으로 표시하고 다른 삭제 실행을 잠근다
활성 페이지에서만 DELETE 뒤 목록 refetch를 await한다
로컬 서버의 삭제 persistence를 새로고침으로 확인한다
BrowserRouter와 Axios의 책임을 코드에서 잇는다
이 실습은 route component가 request key를 만들고 custom hook과 mutation handler가 상태를 관리하는 수동 구조입니다. React Router Data Router의 loader·action을 쓰는 설계도 가능하지만 여기서는 별도 대안이며 두 모델을 섞지 않습니다.
| 검증 | 조작 | 통과 기준과 실패 시 회귀 |
|---|---|---|
| Loading | 네트워크 지연 | 통과: 초기 요청과 mutation 진행이 독립적으로 보임 미충족: 전체 화면 잠금 또는 중복 제출 |
| Error · retry | API 중단 후 복구 | 통과: 조회·refresh만 안전하게 retry하고 mutation 오류는 자동 재실행하지 않음 미충족: 저장 성공을 실패로 오인하거나 mutation 중복 실행 |
| Empty | 빈 posts 배열 | 통과: 빈 배열만 empty, 비배열 성공 응답은 계약 error 미충족: 깨진 응답을 빈 목록으로 오인 |
| 404 | 없는 상세 ID | 통과: not found와 네트워크 장애를 구분 미충족: 빈 상세 또는 일반 오류로 합침 |
| Stale · cancel | ID를 빠르게 변경 | 통과: 이전 요청이 현재 상세를 덮어쓰지 않음 미충족: URL과 다른 게시물 표시 |
| List · detail | 추가·삭제 뒤 이동 | 통과: 목록과 상세가 같은 서버 상태를 가리킴 미충족: 한 화면만 오래된 data 유지 |
| Persistence | 브라우저 새로고침 | 통과: 미충족: 메모리 UI만 성공한 것으로 착각 |
| Seed reset | 초기 seed 복원 | 통과: 다음 실행이 같은 데이터에서 시작 미충족: 반복 실행마다 결과 변화 |
지연 중 상태를 분리한다
초기 조회, create, 대상 ID의 delete가 서로 다른 진행 상태로 보이는지 확인합니다.
실패 위치를 보존한다
조회·refresh만 안전하게 retry하며 mutation 오류는 구분해 표시하고 자동 재실행하지 않습니다.
값 없음의 의미를 나눈다
빈 배열만 success empty이며 비배열 응답은 계약 error, 없는 상세는 404로 렌더링합니다.
빠른 route 변경을 시험한다
이전 요청을 abort하고 늦은 응답이 현재 URL의 상세를 덮어쓰지 않는지 확인합니다.
화면 간 서버 상태를 맞춘다
추가·삭제 뒤 awaited refetch를 거쳐 목록과 상세가 같은 데이터를 가리키는지 확인합니다.
새로고침과 seed 복원을 확인한다
변경이 db.json에 남는지 확인하고 다음 반복 전에 보관한 seed를 복원합니다.
검증 순서는 목록 조회 → 상세 조회 → 추가 → 삭제 → 실패·지연 → 빠른 route 변경 → 새로고침 → seed 복원으로 고정합니다. 각 단계에서 네트워크 응답과 화면 state를 함께 기록합니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 원도표 조건·주석 | /posts → GET 목록 → Array.isArray로 확인하고 비배열 응답은 계약 오류로 처리 |
| 원도표 표 행 | 검증 | 조작 | 통과 기준과 실패 시 회귀 |
| 원도표 표 행 | Loading | 네트워크 지연 | 통과: 초기 요청과 mutation 진행이 독립적으로 보임; 미충족: 전체 화면 잠금 또는 중복 제출 |
| 원도표 표 행 | Error · retry | API 중단 후 복구 | 통과: 조회·refresh만 안전하게 retry하고 mutation 오류는 자동 재실행하지 않음; 미충족: 저장 성공을 실패로 오인하거나 mutation 중복 실행 |
| 원도표 표 행 | Empty | 빈 posts 배열 | 통과: 빈 배열만 empty, 비배열 성공 응답은 계약 error; 미충족: 깨진 응답을 빈 목록으로 오인 |
| 원도표 표 행 | 404 | 없는 상세 ID | 통과: not found와 네트워크 장애를 구분; 미충족: 빈 상세 또는 일반 오류로 합침 |
| 원도표 표 행 | Stale · cancel | ID를 빠르게 변경 | 통과: 이전 요청이 현재 상세를 덮어쓰지 않음; 미충족: URL과 다른 게시물 표시 |
| 원도표 표 행 | List · detail | 추가·삭제 뒤 이동 | 통과: 목록과 상세가 같은 서버 상태를 가리킴; 미충족: 한 화면만 오래된 data 유지 |
| 원도표 설명 | 통과: db.json의 추가·삭제 결과가 유지됨 |
| 원도표 표 행 | Persistence | 브라우저 새로고침 | 통과: db.json의 추가·삭제 결과가 유지됨; 미충족: 메모리 UI만 성공한 것으로 착각 |
| 원도표 표 행 | Seed reset | 초기 seed 복원 | 통과: 다음 실행이 같은 데이터에서 시작; 미충족: 반복 실행마다 결과 변화 |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| mutation 시작 | read: 별도 조회 상태 / mutation: 진행 중 | POST 또는 대상 ID DELETE의 독립 pending을 켠다 | 중복 제출과 다른 삭제의 허용 정책을 분리한다 |
| 서버 성공 | server: 변경 완료 / list: 이전 결과 가능 | 서버 변경 뒤 목록 refetch를 await한다 | 저장 성공이 새 목록의 반영 완료와 같지는 않다 |
| refresh 결과 | 저장: 성공 유지 / refresh: 성공/실패별 안내 | 갱신 성공은 현재 목록, 실패는 저장 성공과 별도 refresh 오류를 표시한다 | 갱신 실패를 POST나 DELETE의 재실행 허가로 해석하지 않는다 |
검증 순서 및 확인 사항: 조회·추가·삭제 경로 점검
실습 검증은 목록 조회 -> 상세 조회 -> 추가 -> 삭제 -> 실패 응답 순서로 고정합니다.
이 순서로 진행하면 C/R/D 경로별 회귀를 단계적으로 확인할 수 있습니다.
준비 계약 확인: 앞 문서에서 Vite 프로젝트 생성, react-router-dom@7, Axios, json-server, db.json, db.seed.json, api script 준비를 마쳤는지 확인합니다.
파일 구조 생성: 위에 제시된 디렉터리 구조에 따라 파일들을 생성합니다.
코드 복사/붙여 넣기: 각 파일에 해당하는 코드를 정확히 복사하여 붙여 넣습니다. (index.css, api/mockApi.js, hooks/useAxios.js, components 폴더 안의 모든 .jsx 파일, pages 폴더 안의 모든 .jsx 파일, App.jsx까지)
두 프로세스 실행: 터미널 A에서 npm run api, 터미널 B에서 npm run dev를 실행하고 Mock API가 4000번 포트에서 응답하는지 확인합니다.
- 루트 (
/):/posts로 이동합니다. -
게시판 (
/posts)- 로딩 스피너가 잠시 보인 후, 게시물 목록이 나타나는지 확인합니다.
- 응답이 빈 배열이면 오류 화면 대신 빈 목록 안내가 나타나는지 확인합니다.
- 200 응답이어도 body가 배열이 아니면 빈 목록이 아니라 응답 계약 오류로 표시되는지 확인합니다.
- 새 게시물 추가 후 refetch가 끝나야 목록에 새 항목이 나타나는지 확인합니다. mutation은 성공했지만 refetch가 실패한 경우에는 추가 오류가 아니라 목록 갱신 오류로 안내해야 합니다.
- 각 게시물 옆의 삭제 버튼을 클릭해 해당 ID의 pending 상태만 표시되는지, 완료 후 목록이 다시 조회되는지 확인합니다. 로컬
json-server의 DELETE는db.json에 반영되므로 새로고침 뒤에도 삭제 상태가 유지됩니다. - 네트워크 탭에서 GET, POST, DELETE 요청이 올바르게 전송되는지 확인합니다.
-
게시물 상세 (
/posts/1또는 다른 ID)- 목록에서 게시물 제목을 클릭하거나 URL에 직접
/posts/게시물ID를 입력하여 특정 게시물의 상세 내용이 로딩 스피너 후 나타나는지 확인합니다. - 존재하지 않는 게시물 ID (예:
/posts/99999)로 접근하여 에러 메시지가 잘 표시되는지 확인합니다.
- 목록에서 게시물 제목을 클릭하거나 URL에 직접
- 로딩·에러·재시도: 네트워크 속도를 늦추고 API를 잠시 중단해 초기 loading, error, retry가 순서대로 동작하는지 확인합니다.
- 경합과 취소: 상세 ID를 빠르게 바꾸거나 페이지를 떠났을 때 이전 요청이 취소되고, 늦게 끝난 응답이 현재 화면을 덮어쓰지 않는지 확인합니다.
- 일관성과 초기화: 목록과 상세가 같은 서버 상태를 가리키는지 확인하고, 반복 실습 전에는 보관한 seed로
db.json을 복원합니다.
혼자 풀기: 요청별 통과 기준 만들기
seed에 게시물 한 건이 있습니다. 새 게시물 POST는 서버에 반영되었고 이어지는 GET은 네트워크 실패했다고 가정합니다. 현재 페이지는 계속 활성 상태입니다. ① 서버의 게시물 수 ② 화면의 기존 목록 수 ③ 폼의 입력 ④ createError와 refreshError ⑤ 올바른 다음 요청을 적으세요. 실패한 GET 뒤 아직 재시도하지 않았습니다.
서버 상태와 조회 화면을 나누어 풀이
서버에는 두 건이 있고 화면은 이전 GET 결과 한 건을 유지합니다. POST 뒤 GET 실패는 refreshAfterMutation에서 처리하므로 createError는 null이며 refreshError가 기록됩니다. 활성 페이지에서 handleAddPost는 true를 반환하여 폼 입력은 비워집니다. 다음 행동은 GET 목록 조회의 retry입니다. POST 재실행은 이미 저장한 게시물을 중복 생성할 수 있습니다. retry GET이 두 건을 받으면 화면도 두 건으로 맞춰집니다. 이 수량은 요청 결과가 위 가정대로 확정된 사례이며 일반적인 응답 유실까지 서버 저장 성공으로 단정하는 규칙이 아닙니다.
검증 기록 예시
정상 시작에서는 GET /posts가 배열 한 건, 추가 성공 뒤에는 POST 한 번과 GET 두 건, 생성 ID를 삭제한 뒤에는 DELETE 한 번과 GET 한 건을 기대합니다. 각 단계의 실제 HTTP 상태·body·화면 수를 기록하세요. 원 seed의 1번을 삭제했다면 결과는 생성 항목만 남으므로 ‘삭제 뒤 항상 원 게시물’이라고 외우지 않습니다. 없는 상세 ID는 서버 404와 SinglePostPage의 전용 안내가 연결되는지 확인하며, SPA의 잘못된 화면 경로와 API 리소스의 404는 같은 검사가 아닙니다.
Axios, useEffect, 커스텀 훅을 조합하면 데이터 페칭 로직을 컴포넌트 밖으로 분리하고 재사용할 수 있습니다.
이 실습에서는 다음 개발 패턴을 확인했습니다.
- 비동기 요청을 위한
Axios인스턴스 설정 - GET 조회에 집중한
useAxios커스텀 훅의 설계 및 구현 - 로딩, 에러, 데이터 상태를 컴포넌트에 통합하여 사용자에게 시각적인 피드백 제공
- C/R/D 작업을 위한 GET, POST, DELETE 요청 처리
이 실습의 핵심은 요청 함수를 외우는 것이 아니라, 조회·상세·추가·삭제·실패 응답을 같은 순서로 반복 검증하는 습관입니다.
화면이 보이는지만 확인하지 말고 로딩, 에러, 재시도, 목록 갱신까지 함께 확인해야 외부 API 연동의 회귀를 줄일 수 있습니다.