Sanity를 사용한 프로덕션 팁: 3,000개 이상의 게시물로부터 배운 교훈

3년 이상 여러 클라이언트 프로젝트에서 Sanity를 주요 CMS로 운영해오고 있습니다. 3,000개 게시물 정도에 도달하면, Sanity를 문서에서 말하는 것처럼 생각하는 것을 멈추고 실제로 프로덕션에서 생존하는 것으로 생각하기 시작합니다. 이 글은 그런 뇌의 내용물을 쏟아낸 것입니다. -- 우리가 후회했던 모든 스키마 결정, 빌드를 무릎을 꿇린 모든 GROQ 쿼리, 그리고 편집자들이 우리에게 Word 문서를 이메일로 보내는 대신 실제로 CMS를 사용하고 싶어 만든 모든 Studio 커스터마이제이션.

이것은 시작 가이드가 아닙니다. 여기에 있다면, 아마도 이미 Sanity Studio를 설정했고, 몇 가지 스키마를 만들었으며, 아마도 한두 개의 사이트를 출시했을 것입니다. 내가 공유하고 싶은 것은 실제 콘텐츠 팀, 실제 편집 워크플로우, 그리고 규모에서의 실제 성능 예산을 다루고 난 후에만 나타나는 패턴입니다.

목차

Sanity Studio 프로덕션 팁: 3,000개 이상의 게시물로부터 배운 교훈

실제 콘텐츠 팀을 견디는 스키마 설계

스키마 설계는 대부분의 Sanity 프로젝트가 조용히 실패하는 곳입니다. 극적인 폭발적 실패처럼 보이지는 않습니다. -- 편집 신뢰도의 느린 침식처럼 더 많습니다. 콘텐츠 팀이 특정 필드를 피하기 시작합니다. 그들은 해결 방법을 만듭니다. 6개월 후, 스키마가 "너무 복잡했기" 때문에 구조화된 콘텐츠의 절반이 실제로 단일 리치 텍스트 블록에 집어넣어집니다.

객체 중첩 과다 중단

우리의 가장 큰 초기 실수는 깊게 중첩된 객체 구조를 생성하는 것이었습니다. 우리는 콘텐츠를 데이터베이스 스키마처럼 모델링했습니다. -- 정규화되고, 우아하고, 기술적으로 올바른. 블로그 포스트는 author 참조를 가졌고, 이는 bio 객체를 가졌고, 이는 socialLinks 객체 배열을 가졌고, 각각은 platform 참조를 가졌습니다.

편집자들은 그것을 싫어했습니다. 편집자가 작가의 Twitter 핸들을 업데이트해야 할 때마다 그들은 5번 클릭을 깊이 들어가야 했습니다. 우리가 지금 하는 것은 다음과 같습니다:

// 이전: 과도하게 엔지니어링됨
export default defineType({
  name: 'author',
  type: 'document',
  fields: [
    defineField({
      name: 'name',
      type: 'string',
    }),
    defineField({
      name: 'bio',
      type: 'object',
      fields: [
        defineField({
          name: 'content',
          type: 'array',
          of: [{ type: 'block' }],
        }),
        defineField({
          name: 'socialLinks',
          type: 'array',
          of: [
            defineArrayMember({
              type: 'object',
              fields: [
                { name: 'platform', type: 'reference', to: [{ type: 'platform' }] },
                { name: 'url', type: 'url' },
              ],
            }),
          ],
        }),
      ],
    }),
  ],
})

// 이후: 평평하고 편집자-친화적
export default defineType({
  name: 'author',
  type: 'document',
  fields: [
    defineField({ name: 'name', type: 'string', validation: (r) => r.required() }),
    defineField({ name: 'bio', type: 'array', of: [{ type: 'block' }] }),
    defineField({ name: 'twitter', type: 'url', title: 'Twitter / X URL' }),
    defineField({ name: 'linkedin', type: 'url', title: 'LinkedIn URL' }),
    defineField({ name: 'github', type: 'url', title: 'GitHub URL' }),
  ],
})

네, 평평한 버전은 덜 "순수합니다." 또한 100% 올바르게 사용됩니다. 트레이드오프 수용됨.

필드 그룹을 적극적으로 사용

