profound-logoProfound CMS
⌘K
Admin
Theme
DocsTutorialBlogPhilosophy
DocsTutorialBlogPhilosophy

Tutorials

Build & Ship an Airport DirectoryDeployments스트라이프 스토어프런트

CMS 기능

Documentation Site TemplateFeature Template BuilderTranslation ServiceOrganizations & Website HeirarchyConnect Profound CMS to your AI clientSettings Integrations설정-API-키Settings UsageSettings Websites
All Systems Operational
Powered Byprofound-logo
Theme

스트라이프 스토어프런트

실습형 안내서: Profound CMS에서 콘텐츠 기반 Stripe 스토어프런트를 구축합니다 — 상인이 코드 없이 편집하는 카탈로그, 두 개의 파라메트릭 라우트, 헤드리스 장바구니, Stripe 호스팅 체크아웃.

동작 중인 완성된 스토어 — 카테고리를 둘러보고, 상품을 열고, 장바구니에 담고, 결제하기.

Profound CMS 위에서 콘텐츠 기반 스토어를 구축하는 실습형 튜토리얼입니다. CMS에 모델링된 상품 카탈로그(카테고리 + 아이템), 하나의 라우트 세트에서 만들어지는 목록 및 상세 페이지, 그리고 헤드리스 컴포넌트로 제공되는 Stripe 호스팅 체크아웃을 다룹니다.

핵심은 손수 작성한 Next.js와 Profound 관리 도구입니다. Claude Code(Profound MCP를 통해)가 세 가지 작업—카탈로그 시드, 디자인 시스템 연결, 스토어프런트 컴포넌트(헤드리스 장바구니 포함) 작성—을 크게 도와줍니다. 세 부분으로 구성됩니다: 설정, 구축, 프로덕션.

한 줄로 끝나는 결제. 우리는 Stripe 호스팅 체크아웃을 사용합니다. 쇼핑객은 당신의 페이지가 아닌 Stripe 페이지에서 결제합니다. 앱이 서버 측에서 하는 일은 단 두 가지—Checkout 세션 생성과 웹훅 하나 검증뿐입니다. 카드 필드도, Stripe Elements도, PCI 부담도 없습니다.

만들게 될 것

  • CMS에 게시된 작은 카탈로그 — 세 개의 카테고리와 여덟 개의 상품(“Edison’s Inventions” 데모) — 상인은 코드 없이 각 항목을 편집할 수 있습니다.
  • 두 개의 파라메트릭 라우트(/products/{item_code}, /categories/{category_code})와 정적 /cart — 모두 동일한 컴포넌트 세트에서 렌더링됩니다.
  • 헤드리스 장바구니(useCart)와 Stripe 호스팅 체크아웃 — 가격은 항상 Stripe Price ID를 통해 서버 측에서 결정됩니다.
  • Vercel에 배포된 스토어 — 팀을 위한 라이브 프리뷰와 페이지 내 편집 지원.

사전 준비물

  • Bun ≥ 1.3 — curl -fsSL https://bun.sh/install | bash
  • Claude Code 및 Profound MCP(1부에서 설치).
  • Profound CMS 계정.
  • Stripe 계정. 튜토리얼은 테스트 모드에서 실행되어 실제 돈이 움직이지 않습니다. 라이브 키로 전환하면 동일한 흐름이 유지되므로 원하는 경우 실제 계정 키를 사용하세요. (테스트 모드는 사업자/계좌 정보가 필요 없습니다.)
  • 로컬 웹훅용 Stripe CLI(stripe login).
  • 배포용: GitHub CLI(gh)와 GitHub에 연결된 Vercel 계정.

구성 요소가 맞물리는 방식

