1. 오늘 학습 주제
장바구니 프로젝트를 진행하면서, 실제 서비스 구조와 비슷하게 데이터를 다루는 흐름을 학습했다.
특히 json-server로 상품 데이터를 구성하고, API 요청 로직을 별도 파일로 분리했으며, Zustand의 persist 미들웨어를 사용해 장바구니 상태를 유지하는 구조를 익혔다.
- data.json으로 상품 데이터 구성하기
- json-server를 통해 상품 목록 API 연결하기
- src/api/products.js로 API Layer 분리하기
- Zustand store로 장바구니 전역 상태 관리하기
- persist 미들웨어로 새로고침 후에도 cart 유지하기
- 상품 목록 페이지와 장바구니 페이지의 데이터 흐름 이해하기
2. json-server와 data.json
실제 백엔드 없이도 상품 데이터를 불러오기 위해 json-server를 사용하는 구조를 학습했다.
data.json 파일은 가짜 데이터베이스 역할을 하며, JSON 구조를 기반으로 REST API처럼 동작한다.
{
"products": [
{
"id": 1,
"name": "베이직 티셔츠",
"price": 19900,
"image": "tshirt.jpg"
},
{
"id": 2,
"name": "와이드 데님 팬츠",
"price": 45900,
"image": "denim.jpg"
}
]
}
- data.json은 가짜 데이터 저장소 역할을 한다.
- products 키가 하나의 리소스가 된다.
- 프론트엔드에서는 실제 API처럼 요청을 보내 데이터를 가져올 수 있다.
3. API Layer 분리
처음에는 page.js 안에서 직접 fetch를 사용했지만, 이후 API 요청 로직을 별도 파일로 분리하는 구조를 적용했다.
src/api/products.js에서 상품 목록 요청을 관리했다.
export async function fetchProducts() {
const response = await fetch("http://localhost:3001/products", {
cache: "no-store",
});
if (!response.ok) {
throw new Error("상품 데이터를 불러오지 못했습니다.");
}
return response.json();
}
- API 요청 로직을 별도 파일로 분리하면 유지보수가 쉬워진다.
- 페이지 컴포넌트는 화면 렌더링에 더 집중할 수 있다.
- 나중에 백엔드 주소가 바뀌어도 API 파일만 수정하면 된다.
4. 상품 목록 조회
메인 페이지에서는 fetchProducts()를 호출해 상품 목록을 받아오고, ProductList를 통해 화면에 렌더링했다.
import { fetchProducts } from "@/api/products";
export default async function Home() {
const products = await fetchProducts();
return <ProductList products={products} />;
}
- 상품 목록 데이터는 API Layer를 통해 가져온다.
- 메인 페이지는 가져온 데이터를 컴포넌트에 전달하는 역할을 한다.
- 데이터 요청과 UI 렌더링의 역할을 나누는 것이 중요하다.
5. 장바구니 전역 상태 관리
장바구니는 여러 페이지에서 같은 상태를 공유해야 하므로 Zustand를 사용했다.
useCartStore를 만들어 cart 상태와 관련 함수들을 한 곳에서 관리했다.
export const useCartStore = create(
persist(
(set, get) => ({
cart: [],
addToCart: (product) => {},
removeFromCart: (productId) => {},
increaseQty: (productId) => {},
decreaseQty: (productId) => {},
clearCart: () => set({ cart: [] }),
getTotalItems: () => {},
getTotalPrice: () => {},
}),
{
name: "cart-storage",
}
)
);
- 장바구니 상태는 Zustand store에서 전역으로 관리한다.
- 상품 추가, 삭제, 수량 변경 로직을 store 안에 모아둘 수 있다.
- 여러 컴포넌트에서 같은 상태를 쉽게 공유할 수 있다.
6. addToCart 동작 방식
상품을 장바구니에 담을 때는 무조건 새로 추가하는 것이 아니라, 이미 같은 상품이 있으면 수량만 증가시키도록 구현했다.
const existingItem = state.cart.find((item) => item.id === product.id);
- 같은 상품이 이미 있으면 quantity를 증가시킨다.
- 없는 상품이면 새 항목으로 추가한다.
- 장바구니에서 중복 상품이 여러 줄 생기지 않도록 처리할 수 있다.
7. 수량 증가, 감소, 삭제
장바구니 페이지에서는 각 상품별로 수량 증가, 수량 감소, 삭제 기능을 구현했다.
increaseQty: (productId) =>
set((state) => ({
cart: state.cart.map((item) =>
item.id === productId
? { ...item, quantity: item.quantity + 1 }
: item
),
}))
decreaseQty: (productId) =>
set((state) => ({
cart: state.cart.flatMap((item) => {
if (item.id !== productId) return [item];
if (item.quantity === 1) return [];
return [{ ...item, quantity: item.quantity - 1 }];
}),
}))
- 수량 증가는 해당 상품의 quantity를 1 증가시킨다.
- 수량 감소는 1보다 클 때는 감소, 1일 때는 삭제 처리한다.
- 상품 삭제는 filter()를 사용해 장바구니에서 제거한다.
8. 총 상품 수와 총 결제 금액 계산
장바구니에는 단순 목록만 필요한 것이 아니라, 총 수량과 총 금액 같은 파생값도 필요하다.
이 계산을 컴포넌트가 아니라 store에서 처리하도록 구성했다.
getTotalItems: () =>
get().cart.reduce((total, item) => total + item.quantity, 0),
getTotalPrice: () =>
get().cart.reduce(
(total, item) => total + item.price * item.quantity,
0
),
- 총 수량은 각 상품의 quantity 합으로 계산한다.
- 총 금액은 price * quantity의 합으로 계산한다.
- 계산 로직을 store에 두면 여러 컴포넌트에서 재사용하기 쉽다.
9. Zustand persist 미들웨어
Zustand의 persist 미들웨어를 사용해 장바구니 상태를 브라우저 저장소에 유지하는 방법도 학습했다.
import { persist } from "zustand/middleware";
persist(
(set, get) => ({
cart: [],
}),
{
name: "cart-storage",
}
)
- persist는 Zustand 상태를 localStorage에 저장하는 기능이다.
- 페이지를 새로고침해도 cart 상태를 복원할 수 있다.
- 장바구니, 테마, 로그인 유지 같은 기능에 유용하다.
10. persist의 동작 방식
cart 상태가 바뀔 때마다 브라우저 저장소에도 함께 반영되도록 설정했다.
저장 키 이름은 cart-storage로 지정했다.
- 저장소에 어떤 이름으로 저장할지 직접 정할 수 있다.
- 상태가 바뀌면 브라우저 저장소에도 반영된다.
- 다음에 다시 페이지를 열어도 이전 cart 상태를 유지할 수 있다.
11. 장바구니 페이지 구성
장바구니 페이지는 크게 두 부분으로 나뉜다.
- 상품 목록 영역
- Summary 영역
CartItem에서는 상품명, 가격, 수량, 소계를 보여주고 수량 증가/감소, 삭제 기능을 연결했다.
CartSummary에서는 총 수량, 총 금액, 결제하기 버튼을 보여주도록 구성했다.
- CartItem은 개별 상품 단위의 UI와 동작을 담당한다.
- CartSummary는 전체 합계 정보를 보여준다.
- 기능별로 컴포넌트를 분리하면 역할이 더 명확해진다.
12. 결제하기 버튼과 홈 이동
장바구니 페이지에 홈으로 돌아가기 버튼을 추가하고, 결제하기 버튼을 누르면 총 결제 금액이 alert로 뜨도록 구현했다.
window.alert(`총 결제 금액은 ${totalPrice.toLocaleString("ko-KR")}원입니다.`);
- 홈 버튼으로 메인 페이지와 장바구니 페이지 이동 흐름을 만들 수 있다.
- 결제 버튼은 현재 총 금액이 정상적으로 계산되는지 확인하는 테스트 역할도 한다.
- 기능 구현 후 사용자 동작 흐름까지 점검하는 것이 중요하다.
13. 오늘 학습 흐름 정리
13.1 서버 데이터 연결
- data.json으로 상품 데이터 준비
- json-server로 가짜 API 구성
- src/api/products.js로 요청 함수 분리
- 메인 페이지에서 상품 목록 조회
13.2 클라이언트 상태 관리
- Zustand store로 cart 상태 관리
- persist 미들웨어로 상태 유지
- /carts 페이지에서 상품 목록, 수량 조절, 삭제, 합계 계산 구현
14. 오늘 배운 핵심 정리
- data.json은 json-server에서 가짜 데이터베이스 역할을 한다.
- 상품 데이터는 http://localhost:3001/products로 조회할 수 있다.
- API 요청 로직은 src/api/products.js처럼 별도 파일로 분리하면 좋다.
- 메인 페이지는 API 호출 함수만 가져와 데이터를 렌더링하는 역할을 한다.
- 장바구니 상태는 Zustand store인 useCartStore에서 전역으로 관리한다.
- addToCart는 같은 상품이 있으면 새로 추가하지 않고 수량만 증가시킨다.
- increaseQty, decreaseQty, removeFromCart로 장바구니 조작이 가능하다.
- 총 수량과 총 결제 금액은 store에서 계산하는 파생값이다.
- persist 미들웨어를 사용하면 cart 상태를 localStorage에 저장할 수 있다.
- 새로고침 후에도 장바구니 상태가 유지되는 이유는 persist 덕분이다.
- 기능 구현뿐 아니라 API Layer 분리와 데이터 흐름 설계가 중요하다는 점을 배웠다.
'TIL > [TIL]' 카테고리의 다른 글
| [TIL]Java 기초 문법, 연산자, 메서드와 패키지/임포트 (0) | 2026.04.29 |
|---|---|
| [TIL]Java Literal과 자료형 연산 이해 (0) | 2026.04.28 |
| [TIL]React Query, json-server, Zustand persist, CRUD 테스트 (0) | 2026.04.24 |
| [TIL]Zustand를 활용한 전역 상태 관리와 Immer 개요 (0) | 2026.04.24 |
| [TIL]React Custom Hook과 Next.js App Router (0) | 2026.04.22 |