Sanity CMS 다국어 콘텐츠를 next‑intl로 Next.js에 연결하는 방법

발행: (2026년 6월 9일 PM 05:06 GMT+9)
5 분 소요
원문: Dev.to

출처: Dev.to

How I wire Sanity CMS multilingual content to Next.js with next-intl 커버 이미지

Nayan Kyada

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을 설정했다고 가정합니다. 통

0 조회
Back to Blog

관련 글

더 보기 »

Eidentic 소개

Today we're releasing Eidentic, an open-source TypeScript SDK for building AI agents with self-improving memory and the production fundamentals built in — not b...

Typescript의 타입

Introdução Tipos são uma forma de definir a “forma” ou o contrato dos dados que estamos usando no código. Pensando em Javascript puro, ele é dinâmico: você pode...