Profound는 콘텐츠와 렌더링을 분리합니다.

  • 컴포넌트는 콘텐츠 구조를 정의합니다. Route Slug 필드가 있는 커스텀 컴포넌트는 라우팅이 가능합니다(category, item). UI Element 태그가 붙은 컴포넌트는 페이지에 배치할 수 있습니다(nav, product_grid, …).
  • 문서는 실제 콘텐츠(상품, 카테고리)입니다.
  • UI 요소는 페이지 섹션입니다. 각 스칼라 필드는 정적 값 또는 렌더 시 평가되는 CEL 표현식을 받을 수 있습니다.
  • 파라메트릭 라우트는 URL을 문서 + UI 요소에 매핑하고, 라우트 파라미터를 전달합니다(CEL에서는 meta.params.*, React에서는 routeParams).
  • Next.js 앱은 cms-renderer를 통해 콘텐츠를 읽고, Stripe는 일반 API 라우트로 추가합니다.

구축을 좌우하는 단 하나의 규칙: CEL은 string/number 필드에만 바인딩됩니다. 따라서 네비게이션 브랜드, 푸터, 헤딩 같은 스칼라 크롬은 CEL로 바꾸고, 리치 콘텐츠나 컬렉션(상품 그리드, 이미지 갤러리, 리치 텍스트)은 React 컴포넌트 안에서 라우트 파라미터를 통해 가져옵니다. 그리고 Stripe가 가격의 단일 진실 공급원입니다. CMS의 price는 표시 용도일 뿐이며, 실제 청구 금액은 항상 Stripe Price ID를 사용해 서버 측에서 결정됩니다.

1부 — 설정

최종 상태: 게시된 작은 카탈로그, 그것을 읽도록 구성된 앱, Stripe 설치, 디자인 적용 — 아직 렌더링은 하지 않습니다.

1. 가입 및 사이트 생성

Profound에 가입하세요(WorkOS 인증). store라는 이름으로 웹사이트를 만든 뒤, 웹사이트 ID(관리자 URL에 있는 UUID)와 읽기 티어 API 키(Deployments → Create API key)를 복사합니다. 앱은 읽기 전용으로만 사용합니다. 이후 카탈로그 시드는 MCP를 통해 진행하며, 별도로 인증합니다.

2. 앱 스캐폴딩, 연결, Stripe 추가

bunx create-profound-next store
cd store
bun add stripe

이 스캐폴드는 Profound에 맞춰 사전 구성된 Next.js App Router 프로젝트입니다(cms-renderer SDK, catch-all 라우트, generate-schemas 스크립트, <Refresher> 포함). 스타일은 없습니다. bun add stripe는 서버용 SDK를 설치합니다 — 호스팅 체크아웃에 필요한 결제 의존성은 이것뿐입니다.

.env.local에 값을 추가합니다.

# CMS
PROFOUND_API_KEY=<your read key>
NEXT_PUBLIC_PROFOUND_WEBSITE_ID=<your website id>
NEXT_PUBLIC_CMS_API_URL=https://cms.dev.tryprofound.com
NEXT_PUBLIC_BUNNY_CDN_URL=https://cms-profound.b-cdn.net   # CMS 호스팅 이미지를 제공

# Stripe
STRIPE_SECRET_KEY=sk_test_...            # 여기서는 테스트 키 사용; 라이브 전환 시 실키로 교체
STRIPE_WEBHOOK_SECRET=whsec_...          # Build 4단계에서 채움
NEXT_PUBLIC_SITE_URL=http://localhost:3000

STRIPE_SECRET_KEY는 Stripe → Developers → API keys에서 가져옵니다. 빌드 과정에서 실제 결제가 발생하지 않도록 테스트 키(sk_test_…)를 사용합니다. 실제 결제를 받으려면 라이브 키로 교체하세요. bun dev를 실행하고 localhost:3000을 열면 시작 화면이 렌더링됩니다.

호스팅 체크아웃은 브라우저를 Stripe URL로 리디렉션하므로 서버 비밀 키만 있으면 됩니다. 퍼블리시어블 키나 클라이언트 Stripe SDK는 필요 없습니다.

3. category, product_image, item 컴포넌트 정의

