
CMS란?
코딩없이 콘텐츠를 생성, 조회, 수정 및 삭제(CRUD)할 수 있게 해주는 도구입니다.
CMS 없이 직접 파일로 관리한다면,
- 글 하나를 작성하거나 사소한 오타를 수정하기 위해서도
git push및 배포를 반복해야 함
CMS로 콘텐츠를 관리한다면,
- 실제 포스팅 데이터(body)와 실제로 보여지는 템플릿(head)을 분리하여 컨텐츠 관리
- 웹사이트 개발, 배포와 상관없이 비개발자도 손쉽게 콘텐츠를 관리
CMS vs headless CMS
두 가지의 차이는 내부 동작의 차이에 있습니다.
CMS
- UI와 데이터가 php페이지에 바로 html 렌더링
- 사용자에게 보이는 화면과 데이터를 모두 관리
headless CMS
- 데이터가 UI와 완전히 분리된 곳에 존재
- 쿼리와 api로 데이터 호출
- 콘텐츠 하나로 웹, 앱, 다른 플랫폼까지 그대로 재사용 가능
sanity 설정
- sanity.io로그인 > Studios & Apps > New project 클릭

2. 프로젝트 이름 입력 후 Create Project

3. sanity cli 다운로드 및 sanity 스튜디오 생성

4. npm run dev 로 sanity 스튜디오 실행되는지 확인

5. 도큐먼트 타입 생성

5. npm run dev sanity 스튜디오 재실행

스튜디오를 실행하면 지정한 도큐먼트 타입에 따라 데이터를 입력할 수 있습니다.
데이터를 모두 입력 후 publish하면 sanity에 콘텐츠를 저장할 수 있습니다.
Next.js에서 Sanity 데이터 불러오기
- 로컬에서 sanity 스튜디오 실행

2. 새로운 Next.js 프로젝트 생성 및 sanity사용을 위한 dependency 다운로드

3. Next.js 프로젝트에 sanity 설정 추가

4. 글 목록 화면 - 쿼리를 통해 sanity에 저장된 데이터 fetch 및 렌더링

5. 글 상세 화면 - 포스트 화면에 저장된 상세 데이터 fetch 및 렌더링