문서 유형에 8-10개 이상의 필드가 있으면 편집자들이 스크롤을 시작하고 것들을 놓칩니다. Sanity v3의 필드 그룹은 과소평가되어 있습니다. 우리는 6개 이상의 필드가 있는 모든 문서 유형에 이들을 넣습니다:

export default defineType({
  name: 'post',
  type: 'document',
  groups: [
    { name: 'content', title: 'Content', default: true },
    { name: 'seo', title: 'SEO' },
    { name: 'settings', title: 'Settings' },
  ],
  fields: [
    defineField({ name: 'title', type: 'string', group: 'content' }),
    defineField({ name: 'body', type: 'array', of: [{ type: 'block' }], group: 'content' }),
    defineField({ name: 'seoTitle', type: 'string', group: 'seo' }),
    defineField({ name: 'seoDescription', type: 'text', rows: 3, group: 'seo' }),
    defineField({ name: 'publishDate', type: 'datetime', group: 'settings' }),
    defineField({ name: 'featured', type: 'boolean', group: 'settings' }),
  ],
})

제어하지 않고 안내하는 검증

우리는 검증을 실행 강제가 아닌 UX로 생각하는 법을 배웠습니다. 모든 필드에 대한 하드 required() 검증은 편집자들이 초안을 저장할 수 없다는 의미입니다. 일반적인 오류 상태보다 그것이 중요한지 설명하는 커스텀 검증 메시지는 훨씬 더 나은 준수를 얻습니다:

defineField({
  name: 'excerpt',
  type: 'text',
  rows: 3,
  validation: (rule) =>
    rule
      .max(160)
      .warning('Excerpts over 160 characters get truncated in search results and social cards.'),
})

이것은 error가 아니라 warning임을 주목하세요. 편집자는 여전히 게시할 수 있습니다. 그들은 단지 결과를 알고 있습니다.

규모에서의 GROQ 성능: 실제로 중요한 것

GROQ는 그렇지 않을 때까지 멋집니다. 500개 문서에서 모든 것이 빠릅니다. 3,000개 이상의 문서, 참조, 이미지, 포터블 텍스트를 가지고 있으면 당신은 것들을 주목하기 시작합니다.

프로젝션은 선택사항이 아님

단일 가장 큰 GROQ 성능 레버는 프로젝션입니다. 3개 필드만 필요할 때 전체 문서를 가져오지 마세요. 나는 generateStaticParams 호출에서 GROQ 프로젝션을 수정하는 것만으로 Next.js 빌드가 4분에서 90초로 가는 것을 본 적이 있습니다.

// 느림: 포터블 텍스트, 이미지, 참조를 포함한 모든 것을 가져옴
*[_type == "post"]

// 빠름: 목록 페이지가 실제로 필요한 것만
*[_type == "post"] | order(publishedAt desc) [0...20] {
  _id,
  title,
  slug,
  publishedAt,
  "authorName": author->name,
  "thumbnailUrl": thumbnail.asset->url
}

author->name 인라인 역참조는 매우 중요합니다. 전체 저자 문서를 가져오지 않습니다. 3,000개의 포스트가 각각 50명 저자 중 하나를 참조할 때 차이는 측정할 수 있습니다.

아무도 말하지 않는 조인 문제

Sanity의 GROQ 문서는 역참조를 무료인 것처럼 보여줍니다. 그렇지 않습니다. 쿼리의 모든 ->는 본질적으로 조인입니다. 100개의 결과를 반환하는 목록 쿼리에 3-4개를 쌓으면 느껴질 것입니다.

우리는 이제 우리 프로젝트의 모든 GROQ 쿼리를 프로파일합니다. 우리의 경험 법칙은 다음과 같습니다:

패턴 문서 평균 응답 시간
단순 가져오기, 참조 없음 3,000 ~120ms
하나 수준의 -> 역참조 3,000 ~250ms
두 수준의 -> 3,000 ~600ms
내부의 -> 포함 중첩 배열 3,000 ~1,200ms+

이것들은 2025년 중반의 우리 Sanity API 대시보드의 실제 숫자입니다. 문서 크기에 따라 당신의 경험은 다를 수 있지만 추세는 일관성 있습니다.

우리가 끊임없이 사용하는 GROQ 패턴

미리보기 대 게시된 조건부 가져오기:

*[_type == "post" && slug.current == $slug && ($preview || !(_id in path('drafts.**')))] [0] {
  ...,
  "author": author-> { name, slug, image },
  "categories": categories[]-> { title, slug }
}