세 개의 커스텀 컴포넌트를 생성합니다(Components → Create new component). 데이터 소스이므로 UI Element 태그는 붙이지 않습니다. 각 컴포넌트를 Active로 설정합니다.

CMS에는 “이미지 배열” 필드가 없으므로 갤러리는 작은 product_image 컴포넌트를 참조하는 참조 배열로 구현합니다. category와 product_image를 먼저 만들어 Active로 설정한 뒤 item을 생성하세요. 참조 필드는 Active 컴포넌트만 대상으로 할 수 있습니다.

  • category — name(Text), code(Text, Route Slug), description(Rich text), heroImage(Image)
  • product_image — image(Image)
  • item — name(Text), code(Text, Route Slug), description(Rich text), images(참조 배열 → product_image), price(Number, 센트 단위 — 표시용), currency(Select, usd), stripePriceId(Text), category(Reference → category), active(Boolean)

모든 필드는 선택 사항으로 둡니다. 관리자는 필드 이름을 스네이크 케이스로 변환합니다(“Stripe Price Id” → stripe_price_id). 코드에서 참조할 이름이므로 다음 단계에서 generate-schemas를 통해 실제 이름을 확인하세요. 라우트 핸들은 code로 지정합니다(slug 아님). 이는 Route Slug이자 나중에 깔끔한 documents.getByCode 조회 키가 됩니다.

item 컴포넌트 — Route Slug로 지정된 code, product_image를 참조하는 images, stripePriceId, category 참조를 포함합니다.

4. 컴포넌트를 로컬 타입으로 가져오기

bun run generate-schemas

generated/cms-schemas.ts에 Zod 스키마와 타입(categorySchema/Category, itemSchema/Item)을 작성합니다. 연결 확인 용도로도 사용됩니다 — 자격 증명이 잘못되면 여기서 실패합니다.

5. Profound MCP로 카탈로그 시드

MCP를 한 번 설치하고 인증합니다.

claude mcp add --transport http Profound http://107.21.107.99:8081/mcp

mcp__Profound__authenticate를 실행하고 WorkOS 인증 흐름을 완료한 뒤, Claude에게 다음과 같이 요청합니다.

Edison’s Inventions라는 스토어용 작은 이커머스 카탈로그를 만들어 주세요. 세 개의 카테고리와 아래 상품들에 대해, 각 상품마다 당시 분위기에 맞는 짧은 description, 센트 단위의 price, currency: "usd", active: true가 필요합니다.

  • Lighting & Power(code: lighting): Incandescent Lightbulb(incandescent-lightbulb, $24), Electric Dynamo(electric-dynamo, $890), Electric Pen(electric-pen, $49)
  • Sound Recording(code: sound): Tinfoil Phonograph(tinfoil-phonograph, $249), Carbon Microphone(carbon-microphone, $59), Dictaphone(dictaphone, $179)
  • Motion Pictures(code: motion): Kinetoscope(kinetoscope, $399), Kinetograph Camera(kinetograph, $549)

각 카테고리는 name과 소문자 code가 필요합니다. 각 아이템에는 name, 소문자 code, description, price(센트), currency, active가 필요합니다. data/catalog.json에 저장하고 우리의 category 및 item 컴포넌트와 일치하는지 검증하세요. 그런 다음 Profound MCP를 사용해 각 문서를 게시 상태로 생성하세요. 카테고리를 먼저 만들고 ID를 기록한 뒤, 아이템 생성 시 category를 참조로 연결합니다 — { "_type": "reference", "_ref": "<category-id>", "_schema": "category" }. 아이템은 병렬로 생성해도 됩니다.

Claude는 data/catalog.json을 작성하고 검증한 뒤, 병렬 create_document 호출(status: "published")로 문서를 생성합니다. 참조가 이미 존재하는 ID를 가리키도록 카테고리를 먼저 시드하세요.

게시되고 Live 상태인 세 개의 카탈로그 카테고리.

각 카테고리에 연결된 여덟 개의 상품.

