지난 18개월간 법률 기술 스타트업부터 엔터프라이즈 지식 기반에 이르기까지 다양한 클라이언트를 위해 Next.js 애플리케이션에 AI 기능을 구축해왔습니다. 멋진 RAG 데모와 실제 트래픽을 처리하면서 돈을 낭비하지 않는 프로덕션 시스템 사이의 간격은 엄청납니다. 이 글에서는 어려운 교훈들을 다룹니다 -- 일관성 있는 프롬프트를 엔지니어링하는 방법, UI를 깨뜨리지 않고 응답을 스트리밍하는 방법, 비용을 60-80% 줄이기 위해 지능적으로 캐시하는 방법, 그리고 금요일 오전 2시에 무너지지 않는 RAG 파이프라인을 배포하는 방법입니다.

목차

Prompt Engineering with Next.js: Streaming RAG & Caching in Production

2026년 Next.js의 RAG가 의미 있는 이유

Next.js는 AI 기반 웹 앱의 기본 선택이 되었으며, 이것은 단순한 과장이 아닙니다. App Router, Server Actions, Route Handlers, React Server Components의 조합은 RAG 파이프라인을 위한 진정으로 좋은 아키텍처를 제공합니다. 임베딩 로직을 서버 측에 유지하고, Route Handlers를 통해 응답을 스트리밍하고, 여러 레이어에서 적극적으로 캐시할 수 있습니다.

Vercel AI SDK(현재 v4.x)는 크게 성숙했습니다. 스트리밍, 도구 호출, 구조화된 출력을 기본적으로 처리합니다. 하지만 SDK는 단지 배관일 뿐입니다 -- 실제 도전은 그 주변의 모든 것입니다: 프롬프트 설계, 검색 품질, 캐싱 전략, 오류 처리입니다.

Next.js의 일반적인 프로덕션 RAG 흐름은 다음과 같습니다:

  1. 사용자가 쿼리를 제출합니다
  2. 쿼리가 임베딩됩니다 (또는 임베딩 캐시에 도달합니다)
  3. 벡터 검색이 관련 청크를 검색합니다
  4. 청크가 순위 지정되고 필터링됩니다
  5. 신중하게 엔지니어링된 프롬프트가 컨텍스트를 조립합니다
  6. LLM이 응답을 스트리밍합니다
  7. 응답이 유사한 향후 쿼리를 위해 캐시됩니다

각 단계에는 실패 모드가 있습니다. 각각을 자세히 살펴보겠습니다.

RAG를 위한 프롬프트 엔지니어링 기초

RAG를 위한 프롬프트 엔지니어링은 순수 LLM 상호작용을 위한 프롬프트 엔지니어링과 근본적으로 다릅니다. 단지 모델에 질문을 하는 것이 아닙니다 -- 구체적인 컨텍스트 윈도우를 제공하고 훈련 데이터가 충돌할 때 무시하면서 그 컨텍스트에서 답변을 합성하도록 요청합니다.

시스템 프롬프트 아키텍처

저는 다양한 도메인에서 잘 작동하는 3부 시스템 프롬프트 구조에 정착했습니다:

const buildSystemPrompt = (config: RAGConfig) => `
You are ${config.assistantName}, a ${config.role} for ${config.company}.

## CONTEXT RULES
- Answer ONLY based on the provided context documents
- If the context doesn't contain enough information, say so explicitly
- Never fabricate citations or reference numbers
- When multiple context documents conflict, note the discrepancy


![Prompt Engineering with Next.js: Streaming RAG & Caching in Production - architecture](https://zpkyypersyvzhywdxqij.supabase.co/storage/v1/object/public/public-assets/blog-body/2a8bfe14-ca88-4f2f-a5b0-75c780b0689c-2.jpg)

## RESPONSE FORMAT
- Use markdown formatting for readability
- Cite sources using [Source: document_id] notation
- Keep responses under ${config.maxResponseTokens} tokens unless the user asks for detail
- Use ${config.tone} tone

## DOMAIN RULES
${config.domainRules.join('\n')}
`;

핵심 통찰: 도메인 규칙은 RAG를 실제로 유용하게 만드는 것들을 넣는 곳입니다. 법률 클라이언트의 경우, "항상 법령이 적용되는 관할권을 기록하세요."일 수 있습니다. 의료 지식 기반의 경우, "복용량 권장사항을 제공하지 마세요; 항상 의료 제공자에게 지시하세요."

