Skip to content

BackEnd‐Architecture

LeeSongMi edited this page Oct 9, 2026 · 6 revisions

🏗️ Architecture

풀스택 구조

별도 백엔드 서버 없이 Next.js 하나로 쇼핑몰, 관리자 페이지, API를 함께 구현한다.

상품 · 장바구니 · 주문을 다루려면 DB에 접근하는 API가 필요하지만, 백엔드 서버를 따로 두면 구축과 배포에 시간이 더 든다. 8시간 목표 안에 끝내기 위해 Server Component와 Server Action으로 서버 로직을 같은 프로젝트에서 처리한다.


데이터 흐름

작업 방식
조회 Server Component에서 services 호출. DB 조회는 'use cache' + cacheLife + cacheTag로 캐시하거나 <Suspense>로 감싼다 (Next.js 16 Cache Components)
변경 (생성 · 수정 · 삭제) Server Action (Next.js가 POST 엔드포인트를 자동 생성)
입력 검증 Server Action 안에서 Zod로 검증한 뒤 DB 반영

Route Handler 대신 Server Action

  • 외부에 공개할 API가 없다.
  • Server Action은 JSON을 거치지 않아 Date 같은 값이 Prisma 타입 그대로 전달된다. Route Handler + fetch는 JSON 변환 과정에서 Date가 문자열이 되어, Prisma 타입과 실제 값이 어긋난다.
  • Server Action도 화면을 거치지 않고 호출할 수 있는 공개 엔드포인트이므로, 입력 검증과 권한 확인은 반드시 서버에서 한다.

Services 계층

DB 로직은 Server Action 안에 직접 쓰지 않고 src/server/services의 함수로 분리한다. 외부 API가 필요해지면 같은 함수를 Route Handler로 감싸기만 하면 된다.

src/server/services/*  ← Prisma 로직 (쇼핑몰 · 관리자 공용)
 ├─ Server Action   (현재 사용)
 └─ Route Handler   (외부 API가 필요할 때 추가)

폴더 구조와 경계

src/app/(shop)/…, src/app/admin/…   라우트 (얇게: features 컴포넌트를 조합)
src/features/shop/<도메인>/          쇼핑몰 전용 (components · hooks · actions.ts · store.ts)
src/features/admin/<도메인>/         관리자 전용
src/shared/                          공통 (ui · lib · constants · styles)
src/server/                          서버 전용 (db · services · auth)

병렬 작업(여러 Claude 세션)이 서로의 파일을 건드리지 않도록 경계를 ESLint로 강제한다.

  • features/shop ↛ features/admin, features/admin ↛ features/shop
  • shared ↛ features, server
  • 두 영역에서 쓰는 것은 shared/로 올린다.
  • schema.prisma · shared/ui · 토큰은 공통 영역이라 기능 작업 중에는 직접 고치지 않고 별도 PR로 바꾼다.

캐시 (Next.js 16 Cache Components)

  • 상품 조회는 'use cache' + cacheLife("hours") + cacheTag로 캐시한다. 태그: products(상품 전체), product:<slug>(상세), product-sales(판매수가 바뀌면 갱신되는 정렬 목록).
  • searchParams나 쿠키를 읽는 화면(상품 목록, 주문조회)은 요청 시점 값이 필요하므로 <Suspense>로 감싸 부분 렌더링(Partial Prerendering)한다. usePathname을 쓰는 클라이언트 컴포넌트도 같은 이유로 Suspense가 필요하다.
  • 변경 후 캐시 갱신은 Server Action 안에서 updateTag로 한다 (주문 생성 → product-sales).

규칙

  • src/server/** 파일 상단에 import 'server-only'를 둬, 클라이언트 컴포넌트가 실수로 import하면 빌드 에러가 나게 한다.
  • 'use server'는 actions.ts에만 붙인다. export된 함수는 전부 공개 엔드포인트가 되기 때문이다.
  • 클라이언트 컴포넌트는 Prisma 타입을 import type으로만 가져와, Prisma 클라이언트가 브라우저 번들에 포함되지 않게 한다.
  • 검증 실패는 throw 대신 safeParse 결과를 { errors }로 반환한다. 운영 환경에서는 throw한 메시지가 숨겨져 폼에 필드별 오류를 보여줄 수 없다.

인증 없는 공개 관리자 페이지 보호

관리자 페이지는 포트폴리오 확인을 위해 누구나 볼 수 있게 공개하고, 쇼핑몰에서 버튼으로 바로 진입할 수 있게 한다. 하지만 화면에서 버튼을 숨기는 것만으로는 데이터를 보호할 수 없다.

위험 대응
방문자가 상품 · 주문 데이터를 임의로 변경 변경 Server Action마다 서버에서 환경 변수 ADMIN_PASSWORD와 입력 비밀번호를 비교한 뒤 실행
비회원 주문의 개인정보 노출 주문서에 실제 정보 입력 금지 안내, 이름 · 연락처 · 주소를 서버에서 마스킹한 뒤 전달
비밀번호 유출 ADMIN_PASSWORD에 NEXT_PUBLIC_ 접두사를 붙이지 않아 브라우저 코드에 포함되지 않게 함

마스킹은 화면 표시 단계가 아니라 서버의 조회 단계에서 처리한다. 화면에서만 가리면 원본 값이 브라우저 개발자 도구에 그대로 보인다.

비회원 주문조회

  • 주문번호와 주문자 연락처가 모두 일치할 때만 해당 주문 1건의 상세를 보여준다.
  • 조회 실패 시 "주문번호 없음"과 "연락처 불일치"를 구분하지 않고 같은 메시지를 보여준다. 구분하면 다른 사람의 주문번호 존재 여부를 알아낼 수 있다.

주요 흐름

비회원 주문

상품 목록 → 상품 상세 → 장바구니 담기 (Zustand + persist)
 └─ 주문서 작성 (주문자 · 배송 정보)
     └─ 결제창 표시 (실제 결제 없음)
         └─ Server Action: Zod 검증 → DB에서 상품 가격 재조회 → 주문 생성
             └─ 장바구니 비우기 → 주문 완료 → 관리자 페이지에서 주문 확인

장바구니는 브라우저에 저장되므로, 주문 금액은 클라이언트가 보낸 가격이 아니라 DB의 가격으로 계산한다.

관리자 데이터 변경

관리자 폼 입력 + 관리자 비밀번호
 └─ Server Action 호출
     └─ 비밀번호 확인 → Zod 검증 → Prisma로 DB 반영
         └─ updateTag(<tag>) → 서버 캐시 갱신 → 바뀐 목록이 화면에 바로 반영

배포

  • 배포 주소: bakery.im-pine.com
  • Hosting: Vercel (main 브랜치 기준 자동 배포)
  • 배포할 때마다 vercel-build(prisma migrate deploy && next build)가 DB 마이그레이션을 먼저 적용한다. 일반 build에는 마이그레이션을 넣지 않아 로컬 · CI 빌드는 DB를 바꾸지 않는다.
  • 새 DB에는 최초 한 번 시드(상품 · 데모 주문)를 직접 넣고(DATABASE_URL="<배포 DB>" pnpm db:seed) 다시 배포한다. 시드는 자동으로 돌리지 않는다(데이터가 중복될 수 있음).
  • 상품이 하나도 없는 새 DB에서도 첫 배포가 되도록 상품 상세의 generateStaticParams가 비어 있으면 자리표시 주소를 돌려준다. Cache Components는 이 목록이 비면 빌드를 거부하기 때문이다 (Troubleshooting 참고).
  • 로컬 DB는 Docker의 PostgreSQL 컨테이너에 이 프로젝트 전용 DB를 만들어 사용한다.
  • 배포 환경의 DB 연결 정보는 Vercel 환경 변수 DATABASE_URL로 주입하며, 서버리스 환경에서 연결 수가 늘어나지 않도록 pooled 연결 주소를 쓴다.
  • 개발 서버의 hot reload로 Prisma 연결이 쌓이지 않도록 Prisma 클라이언트는 globalThis 싱글톤으로 만든다.

💻 Code
📎 Reference

🖥️ FrontEnd

⚙️ BackEnd

📏 Project

Clone this wiki locally