6. Stripe 가격 생성 및 일부 상품 연결

카탈로그는 CMS에 준비되었습니다. 이제 몇몇 상품에 실제 Stripe 가격을 연결합니다 — 상인은 두 개의 관리자 패널에서 코드 없이 수행합니다.

  1. Stripe 대시보드 → Products → + Add product, 일시불 가격을 설정하고 Price ID(price_…)를 복사합니다.
  2. 대표 상품 3개 정도(예: Lightbulb, Phonograph, Kinetoscope)에 대해 반복합니다.
  3. Profound 관리자 → item → Documents → 각 Price ID를 stripePriceId에 붙여 넣고 저장합니다.

CMS는 카탈로그를, Stripe는 실제 가격을 보유합니다. 둘은 상인이 붙여 넣는 문자열 하나로 연결됩니다. 자동화를 선호한다면 공식 Stripe MCP로 Product/Price를 생성하고, 반환된 ID를 동일하게 붙여 넣을 수 있습니다.

7. 디자인 시스템 적용 및 AI 연결

스캐폴드는 스타일을 제공하지 않습니다. 프로젝트 루트에 DESIGN.md(Tailwind v4 @theme 블록 + 토큰)를 추가하세요 — 직접 만들거나 refero.design에서 다운로드할 수 있습니다. 그런 다음 Claude에게 스타일링 범위만 지정해 요청합니다.

방금 추가한 디자인 파일을 읽고, 필요하다면 Tailwind를 설정한 뒤 테마와 폰트를 연결해 스타일이 적용되도록 해 주세요. 폰트는 next/font를 사용하고, 런타임에 Google에서 불러오지 마세요. 스타일링만 처리하고 페이지나 컴포넌트는 아직 만들지 마세요.

src/app/globals.css에 @import "tailwindcss";와 @theme 블록이 있는지 확인하고, localhost:3000에서 토큰이 적용되는지 확인하세요. 프롬프트를 간결하게 유지하세요(너무 개방적이면 에이전트가 전체 홈페이지를 스캐폴딩할 수 있습니다). 폰트는 반드시 next/font로 로드하고, 런타임 Google import는 피하세요.

8. 상품 이미지 추가(선택)

선택 사항입니다 — 이미지 없이도 작동하는 결제 흐름을 만들 수 있습니다. 추가하려면: 이미지마다 product_image 문서를 생성하고(image 필드에 업로드), 상품의 images 배열에서 참조합니다. 직접 촬영한 사진을 사용하거나, 이미지 모델로 일관된 세트를 생성하세요(Claude에게 DESIGN.md 기반 프롬프트와 통일된 Midjourney --sref를 지정하면 모든 이미지가 같은 톤을 유지합니다).

독립 실행형 cms-renderer에는 이미지 URL 헬퍼가 없으므로 buildAssetUrl을 src/lib/image.ts에 구현하세요(약 40줄). 이 함수는 NEXT_PUBLIC_BUNNY_CDN_URL을 접두사로 붙이고 확장자를 추가합니다. 2부 3단계의 컴포넌트에서 사용합니다.

2부 — 구축

렌더링 레이어와 체크아웃을 구축해 테스트 모드 실제 구매까지 완료합니다.

1. 다섯 개의 UI 요소 컴포넌트 정의

다섯 개의 컴포넌트를 생성하고 각각 Active로 설정한 뒤 UI Element 태그를 붙입니다(Settings → Tags). Route Slug는 필요 없습니다.

  • nav → brand
  • product_grid → heading
  • product_detail → heading
  • cart_summary → heading
  • footer → text

UI Element 태그가 있어야 페이지 빌더의 Add UI Element 목록에 표시됩니다(Active만으로는 부족합니다). 각 필드는 스칼라이며(CEL이 바인딩할 수 있는 타입), 실제 카탈로그 데이터는 여기서 다루지 않습니다. ProductGrid 및 ProductDetail은 3단계에서 라우트 파라미터를 통해 데이터를 가져옵니다.

