Sanity CMS 다국어 콘텐츠를 next‑intl로 Next.js에 연결하는 방법
출처: Dev.to
Sanity CMS의 다국어 지원을 Next.js App Router와 next-intl로 연결하는 작업은 각각의 요소는 독립적으로 잘 동작하지만, 서로 연결하는 과정이 까다로운 설정 중 하나입니다. 이 글에서는 실제 프로덕션 프로젝트에서 두 시스템을 어떻게 연결했는지—스키마 설계, 로케일별 GROQ 쿼리 패턴, 그리고 팀마다 최소 한 번은 겪게 되는 슬러그 중복 함정—을 자세히 문서화합니다.
각 도구가 실제로 담당하는 역할
코드를 건드리기 전에 책임 범위를 명확히 해야 합니다. Sanity의 document internationalisation 플러그인은 콘텐츠 저장을 담당합니다 — 로케일당 하나의 문서를 만들고 _id 앞에 공통 접두사를 붙입니다(예: page.en, page.fr). next-intl은 라우팅 및 메시지 전달을 담당합니다 — URL의 로케일 세그먼트, useTranslations, 그리고 [locale] 동적 세그먼트 등. 기본적으로 두 도구는 서로를 알지 못합니다. 여러분의 역할은 이 둘 사이를 연결하는 브리지 레이어를 만드는 것입니다.
번역 필드용 스키마 설계
먼저 플러그인을 설치합니다:
npm i @sanity/document-internationalization
Enter fullscreen mode
Exit fullscreen mode
그 다음 sanity.config.ts에 플러그인을 설정합니다:
// sanity.config.ts
import { defineConfig } from 'sanity'
import { documentInternationalization } from '@sanity/document-internationalization'
export default defineConfig({
// ...이미 사용 중인 project, dataset, plugins
plugins: [
documentInternationalization({
supportedLanguages: [
{ id: 'en', title: 'English' },
{ id: 'fr', title: 'French' },
{ id: 'de', title: 'German' },
],
schemaTypes: ['page', 'post'],
}),
],
})
Enter fullscreen mode
Exit fullscreen mode
이제 스키마를 설계합니다. 카테고리 레퍼런스나 발행일처럼 언어에 관계없는 필드는 공유 문서 타입에 두고, 번역 가능한 모든 필드는 로케일 문서에 배치합니다.
// schemas/post.ts
import { defineType, defineField } from 'sanity'
export const post = defineType({
name: 'post',
title: 'Post',
type: 'document',
fields: [
defineField({
name: 'language',
type: 'string',
readOnly: true,
hidden: true,
}),
defineField({
name: 'slug',
type: 'slug',
// 중요: 언어별로 고유성을 제한 — pitfalls 섹션 참고
options: {
source: 'title',
isUnique: async (slug, context) => {
const { document, getClient } = context
const client = getClient({ apiVersion: '2024-01-01' })
const id = document._id.replace(/^drafts\./, '')
const params = { draft: `drafts.${id}`, published: id, language: document.language, slug }
const query = `!defined(*[!(_id in [$draft, $published]) && language == $language && slug.current == $slug][0]._id)`
return await client.fetch(query, params)
},
},
}),
defineField({ name: 'title', type: 'string' }),
defineField({ name: 'body', type: 'array', of: [{ type: 'block' }] }),
],
})
Enter fullscreen mode
Exit fullscreen mode
플러그인은 schemaTypes에 등록하면 자동으로 language 필드를 주입하지만, 타입 생성기가 문제 없이 인식하도록 명시적으로 정의해 두었습니다.
로케일별 GROQ 쿼리 패턴
문서 국제화가 적용되면 각 로케일 문서에 언어 정보가 포함됩니다. 쿼리 패턴은 매우 단순합니다 — 항상 language 로 필터링합니다:
// slug와 로케일로 단일 포스트를 가져오기
*[
_type == "post"
&& language == $locale
&& slug.current == $slug
&& !(_id in path("drafts.**"))
][0] {
_id,
title,
slug,
language,
body[]
}
Enter fullscreen mode
Exit fullscreen mode
대체 로케일 URL(SEO hreflang용 정규화된 대체 URL)이 필요한 리스트 페이지에서는 플러그인이 추가한 _translations 메타데이터 레퍼런스를 통해 모든 언어 변형을 가져옵니다:
// hreflang을 만들기 위해 문서의 모든 로케일 버전을 가져오기
*[
_type == "translation.metadata"
&& references($id)
][0] {
translations[]-> {
language,
slug
}
}
Enter fullscreen mode
Exit fullscreen mode
두 번째 쿼리는 Next.js 메타데이터의 alternates.languages 객체를 구성할 때 사용합니다.
App Router에서 next-intl에 연결하기
이미 app/[locale]/ 아래에 [locale] 세그먼트를 사용해 next-intl을 설정했다고 가정합니다. 통