카운트가 있는 페이지네이션된 쿼리:

{
  "posts": *[_type == "post"] | order(publishedAt desc) [$start...$end] {
    _id, title, slug, publishedAt,
    "authorName": author->name
  },
  "total": count(*[_type == "post"])
}

N+1 없이 관련된 포스트:

*[_type == "post" && slug.current == $slug][0] {
  ...,
  "related": *[_type == "post" && _id != ^._id && count(categories[@._ref in ^.^.categories[]._ref]) > 0] | order(publishedAt desc) [0...3] {
    title, slug, publishedAt
  }
}

그 관련된 포스트 쿼리는 밀도가 높지만, 이것은 Sanity의 인프라에서 서버 측에서 실행되므로 일반적으로 두 개의 왕복을 만드는 것보다 빠릅니다.

투자할 가치가 있는 Studio 커스터마이제이션

바닐라 Sanity Studio는 개발자에게 좋습니다. 주당 20개의 포스트를 배송하는 콘텐츠 팀에게는 좋지 않습니다. 우리가 모든 프로젝트에서 커스터마이즈하는 것은 다음과 같습니다.

커스텀 문서 액션

기본 게시 액션은 모든 설정에서 증분 빌드를 위해 웹훅을 안정적으로 트리거하지 않습니다. 우리는 이것을 래핑합니다:

import { useDocumentOperation } from 'sanity'

export function createPublishWithWebhookAction(originalPublishAction) {
  return function PublishWithWebhook(props) {
    const originalResult = originalPublishAction(props)
    return {
      ...originalResult,
      onHandle: async () => {
        await originalResult.onHandle()
        // ISR 재검증 또는 배포 훅 트리거
        await fetch('/api/revalidate', {
          method: 'POST',
          body: JSON.stringify({ type: props.type, id: props.id }),
        })
      },
    }
  }
}

편집 워크플로우를 위한 Structure Builder

기본 데스크 구조는 모든 문서 유형을 평평한 목록으로 표시합니다. 15개 이상의 문서 유형에서 이것은 혼란입니다. 우리는 Structure Builder를 사용하여 편집-중심 네비게이션을 만듭니다:

import { StructureBuilder } from 'sanity/structure'

export const structure = (S: StructureBuilder) =>
  S.list()
    .title('Content')
    .items([
      S.listItem()
        .title('Blog')
        .child(
          S.list()
            .title('Blog')
            .items([
              S.listItem()
                .title('Published Posts')
                .child(
                  S.documentList()
                    .title('Published')
                    .filter('_type == "post" && !(_id in path("drafts.**"))')
                ),
              S.listItem()
                .title('Drafts')
                .child(
                  S.documentList()
                    .title('Drafts')
                    .filter('_type == "post" && _id in path("drafts.**")')
                ),
              S.listItem()
                .title('All Posts')
                .child(S.documentTypeList('post').title('All Posts')),
            ])
        ),
      S.divider(),
      // ... 다른 콘텐츠 유형
    ])

이것은 설정하는 데 30분이 걸리고 편집자들이 매주 혼동하는 시간을 절약합니다.

Portable Text 커스텀 컴포넌트

우리를 크게 물린 한 가지: 편집자들이 Google Docs에서 Portable Text 편집기로 콘텐츠를 붙여넣는 것. 기본 블록 편집기는 이것을 적절히 처리하지만, 커스텀 블록 유형은 명시적 직렬화기가 필요하거나 빈 상자로 표시되고 편집자들은 패닉합니다.

우리는 모든 블록 유형에 대해 커스텀 컴포넌트를 등록합니다:

defineArrayMember({
  type: 'object',
  name: 'codeBlock',
  title: 'Code Block',
  fields: [
    defineField({ name: 'code', type: 'text' }),
    defineField({ name: 'language', type: 'string',
      options: { list: ['javascript', 'typescript', 'python', 'bash', 'groq'] }
    }),
  ],
  preview: {
    select: { code: 'code', language: 'language' },
    prepare({ code, language }) {
      return {
        title: `Code (${language || 'plain'})`,
        subtitle: code?.slice(0, 80) + '...',
      }
    },
  },
})

preview 구성은 작지만 필수입니다. 그것 없이, 편집자들은 빈 블록을 보고 그것이 무엇인지 모릅니다.

