All Systems Operational
Powered By
profound-logo
profound-logoProfound CMS
⌘K
Admin

템플릿 빌더에서 스크립팅

CMS에서 CEL 표현식을 작성하는 실용 가이드입니다.

Continue Reading
Previous‹편집 모드 지원이 있는 정적 렌더링NextCreate Profound Next›

하이브리드

렌더러 프로젝트매개변수 라우팅컴포넌트 유형Sse관리자 패널 프록시 설정편집 모드 지원이 있는 정적 렌더링템플릿 빌더에서 스크립팅Create Profound Next

헤드리스

빠른 시작JSON과 Claude 코드Component Zod Pull

Mcp

Mcp

CMS 기능

기능 문서 템플릿템플릿 빌더 기능기능 번역기기능 조직

동기부여

우리의 접근법

용어

하이브리드 대 헤드리스

CMS에서 CEL 표현식을 작성하는 실용 가이드입니다.


CEL 작동 방식

CEL(Common Expression Language)은 CMS에 내장된 경량 스크립트 언어입니다. 문서에서 데이터를 가져오고, URL 매개변수를 읽고, 즉석에서 값을 계산하는 동적 표현식을 작성할 수 있게 해줍니다.

CEL 스크립트가 실행될 때 일어나는 일은 다음과 같습니다.

사용자 스크립트                 엔진                          결과
    |                              |                              |
    v                              v                              v
documents.get("article", "intro") --> 데이터베이스에서 가져옴 --> { headline: "환영합니다", body: "..." }
         .headline                --> 필드를 추출함          --> "환영합니다"

CEL을 읽기 전용 쿼리 언어라고 생각하세요. 데이터베이스에서 아무 것도 수정할 수 없으며, 데이터를 읽고 계산된 결과를 반환하기만 합니다. 덕분에 CMS 어디에서나 안전하게 사용할 수 있습니다.


기본 구성 요소

모든 CEL 표현식은 다음 세 가지에 접근할 수 있습니다.

객체설명예시
documentsCMS에서 어떤 문서든 가져오기documents.get("country", "us")
meta현재 요청에 대한 정보(로케일, URL 매개변수 등)meta.locale, meta.params.slug
schema현재 문서의 필드 정의schema.fields

문서 가져오기

CEL의 가장 강력한 기능은 CMS 어디에서든 문서를 가져올 수 있다는 점입니다.

단일 문서 가져오기

구문: documents.get(schemaName, identifier)

"welcome-post"라는 식별자로 저장된 article 문서가 있다고 가정해보겠습니다.

// CMS에 article / welcome-post로 저장됨
{
  "headline": "우리 플랫폼에 오신 것을 환영합니다",
  "author": "Sarah Chen",
  "body": "우리는 기쁜 마음으로 발표합니다...",
  "tags": ["announcement", "news"]
}

전체 문서를 가져오려면:

documents.get("article", "welcome-post")

반환값:

{
  "headline": "우리 플랫폼에 오신 것을 환영합니다",
  "author": "Sarah Chen",
  "body": "우리는 기쁜 마음으로 발표합니다...",
  "tags": ["announcement", "news"]
}

헤드라인만 가져오려면:

documents.get("article", "welcome-post").headline

반환값: "우리 플랫폼에 오신 것을 환영합니다"

작성자를 가져오려면:

documents.get("article", "welcome-post").author

반환값: "Sarah Chen"


URL 매개변수 사용하기

페이지에 /articles/[slug] 같은 동적 라우트가 있을 때, meta.params를 사용해 URL 매개변수를 읽고 올바른 문서를 가져올 수 있습니다.

누군가 /articles/welcome-post를 방문하면:

documents.get("article", meta.params.slug).headline

반환값: "우리 플랫폼에 오신 것을 환영합니다"

이 방식으로 동적 페이지를 만들 수 있습니다. 동일한 CEL 스크립트가 URL에 있는 어떤 슬러그에도 작동합니다.


여러 문서 가져오기

구문: documents.find(schemaName) 또는 documents.find(schemaName, filter)

// 모든 국가 문서를 가져오기
documents.find("country")

반환값:

