Sanity CMS 완벽 가이드 | Headless CMS·구조화된 콘텐츠·GROQ·실시간·실전 활용
이 글의 핵심
Sanity CMS로 유연한 콘텐츠 관리를 구현하는 완벽 가이드. Schema 정의, GROQ 쿼리, 실시간 업데이트, Next.js 통합까지 실전 예제로 정리. Sanity·CMS·Headless CMS 중심으로 설명합니다.
이 글의 핵심
Sanity CMS로 유연한 콘텐츠 관리를 구현하는 완벽 가이드입니다. Schema 정의, GROQ 쿼리, 실시간 업데이트, Next.js 통합까지 실전 예제로 정리했습니다.
실무 경험 공유: WordPress에서 Sanity로 전환하면서, 콘텐츠 관리가 자유로워지고 개발자 경험이 크게 향상된 경험을 공유합니다.
들어가며: “CMS가 불편해요”
실무에서 마주치는 문제들
스키마가 고정돼 있어요
WordPress는 제한적입니다. Sanity는 완전히 커스터마이징 가능합니다. 개발자 경험이 나빠요
기존 CMS는 불편합니다. Sanity는 개발자 친화적입니다. 실시간 협업이 필요해요
동시 편집이 어렵습니다. Sanity는 실시간 협업을 지원합니다.
1. Sanity란?
핵심 특징
Sanity는 구조화된 콘텐츠 플랫폼입니다. 주요 장점
- 유연한 스키마: 완전히 커스터마이징
- GROQ: 강력한 쿼리 언어
- 실시간: 실시간 업데이트
- Portable Text: 리치 텍스트
- 이미지 처리: 자동 최적화
2. 프로젝트 설정
설치
npm create sanity@latest
프로젝트 구조
my-sanity-project/
├── sanity/
│ ├── schemas/
│ │ ├── post.ts
│ │ └── author.ts
│ ├── sanity.config.ts
│ └── sanity.cli.ts
└── app/
3. Schema 정의
Post Schema
// sanity/schemas/post.ts
import { defineField, defineType } from 'sanity';
export default defineType({
name: 'post',
title: 'Post',
type: 'document',
fields: [
defineField({
name: 'title',
title: 'Title',
type: 'string',
validation: (Rule) => Rule.required().min(10).max(100),
}),
defineField({
name: 'slug',
title: 'Slug',
type: 'slug',
options: {
source: 'title',
maxLength: 96,
},
validation: (Rule) => Rule.required(),
}),
defineField({
name: 'author',
title: 'Author',
type: 'reference',
to: [{ type: 'author' }],
}),
defineField({
name: 'mainImage',
title: 'Main image',
type: 'image',
options: {
hotspot: true,
},
}),
defineField({
name: 'categories',
title: 'Categories',
type: 'array',
of: [{ type: 'reference', to: { type: 'category' } }],
}),
defineField({
name: 'publishedAt',
title: 'Published at',
type: 'datetime',
}),
defineField({
name: 'body',
title: 'Body',
type: 'blockContent',
}),
],
});
4. GROQ 쿼리
기본 쿼리
// lib/sanity.ts
import { createClient } from '@sanity/client';
export const client = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
apiVersion: '2024-01-01',
useCdn: true,
});
// 모든 포스트
const posts = await client.fetch(`*[_type == "post"]`);
// 필터링
const publishedPosts = await client.fetch(`
*[_type == "post" && publishedAt < now()] | order(publishedAt desc)
`);
// 특정 필드만
const posts = await client.fetch(`
*[_type == "post"] {
title,
slug,
publishedAt
}
`);
Join (Reference)
const postsWithAuthor = await client.fetch(`
*[_type == "post"] {
title,
slug,
author->{
name,
image
}
}
`);
파라미터
const post = await client.fetch(
`*[_type == "post" && slug.current == $slug][0] {
title,
body,
author->{name}
}`,
{ slug: 'my-post' }
);
5. Next.js 통합
포스트 목록
// app/blog/page.tsx
import { client } from '@/lib/sanity';
async function getPosts() {
return await client.fetch(`
*[_type == "post"] | order(publishedAt desc) {
_id,
title,
slug,
publishedAt,
author->{name}
}
`);
}
export default async function BlogPage() {
const posts = await getPosts();
return (
<ul>
{posts.map((post) => (
<li key={post._id}>
<a href={`/blog/${post.slug.current}`}>{post.title}</a>
</li>
))}
</ul>
);
}
포스트 상세
// app/blog/[slug]/page.tsx
import { client } from '@/lib/sanity';
import { PortableText } from '@portabletext/react';
async function getPost(slug: string) {
return await client.fetch(
`*[_type == "post" && slug.current == $slug][0] {
title,
body,
author->{name, image},
mainImage
}`,
{ slug }
);
}
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return (
<article>
<h1>{post.title}</h1>
<PortableText value={post.body} />
</article>
);
}
6. 이미지 최적화
next-sanity
npm install next-sanity
import imageUrlBuilder from '@sanity/image-url';
import { client } from './sanity';
const builder = imageUrlBuilder(client);
export function urlFor(source: any) {
return builder.image(source);
}
// 사용
<img
src={urlFor(post.mainImage).width(800).height(400).url()}
alt={post.title}
/>
7. 실시간 업데이트
'use client';
import { useEffect, useState } from 'react';
import { client } from '@/lib/sanity';
export default function RealtimePosts({ initialPosts }) {
const [posts, setPosts] = useState(initialPosts);
useEffect(() => {
const subscription = client
.listen(`*[_type == "post"]`)
.subscribe((update) => {
if (update.type === 'mutation') {
setPosts((prev) => {
// 업데이트 로직
return [...prev];
});
}
});
return () => subscription.unsubscribe();
}, []);
return (
<ul>
{posts.map((post) => (
<li key={post._id}>{post.title}</li>
))}
</ul>
);
}
정리 및 체크리스트
핵심 요약
- Sanity: Headless CMS
- 유연한 스키마: 완전히 커스터마이징
- GROQ: 강력한 쿼리 언어
- 실시간: 실시간 업데이트
- Portable Text: 리치 텍스트
- 이미지 처리: 자동 최적화
구현 체크리스트
- Sanity 프로젝트 생성
- Schema 정의
- GROQ 쿼리 작성
- Next.js 통합
- 이미지 최적화
- 실시간 업데이트 구현
- 배포
같이 보면 좋은 글
- Next.js App Router 가이드
- Astro 완벽 가이드
- Contentful 완벽 가이드 | Headless CMS 실전 활용
이 글에서 다루는 키워드
Sanity, CMS, Headless CMS, GROQ, Content, Next.js, Backend
자주 묻는 질문 (FAQ)
Q. WordPress와 비교하면 어떤가요?
A. Sanity가 훨씬 유연하고 개발자 친화적입니다. WordPress는 더 많은 플러그인을 제공합니다.
Q. Contentful과 비교하면 어떤가요?
A. Sanity가 더 유연하고 실시간 기능이 좋습니다. Contentful은 더 성숙합니다.
Q. 무료로 사용할 수 있나요?
A. 네, 무료 플랜이 있습니다. 3 사용자, 10K 문서까지 무료입니다.
Q. 프로덕션에서 사용해도 되나요?
A. 네, Nike, Figma 등 많은 기업에서 사용합니다.