Sanity Studio 프로덕션 팁: 3,000개 이상의 게시물로부터 배운 교훈 - 아키텍처

콘텐츠 마이그레이션 및 데이터 무결성

우리는 Sanity로 5개의 주요 콘텐츠 마이그레이션을 수행했습니다. -- WordPress, Contentful, Prismic, 마크다운 파일, 그리고 커스텀 Rails CMS에서. 모든 하나는 우리에게 고통스러운 것을 가르쳤습니다.

마이그레이션 도구를 사용하되 신뢰하고 검증

Sanity의 @sanity/migrate 패키지 및 CLI의 sanity documents import는 간단한 경우에 잘 작동합니다. 포터블 텍스트 변환과 관련된 모든 것은 커스텀 스크립트를 작성하세요. 항상.

# 마이그레이션 전에 백업을 위해 모든 것을 내보내기
sanity dataset export production ./backup-$(date +%Y%m%d).tar.gz

우리는 모든 마이그레이션, 모든 스키마 배포 전에 이것을 실행합니다. 그리고 솔직히, 매주 월요일 아침 cron을 통해. 데이터셋은 저렴합니다. 손실된 콘텐츠는 아닙니다.

스키마 버전 관리 전략

Sanity는 데이터 레이어에서 스키마 버전을 강제하지 않습니다. 이것은 기능이자 발총 자상입니다. 오래된 문서는 스키마를 변경할 때 자동으로 업데이트되지 않습니다. 우리는 간단한 패턴을 사용합니다:

defineField({
  name: 'schemaVersion',
  type: 'number',
  hidden: true,
  initialValue: 2,
  readOnly: true,
})

그런 다음 마이그레이션 스크립트에서 우리는 *[_type == "post" && schemaVersion < 2]를 쿼리하고 문서를 새 형식으로 일괄 업데이트할 수 있습니다. 이것은 원시적이지만 작동합니다.

배포 및 환경 전략

Sanity의 데이터셋 모델은 여러 환경을 지원하고, 첫 번째 프로덕션 데이터 인시던트 후가 아니라 첫날부터 사용해야 합니다.

우리의 표준 설정

환경 데이터셋 Studio URL 목적
Production production studio.client.com 라이브 콘텐츠 편집
Staging staging staging-studio.client.com 콘텐츠 QA, 스키마 테스트
Development development localhost:3333 스키마 개발

우리는 sanity dataset copy production staging을 사용하여 매주 프로덕션을 스테이징으로 복제합니다. 이것은 스테이징을 현실적으로 유지하면서 스키마 실험 중에 프로덕션 데이터를 위험에 빠뜨리지 않습니다.

프론트엔드의 경우, 우리의 Next.js 개발 프로젝트는 환경 변수를 사용하여 데이터셋을 전환합니다:

const config = {
  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || 'production',
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID,
  apiVersion: '2025-01-01',
  useCdn: process.env.NODE_ENV === 'production',
}

CDN 대 CDN 없음

Sanity의 API CDN은 최종적으로 일관성이 있습니다. 마케팅 사이트의 게시된 콘텐츠의 경우 이것은 좋습니다. -- CDN은 빠르고 뜨기 창은 일반적으로 2초 미만입니다. 미리보기/초안 콘텐츠의 경우 항상 CDN을 우회하세요:

const client = sanityClient.withConfig({
  useCdn: false,
  token: process.env.SANITY_PREVIEW_TOKEN,
  perspective: 'previewDrafts',
})

우리는 미리보기 클라이언트가 CDN을 사용하고 있었고 오래된 데이터를 표시하고 있었다는 것을 깨닫고 디버깅하는 데 몇 시간이 걸린 미리보기 문제를 본 적이 있습니다. 모든 미리보기 및 초안 읽기 컨텍스트에 대해 useCdn: false를 설정하세요.

프로덕션에서의 모니터링 및 디버깅

GROQ 쿼리 프로파일링

Sanity의 관리 콘솔 (manage.sanity.io)은 API 사용 메트릭을 표시하지만 세분성이 항상 충분하지는 않습니다. 우리는 프론트엔드 측에서 느린 쿼리를 기록합니다:

async function sanityFetch<T>(query: string, params?: Record<string, unknown>): Promise<T> {
  const start = performance.now()
  const result = await client.fetch<T>(query, params)
  const duration = performance.now() - start

  if (duration > 500) {
    console.warn(`Slow GROQ query (${duration.toFixed(0)}ms):`, query.slice(0, 200))
  }

  return result
}