[
  { "code": "us", "name": "미국", "flag": "US" },
  { "code": "sa", "name": "사우디아라비아", "flag": "SA" },
  { "code": "gb", "name": "영국", "flag": "GB" }
]
// 필터로 국가 가져오기
documents.find("country", { "where": { "code": "us" } })

반환값:

[
  { "code": "us", "name": "미국", "flag": "US" }
]

실제 활용 예시

예시 1: 다른 문서에서 가져온 히어로 블록 제목

hero-block이 article 문서에서 가져온 헤드라인을 표시해야 합니다.

기사 문서(식별자: "homepage-hero"):

{
  "headline": "더 빠르게 구축하고 더 영리하게 배포하세요",
  "subheadline": "개발자를 위한 현대적인 CMS"
}

히어로 블록의 제목 필드에 입력한 CEL 스크립트:

documents.get("article", "homepage-hero").headline

결과: 히어로에 "더 빠르게 구축하고 더 영리하게 배포하세요"가 표시됩니다.


예시 2: 코드로부터 국가 이름 가져오기

/countries/[code] 페이지를 만들면서 전체 국가 이름을 보여주고 싶습니다.

국가 문서:

// country / us
{ "code": "us", "name": "미국", "flag": "US", "languages": ["en", "es"] }

// country / sa
{ "code": "sa", "name": "사우디아라비아", "flag": "SA", "languages": ["ar", "en"] }

CEL 스크립트:

documents.get("country", meta.params.code).name

누군가 /countries/us를 방문하면:

  • meta.params.code = "us"
  • 결과: "미국"

누군가 /countries/sa를 방문하면:

  • meta.params.code = "sa"
  • 결과: "사우디아라비아"

예시 3: 로케일에 따른 조건부 콘텐츠

사용자의 로케일에 따라 다른 헤드라인을 보여줍니다.

meta.locale == "ar-SA" ? "مرحبا بكم" : "Welcome"

로케일이 "ar-SA"이면: "مرحبا بكم"을 반환합니다. 다른 로케일이면: "Welcome"을 반환합니다.


예시 4: 연쇄 문서 조회

article 문서에 countryCode 필드가 있고, 전체 국가 이름을 얻고 싶습니다.

기사 문서:

{ "headline": "미국 소식", "countryCode": "us" }

CEL 스크립트:

documents.get("country", documents.get("article", "us-news").countryCode).name

동작 과정:

  1. documents.get("article", "us-news")는 { "headline": "미국 소식", "countryCode": "us" }를 반환합니다.
  2. .countryCode가 "us"를 추출합니다.
  3. documents.get("country", "us")는 { "code": "us", "name": "미국", ... }를 반환합니다.
  4. .name이 "미국"을 추출합니다.

결과: "미국"


예시 5: 대체 값 사용하기

문서가 없을 수도 있다면 대체 값을 제공할 수 있습니다.

documents.get("article", meta.params.slug) != null
  ? documents.get("article", meta.params.slug).headline
  : "기사를 찾을 수 없습니다"

또는 특정 필드가 있는지 확인합니다.

documents.get("article", "intro").author != null
  ? documents.get("article", "intro").author
  : "알 수 없는 작성자"

예시 6: 리스트 다루기

기사에 태그가 있고, 특정 태그가 있는지 확인하고 싶습니다.

"featured" in documents.get("article", "welcome-post").tags

반환값: "featured" 태그가 있으면 true

첫 번째 태그 가져오기:

documents.get("article", "welcome-post").tags[0]

반환값: "announcement" (첫 번째 태그)

태그 개수 세기:

size(documents.get("article", "welcome-post").tags)

반환값: 2 (태그 개수)


매개변수화된 라우트와 meta.params

매개변수화된 라우트는 동적이고 현지화된 페이지를 만드는 핵심입니다. /{lang}/landingPage와 같은 라우트 패턴을 정의하면, CMS가 URL에서 매개변수를 추출해 meta.params를 통해 제공합니다.

라우트 매개변수 작동 방식

라우트 패턴 정의: 라우트는 :paramName 또는 {paramName} 구문을 사용해 동적 세그먼트를 정의합니다.

패턴예시 URL추출된 매개변수
/:lang/landingPage/ko/landingPage{ lang: "ko" }
/{country}/{lang}/products/us/en/products{ country: "us", lang: "en" }
/articles/:slug/articles/welcome-post{ slug: "welcome-post" }

