Skip to content

FrontEnd‐State

LeeSongMi edited this page Oct 9, 2026 · 5 revisions

🔀 State

상태 관리 구조

상태를 서버 상태와 클라이언트 상태로 나누고, 각각 다른 도구가 맡는다.

구분 데이터 저장 위치 관리 도구
Server State 상품, 주문, 주문 상태 PostgreSQL Next.js 서버 캐시 (use cache + updateTag)
Client State 장바구니, 상품 옵션 선택 브라우저 localStorage Zustand
Local State 주문서 입력, UI 상태 컴포넌트 React state

서버 상태 — Next.js 서버 캐시

처음 계획과 바뀐 점

설계 단계에서는 관리자 목록의 변경 후 동기화를 위해 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 쇼핑몰 주문 내역, 관리자 주문 목록 · 건수 주문 생성, 관리자 주문 상태 변경

URL 상태 — 목록의 카테고리 · 검색어 · 정렬

상품 목록의 필터 상태는 store가 아니라 주소(?category=&q=&sort=) 에 둔다. 새로고침 · 뒤로 가기 · 링크 공유에서 같은 화면이 나오고, 서버 컴포넌트가 같은 값을 바로 읽어 조회하기 때문이다.

  • 정렬(sort=latest|popular)의 기본값은 인기순이라 주소에서 뺀다. 모르는 값은 인기순으로 처리한다.
  • 카테고리 칩 · 검색어 지우기를 눌러도 정렬은 유지한다.
  • 정렬 메뉴는 클라이언트 컴포넌트가 router.push로 주소만 바꾸고, 목록은 서버가 다시 그린다. useSearchParams를 쓰므로 Suspense 안에 둔다.

Zustand

선택 이유

클라이언트 상태는 장바구니, 상품 옵션처럼 단순한 전역 상태가 대부분이다. Jotai처럼 atom 단위로 나누기보다, 장바구니의 상태와 action(담기 · 수량 변경 · 삭제)을 하나의 store로 묶는 편이 도메인을 이해하기 쉽다고 판단했다. 초기 설정과 보일러플레이트가 적고 persist 적용이 간단해, 개발 속도가 핵심인 이 프로젝트에 맞는다.

Hydration 처리

persist로 저장한 장바구니는 브라우저에만 있어서, 서버가 그린 화면(빈 장바구니)과 브라우저 화면이 달라지는 hydration 불일치가 생길 수 있다. 서버 캐시는 서버 데이터만 다루므로 이 문제를 해결하지 않는다.

persist 옵션: skipHydration: true
 └─ 클라이언트에서 useEffect → useCartStore.persist.rehydrate()

장바구니 store

  • 저장 키 pine-bakery-cart(localStorage). 같은 상품이라도 포장 옵션(선물 포장)이 다르면 별도 줄이다 (구분 기준: 상품 id + giftWrap).
  • store에는 표시용 상품 정보(이름 · 가격 · 이미지 등 Product의 일부)를 스냅샷으로 담는다. 금액은 아래처럼 서버가 다시 계산한다.
  • 쇼핑몰 레이아웃의 한 컴포넌트(CartHydrator)가 마운트 후 한 번 rehydrate()를 호출하고, 수량 배지는 0이면 그리지 않으므로, 서버 렌더(빈 장바구니)와 hydration 직후 화면이 일치하고 그 뒤에 수량이 나타난다.

장바구니 금액

장바구니는 브라우저에 저장되므로 사용자가 가격을 바꿔 보낼 수 있다. 주문 금액은 클라이언트가 보낸 가격이 아니라 서버에서 DB 가격으로 다시 계산한다. (Architecture 참고)


💻 Code
📎 Reference

🖥️ FrontEnd

⚙️ BackEnd

📏 Project

Clone this wiki locally