Repository navigation
FrontEnd‐State
상태를 서버 상태와 클라이언트 상태로 나누고, 각각 다른 도구가 맡는다.
| 구분 | 데이터 | 저장 위치 | 관리 도구 |
|---|---|---|---|
| Server State | 상품, 주문, 주문 상태 | PostgreSQL | Next.js 서버 캐시 (use cache + updateTag) |
| Client State | 장바구니, 상품 옵션 선택 | 브라우저 localStorage | Zustand |
| Local State | 주문서 입력, UI 상태 | 컴포넌트 | React state |
설계 단계에서는 관리자 목록의 변경 후 동기화를 위해 TanStack Query(dehydrate → HydrationBoundary, useMutation → invalidateQueries)를 쓰기로 했다.
실제 구현에서는 쇼핑몰과 관리자 모두 Server Component에서 조회하고, 변경한 Server Action 안에서 updateTag로 서버 캐시를 갱신하는 것만으로 충분해 TanStack Query를 쓰지 않았다.
- Server Action이 끝나면 Next.js가 바뀐 화면을 다시 받아 오므로, 변경 직후 목록이 바로 반영된다. 클라이언트에 같은 데이터를 한 벌 더 들고 있을 필요가 없었다.
- 캐시가 한 곳(서버)뿐이라 "서버 캐시는 갱신했는데 클라이언트 캐시는 오래된" 불일치가 생기지 않는다.
- 패키지와
QueryProvider는 설치돼 있지만 사용하는 화면은 없다. 클라이언트에서 주기적으로 다시 불러오기(polling) · 무한 스크롤이 필요해지면 그때 쓴다.
- 조회: Server Component에서
server/services를 부르고, 조회 함수에'use cache'+cacheLife+cacheTag를 붙인다. - 변경: Server Action(
actions.ts) → services → 같은 Action 안에서updateTag(<tag>). - 요청 시점 값(쿠키 · 주소 값)이 필요한 부분만
<Suspense>로 감싼다 (Troubleshooting 참고).
| 태그 | 붙는 조회 | 갱신하는 곳 |
|---|---|---|
products |
상품 목록 · 상세 · 추천 | 관리자 상품 등록 · 수정 · 삭제 |
product:<slug> |
상품 상세 | 관리자 상품 수정 · 삭제 |
product-sales |
인기순 목록 | 주문 생성, 관리자 주문 취소 · 복원 |
orders |
쇼핑몰 주문 내역, 관리자 주문 목록 · 건수 | 주문 생성, 관리자 주문 상태 변경 |
상품 목록의 필터 상태는 store가 아니라 주소(?category=&q=&sort=) 에 둔다. 새로고침 · 뒤로 가기 · 링크 공유에서 같은 화면이 나오고, 서버 컴포넌트가 같은 값을 바로 읽어 조회하기 때문이다.
- 정렬(
sort=latest|popular)의 기본값은 인기순이라 주소에서 뺀다. 모르는 값은 인기순으로 처리한다. - 카테고리 칩 · 검색어 지우기를 눌러도 정렬은 유지한다.
- 정렬 메뉴는 클라이언트 컴포넌트가
router.push로 주소만 바꾸고, 목록은 서버가 다시 그린다.useSearchParams를 쓰므로 Suspense 안에 둔다.
클라이언트 상태는 장바구니, 상품 옵션처럼 단순한 전역 상태가 대부분이다.
Jotai처럼 atom 단위로 나누기보다, 장바구니의 상태와 action(담기 · 수량 변경 · 삭제)을 하나의 store로 묶는 편이 도메인을 이해하기 쉽다고 판단했다.
초기 설정과 보일러플레이트가 적고 persist 적용이 간단해, 개발 속도가 핵심인 이 프로젝트에 맞는다.
persist로 저장한 장바구니는 브라우저에만 있어서, 서버가 그린 화면(빈 장바구니)과 브라우저 화면이 달라지는 hydration 불일치가 생길 수 있다.
서버 캐시는 서버 데이터만 다루므로 이 문제를 해결하지 않는다.
persist 옵션: skipHydration: true
└─ 클라이언트에서 useEffect → useCartStore.persist.rehydrate()
- 저장 키
pine-bakery-cart(localStorage). 같은 상품이라도 포장 옵션(선물 포장)이 다르면 별도 줄이다 (구분 기준: 상품 id + giftWrap). - store에는 표시용 상품 정보(이름 · 가격 · 이미지 등
Product의 일부)를 스냅샷으로 담는다. 금액은 아래처럼 서버가 다시 계산한다. - 쇼핑몰 레이아웃의 한 컴포넌트(
CartHydrator)가 마운트 후 한 번rehydrate()를 호출하고, 수량 배지는 0이면 그리지 않으므로, 서버 렌더(빈 장바구니)와 hydration 직후 화면이 일치하고 그 뒤에 수량이 나타난다.
장바구니는 브라우저에 저장되므로 사용자가 가격을 바꿔 보낼 수 있다. 주문 금액은 클라이언트가 보낸 가격이 아니라 서버에서 DB 가격으로 다시 계산한다. (Architecture 참고)
📎 Reference
- 🎨 Colors
- ✏️ Design
- 🧩 Components
- 🔀 State
- 🏗️ Architecture
- 🗃️ Data
- ⏱️ Time
- 🤖 Figma MCP
- 🧪 Testing
- 🚀 Performance
- 🛠️ Troubleshooting