매개변수 바인딩: 각 라우트 매개변수는 유효성 검사를 위해 문서 스키마에 바인딩될 수 있습니다.

{
  "pattern": "/{lang}/landingPage",
  "param_bindings": {
    "lang": "language"
  }
}

이 바인딩은 CMS에 다음을 지시합니다.

  1. URL에서 lang 세그먼트를 추출한다.
  2. language 스키마와 대조하여 유효성을 검사한다(content.code가 일치하는 문서를 찾음).
  3. 유효하다면, 전체 문서를 해결된 매개변수에 제공한다.

예시: 언어 기반 랜딩 페이지

라우트 구성:

  • 경로: /{lang}/landingPage
  • 패턴: /{lang}/landingPage
  • 매개변수 바인딩: { "lang": "language" }

환영 인사 문서:

// greeting / ko
{ "code": "ko", "headline": "환영합니다", "subheadline": "우리 플랫폼에 오신 것을 환영합니다" }

// greeting / en
{ "code": "en", "headline": "Welcome", "subheadline": "Welcome to our platform" }

// greeting / ja
{ "code": "ja", "headline"

현지화된 콘텐츠를 가져오는 CEL 스크립트:

documents.get("greeting", meta.params.lang).headline

해결 과정:

URLmeta.params.lang결과
/ko/landingPage"ko""환영합니다"
/en/landingPage"en""Welcome"
/ja/landingPage"ja""ようこそ"

고급 패턴: 국가 + 언어 라우트

/{country}/{lang}/products 같은 라우트의 경우:

라우트 구성:

{
  "pattern": "/{country}/{lang}/products",
  "param_bindings": {
    "country": "country",
    "lang": "language"
  }
}

CEL 스크립트:

// 국가 이름 가져오기
documents.get("country", meta.params.country).name

// 국가에 따라 현지화된 상품 목록 가져오기
documents.find("product", { "where": { "country": meta.params.country } })

// 조합: 사용자의 언어로 국가별 인삿말 표시
documents.get("greeting", meta.params.lang).headline + " from " + documents.get("country", meta.params.country).name

유효성 검사 단계: CMS는 매개변수를 계층적으로 검증합니다. /{country}/{lang} 라우트에서는:

  1. country 매개변수를 country 스키마로 검증합니다.
  2. lang 매개변수를 language 스키마로 검증합니다.
  3. 선택적으로 lang이 country.languages[] 배열에 있는지 확인합니다(계층적 검증).

meta.segments - 원시 URL 경로 접근

meta.segments는 이름이 지정되지 않은 매개변수 없이 위치 기반 접근이 필요할 때 유용한, URL 경로를 배열 형태로 제공합니다.

작동 방식:

URL 경로meta.segments
/articles/tech/ai-news["articles", "tech", "ai-news"]
/ko/landingPage["ko", "landingPage"]
/us/en/products/featured["us", "en", "products", "featured"]
/[]

meta.params와 meta.segments를 사용할 때

사용 사례권장 접근 방식
라우트 패턴에서 이름이 지정된 매개변수meta.params.lang
위치 기반 접근meta.segments[0]
경로 깊이 파악size(meta.segments)
경로에 특정 세그먼트가 포함되어 있는지 확인"admin" in meta.segments

meta.segments 사용 예시

// 첫 번째 세그먼트 가져오기(종종 언어 코드)
meta.segments[0]

// 경로 깊이 확인
size(meta.segments) > 2 ? "deep" : "shallow"

// 관리자 섹션인지 확인
"admin" in meta.segments ? "admin mode" : "public mode"

// 대체: 매개변수가 바인딩되지 않았다면 세그먼트 사용
has(meta.params.lang) ? meta.params.lang : meta.segments[0]

meta 객체 전체 참조

meta 객체는 현재 요청에 대한 모든 컨텍스트를 담고 있습니다.

속성타입설명
meta.localestring현재 로케일 코드(예: "en-US", "ko-KR", "ar-SA")
meta.paramsRecord<string, string>URL 패턴에서 추출된 라우트 매개변수
meta.segmentsstring[]세그먼트로 분리된 URL 경로
meta.docIdstring 또는 null현재 문서 UUID(새 문서라면 null)
meta.titlestring현재 문서 제목

meta.locale

로케일 코드는 언어-지역 형식(BCP 47)을 따릅니다.

// 로케일이 RTL 언어인지 확인
meta.locale == "ar-SA" || meta.locale == "he-IL" ? "rtl" : "ltr"

// 언어 부분만 가져오기
meta.locale.split("-")[0]  // 지원되지 않음 - 대신 meta.params.lang 사용

meta.params

라우트 매개변수는 항상 문자열입니다. CMS는 평가 전에 바인딩된 스키마로 유효성을 검사합니다.

// 이름이 지정된 매개변수 접근
meta.params.lang           // "ko"
meta.params.country        // "us"
meta.params.slug           // "welcome-post"

// 매개변수 존재 여부 확인
has(meta.params.category)  // true/false

// 문서 가져오기에 사용
documents.get("greeting", meta.params.lang)
documents.ref("airports").get(meta.params.code)

meta.segments

배열 형태의 원시 URL 세그먼트입니다.

// 인덱스로 접근(0부터 시작)
meta.segments[0]           // 첫 번째 세그먼트
meta.segments[1]           // 두 번째 세그먼트

// 길이 확인
size(meta.segments)        // 세그먼트 개수

// 포함 여부 확인
"products" in meta.segments  // 경로에 "products"가 포함되어 있는가?

meta.docId

현재 문서의 UUID로, 자기 참조 스크립트에 유용합니다.

// 기존 문서를 편집할 때만 사용 가능
meta.docId != null ? "editing" : "creating new"

// 조건부 로직에 사용
meta.docId != null ? documents.get("article", meta.docId).status : "draft"

meta.title

현재 문서의 제목입니다.

// 화면에 표시하기
"Editing: " + meta.title

// 제목에 따른 조건
meta.title.contains("Draft") ? "work in progress" : "published"

documents.ref() - 연쇄 조회

스키마는 고정되어 있고 식별자만 동적인 경우, 더 깔끔한 구문을 위해 사용합니다.

// 기존 방식
documents.get("airports", meta.params.code).name

// ref() 사용 - 스키마는 분리하고 식별자만 동적으로
documents.ref("airports").get(meta.params.code).name

두 방식 모두 동일하지만, ref()를 사용하면 동적인 부분이 더 명확해집니다.


빠른 참조

문서 가져오기

documents.get("schema", "identifier")       // 단일 문서 가져오기
documents.get("schema", "id").fieldName     // 특정 필드 가져오기
documents.find("schema")                    // 모든 문서 가져오기
documents.find("schema", { "where": {...}}) // 필터된 쿼리
documents.ref("schema").get(identifier)     // 연쇄 조회

컨텍스트 변수

meta.locale          // "en-US", "ar-SA" 등
meta.params.xyz      // "xyz"라는 이름의 URL 매개변수
meta.segments        // URL 경로 배열: ["articles", "intro"]
meta.segments[0]     // 첫 번째 경로 세그먼트
meta.docId           // 현재 문서 ID(또는 null)
meta.title           // 현재 문서 제목

연산자

// 비교
==  !=  <  <=  >  >=

// 논리
&&  ||  !

// 삼항(조건문)
condition ? valueIfTrue : valueIfFalse

// 포함
"value" in listOrMap

자주 쓰는 함수

size(list)                    // 항목 수
size(string)                  // 문자열 길이
"text".startsWith("te")       // true
"text".endsWith("xt")         // true
"text".contains("ex")         // true
has(object.property)          // 속성 존재 여부 확인

오류 메시지

문제가 발생하면 다음 중 하나가 표시됩니다.

오류의미
SYNTAX_ERROR스크립트에 오타가 있습니다(따옴표 누락, 잘못된 연산자 등).
TYPE_ERROR함께 사용할 수 없는 타입을 섞었습니다.
RUNTIME_ERROR스크립트는 실행됐지만 문제를 만났습니다(정의되지 않은 변수 등).
FETCH_LIMIT_EXCEEDED너무 많은 문서를 가져오고 있습니다(최대 50개).
TIMEOUT스크립트가 너무 오래 걸렸습니다(최대 5초).
AST_DEPTH_EXCEEDED표현식 중첩이 너무 깊습니다(최대 깊이: 50).
SCRIPT_TOO_LONG스크립트가 5000자 제한을 초과했습니다.

확장성 및 향후 기능

CEL 엔진은 확장성을 염두에 두고 설계되었습니다. 앞으로 계획된 기능은 다음과 같습니다.

예정: MCP 서버 통합

// 미래 기능: MCP를 통해 외부 서비스 호출
mcp.translate(meta.params.text, "en", meta.params.lang)
mcp.analyze(documents.get("article", meta.params.id).body)

예정: AI 기능

// 미래 기능: AI 기반 콘텐츠 생성
ai.summarize(documents.get("article", meta.params.id).body, 100)
ai.translate(meta.params.text, meta.params.targetLang)
ai.classify(meta.params.input, ["positive", "negative", "neutral"])

이러한 기능은 등록된 함수 시스템을 통해 추가되며, 기존 스크립트와의 하위 호환성을 유지합니다.


팁

  1. 자동 완성 사용 - documents. 또는 meta.를 입력하면 편집기가 사용 가능한 옵션을 보여줍니다.
  2. 간단하게 시작 - 먼저 documents.get("schema", "id")로 테스트한 뒤 .fieldName을 추가하세요.
  3. null 확인 - 문서가 없을 수도 있다면 != null ? ... : ...으로 대체 값을 추가하세요.
  4. 과도한 조회 피하기 - documents.get()이나 documents.find() 호출마다 50회 조회 제한에 포함됩니다.
  5. meta.segments보다 meta.params 선호 - 이름이 지정된 매개변수는 유효성 검사를 거쳐 더 신뢰할 수 있습니다.
  6. 선택적 매개변수에는 has() 사용 - meta.params.category를 사용하기 전에 has(...)로 확인하세요.
  7. 식별자가 동적일 때 documents.ref() 사용 - 스키마는 고정이지만 식별자는 동적일 때 더 명확한 구문입니다.

부록 A: 완전한 매개변수 라우트 예제

이 가이드는 /{lang}/landingPage에서 접근할 수 있는 다국어 랜딩 페이지를 만드는 방법을 설명합니다.

1단계: Greeting 문서 스키마 만들기

CMS 관리자에서 greeting이라는 사용자 정의 스키마를 생성합니다.

{
  "name": "greeting",
  "fields": [
    { "name": "code", "type": "string", "required": true },
    { "name": "headline", "type": "string", "required": true },
    { "name": "subheadline"

2단계: Greeting 문서 생성

각 언어에 대한 문서를 만듭니다.

문서: greeting / ko

{
  "code": "ko",
  "headline": "환영합니다",
  "subheadline": "우리 플랫폼에 오신 것을 환영합니다",
  "ctaText": "시작하기",
  "ctaUrl": "/ko/get-started"
}

문서: greeting / en

{
  "code": "en",
  "headline": "Welcome",
  "subheadline": "Welcome to our platform",
  "ctaText": "Get Started",
  "ctaUrl": "/en/get-started"
}

문서: greeting / ja

{
  "code": "ja",
  "headline": "ようこそ",
  "subheadline": "私たちのプラットフォームへようこそ",
  "ctaText": "始める",
  "ctaUrl": "/ja/get-started"
}

3단계: 라우트 생성

다음 구성을 가진 라우트를 만듭니다.

  • 경로: /{lang}/landingPage
  • 패턴: /{lang}/landingPage
  • 상태: 라이브
  • 매개변수 바인딩:
  {
    "lang": "language"
  }

4단계: CEL 스크립트가 있는 블록 추가

라우트에 히어로 블록을 추가하고 각 필드에 다음 CEL 스크립트를 입력합니다.

헤드라인 필드:

documents.get("greeting", meta.params.lang).headline

서브헤드라인 필드:

documents.get("greeting", meta.params.lang).subheadline

CTA 텍스트 필드:

documents.get("greeting", meta.params.lang).ctaText

CTA URL 필드:

documents.get("greeting", meta.params.lang).ctaUrl

5단계: Next.js에서 사용하기

Next.js 앱에서 catch-all 라우트를 만듭니다.

// app/[...slug]/page.tsx
import { getCmsClient } from '@repo/renderer';

interface PageProps {
  params: { slug: string[] };
}

export default async function Page({ params }: PageProps) {

6단계: 라우트 테스트

다음 URL을 방문하여 현지화된 콘텐츠를 확인합니다.

URL예상 헤드라인
/ko/landingPage환영합니다
/en/landingPageWelcome
/ja/landingPageようこそ

해결 과정

사용자가 /ko/landingPage를 방문하면:

  1. 라우트 매칭: CMS가 /{lang}/landingPage 패턴과 일치시킵니다.
  2. 매개변수 추출: meta.params.lang = "ko"
  3. 유효성 검사: CMS가 "ko"가 language 스키마에 존재하는지 확인합니다.
  4. CEL 평가: documents.get("greeting", meta.params.lang) 같은 스크립트가 한국어 콘텐츠로 평가됩니다.
  5. 응답: 현지화된 블록이 클라이언트에 반환됩니다.

부록 B: 기술 참조

CelMeta 인터페이스(TypeScript)

interface CelMeta {
  /** 현재 로케일 코드(예: 'en-US') */
  locale: string;
  /** URL에서 추출된 라우트 매개변수 */
  params: Record<string, string>;
  /** URL 경로 세그먼트 */
  segments: string[];
  /** 현재 문서 ID(기존 문서를 편집하는 경우) */
  docId

매개변수 추출 알고리즘

extractParams 함수는 URL 경로를 다음과 같이 처리합니다.

Pattern: /{country}/{lang}/products
Path:    /us/en/products

Algorithm:
1. 둘 다 정규화합니다(끝의 슬래시 제거).
2. 세그먼트로 분리합니다: ["us", "en", "products"]와 ["{country}", "{lang}", "products"].

지원되는 매개변수 바인딩 형식

// 단순 바인딩("code" 필드로 조회)
{ "lang": "language" }

// 상세 바인딩(사용자 지정 슬러그 필드)
{
  "lang": {
    "schemaName": "language",
    "slugField": "code"
  },
  "slug": {
    "schemaName": "article",
    "slugField": "slug"
  }
}

문서 조회 우선순위

documents.get(schema, identifier)로 가져올 때:

  1. UUID 일치: 식별자가 유효한 UUID라면 id로 조회합니다.
  2. code 필드: content.code 필드를 확인합니다.
  3. slug 필드: content.slug 필드를 확인합니다.
  4. title 일치: title 필드를 확인합니다.

이를 통해 어떤 고유 식별자로든 유연하게 문서를 참조할 수 있습니다.

:
"ようこそ"
,
"subheadline"
:
"私たちのプラットフォームへようこそ"
}
,
"type"
:
"string"
},
{ "name": "ctaText", "type": "string" },
{ "name": "ctaUrl", "type": "string" }
]
}
const
client
=
getCmsClient
({
cmsUrl: process.env.CMS_URL!,
apiKey: process.env.CMS_API_KEY!,
websiteId: process.env.CMS_WEBSITE_ID!,
});
const path = '/' + params.slug.join('/');
// 라우트와 해결된 매개변수 가져오기
const { route, resolvedParams } = await client.routes.getRouteByPath.query({
websiteId: process.env.CMS_WEBSITE_ID!,
path,
});
// 라우트의 블록 가져오기
const blocks = await client.blocks.getBlocks.query({
websiteId: process.env.CMS_WEBSITE_ID!,
blockIds: route.block_ids,
// CEL 컨텍스트로 해결된 매개변수 전달
context: {
meta: {
locale: resolvedParams?.lang?.document?.content?.code ?? 'en',
params: Object.fromEntries(
Object.entries(resolvedParams ?? {}).map(([k, v]) => [k, v.value])
),
segments: params.slug,
docId: null,
title: route.title ?? '',
},
schema: {},
},
});
// 블록 렌더링
return (
<main>
{blocks.map((block) => (
<BlockRenderer
key={block.id}
block={block}
routeParams={resolvedParams}
language={resolvedParams?.lang?.value}
/>
))}
</main>
);
}
:
string
|
null
;
/** 현재 문서 제목 */
title: string;
}
3.
세그먼트 수를
맞춥니다
(동일해야 함).
4. 각 세그먼트 쌍에 대해:
- 패턴이 : 또는 {}로 시작하면 매개변수로 추출합니다.
- 그렇지 않으면 정확히 일치해야 합니다.
5. 반환: { country: "us", lang: "en" }