프로덕션에서 500ms를 초과하는 모든 것이 조사됩니다. 일반적으로 미리 확인한 쿼리 또는 중첩된 역참조입니다.

웹훅 신뢰성

Sanity 웹훅은 안정적이지만 완벽하지는 않습니다. Sanity 인프라 업데이트 중에 가끔 미누락 웹훅을 본 적이 있습니다. 중요한 워크플로우의 경우 (우리의 Astro 개발 프로젝트에서 재구축을 트리거하는 것처럼), 우리는 폴링 폴백을 구현합니다:

// 5분마다 최근 변경사항을 안전망으로 확인
const POLL_INTERVAL = 5 * 60 * 1000

setInterval(async () => {
  const lastModified = await client.fetch(
    `*[_type == "post"] | order(_updatedAt desc) [0]._updatedAt`
  )
  if (new Date(lastModified) > lastKnownUpdate) {
    await triggerRebuild()
    lastKnownUpdate = new Date(lastModified)
  }
}, POLL_INTERVAL)

실제 프로젝트의 성능 벤치마크

2024-2025년에 Sanity와 헤드리스 프론트엔드를 사용하여 배송한 세 프로덕션 프로젝트의 실제 숫자는 다음과 같습니다:

메트릭 프로젝트 A (Next.js) 프로젝트 B (Astro) 프로젝트 C (Next.js)
총 문서 3,200 1,800 4,100
문서 유형 12 8 18
평균 GROQ 응답 (CDN) 85ms 72ms 130ms
평균 GROQ 응답 (CDN 없음) 180ms 145ms 290ms
전체 정적 빌드 시간 3m 20s 1m 45s 6m 10s
ISR 재검증 1.2s N/A (static) 1.8s
월간 API 요청 ~450K ~180K ~1.2M
Sanity 계획 비용/월 Growth ($99) Free Growth ($99)

프로젝트 C의 더 긴 빌드 시간은 전적으로 GROQ가 아닌 이미지 처리 때문이었습니다. Sanity의 이미지 파이프라인으로 이동하고 @sanity/image-url을 사용하고 적절한 width/height 파라미터를 제공한 후, 빌드는 풀 해상도 이미지를 다운로드하는 것을 중단했습니다.

헤드리스 CMS 개발 프로젝트의 경우 Sanity의 가격은 경쟁력이 있습니다. 무료 티어는 더 작은 사이트에 대해 진정으로 사용할 수 있습니다. 월 $99의 Growth 계획은 대부분의 중간 규모 편집 작업을 다룹니다. 매우 높은 API 요청 볼륨에서만 비용이 문제가 되기 시작하고, 그 때에도 적극적인 CDN 사용 및 스마트 캐싱은 것들을 합리적으로 유지합니다.

Sanity가 올바른 선택이 아닐 때

내가 Sanity에서 클라이언트를 멀리 안내한 경우를 언급하지 않으면 나는 당신을 불리고 있을 것입니다:

  • 매우 관계형 데이터 (복잡한 변형 관계가 있는 제품 카탈로그) -- 목적-구축 전자상거래 플랫폼 또는 심지어 Postgres가 더 의미가 있습니다
  • 매우 비기술 팀 WYSIWYG 페이지 빌더가 필요한 -- Sanity의 Portable Text는 강력하지만 Squarespace가 아닙니다
  • 예산 제한 프로젝트 >200K 월간 API 요청 -- 비용이 당신을 놀라게 할 수 있습니다

다른 모든 것에 대해 -- 특히 편집 콘텐츠, 마케팅 사이트, 그리고 문서화 -- Sanity는 우리의 CMS의 주력이었습니다. 헤드리스 프로젝트에 대한 옵션을 평가 중이면, 우리에게 연락하세요 그리고 우리는 당신의 특정 요구사항에 따라 정직한 평가를 제공할 것입니다.

FAQ

Sanity가 성능 저하 전에 얼마나 많은 문서를 처리할 수 있습니까? 우리는 의미 있는 성능 저하 없이 4,000개 이상의 문서를 가지고 프로덕션 프로젝트를 운영했습니다. Sanity의 호스팅 인프라는 문서 카운트를 수만 개로 잘 처리합니다. 성능 병목은 거의 항상 GROQ 쿼리 작성 방식입니다. -- 특히, 미리 선택되지 않은 가져오기 및 깊은 참조 체인 -- 원시 문서 카운트가 아닙니다.