2. 타입 재생성

bun run generate-schemas

3. 카탈로그 리더, 컴포넌트, 헤드리스 장바구니 생성

다음 프롬프트 하나로 읽기 헬퍼, 다섯 개 컴포넌트, 장바구니, 레지스트리를 생성합니다.

Profound cms-renderer SDK를 사용해 src/ 아래에 스토어프런트를 구축해 주세요.

src/lib/catalog.ts — 서버 측 CMS 리더입니다. cms-renderer/lib/cms-api에서 getCmsClient({ cmsUrl: process.env.NEXT_PUBLIC_CMS_API_URL!, apiKey: process.env.PROFOUND_API_KEY, websiteId: process.env.NEXT_PUBLIC_PROFOUND_WEBSITE_ID! })로 클라이언트를 만들고, getItemByCode(code)를 내보내세요. 이 함수는 cms.documents.getByCode.query({ websiteId, schemaName: "item", code })를 호출하고 **res.document.published_content**를 반환해야 합니다. listItems(categoryCode?)도 내보내세요. 이는 cms.documents.list.query({ websiteId, schemaName: "item", status: "published", limit: 100 })를 호출하고, res.documents를 .published_content로 매핑한 뒤 active !== false인 아이템만 남깁니다. categoryCode가 전달되면 참조가 해당 카테고리 document.id와 일치하는 아이템만 유지합니다. resolveImages(refs)를 구현해 각 item.images 참조를 cms.documents.get.query({ websiteId, id: ref._ref })로 가져오고, buildAssetUrl(1부 8단계)로 이미지 필드를 URL로 변환하세요.

src/components/ — 다섯 개 UI 요소 컴포넌트를 catch-all 라우트의 레지스트리에 등록합니다. 관리자와 일치하도록 이름은 snake_case로 유지합니다: { nav, product_grid, product_detail, cart_summary, footer }. Nav와 Footer는 content prop에서 스칼라 필드를 읽습니다(타입은 cms-renderer/lib/types의 BlockComponentProps<T>). ProductGrid와 ProductDetail은 async 서버 컴포넌트로, routeParams를 읽고 catalog.ts에서 데이터를 가져옵니다: routeParams.<param>는 { value, … }이므로 .value를 사용하세요. ProductGrid는 listItems(routeParams.category_code?.value)를 호출해 카드 링크를 /products/{code}로 렌더링합니다. ProductDetail은 getItemByCode(routeParams.item_code?.value)를 호출해 갤러리(resolveImages), 리치 텍스트 설명, 가격, 장바구니 담기 버튼을 표시합니다. CartSummary는 useCart로부터 장바구니를 렌더링하고 Pay 버튼을 제공합니다. 가격 포맷터는 순수 함수 src/lib/format.ts로 분리해 클라이언트 컴포넌트가 서버 전용 catalog.ts를 가져오지 않도록 합니다.

src/components/AddToCartButton.tsx — "use client"가 선언된 버튼으로 { code, name, priceLabel }을 받고 useCart().addItem({ code, name, priceLabel, quantity: 1 })를 호출합니다. ProductDetail에서 사용하세요.

src/lib/useCart.ts — 헤드리스 장바구니입니다. 라인 아이템은 { code, name, priceLabel, quantity }를 상태로 보관하고 localStorage에 저장합니다. addItem/removeItem/updateQty/subtotal과 checkout()을 제공합니다. checkout()은 { lines: [{ code, quantity }] }(코드와 수량만 — 가격은 절대 포함하지 않음)을 /api/stripe/checkout으로 POST한 뒤 반환된 url로 리디렉션합니다.

모든 컴포넌트는 디자인 시스템에 맞게, 우리만의 컴포넌트로 스타일링하고, 원본 사이트의 레이아웃을 복사하지 마세요.