컨텍스트 윈도우 관리

GPT-4o가 128k 컨텍스트에서 실행되고 Claude 3.5가 200k에서 실행되면서, 모든 것을 집어넣는 것이 유혹적입니다. 하지 마세요. 더 많은 컨텍스트는 더 좋은 답변을 의미하지 않습니다 -- 더 안 좋은 답변을 의미하는 경우가 많습니다.

저는 계층화된 접근 방식을 사용합니다:

const assembleContext = async (
  query: string,
  retrievedChunks: Chunk[]
): Promise<string> => {
  // Tier 1: Top 3 chunks by cosine similarity (always included)
  const primary = retrievedChunks.slice(0, 3);
  
  // Tier 2: Next 5 chunks, but only if similarity > threshold
  const secondary = retrievedChunks
    .slice(3, 8)
    .filter(c => c.similarity > 0.78);
  
  // Tier 3: Metadata-enriched summaries of remaining relevant docs
  const tertiary = retrievedChunks
    .slice(8)
    .filter(c => c.similarity > 0.72)
    .map(c => c.summary); // Pre-computed summaries, not full text
  
  return formatContextTiers(primary, secondary, tertiary);
};

이는 일반적으로 30,000 이상 대신 3,000-8,000 토큰의 컨텍스트를 초래합니다. 응답 품질은 올라가고, 지연 시간은 내려가고, API 청구는 줄어듭니다.

프롬프트 버전 관리

이것은 블로그 게시물에서는 거의 아무도 이야기하지 않지만 프로덕션에서는 모두가 필요로 하는 것입니다. 프롬프트는 변경됩니다. 이러한 변경을 추적해야 합니다.

// prompts/v2.3.ts
export const RAG_PROMPT_V2_3 = {
  version: '2.3',
  createdAt: '2026-03-15',
  changelog: 'Added conflict resolution instruction, reduced hallucination on legal queries by 23%',
  system: `...`,
  userTemplate: (query: string, context: string) => `...`,
};

프롬프트 버전을 데이터베이스가 아닌 코드에 저장합니다. 다른 코드 변경과 마찬가지로 PR에서 검토됩니다. 프로덕션에서 문제가 발생하면, 특정 프롬프트 버전으로 추적할 수 있습니다.

Next.js에서 스트리밍 RAG 설정

Vercel AI SDK를 사용한 Route Handler

프로덕션 준비 스트리밍 RAG 엔드포인트입니다:

// app/api/chat/route.ts
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
import { retrieveContext } from '@/lib/rag/retriever';
import { buildPrompt } from '@/lib/rag/prompts';
import { checkCache, setCache } from '@/lib/rag/cache';
import { rateLimiter } from '@/lib/middleware/rate-limit';

export const runtime = 'nodejs'; // Not edge -- you need Node for most vector DBs
export const maxDuration = 30;

export async function POST(req: Request) {
  const { messages, sessionId } = await req.json();
  const lastMessage = messages[messages.length - 1].content;

  // Rate limiting
  const allowed = await rateLimiter.check(sessionId);
  if (!allowed) {
    return new Response('Rate limited', { status: 429 });
  }

  // Check semantic cache first
  const cached = await checkCache(lastMessage);
  if (cached) {
    return new Response(cached.response, {
      headers: { 'X-Cache': 'HIT', 'Content-Type': 'text/plain' },
    });
  }

  // Retrieve context
  const context = await retrieveContext(lastMessage, {
    topK: 10,
    minSimilarity: 0.72,
    namespace: 'production',
  });

  // Build the prompt
  const systemPrompt = buildPrompt(context);

  // Stream the response
  const result = streamText({
    model: openai('gpt-4o'),
    system: systemPrompt,
    messages,
    temperature: 0.3, // Low temp for factual RAG
    maxTokens: 1500,
    onFinish: async ({ text }) => {
      // Cache the completed response
      await setCache(lastMessage, text, context.chunks.map(c => c.id));
    },
  });

  return result.toDataStreamResponse();
}

클라이언트 측 스트리밍 UI

프론트엔드에서 useChat 훅이 스트리밍을 잘 처리합니다:

// components/ChatInterface.tsx
'use client';

