CMS에서 CEL 표현식을 작성하는 실용 가이드입니다.
CMS에서 CEL 표현식을 작성하는 실용 가이드입니다.
CEL(Common Expression Language)은 CMS에 내장된 경량 스크립트 언어입니다. 문서에서 데이터를 가져오고, URL 매개변수를 읽고, 즉석에서 값을 계산하는 동적 표현식을 작성할 수 있게 해줍니다.
CEL 스크립트가 실행될 때 일어나는 일은 다음과 같습니다.
사용자 스크립트 엔진 결과
| | |
v v v
documents.get("article", "intro") --> 데이터베이스에서 가져옴 --> { headline: "환영합니다", body: "..." }
.headline --> 필드를 추출함 --> "환영합니다"
CEL을 읽기 전용 쿼리 언어라고 생각하세요. 데이터베이스에서 아무 것도 수정할 수 없으며, 데이터를 읽고 계산된 결과를 반환하기만 합니다. 덕분에 CMS 어디에서나 안전하게 사용할 수 있습니다.
모든 CEL 표현식은 다음 세 가지에 접근할 수 있습니다.
| 객체 | 설명 | 예시 |
|---|---|---|
documents | CMS에서 어떤 문서든 가져오기 | 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"
페이지에 /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" }
]
hero-block이 article 문서에서 가져온 헤드라인을 표시해야 합니다.
기사 문서(식별자: "homepage-hero"):
{
"headline": "더 빠르게 구축하고 더 영리하게 배포하세요",
"subheadline": "개발자를 위한 현대적인 CMS"
}
히어로 블록의 제목 필드에 입력한 CEL 스크립트:
documents.get("article", "homepage-hero").headline
결과: 히어로에 "더 빠르게 구축하고 더 영리하게 배포하세요"가 표시됩니다.
/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"사용자의 로케일에 따라 다른 헤드라인을 보여줍니다.
meta.locale == "ar-SA" ? "مرحبا بكم" : "Welcome"
로케일이 "ar-SA"이면: "مرحبا بكم"을 반환합니다.
다른 로케일이면: "Welcome"을 반환합니다.
article 문서에 countryCode 필드가 있고, 전체 국가 이름을 얻고 싶습니다.
기사 문서:
{ "headline": "미국 소식", "countryCode": "us" }
CEL 스크립트:
documents.get("country", documents.get("article", "us-news").countryCode).name
동작 과정:
documents.get("article", "us-news")는 { "headline": "미국 소식", "countryCode": "us" }를 반환합니다..countryCode가 "us"를 추출합니다.documents.get("country", "us")는 { "code": "us", "name": "미국", ... }를 반환합니다..name이 "미국"을 추출합니다.결과: "미국"
문서가 없을 수도 있다면 대체 값을 제공할 수 있습니다.
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
: "알 수 없는 작성자"
기사에 태그가 있고, 특정 태그가 있는지 확인하고 싶습니다.
"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 (태그 개수)
매개변수화된 라우트는 동적이고 현지화된 페이지를 만드는 핵심입니다. /{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에 다음을 지시합니다.
lang 세그먼트를 추출한다.language 스키마와 대조하여 유효성을 검사한다(content.code가 일치하는 문서를 찾음).라우트 구성:
/{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
해결 과정:
| URL | meta.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} 라우트에서는:
country 매개변수를 country 스키마로 검증합니다.lang 매개변수를 language 스키마로 검증합니다.lang이 country.languages[] 배열에 있는지 확인합니다(계층적 검증).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.lang |
| 위치 기반 접근 | meta.segments[0] |
| 경로 깊이 파악 | size(meta.segments) |
| 경로에 특정 세그먼트가 포함되어 있는지 확인 | "admin" in 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.locale | string | 현재 로케일 코드(예: "en-US", "ko-KR", "ar-SA") | |
meta.params | Record<string, string> | URL 패턴에서 추출된 라우트 매개변수 | |
meta.segments | string[] | 세그먼트로 분리된 URL 경로 | |
meta.docId | string 또는 null | 현재 문서 UUID(새 문서라면 null) | |
meta.title | string | 현재 문서 제목 |
로케일 코드는 언어-지역 형식(BCP 47)을 따릅니다.
// 로케일이 RTL 언어인지 확인
meta.locale == "ar-SA" || meta.locale == "he-IL" ? "rtl" : "ltr"
// 언어 부분만 가져오기
meta.locale.split("-")[0] // 지원되지 않음 - 대신 meta.params.lang 사용
라우트 매개변수는 항상 문자열입니다. 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)
배열 형태의 원시 URL 세그먼트입니다.
// 인덱스로 접근(0부터 시작)
meta.segments[0] // 첫 번째 세그먼트
meta.segments[1] // 두 번째 세그먼트
// 길이 확인
size(meta.segments) // 세그먼트 개수
// 포함 여부 확인
"products" in meta.segments // 경로에 "products"가 포함되어 있는가?
현재 문서의 UUID로, 자기 참조 스크립트에 유용합니다.
// 기존 문서를 편집할 때만 사용 가능
meta.docId != null ? "editing" : "creating new"
// 조건부 로직에 사용
meta.docId != null ? documents.get("article", meta.docId).status : "draft"
현재 문서의 제목입니다.
// 화면에 표시하기
"Editing: " + meta.title
// 제목에 따른 조건
meta.title.contains("Draft") ? "work in progress" : "published"
스키마는 고정되어 있고 식별자만 동적인 경우, 더 깔끔한 구문을 위해 사용합니다.
// 기존 방식
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.translate(meta.params.text, "en", meta.params.lang)
mcp.analyze(documents.get("article", meta.params.id).body)
// 미래 기능: 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"])
이러한 기능은 등록된 함수 시스템을 통해 추가되며, 기존 스크립트와의 하위 호환성을 유지합니다.
documents. 또는 meta.를 입력하면 편집기가 사용 가능한 옵션을 보여줍니다.documents.get("schema", "id")로 테스트한 뒤 .fieldName을 추가하세요.!= null ? ... : ...으로 대체 값을 추가하세요.documents.get()이나 documents.find() 호출마다 50회 조회 제한에 포함됩니다.meta.params.category를 사용하기 전에 has(...)로 확인하세요.이 가이드는 /{lang}/landingPage에서 접근할 수 있는 다국어 랜딩 페이지를 만드는 방법을 설명합니다.
CMS 관리자에서 greeting이라는 사용자 정의 스키마를 생성합니다.
{
"name": "greeting",
"fields": [
{ "name": "code", "type": "string", "required": true },
{ "name": "headline", "type": "string", "required": true },
{ "name": "subheadline"
각 언어에 대한 문서를 만듭니다.
문서: 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"
}
다음 구성을 가진 라우트를 만듭니다.
/{lang}/landingPage/{lang}/landingPage {
"lang": "language"
}
라우트에 히어로 블록을 추가하고 각 필드에 다음 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
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) {
다음 URL을 방문하여 현지화된 콘텐츠를 확인합니다.
| URL | 예상 헤드라인 |
|---|---|
/ko/landingPage | 환영합니다 |
/en/landingPage | Welcome |
/ja/landingPage | ようこそ |
사용자가 /ko/landingPage를 방문하면:
/{lang}/landingPage 패턴과 일치시킵니다.meta.params.lang = "ko"language 스키마에 존재하는지 확인합니다.documents.get("greeting", meta.params.lang) 같은 스크립트가 한국어 콘텐츠로 평가됩니다.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)로 가져올 때:
id로 조회합니다.content.code 필드를 확인합니다.content.slug 필드를 확인합니다.title 필드를 확인합니다.이를 통해 어떤 고유 식별자로든 유연하게 문서를 참조할 수 있습니다.