이번 편의 결과물: bookshelf의 현재 데이터 흐름(로컬 배열 + useBooks)에서 어떤 지점이 실제 서버와 연결되면 깨지는지 짚어봅니다. 코드는 바꾸지 않습니다. · 다루는 개념: 서버 상태/클라이언트 상태 구분, 캐시·정합성 문제, 전역 스토어에 서버 데이터를 넣었을 때 깨지는 지점, TanStack Query가 해결하는 문제
이 편에서 만드는 파일
이 편은 개념 편입니다. 새로 만들거나 고치는 파일이 없습니다. 대신 react_2를 마친 bookshelf의 기존 파일을 근거로 문제를 짚습니다.
bookshelf/src/
├── hooks/useBooks.js ← 지금은 localStorage 기반. 04편부터 서버 조회로 바뀔 대상
└── pages/BookListPage.jsx ← 지금은 useBooks가 준 배열을 그대로 렌더링개념 정리
클라이언트 상태와 서버 상태는 다른 문제다
useState나 useReducer로 다루는 값(입력 폼 값, 모달 열림 여부, 정렬 기준)은 클라이언트 상태입니다. 이 값의 원본은 화면 자체이고, 컴포넌트가 값을 100퍼센트 소유합니다. 반면 책 목록처럼 서버 어딘가에 저장된 데이터를 화면에 보여주는 값은 서버 상태입니다. 화면은 그 데이터의 스냅샷일 뿐이고, 원본은 언제든 다른 요청·다른 사용자·다른 탭에 의해 바뀔 수 있습니다.
| 구분 | 클라이언트 상태 | 서버 상태 |
|---|---|---|
| 원본 위치 | 컴포넌트(메모리) | 서버(DB, API) |
| 소유권 | 화면이 완전히 소유 | 화면은 사본만 가짐 |
| 변경 방식 | setState 호출 | 네트워크 요청 후 응답 반영 |
| 동시성 문제 | 없음(단일 탭 안에서만 존재) | 있음(다른 클라이언트가 먼저 바꿀 수 있음) |
| 항상 따라오는 상태 | 값 자체 | 로딩·에러·성공 3가지 |
지금의 useBooks는 왜 아직 괜찮았는가
useBooks.js는 localStorage를 원본으로 씁니다. localStorage는 같은 브라우저·같은 탭 안에서는 사실상 동기적으로 읽고 쓸 수 있어서, 서버 상태의 어려움(네트워크 지연, 동시 수정, 캐시 무효화)이 드러나지 않았습니다. 04편부터 데이터 원본이 실제 API(json-server)로 바뀌면 상황이 달라집니다.
전역 스토어에 서버 데이터를 그대로 넣으면 깨지는 지점
Context나 Zustand 같은 클라이언트 상태 도구에 API 응답을 그대로 저장하는 방식은 흔한 실수입니다. 다음 질문에 답이 없기 때문입니다.
- 이 데이터는 언제 오래된(stale) 것으로 취급하고 다시 가져올 것인가
- 같은 데이터를 두 컴포넌트가 동시에 요청하면 요청을 한 번으로 합칠 것인가
- 요청이 실패하면 누가 재시도하고, 몇 번까지 재시도할 것인가
- 화면을 벗어났다가 돌아왔을 때 캐시를 재사용할 것인가, 새로 가져올 것인가
이 네 가지를 직접 구현하려면 useEffect 안에 로딩·에러 플래그, 타이머, 요청 취소 로직이 쌓입니다. 기능이 늘수록 같은 코드가 컴포넌트마다 반복됩니다.
TanStack Query가 대신 맡는 것
TanStack Query는 서버 상태를 위한 전용 캐시 계층입니다. queryKey(예: ['books'])로 데이터를 식별하고, staleTime이 지나면 자동으로 다시 가져오며, 같은 키로 들어온 요청은 하나로 합칩니다. 로딩·에러·성공 상태도 훅이 직접 관리해 반환합니다. 04편부터 이 역할을 useQuery와 useMutation에 맡깁니다.
직접 해보기
지금의 BookListPage.jsx를 읽고, “만약 useBooks가 fetch로 서버에서 책 목록을 가져온다면” 어떤 코드가 추가로 필요할지 목록으로 적어보세요.
로딩 중 화면, 에러 화면, 데이터가 바뀐 뒤 다시 가져오는 로직, 요청이 겹칠 때 중복 제거 로직이 필요합니다. 04편에서 이 목록을 useQuery 옵션 하나로 대체하는 과정을 확인합니다.
자주 하는 실수
| 증상 | 원인 | 고치는 법 |
|---|---|---|
| 화면 전환마다 API를 다시 호출한다 | 캐시 없이 useEffect로 매번 fetch | queryKey 기반 캐시로 재사용 |
| 같은 데이터가 컴포넌트마다 따로 로딩된다 | 전역 스토어 없이 각자 fetch | 같은 queryKey를 쓰면 요청이 자동으로 합쳐짐 |
| 서버 데이터가 오래돼도 화면이 안 바뀐다 | 갱신 시점을 정하지 않음 | staleTime으로 언제 다시 가져올지 명시 |