이미지 최적화하기
이미지 최적화란?
| 구분 | 직접 관리할 때 | Sanity(@sanity/image-url)로 관리할 때 |
|---|---|---|
| 리사이즈 | 업로드 전 직접 리사이즈 필요 | 요청 시점에 CDN이 자동 처리 |
| 포맷 변환 | webp 등으로 직접 변환 필요 | CDN이 요청 시 자동 변환 |
| 사이즈별 관리 | 화면마다 이미지를 여러 개 만들어 저장 | 원본 하나만 업로드, width·height·crop 값만 지정해 그때그때 요청 |
next/image 기본 최적화 vs CDN 위임
두 방식의 차이는 리사이즈를 누가, 언제 처리하느냐에 있습니다.
next/image 기본 최적화
- Next.js 서버가 이미지 요청마다 직접 리사이즈 처리
- 외부 CDN 이미지를 쓰려면
remotePatterns같은 별도 설정 필요
CDN 위임 (커스텀 로더)
- 리사이즈 자체를 Sanity CDN에 그대로 위임
auto=format파라미터로 브라우저에 맞는 포맷(webp/avif)을 CDN이 자동 선택hotspot기준으로 크롭 위치까지 CMS 단에서 미리 지정 가능
Next.js에 이미지 최적화 적용하기
@sanity/image-urldependency 다운로드
npm install @sanity/image-url2. urlFor 빌더 함수 작성 — 이미지 소스를 URL로 변환
// lib/sanity/image.ts
import { createImageUrlBuilder } from '@sanity/image-url';
import type { SanityImageSource } from '@sanity/image-url';
import { client } from './client';
const builder = createImageUrlBuilder(client);
export function urlFor(source: SanityImageSource) {
return builder.image(source);
}3. 커스텀 이미지 로더 작성 — width, quality 값을 CDN 쿼리스트링으로 매핑
// lib/sanity/image-loader.ts
'use client';
interface SanityLoaderArgs {
src: string;
width: number;
quality?: number;
}
export default function sanityImageLoader({ src, width, quality }: SanityLoaderArgs) {
const url = new URL(src);
url.searchParams.set('auto', 'format');
if (!url.searchParams.has('fit')) {
url.searchParams.set('fit', 'max');
}
url.searchParams.set('w', width.toString());
url.searchParams.set('q', (quality || 75).toString());
return url.href;
}4. next/image의 loader prop에 커스텀 로더 연결
// components/PostCard.tsx
import Image from 'next/image';
import { urlFor } from '@/lib/sanity/image';
import sanityImageLoader from '@/lib/sanity/image-loader';
<Image
src={urlFor(post.thumbnail).width(800).height(450).fit('crop').url()}
loader={sanityImageLoader}
alt={post.thumbnail.alt ?? post.title}
fill
/>5. 화면(썸네일, 목록 카드, 본문 이미지)마다 width·height·fit 값을 다르게 지정해 렌더링
// 포스트 상세 페이지 썸네일 — app/post/[...slug]/page.tsx
src={urlFor(post.thumbnail).width(1200).height(630).fit('crop').url()}
// 목록 카드 썸네일 — components/PostCard.tsx
src={urlFor(post.thumbnail).width(800).height(450).fit('crop').url()}
// 본문 삽입 이미지 — components/portable-text/PostBody.tsx
src={urlFor(value).width(1200).fit('max').url()}Webhook으로 콘텐츠 자동 재배포하기
이미지까지 최적화했다면, 이제 마지막으로 남는 문제가 하나 있습니다. Sanity Studio에서 콘텐츠를 수정해도 화면에는 바로 반영되지 않는다는 점입니다. Next.js가 빌드 시점에 데이터를 가져와 정적으로 페이지를 생성해두기 때문인데, 이 문제를 해결하는 방법이 Webhook과 ISR(Incremental Static Regeneration) 연동입니다.
왜 콘텐츠가 바로 반영되지 않을까?
Next.js는 성능을 위해 페이지를 빌드 시점에 미리 HTML로 만들어둡니다. 문제는 콘텐츠가 그 이후에 바뀌었을 때입니다.
- 재배포 없이 방치 — 콘텐츠를 수정해도 캐시된 예전 페이지가 계속 보임
- 매번 전체 재배포 — 콘텐츠 하나 고칠 때마다 사이트 전체를 다시 빌드해야 해서 비효율적
- Webhook + ISR 연동 — 콘텐츠가 바뀐 페이지만 감지해서 필요한 부분만 재생성
전체 재배포 vs On-demand ISR
두 방식의 차이는 재생성 범위를 얼마나 좁힐 수 있느냐에 있습니다.
전체 재배포 (Deploy Hook)
- Sanity에서 발행 이벤트 발생 시 Vercel 전체 재배포 트리거
- 설정이 간단하지만, 사이트 규모가 커질수록 재배포 시간이 길어짐
- 트래픽이 많은 시간대에 재배포가 겹치면 순간적으로 부하 발생 가능
On-demand ISR (revalidatePath / revalidateTag)
- 변경된 콘텐츠에 해당하는 페이지만 캐시 무효화
- 나머지 페이지는 그대로 캐시 유지 → 재배포 시간과 서버 부하 최소화
- 페이지 단위(
revalidatePath) 또는 태그 단위(revalidateTag)로 세밀하게 제어 가능
Next.js에 Webhook 자동화 적용하기
- Sanity 프로젝트 설정 → API → Webhooks에서 콘텐츠 변경 감지 시 호출할 API 라우트 URL 등록

2. Next.js에 Webhook을 받을 API 라우트 작성 (/app/api/revalidate/route.ts)
import { revalidatePath } from 'next/cache';
import { type NextRequest, NextResponse } from 'next/server';
import { parseBody } from 'next-sanity/webhook';
// Sanity 웹훅 Projection 예시: { "_type": _type, "slug": slug.current }
interface WebhookPayload {
_type: string;
slug?: string;
}
export async function POST(req: NextRequest) {
const secret = process.env.SANITY_REVALIDATE_SECRET;
if (!secret) {
return NextResponse.json({ message: 'Secret이 설정되지 않았습니다' }, { status: 500 });
}
// Sanity가 보낸 요청의 서명을 secret으로 검증
const { isValidSignature, body } = await parseBody<WebhookPayload>(req, secret);
if (!isValidSignature) {
return NextResponse.json({ message: '유효하지 않은 요청입니다' }, { status: 401 });
}
if (body?._type !== 'post') {
return NextResponse.json({ revalidated: false });
}
// 홈과 해당 글 상세 페이지만 캐시 무효화
revalidatePath('/');
if (body.slug) {
revalidatePath(`/post/${body.slug}`);
}
return NextResponse.json({ revalidated: true });
}- 요청 payload에서 어떤 문서(
_type,slug)가 변경됐는지 파싱 - 파싱한 정보를 바탕으로
revalidatePath()또는revalidateTag()호출 - Sanity Webhook의 시크릿 토큰을 검증 로직에 추가해 무단 요청 차단