프롬프트 실행 후 알아둘 세 가지:

  • 스칼라 크롬은 content에 포함됩니다({ content }: BlockComponentProps<T>). 필드를 최상위 props로 디스트럭처링하면 블록이 비어 있을 수 있습니다. 카탈로그 데이터는 라우트 파라미터 + catalog.ts 호출로 가져옵니다. CEL은 리스트나 갤러리를 바인딩할 수 없기 때문입니다. 장바구니는 상품 코드만 유지하고, 가격은 유지하지 않습니다.
  • routeParams.<param>는 { value, schemaName, document } 형태입니다 — .value를 사용하세요. 조회 결과는 .content가 아닌 **published_content**입니다. 레지스트리 키는 관리자와 일치하도록 snake_case여야 합니다.
  • @types/react/@types/react-dom을 v19로 올리세요 — 스캐폴드는 v18을 사용하며, React 19에서 async 서버 컴포넌트 블록이 오류를 일으킵니다.

4. Stripe 서버 코드 작성(뼈대)

앱에서 필요한 결제 코드는 세 개의 짧은 서버 파일뿐입니다. 모두 getItemByCode를 재사용해 서버 측에서 가격을 결정합니다.

src/lib/stripe.ts:

import Stripe from "stripe";
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

src/app/api/stripe/checkout/route.ts — 각 아이템을 CMS에서 조회하고 Stripe 가격으로 청구합니다.

import { NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
import { getItemByCode } from "@/lib/catalog"; // 서버 측, 읽기 전용 키

export async function POST(req: Request) {
  const { lines } = await req.json();                // [{ code, quantity }] — 클라이언트에서 가격 전달 금지
  const line_items = await Promise.all(
    lines.map(async ({ code, quantity }: { code: string; quantity: number }) => {
      const item = await getItemByCode(code);          // 서버에서 CMS 조회
      return { price: item!.stripe_price_id, quantity }; // 가격은 CMS에서, 클라이언트 아님
    })
  );
  const session = await stripe.checkout.sessions.create({
    mode: "payment",
    line_items,
    success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/cart?status=success`,
    cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL}/cart?status=cancelled`,
  });
  return NextResponse.json({ url: session.url });     // 클라이언트가 여기로 리디렉션
}

src/app/api/stripe/webhook/route.ts — 신뢰할 수 있는 완료 신호입니다.

import { stripe } from "@/lib/stripe";

export async function POST(req: Request) {
  const body = await req.text();                      // RAW 본문 — 서명 검증에 필요
  const sig = req.headers.get("stripe-signature")!;
  let event;
  try {
    event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!);
  } catch {
    return new Response("Bad signature", { status: 400 });
  }
  if (event.type === "checkout.session.completed") {
    // 주문 이행: 주문 기록 / 영수증 발송 등 처리
  }
  return new Response(null, { status: 200 });
}

