Braze 입문 6편. Personalization 3 layer (Static · Dynamic · Connected), Liquid templating 의 기원 (Shopify open source spec), 3 구성 (Variable · Tag · Filter) 깊이, Braze 의 Liquid 확장 (Catalog · Subscription · Random sample), Connected Content 의 외부 API lookup (timeout · cache · fallback · 보안), AI/ML 통합 (OpenAI · Claude 연동 패턴), 어디에 Liquid 박나 (메시지 · subject · URL · push payload) 까지 풀어쓴 학습 노트.
이 글은 Braze 입문에서 운영까지 시리즈 6편이에요. 1편 큰 그림 부터 5편 REST API · Currents 까지 데이터 흐름과 발송 인프라를 봤다면, 이번 6편은 메시지가 진짜 개인화되는 자리 — Liquid + Connected Content.
왜 Personalization 이 중요한가
Personalization(개인화) — 메시지를 사용자별로 다르게 보여 주는 기법. 같은 캠페인이라도 효과 차이가 큽니다.
A: "여름 세일 30% 할인"
→ CTR 2.1%
B: "{{ ${first_name} }}님, 좋아하시는 {{ ${last_viewed_category} }} 30% 할인"
→ CTR 5.8% (약 3 배)
이름 한 줄과 관심 카테고리만 더해도 engagement 가 3 배. Personalization 은 ROI 의 가장 강력한 lever 입니다.
Personalization 의 3 Layer
| Layer | 데이터 출처 | 시점 |
|---|---|---|
| Static | 하드코딩 콘텐츠 | 작성 시 |
| Dynamic (Liquid) | User Attribute · Event · Catalog | 발송 시 (Braze 안 데이터) |
| Connected (Connected Content) | 외부 API | 발송 시 (외부 시스템 호출) |
세 layer 는 조합으로 동작해요. 같은 메시지 안에 static text 와 Liquid 변수, Connected Content 결과가 자연스럽게 섞입니다.
Liquid — 메시지의 templating 언어
Liquid = Shopify 가 만든 open source templating(템플릿) 언어. Braze · Klaviyo · Iterable 같은 marketing 도구가 표준으로 채택했어요.
1편의 Liquid 잠깐 소개 를 깊이 풀어 봅니다.
Liquid 의 3 구성
{{ variable }} ← Variable (값 출력)
{% tag %} ← Tag (제어 흐름)
{{ value | filter }} ← Filter (변환)
세 가지 building block 이 전부. 아무리 복잡한 personalization 도 이 셋의 조합으로 풀립니다.
표시 형식
| 구문 | 의미 |
|---|---|
{{ ... }} |
Variable 출력 |
{% ... %} |
Tag (제어 흐름) |
{% if ... %} ... {% endif %} |
조건 분기 |
{% for ... in ... %} ... {% endfor %} |
반복 |
{{ value | filter }} |
변환 적용 |
다른 templating 도구 Mustache · Handlebars · Jinja 와 비슷한 syntax 예요.
Variable — 값 출력
표준 User Attribute
안녕하세요, {{ ${first_name} | default: '회원님' }}님!
회원님의 등급: {{ ${tier} | default: 'Bronze' }}
가입일: {{ ${date_of_first_session} | date: '%Y-%m-%d' }}
국가: {{ ${country} | default: '한국' }}
${...} 형식이 Braze 의 User attribute 예요. 항상 default 필터를 권장해요. 빈 값에서 오는 어색한 메시지를 막아 줍니다.
Custom Attribute
신규 사용자 카테고리: {{ ${signup_source} | default: 'organic' }}
LTV: {{ ${lifetime_value} | default: 0 | currency }}
3편 Custom Attribute 에서 만든 모든 attribute 가 Liquid 변수로 그대로 들어옵니다.
Event Property
캠페인이 event 로 trigger 됐다면 그 event 의 properties 를 꺼낼 수 있어요.
{# Event "checkout_completed" trigger 시 #}
주문 #{{ event_properties.${order_id} }} 가 접수됐어요.
금액: {{ event_properties.${cart_value} | currency }}
상품 수: {{ event_properties.${items_count} }}개
Trigger Properties (API trigger)
5편의 /campaigns/trigger/send 에서 넘긴 properties 가 여기로 들어와요.
{# /campaigns/trigger/send 의 trigger_properties #}
배송 추적: {{ ${tracking_number} }}
예상 배송: {{ ${eta} | date: '%Y-%m-%d %H:%M' }}
Canvas Entry Properties
/canvas/trigger/send 의 entry properties — journey 전체에서 살아 있어요.
캠페인 출처: {{ canvas_entry_properties.${campaign_source} }}
가입 의도: {{ canvas_entry_properties.${signup_intent} }}
Tag — 제어 흐름
if · elsif · else — 조건 분기
{% if ${tier} == 'premium' %}
Premium 회원 전용 30% 할인 쿠폰을 드려요.
{% elsif ${tier} == 'gold' %}
Gold 회원 20% 할인 쿠폰입니다.
{% else %}
첫 결제 10% 할인 쿠폰을 드려요.
{% endif %}
unless — 부정 조건
{% unless ${has_completed_first_purchase} %}
첫 구매 시 5,000원 적립금이 자동 지급돼요.
{% endunless %}
for — 반복
{# Catalog 의 추천 상품 list 출력 #}
{% catalog_items products ${last_viewed_product_ids} %}
{% for item in items %}
- {{ item.name }} ({{ item.price | currency }})
{% endfor %}
case · when — 다중 분기
{% case ${preferred_category} %}
{% when 'shoes' %}
여름 슈즈 신상 30% 할인!
{% when 'bags' %}
가방 신상품 모음
{% when 'accessories' %}
액세서리 추천
{% else %}
오늘의 추천 상품
{% endcase %}
assign · capture — 변수 할당
{# 변수 할당 #}
{% assign discount = ${cart_value} | times: 0.1 %}
{# 복잡한 출력을 변수로 capture #}
{% capture greeting %}
안녕하세요, {{ ${first_name} | default: '회원님' }}님!
{% endcapture %}
{{ greeting }}
Filter — 값 변환
Liquid 의 가장 강력한 자리예요. 같은 값을 다양한 형태로 바꿔 줍니다.
문자열 filter
{{ ${first_name} | upcase }} → "JOHN"
{{ ${first_name} | downcase }} → "john"
{{ ${email} | replace: '@', '_at_' }} → "user_at_example.com"
{{ "Hello World" | truncate: 5 }} → "Hello..."
{{ ${name} | strip }} → 앞뒤 공백 제거
Date filter
{{ ${signup_date} | date: '%Y-%m-%d' }} → "2026-05-17"
{{ ${signup_date} | date: '%Y년 %m월 %d일' }} → "2026년 05월 17일"
{{ 'now' | date: '%H:%M' }} → 현재 시각
날짜 format 은 strftime(C 표준 날짜 포맷) 규약. %Y · %m · %d · %H · %M · %S 같은 토큰을 그대로 써요.
Number filter
{{ ${cart_value} | times: 0.9 }} → 10% 할인 가격
{{ ${cart_value} | divided_by: 1000 }} → 1000 으로 나누기
{{ 50000 | money_with_currency: 'KRW' }} → 50,000원 (Braze 확장)
{{ ${value} | round: 2 }} → 소수점 2자리
{{ ${value} | floor }} → 내림
{{ ${value} | ceil }} → 올림
Array filter
{{ ${tags} | join: ', ' }} → "vip, premium, korea"
{{ ${tags} | size }} → array 길이
{{ ${tags} | first }} → 첫 요소
{{ ${tags} | last }} → 마지막 요소
{{ ${products} | sort: 'price' }} → price 기준 정렬
{{ ${tags} | uniq }} → 중복 제거
Default filter — 가장 자주 쓰는
{{ ${first_name} | default: '회원님' }}
{{ ${tier} | default: 'Bronze' }}
{{ ${last_purchase_date} | default: '없음' }}
모든 personalization 의 안전선이에요. attribute 가 비어 있을 때 어색한 빈 값이 메시지에 박히는 사고를 막아 줍니다.
Filter 체이닝
{{ ${first_name} | downcase | capitalize | default: '회원님' }}
{{ ${cart_value} | times: 0.9 | round: 0 | money_with_currency: 'KRW' }}
여러 filter 를 왼쪽에서 오른쪽으로 순차 적용합니다.
Braze 의 Liquid 확장
표준 Liquid 에 Braze 만의 특수 tag/filter 가 더해져요.
Catalog tag
{% catalog_items products "${last_viewed_product_id}" %}
{% if items.first %}
{{ items.first.name }} 이 다시 입고됐어요!
가격: {{ items.first.price | money_with_currency: 'KRW' }}
{% endif %}
5편 Catalogs 와 결합하면 메시지 안에서 직접 상품 정보를 조회할 수 있어요.
Random sample
{% assign random_product = products | sample: 1 %}
오늘의 추천: {{ random_product.first.name }}
사용자마다 다른 상품을 노출해서 대량 발송에 다양성을 줍니다.
Subscription 검사
{% if ${email_marketing_subscription} == 'subscribed' %}
뉴스레터 옵션을 활성 해 두셨네요. 감사합니다.
{% endif %}
abort_message — 발송 중단
{% if ${tier} == 'banned' %}
{% abort_message('User is banned') %}
{% endif %}
안녕하세요...
조건에 걸린 해당 사용자만 발송을 건너뛰고, 캠페인 전체는 계속 진행돼요.
어디에 Liquid 박나 — 메시지의 모든 곳
| 필드 | Liquid 사용 |
|---|---|
| Email subject | 안녕하세요 {{ ${first_name} }}님 |
| Email body | HTML 안 모든 콘텐츠 |
| Push title | {{ ${first_name} }}, 새 알림 |
| Push body | 메시지 본문 |
| SMS body | 짧은 메시지 |
| In-app message | 모든 콘텐츠 필드 |
| Content Card | title · description · 이미지 alt |
| Deep link URL | myapp://product/{{ ${last_product_id} }} |
| Webhook URL/body | API 호출 URL · payload |
| Connected Content URL | 외부 API endpoint |
거의 모든 필드에 들어가요. 메시지의 어디든 개인화가 가능합니다.
Connected Content — 외부 API lookup
Connected Content 가 답하는 자리
Liquid 만으로는 부족한 경우들이 있어요. 실시간 상품 재고(Catalogs sync 보다 신선해야 할 때), AI 추천 모델의 결과, 날씨나 환율 같은 외부 데이터, 우리 회사 다른 시스템의 사용자 데이터, 제 3자 service 의 lookup — 이런 자리를 Connected Content 가 메웁니다. 메시지를 보내기 직전 외부 API 를 호출해서 응답을 본문에 반영해요.
기본 syntax
{% connected_content
https://api.example.com/recommendations
?user_id={{ ${external_id} }}
:save recommendations
%}
추천 상품:
{% for item in recommendations.items %}
- {{ item.name }} ({{ item.price | money_with_currency: 'KRW' }})
{% endfor %}
:save 키워드로 응답을 변수에 저장해서 메시지 본문에서 그대로 활용합니다.
HTTP Method · Headers · Auth
{% connected_content
https://api.example.com/users/recommendations
:method post
:headers {
"Authorization": "Bearer YOUR_TOKEN",
"Content-Type": "application/json"
}
:body { "user_id": "{{ ${external_id} }}", "limit": 5 }
:save recommendations
:content_type application/json
%}
GET · POST · PUT · DELETE 모두 지원하고, JSON body 자체가 Liquid 로 동적으로 채워져요.
Cache 정책
{% connected_content
https://api.example.com/products/recommendations
:cache_max_age 300
:save recommendations
%}
cache_max_age(초) 로 같은 URL 응답을 지정한 시간 동안 캐싱해요. 부하가 줄고 발송이 빨라집니다.
대표 cache 시간은 상품 정보 1시간(3600초), 추천 모델 5분(300초), 재고 정보 1분(60초), 환율 1시간 정도가 무난해요.
Timeout
{% connected_content
https://api.example.com/...
:timeout 5
%}
기본 timeout(Braze 가 정해 둔 값) 은 보통 몇 초 수준이에요. 너무 길게 잡으면 발송이 통째로 지연됩니다.
여기서 시험 함정이 하나 있어요 — Connected Content timeout 이 곧 발송 latency. 우리 API 가 느리면 Braze 의 전체 발송이 같이 늘어져요. 우리 API 는 500ms 이내 응답이 권장됩니다.
Fallback — 외부 API 실패 시
{% connected_content
https://api.example.com/recommendations?user_id={{ ${external_id} }}
:save recommendations
:rescue 'recommendations_failed'
%}
{% if recommendations_failed %}
오늘의 추천 상품을 확인해 보세요!
{% else %}
{% for item in recommendations.items %}
- {{ item.name }}
{% endfor %}
{% endif %}
:rescue 는 실패하면 지정 변수를 true 로 세팅해 줘요. 기본 메시지로 fallback 가능합니다.
핵심 원칙은 하나. Connected Content 가 실패해도 발송 자체는 진행되고, 해당 부분만 fallback 처리돼요.
AI/ML 통합 — 외부 모델 활용
Connected Content 의 가장 강력한 활용처가 AI 모델 호출이에요.
OpenAI GPT 연동
{% connected_content
https://api.openai.com/v1/chat/completions
:method post
:headers {
"Authorization": "Bearer {{ env.OPENAI_KEY }}",
"Content-Type": "application/json"
}
:body {
"model": "gpt-4o-mini",
"messages": [
{"role": "system", "content": "사용자 관심사 기반 짧은 추천 메시지를 작성하세요."},
{"role": "user", "content": "사용자 카테고리: {{ ${preferred_category} }}, 최근 구매: {{ ${last_purchase_name} }}"}
],
"max_tokens": 100
}
:save ai_message
:cache_max_age 3600
:rescue 'ai_failed'
%}
{% if ai_failed %}
새로운 상품을 만나 보세요!
{% else %}
{{ ai_message.choices.first.message.content }}
{% endif %}
개인화된 AI 메시지가 본문 안으로 자동 삽입돼요.
Anthropic Claude 연동
{% connected_content
https://api.anthropic.com/v1/messages
:method post
:headers {
"x-api-key": "{{ env.ANTHROPIC_KEY }}",
"anthropic-version": "2023-06-01",
"content-type": "application/json"
}
:body {
"model": "claude-3-5-sonnet-20241022",
"max_tokens": 100,
"messages": [{
"role": "user",
"content": "고객 {{ ${first_name} }} 에게 맞는 짧은 추천 메시지 (한국어)"
}]
}
:save claude_response
:cache_max_age 3600
:rescue 'claude_failed'
%}
{{ claude_response.content.first.text | default: '오늘의 추천' }}
자체 ML 모델 추천
{% connected_content
https://ml.internal.example.com/recommend
?user_id={{ ${external_id} }}
&category={{ ${preferred_category} }}
&k=5
:save recommendations
:cache_max_age 600
%}
우리 회사의 ML(머신러닝) 모델 API 를 호출해서 학습된 추천 결과를 메시지에 그대로 실어요.
AI 사용의 함정
정말 중요한 시험 함정 — AI 응답의 prompt injection(프롬프트 조작 공격) 과 부적절 콘텐츠 위험이에요.
사용자 attribute "first_name" = "; DROP TABLE users; --"
→ LLM prompt 에 그대로 들어가면 위험
또는
사용자가 input field 에 욕설 박음
→ AI 가 그 단어를 그대로 메시지에
→ 모든 사용자에게 욕설
해결은 네 갈래로 갑니다. attribute 를 마케팅 시스템 통과 전에 정제(sanitize) 하고, AI prompt 의 system message 에 "한국어로 추천만, 욕설·공격 X" 같은 강력한 가드를 박고, AI 응답을 키워드 필터와 length check 로 후처리하고, 의심스러운 응답은 fallback 으로 돌립니다.
AI 통합의 비용
GPT-4o-mini: ~$0.15 / 1M input tokens, $0.60 / 1M output
캠페인 100K 사용자 × 200 token = 20M token = ~$3 ~ $12
Claude Haiku: ~$0.80 / 1M input, $4 / 1M output (조금 비쌈)
캠페인 1회 발송이 몇 달러에서 수십 달러 선이에요. cache_max_age 와 fallback 을 함께 묶으면 비용이 통제됩니다.
보안 — Connected Content 함정
URL Injection
{# 나쁜 패턴 #}
{% connected_content
https://api.example.com/lookup?id={{ ${user_input} }}
%}
# user_input = "1; rm -rf /" 또는 SQL injection 같은 공격
${...} 가 자동으로 URL-encoded 되는지는 Braze 가 어느 정도 보호해 줘요. 다만 완벽 의존은 위험합니다. 우리 API 측에서도 validation 을 걸어 둬야 해요.
Sensitive Data 노출
{# 위험 — 외부 API 응답에 PII 포함 시 메시지에 박힘 #}
{% connected_content
https://api.example.com/user-details?id={{ ${external_id} }}
:save user_details
%}
전화: {{ user_details.phone }} ← PII 노출
사회보장번호: {{ user_details.ssn }} ← 심각한 위험
Connected Content 의 응답 자체가 PII(개인식별정보) safe 하도록 우리 API 를 설계해야 해요. 민감 데이터는 응답 schema 에서 아예 제외합니다.
Rate Limit Hit (외부 API)
대량 캠페인이면 수십만 명에게 Connected Content 호출이 동시에 나가요. 우리 API 가 Rate Limit hit 을 맞을 수 있습니다.
해결은 세 가지. cache_max_age 를 적극 활용하고, 우리 API 를 Cache·CDN(콘텐츠 전송 네트워크) 으로 scaling 하고, 발송 직전에 background pre-warming 으로 cache 를 채워 둡니다.
Liquid 사용 패턴 — 5 실전
Pattern 1: 안전한 personalization
안녕하세요, {{ ${first_name} | default: '회원님' }}님!
{% if ${tier} %}
회원님의 등급: {{ ${tier} | upcase }}
{% endif %}
{% if ${last_purchase_date} %}
마지막 구매일: {{ ${last_purchase_date} | date: '%Y-%m-%d' }}
{% endif %}
모든 변수에 default 를 두고 조건 확인까지 거치면 어색한 빈 값을 피할 수 있어요.
Pattern 2: A/B 테스트 (Liquid 분기)
{% assign variant = ${external_id} | md5 | slice: 0, 1 %}
{% if variant == '0' or variant == '1' %}
[A 그룹] 빨강 버튼 캠페인
{% else %}
[B 그룹] 초록 버튼 캠페인
{% endif %}
사용자 ID 의 hash 로 deterministic(결정적·재현 가능한) 그룹 분할을 합니다. A/B 가 매번 같은 결과로 재현돼요.
Pattern 3: 다국어 — locale 분기
{% case ${language} %}
{% when 'ko' %}
안녕하세요, {{ ${first_name} | default: '회원님' }}님!
{% when 'ja' %}
こんにちは、{{ ${first_name} | default: 'お客様' }}様!
{% when 'en' %}
Hello, {{ ${first_name} | default: 'there' }}!
{% else %}
Hello, {{ ${first_name} | default: 'there' }}!
{% endcase %}
한 캠페인으로 모든 언어 사용자를 커버해요. Liquid 분기가 자동 번역 역할을 합니다.
Pattern 4: Catalog + 추천
{% catalog_items products ${recently_viewed_ids} %}
{% if items.size > 0 %}
{{ ${first_name} | default: '회원님' }}님, 최근 본 상품을 확인해 보세요:
{% for item in items %}
- {{ item.name }} ({{ item.price | money_with_currency: 'KRW' }})
{% endfor %}
지금 다시 확인하기:
{% for item in items %}
myapp://product/{{ item.id }}
{% endfor %}
{% else %}
오늘의 인기 상품을 만나 보세요!
{% endif %}
5편 Catalogs 와 결합한 실시간 상품 메시지예요.
Pattern 5: AI personalization + fallback
{% connected_content
https://api.openai.com/v1/chat/completions
:method post
:headers {
"Authorization": "Bearer {{ env.OPENAI_KEY }}",
"Content-Type": "application/json"
}
:body {
"model": "gpt-4o-mini",
"messages": [{
"role": "system",
"content": "한국어로 짧은 (3 문장 이하) 마케팅 메시지를 작성. 욕설·공격적 표현·민감 데이터 절대 포함 X."
}, {
"role": "user",
"content": "고객 {{ ${first_name} | default: '회원님' }}, 카테고리 {{ ${preferred_category} | default: '의류' }}"
}],
"max_tokens": 150,
"temperature": 0.7
}
:save ai_response
:cache_max_age 3600
:timeout 5
:rescue 'ai_failed'
%}
{% if ai_failed %}
안녕하세요, {{ ${first_name} | default: '회원님' }}님!
오늘의 새로운 상품을 만나 보세요.
{% else %}
{{ ai_response.choices.first.message.content }}
{% endif %}
운영 환경에서 표준이 된 AI personalization 형이에요. cache + timeout + fallback + 안전 가드가 한 묶음으로 들어가 있습니다.
자주 만나는 사고
사고 1: Default 누락 → 어색한 빈 값
원인은 {{ ${first_name} }} 만 박은 거예요. 해결은 모든 personalization 에 | default: '...' 를 빠짐없이 거는 것.
사고 2: Liquid syntax 오류
{% if x %} 가 누락되거나 {% endif %} 가 빠지면 해당 사용자만 발송이 실패해요. 발송 전에 Liquid preview 도구로 한 번 돌려 보거나, test 사용자에게 미리 보내 봅니다.
사고 3: Connected Content timeout
우리 API 가 5초를 넘기면 Braze 가 timeout 을 잡고 fallback 또는 실패로 넘어가요. 우리 API 의 500ms 이내 응답을 보장하고, cache 를 적극 활용합니다.
사고 4: Connected Content 의 데이터 누설
외부 API 응답에 PII 가 섞이면 그대로 메시지에 박힙니다. 우리 API 측에서 발송용 응답 schema 를 분리하고 PII 를 제외해야 해요.
사고 5: AI prompt injection
사용자 input 이 LLM prompt 에 그대로 들어가면 의도 외 위험 콘텐츠가 나올 수 있어요. input sanitization 과 system prompt 강력 가드, 응답 후처리 필터 — 세 겹으로 막습니다.
사고 6: 대량 발송 시 외부 API rate limit
100K 사용자에 Connected Content 가 한 번씩 호출되면 100K 외부 API 호출이 한꺼번에 나가요. cache_max_age 를 적극 쓰고, 우리 API 를 scaling 하고, background pre-warming 까지 묶으면 견딥니다.
사고 7: AI 응답 비결정성
temperature: 0.7 이면 같은 사용자도 매번 다른 응답이 나와요. 일관성이 부족해 보입니다. temperature: 0.3 처럼 낮게 두고 cache 를 함께 쓰면 같은 사용자에게 같은 응답이 갑니다.
사고 8: Catalog 안 item 없을 때
{% catalog_items %} 가 빈 결과를 돌려주면 items.first 가 nil 이 되고 메시지에 오류가 납니다. {% if items.size > 0 %} 가드를 걸고 fallback 메시지를 준비해 둬요.
운영 권장 패턴
Pattern 1: Personalization 안전선 — 4 layer
{# 1. 가장 안전한 fallback #}
{% assign safe_name = ${first_name} | default: '회원님' | strip %}
{# 2. validation #}
{% if safe_name == '' or safe_name == nil %}
{% assign safe_name = '회원님' %}
{% endif %}
{# 3. 사용 #}
안녕하세요, {{ safe_name }}님!
{# 4. 안 보이게 강제 (verbose 한 메시지 회피) #}
{% capture _ %}{{ random_field | default: '' }}{% endcapture %}
다층 안전선이에요.
Pattern 2: AI personalization 셋업
operational_pattern:
- cache_max_age: 3600 # 1시간 cache
- timeout: 5 # 5초 한도
- fallback: 기본 마케팅 메시지 (AI 실패 시)
- max_tokens: 150 # 응답 길이 한도
- temperature: 0.3 # 일관성 우선
- system_prompt:
- "한국어"
- "짧게 (3 문장 이하)"
- "욕설·공격적 표현 X"
- "민감 데이터 X"
- "추천 형식"
- response_filter:
- keyword_block: ["기밀", "비밀", "..."]
- length_check: < 200 chars
운영 표준입니다.
Pattern 3: Connected Content 의 우리 API 설계
# 발송용 추천 API
@app.get("/marketing/recommendations/{user_id}")
async def get_marketing_recommendations(user_id: str):
# 1. Cache 확인 (CDN · Redis)
cached = await cache.get(f"rec:{user_id}")
if cached:
return cached
# 2. 추천 계산
recommendations = await recommendation_service.get(user_id, limit=5)
# 3. 응답 schema — PII 제외
response = {
"items": [
{
"id": r.product_id,
"name": r.product_name,
"price": r.price,
"image_url": r.image_url
# 의도적으로 user 의 phone · email · ssn 제외
}
for r in recommendations
]
}
# 4. Cache 저장
await cache.set(f"rec:{user_id}", response, ttl=600)
return response
marketing-safe response schema 와 fast cache 가 핵심이에요.
Pattern 4: Liquid 검증 워크플로
1. Console 의 Liquid Preview 도구 사용
2. Test segment (10명 정도) 에 미리 발송
3. test 사용자 actual 메시지 검수
4. 변수 누락 · 어색한 메시지 detect
5. 수정 후 production 발송
production 폭격을 피하는 흐름이에요.
Pattern 5: Multi-language 표준화
{% assign lang = ${language} | default: 'ko' %}
{% case lang %}
{% when 'ko' %}
{% assign greeting = '안녕하세요' %}
{% assign closing = '감사합니다' %}
{% when 'ja' %}
{% assign greeting = 'こんにちは' %}
{% assign closing = 'ありがとうございます' %}
{% else %}
{% assign greeting = 'Hello' %}
{% assign closing = 'Thank you' %}
{% endcase %}
{{ greeting }}, {{ ${first_name} | default: '...' }}!
[본문]
{{ closing }}.
다국어 메시지의 일관 구조입니다.
시험 직전 한 번 더 — Liquid · Connected Content 함정 압축 노트
Personalization 3 Layer
- Static — 하드코딩 (작성 시)
- Dynamic (Liquid) — Braze 안 데이터 (User · Event · Catalog · Trigger · Canvas Entry)
- Connected — 외부 API lookup (발송 직전)
Liquid 3 구성
- Variable =
{{ var }}(값 출력) - Tag =
{% tag %}(제어 흐름) - Filter =
{{ value | filter }}(변환)
Variable 종류
${attribute}— User Attributeevent_properties.${prop}— Event 의 properties${trigger_properties.${prop}}— API triggercanvas_entry_properties.${prop}— Canvas entry- Catalog · Subscription state 등
Tag 핵심 5
{% if %} ... {% elsif %} ... {% else %} ... {% endif %}{% unless %} ... {% endunless %}{% for x in list %} ... {% endfor %}{% case %} {% when %} {% endcase %}{% assign var = ... %}·{% capture %}
Filter 카테고리
- String — upcase · downcase · replace · truncate · strip
- Date —
date: '%Y-%m-%d'(strftime 표준) - Number — times · divided_by · round · floor · ceil · money_with_currency
- Array — join · size · first · last · sort · uniq · sample
- Default — 가장 자주 (모든 personalization 의 안전선)
- Chaining —
| filter1 | filter2 | filter3
Braze 확장
{% catalog_items %}— Catalog lookup{% connected_content %}— 외부 API{% abort_message %}— 발송 중단 (해당 사용자만)${email_marketing_subscription}— Subscription state
Liquid 사용 위치 — 거의 모든 필드
- Email subject · body · sender name
- Push title · body · deep link
- SMS body
- In-app · Content Card 의 모든 텍스트
- Webhook URL · body
- Deep Link URL
Connected Content 핵심
- 외부 API 호출 (메시지 발송 직전)
- HTTP GET/POST/PUT/DELETE + headers + body
:save응답 저장:cache_max_age(초) — 응답 캐싱:timeout(초) — 한도:rescue 'var'— 실패 시 fallback- timeout = 발송 latency → 우리 API 500ms 이내 권장
AI 통합
- OpenAI · Claude · 자체 모델 모두 Connected Content 로
- Cache 적극 — 같은 사용자 같은 응답
- Fallback 필수 — AI 실패 시 기본 메시지
- Prompt injection 함정 — system prompt 강력 가드 + input sanitization + 응답 후처리
- 비용 = 캠페인 100K = 몇 $ ~ 수십 $ (model 별)
- 운영 표준 — max_tokens · low temperature · cache · timeout · fallback
보안 함정
- URL Injection — 우리 API 측 validation 도 추가
- Sensitive Data 노출 — 우리 API 응답 schema 에서 PII 제외
- Rate Limit (외부 API) — cache + scaling + pre-warming
- AI prompt injection — system prompt + sanitize + filter
사고 함정
- Default 누락 ("안녕하세요, 님!")
- Liquid syntax 오류 (해당 사용자 발송 실패)
- Connected Content timeout (느린 API)
- 외부 API 의 PII 노출
- AI prompt injection
- 대량 발송 외부 API rate limit
- AI 응답 비결정성 (temperature 낮게)
- Catalog 빈 결과 (가드)
운영 패턴
- Personalization 4 layer 안전선
- AI 셋업 표준 (cache · timeout · fallback · 가드)
- 우리 API marketing-safe schema
- Test segment 미리 발송
- Multi-language 표준화
공식 문서: Braze Liquid · Connected Content 에서 원문을 확인할 수 있어요.
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 1편 — Customer Engagement Platform · 5 핵심 개념
- 2편 — SDK 통합 · REST API · 첫 캠페인 30분
- 3편 — Developer Guide · Identity · 데이터 모델 깊이
- 4편 — Push · In-app · Content Card · Feature Flag
- 5편 — REST API 9 카테고리 · Messaging · Currents 깊이
다음 글: