이 글은 누구를 위한 것인가
- 마케팅팀이 개발자 없이 랜딩 페이지와 캠페인을 직접 만들고 싶은 팀
- 콘텐츠(블로그, 룩북)와 상품을 자연스럽게 연결하고 싶은 이커머스 PM
- 기존 이커머스 플랫폼에 Headless CMS를 추가하는 방법이 필요한 개발자
들어가며
전통적 이커머스는 상품 카탈로그가 중심이다. 하지만 현대 이커머스에서 콘텐츠는 구매 여정의 핵심이다. "이 재킷과 어울리는 스타일 가이드", "성분을 이해하면 더 잘 쓰는 스킨케어 루틴" — 콘텐츠가 구매를 유도한다.
Headless CMS는 마케팅팀에게 자율성을 주고, 개발팀에게는 기술적 자유를 준다. 두 시스템을 제대로 연결하는 것이 핵심이다.
이 글은 bluefoxdev.kr의 콘텐츠 커머스 전략 가이드 를 참고하고, Headless CMS 통합 구현 관점에서 확장하여 작성했습니다.
1. Headless CMS vs 전통 CMS
[전통 이커머스 CMS]
상품 DB ─── 이커머스 플랫폼(Shopify/자체) ─── 단일 프론트엔드
(CMS 기능 내장, 제한적)
[Headless 아키텍처]
Contentful/Sanity (콘텐츠) ─┐
상품 DB / 이커머스 API ─┼─→ GraphQL/REST ─→ Next.js 프론트엔드
검색 (Algolia) ─┘ → 모바일 앱
→ 기타 채널
장점:
✅ 마케팅팀이 코드 없이 페이지 구성
✅ 콘텐츠-상품 연결 유연
✅ 멀티채널 동일 콘텐츠 활용
✅ 성능 최적화 (ISR, CDN)
2. Sanity 스키마 설계
// sanity/schemas/landingPage.ts
export const landingPage = {
name: 'landingPage',
type: 'document',
title: '랜딩 페이지',
fields: [
{ name: 'title', type: 'string', title: '페이지 제목' },
{ name: 'slug', type: 'slug', title: 'URL', options: { source: 'title' } },
{
name: 'sections',
type: 'array',
title: '섹션',
of: [
{ type: 'heroSection' },
{ type: 'productGrid' }, // 상품 진열
{ type: 'editorialContent' }, // 룩북/스타일 가이드
{ type: 'bannerSection' },
],
},
],
};
// 상품 진열 섹션: CMS에서 상품 선택
export const productGrid = {
name: 'productGrid',
type: 'object',
title: '상품 진열',
fields: [
{ name: 'heading', type: 'string', title: '제목' },
{
name: 'productIds',
type: 'array',
title: '상품 ID 목록',
of: [{ type: 'string' }],
description: '이커머스 플랫폼의 상품 ID를 입력하세요',
},
{
name: 'layout',
type: 'string',
title: '레이아웃',
options: {
list: ['2columns', '3columns', '4columns', 'carousel'],
},
},
{
name: 'badge',
type: 'string',
title: '뱃지 (선택)',
description: 'NEW, BEST, SALE 등',
},
],
};
// 에디토리얼 콘텐츠: 룩북, 스타일 가이드
export const editorialContent = {
name: 'editorialContent',
type: 'object',
title: '에디토리얼',
fields: [
{ name: 'headline', type: 'string' },
{ name: 'body', type: 'portableText' },
{
name: 'featuredProduct',
type: 'object',
title: '연관 상품',
fields: [
{ name: 'productId', type: 'string' },
{ name: 'callToAction', type: 'string', title: '버튼 문구' },
],
},
{
name: 'images',
type: 'array',
of: [{ type: 'image', options: { hotspot: true } }],
},
],
};
3. Next.js 통합 구현
3.1 데이터 페칭 레이어
// lib/cms.ts - Sanity 클라이언트
import { createClient } from '@sanity/client';
import imageUrlBuilder from '@sanity/image-url';
export const sanityClient = createClient({
projectId: process.env.SANITY_PROJECT_ID!,
dataset: 'production',
apiVersion: '2026-04-23',
useCdn: true,
});
// lib/commerce.ts - 이커머스 API 클라이언트
async function getProducts(productIds: string[]): Promise<Product[]> {
if (!productIds.length) return [];
const params = new URLSearchParams();
productIds.forEach(id => params.append('ids', id));
const response = await fetch(
`${process.env.COMMERCE_API_URL}/products?${params}`,
{ next: { revalidate: 300 } } // 5분 캐시
);
return response.json();
}
3.2 페이지 렌더링 (ISR)
// app/landing/[slug]/page.tsx
import { sanityClient } from '@/lib/cms';
import { getProducts } from '@/lib/commerce';
async function getLandingPage(slug: string) {
return sanityClient.fetch(`
*[_type == "landingPage" && slug.current == $slug][0] {
title,
sections[] {
_type,
...
}
}
`, { slug });
}
export default async function LandingPage({ params }: { params: { slug: string } }) {
const page = await getLandingPage(params.slug);
if (!page) notFound();
// 페이지 내 모든 상품 ID 수집
const allProductIds = page.sections
.filter((s: any) => s._type === 'productGrid')
.flatMap((s: any) => s.productIds ?? []);
// 상품 데이터 일괄 페칭
const products = await getProducts([...new Set(allProductIds)]);
const productMap = Object.fromEntries(products.map(p => [p.id, p]));
return (
<div>
{page.sections.map((section: any, i: number) => (
<SectionRenderer
key={i}
section={section}
productMap={productMap}
/>
))}
</div>
);
}
// ISR 설정
export const revalidate = 3600; // 1시간마다 재생성
3.3 섹션 렌더러
// components/SectionRenderer.tsx
function SectionRenderer({ section, productMap }: SectionRendererProps) {
switch (section._type) {
case 'heroSection':
return <HeroSection data={section} />;
case 'productGrid':
const sectionProducts = (section.productIds ?? [])
.map((id: string) => productMap[id])
.filter(Boolean);
return (
<ProductGrid
heading={section.heading}
products={sectionProducts}
layout={section.layout}
badge={section.badge}
/>
);
case 'editorialContent':
const featuredProduct = section.featuredProduct?.productId
? productMap[section.featuredProduct.productId]
: null;
return (
<EditorialSection
data={section}
featuredProduct={featuredProduct}
/>
);
default:
return null;
}
}
4. Sanity Studio 커스터마이징
// sanity/components/ProductPicker.tsx
// 편집자가 CMS에서 상품을 검색해서 선택하는 UI
import { useState } from 'react';
import { set, unset } from 'sanity';
export function ProductPicker({ value, onChange }: ProductPickerProps) {
const [query, setQuery] = useState('');
const [results, setResults] = useState<Product[]>([]);
const search = async (q: string) => {
const res = await fetch(`/api/products/search?q=${q}`);
setResults(await res.json());
};
return (
<div>
<input
placeholder="상품명 또는 SKU 검색..."
value={query}
onChange={e => {
setQuery(e.target.value);
search(e.target.value);
}}
/>
{results.map(product => (
<div
key={product.id}
onClick={() => onChange(set([...(value ?? []), product.id]))}
style={{ cursor: 'pointer', padding: '8px', border: '1px solid #eee' }}
>
<img src={product.thumbnail} width={40} />
{product.name} — {product.price.toLocaleString()}원
{value?.includes(product.id) && ' ✓'}
</div>
))}
<div>
<strong>선택된 상품:</strong>
{(value ?? []).map((id: string) => (
<span key={id} onClick={() => onChange(set(value.filter((v: string) => v !== id)))}>
{id} ✕
</span>
))}
</div>
</div>
);
}
5. 콘텐츠-상품 연결 전략
[콘텐츠 타입별 상품 연결 패턴]
1. 룩북 (Editorial)
콘텐츠: 스타일 화보 이미지 + 텍스트
연결: 이미지 내 상품 태그 (핫스팟)
효과: 스타일을 보고 바로 구매
2. 레시피/가이드 (How-to)
콘텐츠: 단계별 가이드
연결: 각 단계에서 필요한 상품 표시
효과: 필요성 인식 → 즉시 구매
3. 캠페인 랜딩 (Landing)
콘텐츠: 마케팅 메시지 + 영상
연결: 메인 상품 + 관련 상품 그리드
효과: 트래픽 → 전환
4. 시즌 큐레이션 (Collection)
콘텐츠: 테마 (여름, 캠핑 등)
연결: 에디터 선택 상품 목록
효과: 브랜드 세계관 경험
마무리
Headless CMS 도입의 가장 큰 가치는 마케팅팀의 자율성이다. 개발자에게 "랜딩 페이지 하나만 만들어줘"라는 요청이 줄어든다.
구현 복잡도를 고려하면, Shopify 기반이라면 Contentful, 자체 구축 플랫폼이라면 Sanity가 통합하기 더 자연스럽다. 어느 쪽이든 콘텐츠-상품 ID 연결 스키마 설계가 핵심이다.