Repository navigation
BackEnd‐Architecture
LeeSongMi edited this page Oct 9, 2026
·
6 revisions
별도 백엔드 서버 없이 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 반영 |
- 외부에 공개할 API가 없다.
- Server Action은 JSON을 거치지 않아
Date같은 값이 Prisma 타입 그대로 전달된다. Route Handler +fetch는 JSON 변환 과정에서Date가 문자열이 되어, Prisma 타입과 실제 값이 어긋난다. - Server Action도 화면을 거치지 않고 호출할 수 있는 공개 엔드포인트이므로, 입력 검증과 권한 확인은 반드시 서버에서 한다.
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로 바꾼다.
- 상품 조회는
'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
- 🎨 Colors
- ✏️ Design
- 🧩 Components
- 🔀 State
- 🏗️ Architecture
- 🗃️ Data
- ⏱️ Time
- 🤖 Figma MCP
- 🧪 Testing
- 🚀 Performance
- 🛠️ Troubleshooting