Next.js로 쇼핑몰 만들기 #1 시작과 설계
지난 시리즈에서는 Next.js로 블로그를 만들었습니다. 그 마지막 글에서 다음 도전으로 쇼핑몰을 권했는데, 이번 시리즈가 바로 그것입니다. 같은 App Router 위에서 출발하지만, 다루는 문제의 결이 다릅니다. 블로그는 읽기가 중심이었고, 쇼핑몰은 사용자 상태(장바구니), 트랜잭션(주문 생성), **결과의 영속(주문 조회)**까지 다뤄야 합니다. Server Components와 Server Actions의 분담이 가장 자연스럽게 드러나는 주제입니다.
5편으로 나눠 점진적으로 쌓아갑니다.
- #1 시작과 설계 ← 이번 글
- #2 상품 카탈로그 (목록 / 상세 / 필터)
- #3 장바구니 (Client Context + localStorage)
- #4 체크아웃과 결제 시뮬레이션
- #5 주문 완료 페이지와 배포
요구사항 정의 #
코드를 짜기 전에 우리가 만들 쇼핑몰이 무엇을 할 수 있어야 하는지 적어둡니다.
핵심 기능
- 상품 목록 페이지 (전체 / 카테고리 필터)
- 상품 상세 페이지 (이미지, 설명, 가격, 장바구니에 담기)
- 장바구니 페이지 (수량 변경, 항목 삭제, 합계 표시)
- 체크아웃 페이지 (배송 정보 입력, 결제 진행)
- 주문 완료 페이지 (주문 번호, 주문 내역 확인)
비기능 요구사항
- 새로고침해도 장바구니 유지 (localStorage)
- 결제 실패 시 사용자에게 알리고 다시 시도 가능
- 모바일에서도 사용 가능한 레이아웃
- 검색 엔진 친화적 URL 구조
기술 결정 (앞에서 정해두면 흔들림 없음) #
블로그 시리즈에서 했던 것처럼 큰 결정을 먼저 못 박아둡니다. 매 글에서 “이건 어떻게?“라고 다시 묻지 않게 하기 위함입니다.
- 프레임워크: Next.js App Router (Server Components / Server Actions)
- 상품 데이터: JSON 파일 (DB 없음. 학습 단순화)
- 장바구니: Client Context + localStorage (서버 세션 없음)
- 주문 저장소: 모듈 스코프 메모리 Map (서버 재시작 시 초기화. 실서비스는 DB가 필요하지만 학습 단순화)
- 결제: 시뮬레이션 (외부 PG 연동 없이 가짜 결과를 80% 성공 / 20% 실패로 분기)
- 배포: Vercel
각 결정의 이유를 한 줄씩 정리합니다.
| 결정 | 이유 |
|---|---|
| App Router | 블로그 시리즈와 동일 패러다임 유지. RSC + Actions가 가장 빛나는 영역 |
| JSON 상품 데이터 | DB 셋업 부담을 학습에서 떼어내고 라우팅,렌더,폼 로직에 집중 |
| Context + localStorage | 비로그인 쇼핑몰의 표준 패턴. 서버 세션 도입은 인증 영역까지 끌어들임 |
| 메모리 주문 저장소 | “주문이 만들어지고 결과 페이지에서 보인다"는 흐름을 단순한 코드로 보여줌 |
| 결제 시뮬레이션 | 외부 PG 가입,키 발급,웹훅 셋업 없이 결제 플로우의 분기를 학습 |
실서비스에서는 어디서 어떤 도구를 끼워 넣어야 하는지 마지막 글에서 짚어두겠습니다.
컴포넌트와 라우트 설계 #
라우트 트리를 먼저 그려보면 폴더 구조가 자연스럽게 결정됩니다.
/ ← 홈 (베스트셀러 / 카테고리 입구)
/products ← 상품 목록 (?category=... 로 필터)
/products/[slug] ← 상품 상세
/cart ← 장바구니
/checkout ← 배송 정보 + 결제
/orders/[orderId] ← 주문 완료 / 조회/products?category=apparel 처럼 카테고리는 searchParams 로 표현합니다. 동적 폴더 [category] 대신 쿼리스트링을 쓰는 이유는, 카테고리는 “검색 필터의 한 종류"에 가깝고 가격대,정렬 같은 다른 필터가 추가될 때 자연스럽게 확장되기 때문입니다. 블로그 시리즈 #3에서 태그는 /tags/[tag] 동적 폴더로 갔던 것과 대비됩니다 — 거기는 “태그 그 자체가 페이지"였지만, 쇼핑몰의 카테고리는 “상품 목록을 좁히는 조건"입니다.
컴포넌트 트리는 페이지마다 다르지만 큰 그림은 다음과 같습니다.
RootLayout
└── CartProvider — 장바구니 상태(Client Context)
├── Header — 로고 / 카테고리 링크 / 장바구니 아이콘(수량 뱃지)
├── {children} — 각 페이지
└── Footer장바구니 상태는 거의 모든 페이지의 Header에서 참조하므로(아이콘 뱃지) RootLayout 바로 아래에 Provider를 두는 게 자연스럽습니다.
상품 데이터 모델 #
각 상품이 가질 정보를 정해둡니다. JSON 파일 한 줄로 표현되는 형태입니다.
{
"slug": "minimal-tee-black",
"name": "미니멀 티셔츠 (블랙)",
"category": "apparel",
"price": 24000,
"description": "100% 면. 무지 디자인. 어디에나 어울립니다.",
"image": "/images/products/minimal-tee-black.jpg",
"stock": 42
}각 필드의 의미:
| 필드 | 타입 | 설명 |
|---|---|---|
slug | string | URL 식별자. /products/[slug] 의 slug |
name | string | 상품명 |
category | string | 카테고리 ID (apparel, goods, book 등) |
price | number | 원화 금액 (정수, 부가세 포함) |
description | string | 상품 설명 (간단 텍스트) |
image | string | public/ 기준 이미지 경로 |
stock | number | 재고. 0이면 품절 표시 |
블로그의 frontmatter처럼 데이터의 모양을 먼저 못 박는 것이 코드 작성을 훨씬 수월하게 만듭니다.
주문 데이터 모델 #
주문은 결제가 완료되는 순간 만들어집니다. 형태는 다음과 같습니다.
{
id: 'ord_1748124000_a8c2',
createdAt: '2026-05-25T10:00:00.000Z',
items: [
{ slug: 'minimal-tee-black', name: '미니멀 티셔츠 (블랙)', price: 24000, quantity: 2 },
{ slug: 'mug-white', name: '머그컵 (화이트)', price: 12000, quantity: 1 },
],
total: 60000,
shipping: {
name: '홍길동',
phone: '010-1234-5678',
address: '서울시 어딘가 123',
},
status: 'paid',
}id 는 결제 직후 서버에서 발급. status 는 일단 paid 하나만 다루지만, 실서비스라면 pending / failed / shipped 같은 상태가 추가됩니다.
프로젝트 생성 #
이제 진짜 코드로 들어갑니다.
npx create-next-app@latest my-shop
cd my-shop옵션은 App Router, JavaScript, src 디렉터리, Tailwind 사용으로 진행합니다. 블로그 시리즈와 동일한 옵션이며, Tailwind는 카탈로그 화면을 빠르게 다듬는 데 유용합니다.
폴더 구조 #
처음부터 큰 그림으로 폴더를 잡아두면 글마다 옮겨 다닐 일이 줄어듭니다.
my-shop/
├── data/
│ └── products.json ← 상품 시드 데이터
├── public/
│ └── images/
│ └── products/ ← 상품 이미지
├── src/
│ └── app/
│ ├── layout.js ← RootLayout + CartProvider
│ ├── page.js ← '/' 홈
│ ├── products/
│ │ ├── page.js ← '/products' 목록
│ │ └── [slug]/
│ │ └── page.js ← '/products/[slug]' 상세
│ ├── cart/
│ │ └── page.js ← '/cart' 장바구니
│ ├── checkout/
│ │ └── page.js ← '/checkout' 체크아웃
│ ├── orders/
│ │ └── [orderId]/
│ │ └── page.js ← '/orders/[orderId]' 주문 완료
│ └── lib/
│ ├── products.js ← 상품 조회 유틸
│ └── orders.js ← 주문 저장/조회 유틸
└── package.jsondata/ 와 src/app/lib/ 의 분리는 의도된 것입니다. data/ 는 콘텐츠 (수정 빈도가 다른 자료), lib/ 는 그 데이터를 다루는 코드 입니다. 블로그 시리즈에서 posts/ 와 src/app/lib/posts.js 를 분리했던 것과 같은 패턴입니다.
상품 시드 데이터 #
data/products.json 에 상품 몇 개를 넣어둡니다. 카탈로그 화면을 만드는 #2에서 이걸로 화면을 채울 것입니다.
[
{
"slug": "minimal-tee-black",
"name": "미니멀 티셔츠 (블랙)",
"category": "apparel",
"price": 24000,
"description": "100% 면. 무지 디자인. 어디에나 어울립니다.",
"image": "/images/products/minimal-tee-black.jpg",
"stock": 42
},
{
"slug": "minimal-tee-white",
"name": "미니멀 티셔츠 (화이트)",
"category": "apparel",
"price": 24000,
"description": "블랙과 같은 원단의 화이트 컬러.",
"image": "/images/products/minimal-tee-white.jpg",
"stock": 38
},
{
"slug": "mug-white",
"name": "머그컵 (화이트)",
"category": "goods",
"price": 12000,
"description": "두툼한 도자기 머그. 식기세척기 사용 가능.",
"image": "/images/products/mug-white.jpg",
"stock": 80
},
{
"slug": "tote-canvas",
"name": "캔버스 토트백",
"category": "goods",
"price": 18000,
"description": "데일리용 캔버스 토트백. A4가 여유롭게 들어갑니다.",
"image": "/images/products/tote-canvas.jpg",
"stock": 25
},
{
"slug": "react-book",
"name": "리액트 실전 가이드",
"category": "book",
"price": 32000,
"description": "리액트와 Next.js 실전 패턴을 한 권에 정리한 가이드.",
"image": "/images/products/react-book.jpg",
"stock": 0
},
{
"slug": "ts-book",
"name": "타입스크립트 입문서",
"category": "book",
"price": 28000,
"description": "타입스크립트의 기초부터 제네릭까지.",
"image": "/images/products/ts-book.jpg",
"stock": 15
}
]react-book 은 일부러 stock: 0 으로 두었습니다. 품절 표시가 카탈로그에서 어떻게 동작하는지 보기 위한 의도된 케이스입니다.
이미지 파일이 아직 없어도 괜찮습니다. public/images/products/ 폴더만 만들어두고, 자기 프로젝트라면 적당한 이미지를 넣어주세요. 학습 목적이라면 Unsplash 같은 곳에서 받아 적당한 이름으로 저장하면 됩니다. 이미지가 없으면 #2에서 만들 next/image 가 placeholder 를 보여줍니다.
상품 조회 유틸의 첫 윤곽 #
src/app/lib/products.js 에 상품을 읽고 조회하는 함수들의 첫 윤곽을 만들어둡니다. 실제 화면은 #2에서 그리지만 데이터 접근 함수는 미리 준비해두면 흐름이 끊기지 않습니다.
import products from '../../../data/products.json';
export function getAllProducts() {
return products;
}
export function getProductsByCategory(category) {
if (!category) return products;
return products.filter((p) => p.category === category);
}
export function getProductBySlug(slug) {
return products.find((p) => p.slug === slug) ?? null;
}
export function getAllCategories() {
return Array.from(new Set(products.map((p) => p.category)));
}각 함수의 역할:
getAllProducts()— 전체 상품 배열getProductsByCategory(category)— 카테고리로 필터. 인자가 없으면 전체getProductBySlug(slug)— 상품 상세 페이지에서 사용. 없으면nullgetAllCategories()— 카테고리 칩 / 사이드바에 사용
블로그의 posts.js 와 다른 점이 한 가지 있습니다. 블로그는 파일을 읽는 fs 작업이 들어갔지만, 여기서는 JSON을 import 합니다. Next.js 가 빌드 타임에 JSON을 자바스크립트 모듈로 변환해주기 때문입니다. fs 를 쓰면 동적 변경(파일 추가)에 반응하지만, 우리 상품 시드는 그렇게 자주 변하지 않으니 import 가 더 단순합니다.
주문 저장소의 첫 윤곽 #
src/app/lib/orders.js 도 윤곽만 잡아둡니다. 실제 채워지는 건 #4 입니다.
const orders = new Map();
export function saveOrder(order) {
orders.set(order.id, order);
}
export function getOrder(orderId) {
return orders.get(orderId) ?? null;
}모듈 스코프의 Map 하나. 서버가 살아있는 동안만 유지되고, 재시작하면 사라집니다. 학습에는 충분하지만 실서비스라면 DB 가 필요합니다. 마지막 글에서 이걸 어디로 옮겨야 하는지 짚어두겠습니다.
동작 확인 #
지금 단계에서는 페이지를 손대지 않았으니 dev 서버를 띄워도 Next.js 기본 화면이 보일 뿐입니다. 그래도 한 번 잘 돌아가는지 확인은 해둡니다.
npm run devhttp://localhost:3000 에서 기본 화면이 보이면 OK 입니다. 다음 글부터 진짜 화면을 만들어 갑니다.
마무리 #
이번 글에서는 쇼핑몰 빌드 시리즈의 토대를 다졌습니다.
- 요구사항을 명확히 적었다 (목록 / 상세 / 장바구니 / 체크아웃 / 주문 완료)
- 기술 결정을 못 박았다 (App Router / JSON 상품 / Context 장바구니 / 메모리 주문 / 결제 시뮬레이션)
- 라우트 트리와 컴포넌트 레이아웃을 그렸다
- 상품 / 주문 데이터 모델을 정했다
- 프로젝트와 폴더 구조, 상품 시드 데이터를 만들었다
products.js/orders.js유틸 함수의 첫 윤곽을 잡았다
본격적인 화면 작업은 다음 글부터입니다. “Next.js로 쇼핑몰 만들기 #2 상품 카탈로그” 에서는 위에서 만든 getAllProducts / getProductBySlug 를 활용해 /products 와 /products/[slug] 두 페이지를 만들고, 카테고리 필터를 searchParams 로 붙이는 곳까지 가겠습니다.