Sanity 패턴
빠른 가이드: 구조화된 콘텐츠 관리를 위해 Sanity를 사용하고, GROQ 쿼리,
defineType/defineField를 통한 타입 스키마, 데이터 패칭을 위한@sanity/client를 활용하세요. 항상apiVersion을 날짜 문자열로 설정하고, 공개 읽기에는useCdn: true를 사용하며, 드래프트 문서를 명시적으로 처리하고, 이미지 변환에는@sanity/image-url을 사용하며, 리치 텍스트는@portabletext/react로 렌더링하세요.sanity typegen generate로 TypeScript 타입을 생성하세요.
<critical_requirements>
중요: 이 스킬을 사용하기 전에
모든 코드는
CLAUDE.md의 프로젝트 규칙을 따라야 합니다 (케밥-케이스, 명명된 내보내기, 임포트 순서,import type, 명명된 상수)
(createClient에 apiVersion을 '2025-02-19'와 같은 날짜 문자열로 반드시 설정해야 합니다 — 생략하면 레거시 API를 사용하여 오작동할 수 있습니다)**
(공개 읽기 쿼리에는 useCdn: true를, 토큰을 사용하거나 최신 데이터가 필요할 때는 useCdn: false를 반드시 사용해야 합니다)
(동적 값에는 반드시 파라미터화된 GROQ 쿼리($param)를 사용해야 합니다 — 사용자 입력을 GROQ 문자열에 절대 삽입하지 마십시오)
(드래프트 문서를 명시적으로 처리해야 합니다 — 드래프트 문서의 _id는 drafts. 접두사가 있으며, perspective: 'published'를 사용하면 기본적으로 반환되지 않습니다)
(TypeGen 타입 생성을 위해 groq의 defineQuery()를 사용하고 쿼리를 명명된 변수에 할당해야 합니다)
</critical_requirements>
자동 감지: Sanity, sanity, @sanity/client, createClient, GROQ, groq, defineType, defineField, defineArrayMember, @sanity/image-url, urlFor, @portabletext/react, PortableText, portable text, block content, sanity.config, sanity.cli, typegen, sanity studio, content lake
사용 시점:
- 데이터 패칭을 위해
@sanity/client를createClient로 설정할 때 - GROQ 쿼리 작성 시 (필터, 프로젝션, 조인, 정렬, 슬라이싱)
defineType,defineField,defineArrayMember로 콘텐츠 스키마 정의 시@portabletext/react로 Portable Text (블록 콘텐츠) 렌더링 시@sanity/image-url로 이미지 URL 생성 시 (반응형 이미지, 크롭, 핫스팟)- 뮤테이션 수행 시 (생성, 패치, 삭제, 트랜잭션)
client.listen()으로 실시간 리스너 설정 시- Sanity TypeGen으로 TypeScript 타입 생성 시
주요 패턴:
createClient및apiVersion구성을 통한 클라이언트 설정- GROQ 쿼리 언어: 필터, 프로젝션, 정렬, 슬라이싱, 조인, 참조
- 스키마 정의: 문서 타입, 객체 타입, 배열, 참조, 이미지
- 커스텀 컴포넌트를 사용한 Portable Text 렌더링
- 반응형 이미지 및 변환을 지원하는 이미지 URL 빌더
- 뮤테이션: 생성, createOrReplace, 패치, 삭제, 트랜잭션
client.listen()을 통한 실시간 리스너defineQuery()를 사용한 타입 안전 GROQ 쿼리를 위한 TypeGen
사용하지 말아야 할 때:
- GraphQL 전용 API (Sanity는 주로 GROQ를 지원합니다. 필요시 GraphQL 스킬 사용)
- 직접 데이터베이스 접근 (Sanity는 호스팅된 콘텐츠 레이크이며, 데이터베이스가 아닙니다)
- Sanity가 아닌 CMS 플랫폼 (해당 CMS 전용 스킬 사용)
상세 자료:
- 의사 결정 프레임워크 및 빠른 참조표는 reference.md를 참조하세요
클라이언트 및 GROQ:
- examples/core.md — 클라이언트 설정, GROQ 쿼리, 오류 처리, TypeGen
스키마:
- examples/schemas.md — defineType, defineField, 문서 타입, 객체 타입, 참조, 이미지
리치 콘텐츠:
- examples/rich-content.md — Portable Text 렌더링, 이미지 URL 빌더, 반응형 이미지
뮤테이션 및 실시간:
- examples/mutations.md — 생성, 패치, 삭제, 트랜잭션, 실시간 리스너
철학
Sanity는 실시간 콘텐츠 레이크, 쿼리 언어로서의 GROQ (Graph-Relational Object Queries), 그리고 커스터마이징 가능한 편집 환경으로서의 Sanity Studio를 중심으로 구축된 구조화된 콘텐츠 플랫폼입니다.
핵심 원칙:
- 구조화된 콘텐츠 — 콘텐츠는 형태, 검증 및 편집 UI를 설명하는 스키마(
defineType,defineField)로 정의됩니다. 스키마는 코드이지 설정 파일이 아닙니다. - GROQ 우선 쿼리 — GROQ를 사용하면 단일 쿼리로 데이터를 필터링, 프로젝션, 조인 및 재구성할 수 있습니다. REST나 GraphQL과 달리 GROQ 쿼리는 프로젝션에서 정의한 형태를 그대로 반환합니다.
- 데이터로서의 콘텐츠 — 리치 텍스트는 Portable Text(JSON 기반 사양)로 저장되므로 벤더 종속 없이 모든 프론트엔드 프레임워크에서 렌더링할 수 있습니다.
- API 버전 관리 — 모든 클라이언트는
apiVersion날짜 문자열을 지정해야 합니다. 이렇게 하면 코드가 특정 API 동작에 고정되어 프로덕션에 영향을 주는 변경 사항을 방지할 수 있습니다. - CDN 캐싱 — 공개 읽기 쿼리는
useCdn: true를 사용하여 엣지 캐시 응답을 받습니다. 뮤테이션 및 인증된 읽기는 최신 데이터를 위해useCdn: false를 사용합니다. - 타입 생성 — Sanity TypeGen은 스키마와 GROQ 쿼리 모두에서 TypeScript 타입을 생성하여 콘텐츠 모델에서 프론트엔드까지 엔드투엔드 타입 안전성을 제공합니다.
Sanity를 사용해야 할 때:
- 콘텐츠 중심 웹사이트 및 애플리케이션 (블로그, 마케팅 사이트, 문서)
- 커스터마이징 가능한 스튜디오에서 실시간 협업 편집이 필요한 프로젝트
- 단일 콘텐츠 소스의 다중 채널 콘텐츠 전달 (웹, 모바일, IoT)
- GROQ 및 TypeGen으로 타입 안전 콘텐츠 쿼리를 원하는 팀
사용하지 말아야 할 때:
- ACID 보장이 필요한 트랜잭션 데이터 (데이터베이스 사용)
- 대규모 사용자 생성 콘텐츠 (Sanity는 편집 콘텐츠에 최적화됨)
- 자체 호스팅 CMS가 필요한 프로젝트 (Sanity의 콘텐츠 레이크는 호스팅되며, Studio는 오픈 소스)
핵심 패턴
패턴 1: createClient를 사용한 클라이언트 설정
프로젝트 ID, 데이터셋, API 버전 및 CDN 환경 설정으로 @sanity/client를 구성합니다. 항상 apiVersion을 날짜 문자열로 설정하고 useCdn을 명시적으로 설정하세요.
import { createClient } from "@sanity/client";
export const client = createClient({
projectId: process.env.SANITY_PROJECT_ID!,
dataset: process.env.SANITY_DATASET!,
apiVersion: "2025-02-19", // 특정 API 버전 날짜로 고정
useCdn: true, // 공개 읽기는 true, 인증/최신 데이터에는 false
});
이중 클라이언트 설정 (공개 + 토큰을 사용한 미리보기)에 대해서는 examples/core.md를 참조하세요.
패턴 2: 필터 및 프로젝션이 포함된 GROQ 쿼리
GROQ 쿼리는 필터, 프로젝션, 정렬 및 슬라이싱을 결합합니다. TypeGen을 위해 항상 defineQuery()를 사용하고 동적 값에는 $param을 사용하세요.
import { defineQuery } from "groq";
const POST_BY_SLUG_QUERY = defineQuery(`
*[_type == "post" && slug.current == $slug][0]{
_id, title, body, "author": author->{name, image}
}
`);
const post = await client.fetch(POST_BY_SLUG_QUERY, { slug: "my-post" });
사용자 입력을 GROQ 문자열에 절대 삽입하지 마십시오. GROQ 주입을 방지하려면 항상 $param 파라미터를 사용하세요. 고급 쿼리 (결합 쿼리, 조건부 프로젝션)에 대해서는 examples/core.md를 참조하세요.
패턴 3: defineType 및 defineField를 사용한 스키마 정의
타입 안전성과 Studio UI를 위해 "sanity"의 defineType, defineField, defineArrayMember로 콘텐츠 구조를 정의합니다.
import { defineType, defineField } from "sanity";
export const postType = defineType({
name: "post",
type: "document",
fields: [
defineField({
name: "title",
type: "string",
validation: (r) => r.required(),
}),
defineField({ name: "slug", type: "slug", options: { source: "title" } }),
],
});
완전한 스키마 (핫스팟이 있는 이미지, 배열, 참조, 미리보기, 객체 타입)에 대해서는 examples/schemas.md를 참조하세요.
패턴 4: Portable Text 렌더링
@portabletext/react로 블록 콘텐츠를 렌더링합니다. 비표준 블록 (이미지, 코드) 및 마크 (링크, 하이라이트)에 대해 커스텀 PortableTextComponents를 정의합니다.
import { PortableText } from "@portabletext/react";
<PortableText value={body} components={components} />;
더 이상 사용되지 않는 @sanity/block-content-to-react 패키지를 사용하지 마십시오. 전체 컴포넌트 예제는 examples/rich-content.md를 참조하세요.
패턴 5: 이미지 URL 빌더
@sanity/image-url을 사용하여 크롭 및 핫스팟 지원으로 최적화된 반응형 이미지 URL을 생성합니다.
import { createImageUrlBuilder } from "@sanity/image-url";
const builder = createImageUrlBuilder(client);
export function urlFor(source: SanityImageSource) {
return builder.image(source);
}
// 사용법: urlFor(image).width(800).auto("format").url()
반응형 srcSet 패턴 및 이미지 변환에 대해서는 examples/rich-content.md를 참조하세요.
패턴 6: 뮤테이션 (생성, 패치, 삭제)
문서 뮤테이션에는 @sanity/client 메서드를 사용합니다. 패치 및 트랜잭션에는 항상 .commit()을 호출하세요.
await client.create({ _type: "post", title: "New Post" });
await client.patch("post-123").set({ title: "Updated" }).commit();
await client.delete("post-123");
createOrReplace, createIfNotExists, 트랜잭션, 배열 삽입 및 가시성 옵션에 대해서는 examples/mutations.md를 참조하세요.
패턴 7: 실시간 리스너
client.listen()으로 문서 변경 사항을 구독합니다. 리스너는 GROQ의 필터 부분만 사용합니다 — 프로젝션과 정렬은 무시됩니다.
const subscription = client.listen(`*[_type == "post"]`).subscribe({
next: (update) => {
/* update.transition: 'update' | 'appear' | 'disappear' */
},
error: (err) => console.error(err),
});
subscription.unsubscribe(); // 완료 후 정리
프로덕션 프론트엔드의 경우, 더 간단한 대안으로 최신 Live Content API를 평가해 보세요. 리스너 옵션 및 주의사항은 examples/mutations.md를 참조하세요.
패턴 8: 타입 안전 GROQ를 위한 TypeGen
sanity.cli.ts에서 overloadClientMethods: true로 TypeGen을 구성하여 타입이 지정된 client.fetch 결과를 얻습니다. TypeGen이 쿼리를 검색할 수 있도록 "groq"의 defineQuery()를 사용하세요.
// sanity.cli.ts — typegen.overloadClientMethods: true 설정
// queries/post-queries.ts — defineQuery()로 쿼리 래핑
// sanity.types.ts — 자동 생성된 결과 타입
const posts = await client.fetch(allPostsQuery); // 타입이 지정된 결과
defineQuery() 없이 인라인 쿼리 문자열을 사용하면 타입이 지정되지 않은 (any) 결과가 생성됩니다. 전체 TypeGen 구성 및 워크플로는 examples/core.md를 참조하세요.
<decision_framework>
의사 결정 프레임워크
useCdn: true vs false
데이터가 공개적이고 개인화되지 않았습니까?
├─ 예 → useCdn: true (엣지 캐시, 빠름)
└─ 아니오 →
├─ 인증된 읽기를 위해 토큰을 사용 중입니까? → useCdn: false
├─ 실시간 최신 데이터가 필요합니까? (미리보기) → useCdn: false
└─ 뮤테이션을 수행 중입니까? → useCdn: false
드래프트 처리
드래프트 문서가 필요합니까?
├─ 예 → perspective: 'previewDrafts' 사용 (토큰 필요)
├─ 아니오 → perspective: 'published' 사용 (API v2025-02-19 이후 기본값)
└─ 혼합 (미리보기 모드 전환)?
└─ 두 개의 클라이언트 생성: 하나는 공개용 (useCdn: true), 다른 하나는 미리보기용 (토큰 + useCdn: false)
GROQ 쿼리 반환 형태
몇 개의 문서를 예상합니까?
├─ 하나 (ID, 슬러그, 싱글톤) → 끝에 [0] 추가 (객체 또는 null 반환)
├─ 여러 개 (목록, 피드) → 슬라이스 접미사 없음 (배열 반환)
└─ 페이지 매김 → [시작...끝] 슬라이스 추가
이미지 처리
이미지는 어떻게 제공되어야 합니까?
├─ 고정 크기 (썸네일, 아바타) → urlFor(img).width(W).height(H).url()
├─ 반응형 (기사 이미지) → 여러 너비의 srcSet
├─ 형식 최적화 → WebP/AVIF를 위한 .auto('format')
└─ 종횡비에 맞게 크롭 → .width(W).height(H).fit('crop')
뮤테이션 메서드 선택
어떤 작업이 필요합니까?
├─ 새 문서 생성 → client.create()
├─ 생성 또는 완전 교체 → client.createOrReplace() (싱글톤용)
├─ 누락된 경우에만 생성 → client.createIfNotExists()
├─ 특정 필드 업데이트 → client.patch(id).set({...}).commit()
├─ 필드 제거 → client.patch(id).unset([...]).commit()
├─ 여러 관련 변경 → client.transaction()...commit()
└─ 문서 삭제 → client.delete(id)
</decision_framework>
<red_flags>
위험 신호
높은 우선순위:
- 클라이언트에
apiVersion누락 —apiVersion을 생략하면 통지 없이 변경될 수 있는 레거시 API 동작을 사용합니다. 항상 날짜 문자열로 고정하세요. - GROQ 쿼리의 문자열 삽입 — 사용자 입력을 GROQ 문자열에 삽입하면 GROQ 주입이 가능해집니다. 항상
$param파라미터를 사용하세요. - 토큰과 함께
useCdn: true사용 — CDN 캐시 응답은 인증 토큰을 무시합니다. 인증된 쿼리는useCdn: false를 사용해야 합니다. - 토큰 없이 드래프트 문서 접근 — 드래프트 (
drafts.*문서)는 API 토큰과perspective: 'previewDrafts'가 필요합니다. 토큰이 없으면 드래프트는 보이지 않습니다.
중간 우선순위:
- 필요한 필드가 적은데
{...}로 모든 필드 가져오기 — 과도한 패칭은 대역폭과 CDN 캐시 효율성을 낭비합니다. 필요한 필드만 프로젝션하세요. - 패치에
.commit()누락 —client.patch(id).set({...})는.commit()없이는 아무 작업도 수행하지 않습니다 — 뮤테이션이 전송되지 않습니다. - GROQ 쿼리에
defineQuery()를 사용하지 않음 — TypeGen은defineQuery()로 래핑되거나 명명된 변수에 할당되지 않은 쿼리에 대한 타입을 생성할 수 없습니다. - 하드코딩된 프로젝트 ID 또는 데이터셋 — 환경 변수를 사용하세요. 하드코딩된 값은 환경 전환을 방해하고 프로젝트 세부 정보를 유출합니다.
- 더 이상 사용되지 않는
@sanity/block-content-to-react사용 —@portabletext/react로 대체되었습니다. 이전 패키지는 유지 관리되지 않습니다.
일반적인 실수:
- 뮤테이션에서 배열 항목의
_key를 잊음 — Sanity 배열의 모든 항목은 고유한_key필드가 있어야 합니다._key가 없는 뮤테이션은 실패합니다. client.create()와 함께_id사용 —create()는 임의의_id를 생성합니다._id를 지정하고 문서가 이미 존재하면 오류가 발생합니다. 멱등 연산에는createOrReplace()또는createIfNotExists()를 사용하세요.[0]이null을 반환하는 경우를 처리하지 않음 — 일치하는 문서가 없으면[0]이 있는 GROQ 쿼리는null을 반환합니다. 반환 값을 사용하기 전에 항상 null을 확인하세요.- 리스너 구독 해제 누락 — 구독 해제 없이
client.listen()을 사용하면 특히 서버리스 환경에서 메모리 누수가 발생합니다. 컴포넌트 언마운트 또는 함수 완료 시 구독을 해제하세요. sanity.config.ts에서schema.types에 타입 등록 누락 — 스튜디오에 표시되려면 모든 문서 및 객체 타입을 schema.types 배열에 등록해야 합니다.
</red_flags>