import { useChat } from 'ai/react';
import { useRef, useEffect } from 'react';

export function ChatInterface() {
  const { messages, input, handleInputChange, handleSubmit, isLoading, error } =
    useChat({
      api: '/api/chat',
      body: { sessionId: getSessionId() },
      onError: (err) => {
        // Don't just console.log -- show the user something useful
        toast.error('Something went wrong. Try rephrasing your question.');
      },
    });

  const scrollRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    scrollRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);

  return (
    <div className="flex flex-col h-full">
      <div className="flex-1 overflow-y-auto p-4 space-y-4">
        {messages.map((m) => (
          <div key={m.id} className={m.role === 'user' ? 'text-right' : ''}>
            <div className="prose prose-sm max-w-none">
              <Markdown>{m.content}</Markdown>
            </div>
          </div>
        ))}
        {isLoading && <TypingIndicator />}
        <div ref={scrollRef} />
      </div>
      <form onSubmit={handleSubmit} className="p-4 border-t">
        <input
          value={input}
          onChange={handleInputChange}
          placeholder="Ask a question..."
          className="w-full p-3 rounded-lg border"
          disabled={isLoading}
        />
      </form>
    </div>
  );
}

스트리밍 엣지 케이스 처리

프로덕션에서 스트리밍은 데모에서는 절대 발생하지 않는 것들을 처리하는 것을 의미합니다:

  • 중간에 연결 끊김: 지수 백오프로 재시도 로직을 구현하세요. AI SDK의 onError 콜백이 당신의 친구입니다.
  • 토큰 제한 초과: 토큰 사용량을 모니터링하고 모델이 하기 전에 하드 컷오프를 구현하세요 (모델의 컷오프는 못생겼습니다).
  • 느린 검색: 벡터 DB 쿼리에 타임아웃을 설정하세요. 검색이 > 2초 걸리면 더 작은 컨텍스트 또는 캐시된 유사 쿼리로 폴백하세요.

캐싱 레이어: 실제 돈을 절약하는 곳

캐싱은 프로덕션 RAG 시스템에 할 수 있는 단일 가장 영향력 있는 최적화입니다. 구현할 가치가 있는 세 가지 레이어가 있습니다.

레이어 1: 임베딩 캐시

모든 쿼리에는 임베딩이 필요합니다. text-embedding-3-small으로 1K 토큰당 $0.00002로 쿼리당 저렴하지만, 누적되고 -- 더 중요하게도 -- 지연 시간을 추가합니다.

import { Redis } from '@upstash/redis';
import { createHash } from 'crypto';

const redis = new Redis({ url: process.env.UPSTASH_REDIS_URL!, token: process.env.UPSTASH_REDIS_TOKEN! });

export async function getEmbedding(text: string): Promise<number[]> {
  const hash = createHash('sha256').update(text.toLowerCase().trim()).digest('hex');
  
  // Check cache
  const cached = await redis.get<number[]>(`emb:${hash}`);
  if (cached) return cached;
  
  // Generate embedding
  const embedding = await openai.embeddings.create({
    model: 'text-embedding-3-small',
    input: text,
  });
  
  const vector = embedding.data[0].embedding;
  
  // Cache for 7 days
  await redis.set(`emb:${hash}`, vector, { ex: 604800 });
  
  return vector;
}

레이어 2: 의미론적 캐시

이것이 중요한 것입니다. 누군가 "환불 정책은 무엇입니까?"를 묻고 다른 누군가 "환불을 받으려면 어떻게 하나요?"를 묻는다면, 동일한 캐시된 응답을 받아야 합니다.

export async function checkSemanticCache(query: string): Promise<CacheResult | null> {
  const embedding = await getEmbedding(query);
  
  // Search the cache index (separate from your content index)
  const results = await pinecone.index('cache').query({
    vector: embedding,
    topK: 1,
    includeMetadata: true,
  });
  
  if (results.matches[0]?.score > 0.95) {
    return {
      response: results.matches[0].metadata.response as string,
      originalQuery: results.matches[0].metadata.query as string,
      cachedAt: results.matches[0].metadata.cachedAt as string,
    };
  }
  
  return null;
}

0.95 임계값이 중요합니다. 너무 낮으면 잘못된 답변을 제공합니다. 너무 높으면 캐시 히트를 얻지 못합니다. 0.95에서 시작하여 도메인을 기반으로 튜닝하세요.

