Next.js로 쇼핑몰 만들기 #1 시작과 설계

8 분 소요

지난 시리즈에서는 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 파일 한 줄로 표현되는 형태입니다.

data/products.json 한 항목 예시
{
  "slug": "minimal-tee-black",
  "name": "미니멀 티셔츠 (블랙)",
  "category": "apparel",
  "price": 24000,
  "description": "100% 면. 무지 디자인. 어디에나 어울립니다.",
  "image": "/images/products/minimal-tee-black.jpg",
  "stock": 42
}

각 필드의 의미:

필드타입설명
slugstringURL 식별자. /products/[slug]slug
namestring상품명
categorystring카테고리 ID (apparel, goods, book 등)
pricenumber원화 금액 (정수, 부가세 포함)
descriptionstring상품 설명 (간단 텍스트)
imagestringpublic/ 기준 이미지 경로
stocknumber재고. 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.json

data/src/app/lib/ 의 분리는 의도된 것입니다. data/콘텐츠 (수정 빈도가 다른 자료), lib/그 데이터를 다루는 코드 입니다. 블로그 시리즈에서 posts/src/app/lib/posts.js 를 분리했던 것과 같은 패턴입니다.

상품 시드 데이터 #

data/products.json 에 상품 몇 개를 넣어둡니다. 카탈로그 화면을 만드는 #2에서 이걸로 화면을 채울 것입니다.

data/products.json
[
  {
    "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에서 그리지만 데이터 접근 함수는 미리 준비해두면 흐름이 끊기지 않습니다.

src/app/lib/products.js
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) — 상품 상세 페이지에서 사용. 없으면 null
  • getAllCategories() — 카테고리 칩 / 사이드바에 사용

블로그의 posts.js 와 다른 점이 한 가지 있습니다. 블로그는 파일을 읽는 fs 작업이 들어갔지만, 여기서는 JSON을 import 합니다. Next.js 가 빌드 타임에 JSON을 자바스크립트 모듈로 변환해주기 때문입니다. fs 를 쓰면 동적 변경(파일 추가)에 반응하지만, 우리 상품 시드는 그렇게 자주 변하지 않으니 import 가 더 단순합니다.

주문 저장소의 첫 윤곽 #

src/app/lib/orders.js 도 윤곽만 잡아둡니다. 실제 채워지는 건 #4 입니다.

src/app/lib/orders.js
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 기본 화면이 보일 뿐입니다. 그래도 한 번 잘 돌아가는지 확인은 해둡니다.

dev 서버
npm run dev

http://localhost:3000 에서 기본 화면이 보이면 OK 입니다. 다음 글부터 진짜 화면을 만들어 갑니다.

마무리 #

이번 글에서는 쇼핑몰 빌드 시리즈의 토대를 다졌습니다.

  • 요구사항을 명확히 적었다 (목록 / 상세 / 장바구니 / 체크아웃 / 주문 완료)
  • 기술 결정을 못 박았다 (App Router / JSON 상품 / Context 장바구니 / 메모리 주문 / 결제 시뮬레이션)
  • 라우트 트리와 컴포넌트 레이아웃을 그렸다
  • 상품 / 주문 데이터 모델을 정했다
  • 프로젝트와 폴더 구조, 상품 시드 데이터를 만들었다
  • products.js / orders.js 유틸 함수의 첫 윤곽을 잡았다

본격적인 화면 작업은 다음 글부터입니다. “Next.js로 쇼핑몰 만들기 #2 상품 카탈로그” 에서는 위에서 만든 getAllProducts / getProductBySlug 를 활용해 /products/products/[slug] 두 페이지를 만들고, 카테고리 필터를 searchParams 로 붙이는 곳까지 가겠습니다.

X