GROQ 또는 Sanity와 GraphQL을 사용해야 합니까? GROQ, 매우 구체적인 이유가 없으면 GraphQL을 사용하세요. GROQ는 Sanity의 문서 모델에 더 표현력이 있고, 프로젝션을 더 자연스럽게 지원하며, Sanity 팀으로부터 1급 관심을 받습니다. GraphQL API는 스키마에서 자동 생성되고 괜찮게 작동하지만, Sanity를 강력하게 만드는 쿼리 유연성의 일부를 잃습니다.

Sanity와 Next.js로 초안 미리보기를 어떻게 처리합니까? 우리는 Next.js Draft Mode를 Sanity의 perspective: 'previewDrafts' 설정과 함께 사용합니다. 미리보기 클라이언트는 CDN을 우회하고 읽기 토큰을 사용합니다. Sanity의 @sanity/preview-kit 패키지는 편집자가 입력하면서 페이지를 업데이트하는 실시간 리스너를 제공합니다. 설정에는 약간의 작업이 필요하지만 편집 경험은 그것이 가치가 있습니다.

Portable Text를 SEO를 위해 구조화하는 최선의 방법은 무엇입니까? Portable Text 블록 스타일을 적절한 의미론적 HTML로 매핑하세요. h2, h3, h4 스타일을 사용하세요 ("큰 텍스트" 또는 "제목"이 아님). FAQ 섹션, 방법 단계, 코드 블록과 같은 구조화된 데이터를 위해 커스텀 블록 유형을 추가하세요. 우리는 @portabletext/react를 사용하여 Portable Text를 HTML로 렌더링합니다. 커스텀 직렬화기를 사용하여 schema.org-친화적 마크업을 출력합니다.

Sanity로 이미지 최적화를 어떻게 처리합니까? Sanity의 이미지 파이프라인은 뛰어납니다. @sanity/image-url을 사용하여 특정 차원 및 형식 파라미터를 가진 URL을 생성하세요. 항상 auto=format을 설정하여 Sanity가 브라우저 지원에 따라 WebP 또는 AVIF을 제공하도록 하세요. Next.js 프로젝트의 경우 우리는 next/image와 함께 Sanity 이미지 로더를 사용합니다. -- 이것은 Sanity의 CDN과 Next.js의 기본 이미지 최적화를 모두 제공합니다.

Sanity가 규모에서 지역화/다국어 콘텐츠를 처리할 수 있습니까? 네, 하지만 스키마 설계는 매우 중요합니다. 우리는 필드 수준 번역 객체가 아닌 문서 수준 국제화 패턴을 사용합니다 (공유 i18nId 필드로 링크된 로케일당 별도 문서). 3개 로케일에 걸쳐 3,000개 이상의 문서를 사용하면 쿼리가 단순하게 유지되고 모든 필드에 5개 이상의 언어 키를 가진 객체를 포함할 때 얻는 거대한 문서 크기를 피합니다.

Sanity API 버전을 얼마나 자주 업데이트해야 합니까? API 버전을 특정 날짜에 고정하세요 (예: 2025-01-01), 변경 로그를 검토한 후 분기별로 업데이트하세요. Sanity의 API 버전은 날짜 기반이고 주요 변경은 드물지만 일어납니다. 우리는 API 버전 간의 문서화되지 않은 GROQ 동작 변경으로 물린 적이 있습니다. -- 버전을 요청한 후 항상 중요한 쿼리를 테스트하세요.

대규모 편집 팀을 위한 Sanity의 비용은 얼마입니까? 월 $99의 Growth 계획 (2025년 중반 기준)에는 1M API 요청, 500K API CDN 요청, 20명의 사용자가 포함됩니다. 주당 20-50개의 포스트를 게시하는 대부분의 편집 팀의 경우 이것은 충분히 이상합니다. 주요 비용 드라이버는 API 요청입니다. -- 모든 GROQ 쿼리가 프론트엔드에서 계산됩니다. CDN을 적극적으로 사용하고 가능한 경우 캐시하세요. 그리고 교통을 곱셈하는 클라이언트 측 가져오기를 피하세요.