Hackle Liquid 생성 스펙 (AI용)
AI 전용 Hackle Liquid 생성 스펙. 링크로 접근하는 AI만 참조하세요.
너(AI)는 Hackle 메시지 템플릿에 쓰일 Liquid를 생성한다. 렌더 엔진은 liqp 0.8.5.3 기반의 Braze 호환 방언이다(순정 Shopify/LiquidJS와 다르다).
철칙: 아래에 나열된 필터·태그만 사용하라. 목록에 없는 것은 절대 만들어 쓰지 마라. 미등록 필터나 문법 오류는 커스텀 필터와 달리 발송 전체를 실패시킨다. 확신이 없으면 표준 필터 조합으로 우회하라.
1. 변수 — 반드시 이 네임스페이스로 참조
개인화 데이터는 아래 4개 네임스페이스 아래의 임의 키로 들어온다. 최상위 변수명은 이 넷뿐이다.
{{ user_properties.<속성> }}
유저 속성
전 채널
{{ event_properties.<속성> }}
이벤트 속성
전 채널
{{ api_properties.<속성> }}
API 속성
전 채널
{{ identifiers.<종류> }}
식별자
Webhook 전용 (그 외 채널은 항상 빈 값)
속성 이름은 대소문자를 구분하지 않는다.
값이 리스트면
,로 이어 붙여 출력된다.존재하지 않는 변수는 빈 문자열로 렌더된다(에러 없음). strict 모드가 아니다.
Webhook 채널은 모든 비-리스트 값을 문자열화한다. 그 외 채널은 숫자를 숫자 타입으로 유지한다.
2. 예시 템플릿 (golden — 이 형태를 따르라)
인사 + 포인트:
{{ user_properties.name | default: '고객' }}님, 현재 포인트는 {{ event_properties.point | number_with_delimiter }}P 입니다.조건 분기 + 한국 시간 날짜:
랜덤 A/B 변형 (random은 태그 → capture로 담는다):
3. 핵심 규칙
목록에 없는 필터/태그를 만들지 마라. (§6 사용 금지 목록 참고)
커스텀 필터(§4 표시)는 잘못된 입력에도 원본을 반환하며 발송을 죽이지 않는다. 반대로 미등록 필터·문법 오류·내장 필터의 예외는 발송 실패로 이어진다.
random은 필터가 아니라 태그다.{% assign x = random %}은 동작하지 않는다(빈 값). 값을 재사용하려면{% capture %}로 담고, 숫자로 쓰려면| plus: 0(또는plus: 1)로 변환하라.date:입력은 날짜 문자열 또는 epoch 초(밀리초 아님).
ISO
Z접미사(2026-07-16T10:30:00Z)는 파싱 못 한다 → 파싱 실패 시 원본 반환. 공백 구분 존(2026-07-16 10:30:00 UTC)이나 존 없는 형식을 써라.timezone은 TZ 이름(
Asia/Seoul)·ISO 오프셋(+09:00)만 지원. 분 단위 숫자(360)는 미지원.locale은 named 파라미터:
date: '%A', locale: 'ko'.
렌더 한도: 3초 / 출력 10만 자 / 반복 1만 회. 초과 시 발송 실패.
4. 지원 필터 (이 목록이 전부다)
* = Hackle 커스텀. 그 외는 표준 Liquid와 동일.
문자열: append: s · prepend: s · capitalize · downcase · upcase · lstrip · rstrip · strip · strip_html · strip_newlines · newline_to_br · remove: s · remove_first: s · replace: from, to · replace_first: from, to · split: sep(→배열) · slice: start[, len] · truncate: n[, ellipsis] · truncatewords: n[, ellipsis] · escape · escape_once
숫자: abs · at_least: n · at_most: n · ceil · floor · round[: digits] · plus: n · minus: n · times: n · divided_by: n(정수끼리는 정수 몫; 소수 결과는 divided_by: n.0) · modulo: n
number_with_delimiter* — 천 단위 쉼표. 예:{{ 1234567 | number_with_delimiter }}→1,234,567
배열: compact · concat: arr · first · last · join: sep · map: 'prop' · reverse · sort(대문자 우선) · sort_natural(대소문자 무시) · uniq · where: 'prop'[, value] · size(문자열 길이도 됨, UTF-16 기준)
날짜:
date: format[, timezone][, locale: 'tag']— 예:{{ v | date: '%Y-%m-%d %H:%M', 'Asia/Seoul', locale: 'ko' }}. (§3의 date 규칙 필독)
URL:
url_encode·url_decodeurl_escape* — URL에 못 쓰는 문자만 이스케이프(공백→%20)url_param_escape* — 쿼리 파라미터 값용(공백→+)
JSON (모두 Hackle 커스텀):
json_escape* — 문자열을 JSON 문자열 값으로 이스케이프json_parse* — JSON 문자열 → 객체. 예:{% assign d = api_properties.payload | json_parse %}{{ d.user.name }}. 파싱 실패 시 nil(발송은 계속)as_json_string* — 값 → JSON 문자열
기타:
default: fallback— 값이 비었거나 falsy면 대체값. 예:{{ user_properties.name | default: '고객' }}fallback_over_length: max, fallback* — 길이(UTF-16) > max면 통째로 fallback으로 대체. 예:{{ user_properties.name | fallback_over_length: 10, '고객' }}
5. 지원 태그 (이 목록이 전부다)
변수:
{% assign x = ... %}·{% capture x %}...{% endcapture %}조건:
{% if %}/{% elsif %}/{% else %}/{% endif %}·{% unless %}...{% endunless %}·{% case x %}{% when v %}...{% else %}...{% endcase %}반복:
{% for i in arr %}...{% endfor %}(범위(1..5)가능) ·{% break %}·{% continue %}·{% cycle 'a', 'b' %}출력 제어:
{% comment %}...{% endcomment %}·{% raw %}...{% endraw %}카운터:
{% increment c %}(0부터) ·{% decrement c %}(-1부터) —assign변수와 별개{% connected_content <url> [:method m] [:body b] [:headers json] [:save 변수] %}— 외부 API(JSON) 호출해 변수에 저장. 실패해도 빈 값으로 계속 렌더. (상세·규칙·예시는 §6){% random %}— 01 float.{% random N %}— 0N-1 정수. 태그다(§3 규칙 참고)
6. connected_content (외부 API 실시간 호출)
발송 순간 외부 API(JSON)를 호출해 응답을 변수에 담아 본문에 끼워 넣는 태그다. 모든 실패는 빈 값으로 처리되고 발송을 죽이지 않는다 → 결과값에는 항상 default 또는 {% if %} 방어코드를 붙여라.
문법
<URL>
필수. https://만 허용, 공인 IP로 해석되는 호스트만(사설/사내망 차단). URL 안에 {{user_properties["id"]}} 등 개인화 변수 삽입 가능.
:method
GET(기본)·POST만. PUT/DELETE/PATCH → 태그 전체 스킵.
:body
POST일 때만. JSON 객체 문자열. GET에선 무시.
:headers
값이 전부 문자열인 JSON 객체. 숫자 값({"Retry":3}) 넣으면 검증 실패 → 스킵. Content-Type: application/json 기본 적용.
:save
응답 JSON 객체를 담을 변수명. 이후 {{변수.필드}}로 접근. 같은 템플릿 뒤쪽 태그·출력에서도 참조 가능.
응답 처리 — 아래는 전부 "빈 값(빈 맵)"으로 저장된다 (발송 계속):
200 + JSON 배열/원시값(객체 아님), 200 + 빈 본문
200 이외 모든 상태코드(3xx·4xx·5xx) — 리다이렉트 안 따라감
응답 본문 1MB 초과, 네트워크 오류·타임아웃
빈 맵의
{{변수.아무필드}}는 빈 문자열로 렌더된다.
한도: 호출당 타임아웃 2초 / 한 메시지 내 누적 호출 2초(초과 후 호출은 HTTP 없이 스킵) / 응답 1MB.
golden 예시
중첩 응답 접근: {{ data.product.name }} · 체이닝: 앞 :save 결과({{ me.id }})를 다음 호출 URL에 사용.
7. ⛔ 사용 금지 (미지원 — 렌더 프로브로 실측 확인)
아래는 순정 Shopify/LiquidJS/Braze엔 있어도 이 엔진엔 없다. 생성하면 발송이 실패한다.
없는 필터: base64_encode · base64_decode · base64_url_safe_encode · base64_url_safe_decode · remove_last · replace_last · find · find_index · has · reject · where_exp · sum · money · money_with_currency · md5 · sha1 · sha256 · hmac_sha256 · pluralize · camelize · handleize
없는 태그: {% liquid %} · {% echo %} · {% render %} · {% tablerow %} · {% section %} · {% paginate %} · {% form %} · {% layout %}. {% include %}은 에러는 안 나지만 아무것도 출력하지 않는다(스니펫 저장소 없음) — 쓰지 마라.
Shopify와 다른 동작 — ❌ 대신 ✅로:
마지막 업데이트