레이어 3: 응답 조각 캐시

구조화된 응답(제품 사양 또는 정책 요약 등)의 경우, 개별 조각을 캐시하세요:

캐시 레이어 히트율 (일반적) 지연 시간 절감 비용 절감
임베딩 캐시 40-60% 50-100ms per query ~$50/mo at 100K queries
의미론적 캐시 15-35% 1-3s per query ~$300-800/mo at 100K queries
조각 캐시 20-40% 500ms-1s per query ~$100-200/mo at 100K queries
결합 60-80% 1-3s average $500-1200/mo at 100K queries

캐시 무효화

클래식한 어려운 문제. RAG의 경우, 두 가지 방법을 사용합니다:

  1. TTL 기반: 모든 캐시는 소스 데이터가 얼마나 자주 변경되는지에 따라 24-72시간 후에 만료됩니다.
  2. 이벤트 기반: 소스 문서가 업데이트될 때, 해당 문서 ID를 참조한 캐시 항목을 무효화합니다 (캐시 메타데이터에 청크 ID를 저장하는 이유입니다).

프로덕션 아키텍처 패턴

전체 스택

대부분의 프로덕션 RAG 배포에 사용하는 아키텍처입니다:

User → Next.js App Router → Route Handler
                              ↓
                         Rate Limiter (Upstash)
                              ↓
                         Semantic Cache Check (Pinecone + Redis)
                              ↓ (miss)
                         Embedding Generation (OpenAI / cached)
                              ↓
                         Vector Search (Pinecone / Weaviate / pgvector)
                              ↓
                         Re-ranking (Cohere Rerank / custom)
                              ↓
                         Prompt Assembly
                              ↓
                         LLM Streaming (OpenAI / Anthropic)
                              ↓
                         Response → Cache Write → User

벡터 데이터베이스 선택

데이터베이스 최고 사용 사례 가격 (2026) Next.js 통합
Pinecone 관리형, 운영 불필요 Free tier → $70/mo starter 탁월 (REST API)
Weaviate Cloud 하이브리드 검색 (벡터 + 키워드) $25/mo starter 좋음 (JS client)
pgvector (Supabase) 이미 Postgres 사용 중 Free tier → $25/mo 훌륭함 (Supabase SDK)
Qdrant Cloud 고성능, 필터링 Free tier → $30/mo 좋음 (JS client)
Turbopuffer 비용 최적화, S3 기반 ~$0.04/GB stored 괜찮음 (REST API)

대부분의 Next.js 프로젝트의 경우, 이미 그 생태계에 있다면 Supabase의 pgvector에서 시작하거나, 운영 오버헤드가 없기를 원한다면 Pinecone을 추천합니다. 우리는 CMS 콘텐츠가 RAG 파이프라인에 공급되는 프로젝트에서 이 모든 것을 프로덕션에서 사용했습니다.

오류 처리 및 폴백

프로덕션 RAG는 우아한 성능 저하가 필요합니다:

export async function handleRAGQuery(query: string) {
  try {
    // Primary path: full RAG
    return await fullRAGPipeline(query);
  } catch (error) {
    if (error instanceof VectorDBError) {
      // Fallback 1: Use cached similar queries
      const fallback = await getFallbackFromCache(query);
      if (fallback) return { ...fallback, degraded: true };
    }
    
    if (error instanceof LLMError) {
      // Fallback 2: Try a different model
      return await fullRAGPipeline(query, { model: 'claude-3-5-sonnet' });
    }
    
    // Fallback 3: Return relevant raw chunks without LLM synthesis
    const chunks = await retrieveContext(query);
    return {
      response: 'I couldn\'t generate a full answer, but here are relevant excerpts:',
      chunks: chunks.slice(0, 3),
      degraded: true,
    };
  }
}

모니터링, 관찰성 및 디버깅

측정할 수 없는 것은 개선할 수 없습니다. 추적할 항목은 다음과 같습니다:

주요 지표

  • 검색 품질: 상위 K 청크가 실제로 관련성이 있습니까? 유사성 점수를 기록하고 주간으로 스팟 체크하세요.
  • 응답 지연 시간 (p50/p95/p99): 스트리밍 TTFB (첫 바이트까지의 시간) 및 총 완료 시간.
  • 캐시 히트율: 레이어별로. 의미론적 캐시 히트율이 10% 미만이면 임계값이 너무 높을 수 있습니다.
  • 쿼리당 토큰 사용량: 평균 및 p99. 프롬프트 주입 시도로 인한 컨텍스트 팽창을 감시하세요.
  • 사용자 피드백 신호: 엄지손가락 위/아래, 복사 이벤트, 후속 질문 (첫 답변이 충분하지 않았음을 나타냅니다).

도구

LLM 관찰성을 위해 다음과 같은 좋은 결과를 얻었습니다:

  • Langfuse: 오픈 소스, 자체 호스팅 가능, 우수한 추적 시각화. 무료 계층이 관대합니다.
  • Helicone: 프록시 기반 로깅, 비용 추적에 훌륭합니다. 한 줄 통합.
  • Braintrust: 평가 및 프롬프트 반복에 좋습니다. eval 프레임워크가 견고합니다.
// Example: Langfuse integration with Vercel AI SDK
import { Langfuse } from 'langfuse';

const langfuse = new Langfuse();

export async function POST(req: Request) {
  const trace = langfuse.trace({ name: 'rag-query' });
  
  const retrievalSpan = trace.span({ name: 'retrieval' });
  const context = await retrieveContext(query);
  retrievalSpan.end({ output: { chunkCount: context.length, topScore: context[0]?.similarity } });
  
  const generationSpan = trace.generation({
    name: 'llm-response',
    model: 'gpt-4o',
    input: messages,
  });
  
  // ... stream response
  
  generationSpan.end({ output: completedText });
  await langfuse.flushAsync();
}

성능 벤치마크 및 비용 분석

프로덕션 배포의 실제 수치 (법률 지식 기반, ~50K 문서, ~2M 청크):

지표 최적화 없음 전체 최적화 포함
중앙값 지연 시간 (TTFB) 2.1s 340ms
P95 지연 시간 (TTFB) 4.8s 1.2s
월간 LLM 비용 (50K 쿼리) $2,400 $680
월간 임베딩 비용 $180 $45
월간 벡터 DB 비용 $70 $70 (unchanged)
캐시 히트율 0% 67%
사용자 만족도 (엄지손가락 위 %) 71% 89%

만족도 향상은 대부분 더 좋은 프롬프트와 재순위 지정에서 비롯되었으며, 캐싱에서는 아닙니다. 하지만 비용과 지연 시간 개선은 거의 entirely 캐싱에서 비롯되었습니다.

쿼리당 비용 분석

월간 50K 쿼리, GPT-4o 사용:

  • 캐시되지 않은 쿼리: ~$0.048 (임베딩 + 검색 + 생성)
  • 의미론적 캐시 히트: ~$0.0004 (임베딩 조회 + 캐시 읽기)
  • 임베딩 캐시 히트 + 의미론적 미스: ~$0.035 (임베딩 스킵, 여전히 생성)

이와 같은 것을 구축하고 아키텍처에 대한 도움이 필요하다면, 우리는 정기적으로 이런 종류의 작업을 합니다 -- 또는 문의하세요.

FAQ

2026년 프로덕션 RAG를 위한 최고의 LLM은 무엇입니까?

대부분의 사용 사례에 대해, GPT-4o는 품질, 속도, 비용의 달콤한 자리에 적중합니다. Claude 3.5 Sonnet은 더 긴 컨텍스트 처리가 필요하거나 더 미묘한 추론이 필요할 때 우수합니다. 비용에 민감한 애플리케이션에는 더 간단한 쿼리의 경우 GPT-4o-mini 또는 Claude 3.5 Haiku가 놀랍게 잘 작동합니다 -- 검색이 좋을 때 직설적인 Q&A에서 GPT-4o와 일치하는 품질을 보았습니다. 모델은 검색 품질 및 프롬프트 엔지니어링보다 덜 중요합니다.

RAG Route Handlers에 Edge Runtime 또는 Node.js Runtime을 사용해야 합니까?

Node.js, 거의 항상. Edge Runtime에는 대부분의 벡터 데이터베이스 및 ORM으로 작업하기 어렵게 만드는 연결 제한이 있습니다. Edge의 콜드 스타트 장점은 사용자가 검색 + 생성을 기다리고 있기 때문에 스트리밍 엔드포인트에 대해 무시할 수 있습니다. 단순 프록시 엔드포인트 또는 비 RAG 경로에 Edge를 사용하세요.