필수 스캐폴드 수정: 스캐폴드의 src/proxy.ts는 모든 /api/* 요청을 CMS로 전달하므로 Stripe 라우트가 작동하지 않습니다. 우선 해당 라우트가 통과하도록 변경하세요.

import { createCmsProxy } from "cms-renderer/lib/proxy";
import { NextResponse, type NextRequest } from "next/server";
import { cmsConfig } from "@/lib/cms-config";

const cmsProxy = createCmsProxy({ upstream: cmsConfig.cmsUrl });
const LOCAL_API_PREFIXES = ["/api/stripe"];

export const proxy = async (request: NextRequest) => {
  if (LOCAL_API_PREFIXES.some((p) => request.nextUrl.pathname.startsWith(p))) {
    return NextResponse.next();                       // 로컬에서 처리
  }
  return cmsProxy(request as unknown as Parameters<typeof cmsProxy>[0]);
};
// 스캐폴드의 `export const config = { matcher: [...] }`는 그대로 유지

확인: curl -X POST localhost:3000/api/stripe/webhook -d x 실행 시 Bad signature가 반환되어야 합니다.

Stripe CLI로 로컬 웹훅을 실행합니다.

stripe login
stripe listen --forward-to localhost:3000/api/stripe/webhook
# whsec_... 값을 STRIPE_WEBHOOK_SECRET에 복사하고 bun dev 재시작

whsec_…는 세션마다 다릅니다. 보안은 두 가지 규칙으로 보장됩니다. 체크아웃은 CMS에서 code로 가격을 다시 가져오므로 조작된 장바구니가 가격을 바꿀 수 없습니다. 웹훅은 원본(raw) 본문으로 서명을 검증합니다.

5. 라우트 생성

관리자 → Pages → Create page에서 세 번 생성합니다. 각 파라미터를 컴포넌트에 매핑하고 슬러그 필드는 code입니다.

  1. /products/{item_code} → item
  2. /categories/{category_code} → category
  3. /cart — 정적 페이지(문자 그대로 /cart를 입력, /{cart} 아님)

6. UI 요소 추가, CEL 연결, 게시

각 라우트에서 Page Builder → Add UI Element → Custom 순으로 컴포넌트를 추가하고, 스칼라 필드를 채운 뒤 Publish합니다.

  • /products/{item_code}: nav, product_detail, footer
  • /categories/{category_code}: nav, product_grid, footer
  • /cart: nav, cart_summary, footer

nav.brand와 footer.text는 정적 문자열로 설정하고, 헤딩도 정적 라벨을 사용하세요.

카테고리 라우트의 Page Builder — product_grid 요소를 선택하고 헤딩을 CEL로 바인딩한 모습.

상품 라우트의 Page Builder — product_detail 요소가 특정 상품에 바인딩된 상태.

파라메트릭 Page Builder 이슈: 두 파라메트릭 라우트에서 UI 요소를 추가하면 저장되지 않습니다(블록이 고아 상태가 되어 페이지가 비어 보입니다). 문제를 해결하기 전까지는 Profound MCP의 update_page를 사용해 해당 페이지의 block_ids를 직접 연결한 뒤 게시하세요. (정적 /cart는 정상적으로 연결됩니다.) 같은 이유로 ProductGrid는 CEL 대신 가져온 카테고리에서 헤딩을 파생합니다.

7. 렌더링 및 구매 테스트

  • /categories/lighting에서 그리드를 확인합니다. 상품을 클릭하면 상세 페이지, 장바구니 담기 버튼이 표시됩니다. /cart로 이동해 Pay 버튼을 확인하세요.
  • 결제를 진행하면 Stripe 호스팅 체크아웃으로 리디렉션됩니다. 테스트 카드 4242 4242 4242 4242, 임의의 미래 만료일/CVC를 사용하세요. 결제 후 /cart?status=success로 돌아오고, stripe listen에는 checkout.session.completed가 표시됩니다.

렌더링된 상품 페이지 — 갤러리, 가격, 장바구니 담기 버튼.

장바구니 — 라인 아이템과 Stripe 결제 버튼.

가격이 연결된 상품만 구매할 수 있습니다 — 6단계에서 Price ID를 입력한 상품 중 하나를 선택하세요.

선택 사항 — 국제화. 각 컴포넌트를 번역하고(35개 언어를 한 번에), 내장된 language 시스템 컴포넌트에 매핑되는 /{language}/… 세그먼트를 추가한 뒤, CEL로 바인딩된 필드를 documents.translated로 전환하세요. 자세한 내용은 공항 디렉터리 튜토리얼 2부 7단계를 참고하세요.

3부 — 프로덕션

1. 배포: GitHub 후 Vercel

git init && git add -A && git commit -m "Stripe storefront"
gh repo create store --private --source=. --push   # --public도 사용 가능

빌드 커맨드: generated/cms-schemas.ts는 .gitignore에 포함되어 있으므로 빌드 시 재생성이 필요합니다. vercel.json을 추가하세요.

{ "$schema": "https://openapi.vercel.sh/vercel.json", "buildCommand": "bun run generate-schemas && next build" }

Vercel에서 Add New → Project로 store 저장소를 가져오고 환경 변수를 추가합니다. PROFOUND_API_KEY, NEXT_PUBLIC_PROFOUND_WEBSITE_ID, NEXT_PUBLIC_CMS_API_URL, NEXT_PUBLIC_BUNNY_CDN_URL, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET(아래에서 설정할 배포 엔드포인트용 값), NEXT_PUBLIC_SITE_URL(프로덕션 URL)을 설정한 뒤 배포합니다.

그다음 배포된 웹훅을 연결합니다(로컬 stripe listen 비밀키는 로컬 전용). Stripe → Developers → Webhooks → + Add endpoint → https://<prod>/api/stripe/webhook, 이벤트는 checkout.session.completed. 생성된 whsec_…를 Vercel에 추가하고 다시 배포합니다.

환경 변수가 없으면 “로컬에서는 되는데 프로덕션에서는 빈 화면”이 됩니다 — 가장 흔한 배포 오류입니다. 여기서는 테스트 키로 배포하지만, 실제 결제를 받을 준비가 되면 STRIPE_SECRET_KEY와 웹훅 비밀키를 라이브 값으로 교체하세요.

2. 라이브 프리뷰와 페이지 내 편집

두 기능 모두 스캐폴드에 포함되어 있습니다.

  • 라이브 프리뷰: <Refresher>는 관리자가 저장할 때 프리뷰 중인 페이지를 즉시 갱신합니다 — 재배포가 필요 없습니다. (이는 편집자를 위한 프리뷰이며, 일반 방문자는 게시된 콘텐츠를 정상적인 재검증 사이클로 보게 됩니다.)
  • 페이지 내 편집: 어떤 URL이든 ?edit_mode=true를 추가하면 편집 오버레이가 나타납니다. 일반 방문자는 깨끗한 페이지를 봅니다.

스캐폴드에 빠진 프리뷰 라우트를 추가하세요. 관리자는 프리뷰 iframe을 /cms-preview_<path>에 로드합니다. 해당 라우트가 없으면 모든 프리뷰가 404가 됩니다. 다음을 추가하세요.

// src/app/cms-preview_/[...slug]/page.tsx
import { ParametricRoutePreviewPage } from "cms-renderer/lib/renderer";
import { registry } from "../../registry";   // 레지스트리를 공유 모듈로 추출
export default async function Page({ params, searchParams }) {
  const { slug } = await params;
 const PreviewPage = ParametricRoutePreviewPage as any; // async RSC; React 19 타입
  return <PreviewPage registry={registry} apiKey={process.env.PROFOUND_API_KEY ?? ""}
    websiteId={process.env.NEXT_PUBLIC_PROFOUND_WEBSITE_ID ?? ""}
    cmsUrl={process.env.NEXT_PUBLIC_CMS_API_URL ?? "https://cms.dev.tryprofound.com"}
    params={Promise.resolve({ slug })} searchParams={searchParams} />;
}

세그먼트 루트용 src/app/cms-preview_/page.tsx도 추가하세요(동일하되 slug: []).

정리

콘텐츠 기반 Stripe 스토어프런트가 완성되었습니다. CMS 카탈로그, 하나의 컴포넌트 세트로 구현한 목록 + 상세 페이지, 그리고 실제로 동작하는 호스팅 체크아웃입니다. AI가 카탈로그를 시드하고, 디자인을 연결하고, 카탈로그 리더 + 컴포넌트 + 헤드리스 장바구니까지 작성했습니다. 우리는 컴포넌트 배치, Stripe Price 연결, 세 개의 라우트 구성, CEL 크롬, 그리고 세 개의 짧은 Stripe 파일만 추가하면 됩니다. CEL은 크롬을 바인딩하고, 컴포넌트는 카탈로그를 가져옵니다. Stripe는 작고 단순하게 유지됩니다 — sessions.create 한 번과 서명된 웹훅 하나뿐이며, 쇼핑객은 Stripe 페이지에서 결제합니다.

Continue Reading
Previous‹Deployments