RAG 응답에서 환각을 어떻게 방지합니까?

실제로 작동하는 세 가지 전략: (1) 컨텍스트가 부족할 때 모델에 명시적으로 "정보가 충분하지 않습니다"라고 말하도록 지시하세요 -- 프롬프트에 예제를 포함하세요. (2) 저온도 (팩트형 쿼리의 경우 0.1-0.3)를 사용하세요. (3) 인용 요구 사항을 구현하세요 -- 모델이 특정 청크를 인용해야 할 때, 환각을 만드는 것은 훨씬 더 어렵습니다. 사후 검증 (주장이 소스 청크에 나타나는지 확인)은 또 다른 안전 레이어를 추가합니다.

RAG 컨텍스트를 위해 몇 개의 청크를 검색해야 합니까?

사용하는 것보다 더 많이 검색하세요. 일반적으로 15-20개 청크를 검색하고, 다시 순위를 매기고, 상위 3-5개를 기본 컨텍스트로 사용하고 다음 3-5개의 요약을 포함합니다. 20개의 전체 청크를 컨텍스트 윈도우에 덤프하면 품질이 저하됩니다. 관련 정보가 있더라도 모델이 무관한 정보로 인해 혼란스러워집니다. 수량보다 품질, 항상.

pgvector는 프로덕션에 충분합니까, 아니면 전용 벡터 데이터베이스가 필요합니까?

~1M 벡터까지, Supabase 또는 Neon의 HNSW 인덱싱이 있는 pgvector는 절대적으로 프로덕션 준비가 되어 있습니다. 쿼리 성능은 탁월하고 기존 Postgres 생태계에 머물 수 있다는 이점을 얻습니다. 1M 벡터 이상 또는 벡터 검색으로 고급 필터링이 필요한 경우, Pinecone 또는 Qdrant와 같은 전용 옵션이 앞서 나가기 시작합니다. 우리는 문제 없이 몇 가지 프로젝트에서 pgvector를 프로덕션에서 실행했습니다.

React에서 로딩 상태를 사용하여 스트리밍 응답을 어떻게 처리합니까?

Vercel AI SDK의 useChat 훅이 isLoading 부울을 제공하지만, 더 미묘합니다. 훅은 여러 상태를 통해 전환됩니다: idle → waiting (아직 토큰 없음) → streaming (토큰 도착) → idle. 최고의 UX를 위해 대기 단계에서 타이핑 표시기를 표시하고 스트리밍 중에 마크다운을 점진적으로 렌더링합니다. 불완전한 마크다운을 우아하게 처리하는 마크다운 렌더러를 사용하세요 -- react-markdown은 작동하지만 깜박일 수 있습니다; 렌더링하기 전에 몇 토큰을 버퍼링하는 것을 고려하세요.

RAG에서 다중 턴 대화를 어떻게 처리합니까?

모든 메시지에서 다시 검색하지 마세요. 새 메시지가 후속인지 (같은 컨텍스트 필요) 또는 주제 변경인지 (새 검색 필요) 결정하려면 대화 기록을 사용하세요. 간단한 분류기 -- 대명사와 참조를 확인하는 정규식 기반도 -- 불필요한 벡터 검색을 많이 절약할 수 있습니다. 다시 검색할 때, 최신 메시지가 아닌 검색 쿼리에 대화 요약을 포함하세요.

RAG를 위해 문서를 얼마나 자주 다시 인덱싱해야 합니까?

소스 데이터가 얼마나 자주 변경되는지에 따라 다릅니다. 정적 지식 기반의 경우, 주간 전체 다시 인덱싱이 문제없습니다. 동적 콘텐츠 (CMS 기반 사이트, 매일 업데이트되는 문서)의 경우, 웹훅으로 트리거된 증분 인덱싱을 설정하세요. 핵심은 모든 것을 다시 인덱싱하지 않고 개별 청크를 업데이트할 수 있는 파이프라인을 갖는 것입니다. 우리는 이것을 헤드리스 CMS 통합에 구축합니다 -- 콘텐츠가 Sanity 또는 Contentful에서 업데이트될 때, 영향을 받는 청크가 자동으로 다시 임베딩되고 upserted됩니다.