Growth · CRM · Article
Braze IAM Survey Modal 구현기:
프로모션 기획을 유저 응답 데이터로 검증하기
한 번 쓰고 버리는 인앱이 아니라, 다음 설문에도 재사용할 수 있는 다단계 분기 설문 엔진을 단일 IAM 메시지 안에 구현하고, 응답을 같은 유저의 실제 행동 데이터와 교차검증해 “계절성이냐 구조냐”라는 질문을 닫은 기록.
배경
왜 묻기로 했나
마케터에게 프로모션은 끝나지 않는 숙제다. 어떤 혜택을 줘야 반응할지, 어떻게 지급해야 참여가 늘지, 이번엔 어떤 구성이 먹힐지 — 한 번 끝내도 다음 분기에 같은 고민이 돌아온다. 참여한 유저와 끝내 참여하지 않은 유저가 서로 다른 걸 원한다는 건 어렴풋이 알지만, 정작 그게 무엇인지는 매번 내부 가설과 과거 성과로 메운다.
그 추측이 더는 통하지 않는 순간이 왔다. 11월 프로모션은 잘 됐는데, 12~1월로 가며 효율이 눈에 띄게 꺾였다. 문제는 원인이 두 갈래로 갈린다는 것이었다 — 연말연시라는 계절성(외부 요인)인지, 아니면 도달·혜택·피로 같은 구조의 문제인지. 둘은 대응이 정반대다. 계절성이면 기다리면 되지만, 구조 문제라면 프로모션 설계 자체를 바꿔야 한다.
기존 성과 데이터로는 이 둘을 갈라낼 수 없었다. 클릭률·전환율은 무엇이 떨어졌는지는 보여줘도 왜 떨어졌는지는 말해주지 않는다. 그래서 이번엔 추측을 멈추고 유저에게 직접 묻기로 했다. 단, “어땠나요”가 아니라 네 개의 가설을 세우고 그걸 검증하도록 설문을 설계했다.
- H1 노출알림 OFF·채널 도달 저하로 프로모션을 인지하지 못했다
- H2 가치12~1월 혜택이 기대 대비 매력이 부족했다
- H3 피로반복 메시지로 피로가 누적돼 무시했다
- H4 계절성연말 바쁨·지출 부담 같은 외부 요인이었다
이 네 가설이 그대로 설문의 골격이 됐다.
설계 원칙
화면보다 표본 설계가 먼저였다
설문은 화면을 잘 만든다고 좋아지지 않는다. 누구에게 물을지, 몇 명이 모여야 믿을 수 있는지를 먼저 정해야 했다. 만들기 전에 세 가지를 잡고 들어갔다.
- 대상을 행동으로 나눴다. 1~10월 핵심 행동 빈도로 활동 수준(Light·Mid·Heavy)을 가르고, 11~1월 참여 패턴과 BigQuery로 교차해 세그먼트를 만들었다. 세그먼트마다 몇 건이 모여야 의미가 있는지는 신뢰수준 90%·오차 8% 기준으로 미리 계산해뒀다. 응답을 많이 받는 것보다, 세그먼트별 최소 표본을 채우는 걸 목표로 잡았다.
- 편향은 설계로 눌렀다. 불만이 큰 유저가 목소리를 키우고, 혜택에 민감한 유저가 몰리고, 정작 지쳐서 떠나려는 유저는 설문도 무시한다. 유형별로 완화 장치를 두고, 1차는 일부러 리워드 없이 돌렸다. 보상을 먼저 걸면 혜택에 민감한 유저만 잔뜩 들어오기 때문이다. 순수 응답을 먼저 모은 뒤, 표본이 부족한 세그먼트에만 2차로 리워드를 붙였다.
- 응답을 행동 데이터와 붙여볼 수 있게 만들었다. 이게 결국 외부 설문툴 대신 IAM을 고른 이유가 됐다.
(표본 산정과 문항·분기 설계는 사내 UX 리서처와 함께 다듬었다.)
구현 · 왜 IAM인가
왜 외부 설문툴이 아니라 IAM 안인가
설문을 받는 방법은 많다. 별도 설문 페이지를 만들 수도 있고, 링크를 푸시·이메일로 보낼 수도 있다. 그럼에도 Braze IAM 모달을 택한 이유는 세 가지였다.
- 유저가 이미 앱 안에 있는 순간에 묻는다. 외부 페이지로 이탈시키지 않으니 응답 장벽이 낮다.
- 대상을 나눠 노출할 수 있다. 프로모션 참여 여부·최근 방문·특정 행동 이벤트를 기준으로 참여자와 미참여자에게 서로 다른 경로를 줄 수 있다.
- 가장 큰 이유는 데이터다. 응답이 그 유저의 Braze 프로필에 바로 붙는다. 외부 설문 응답은 따로 노는 데이터지만, IAM 응답은 Custom Event·Attribute로 남아 세그먼트가 되고, 같은 유저의 실제 행동 데이터와 교차검증까지 된다. 뒤에서 다룰 분석이 가능했던 게 이 덕분이다.
구현 · 페이지 라우터
정적 메시지 한 장을 다단계 플로우로
Braze IAM은 원래 한 장짜리 정적 메시지다. 다단계 설문 도구가 아니다. 그래서 하나의 Custom HTML 메시지 안에 SPA식 페이지 라우터를 직접 넣었다. 모든 질문은 .bz-page[page-id] 단위로 두고, 한 번에 하나만 active 상태로 보여준다.
실제 모달은 이런 화면들로 구성된다 — 인트로, 단일·복수 선택, 월별 이벤트 카드, 5점 척도까지 모두 한 메시지 안에서 전환된다.
이벤트 경험 설문
소요 시간 약 1–2분
의견이 더 좋은 혜택을 만듭니다.
서비스를 처음 이용한
시점은 언제인가요?
11월 이벤트를
본 적 있으신가요?
혜택은
매력적이었나요?
이벤트 소식을 받는
채널을 골라주세요
모두 선택
var pageHistory = [];
function goToPage(pageId) {
var currentPage = document.querySelector('.bz-page.active');
// 인트로 → 첫 질문 진입 시점에 '설문 시작' 이벤트 발화
if (currentPage && currentPage.getAttribute('page-id') === 'intro' && pageId !== 'intro') {
surveyStarted = true;
logSurveyStarted();
}
logProgressEvent(pageId); // 진행 상황을 유저 프로필에 저장
if (currentPage) pageHistory.push(currentPage.getAttribute('page-id'));
document.querySelectorAll('.bz-page').forEach(function (page) {
page.classList.toggle('active', page.getAttribute('page-id') === pageId);
});
}
pageHistory 스택을 두어 ‘이전’ 버튼도 지원한다. 라디오(단일 선택) 질문은 선택 즉시 다음으로 넘어가도록 ‘다음’ 버튼을 숨기고 자동 진행시켰고, 체크박스(복수 선택)는 하나 이상 골라야 버튼이 활성화되게 했다. 질문 한 페이지에 머무는 시간을 줄이는 게 완료율과 직결되기 때문이다.
구현 · 분기 설계
같은 모달, 응답자마다 다른 설문
이번 서베이의 핵심은 모두에게 같은 질문을 던지지 않는다는 것이었다. 응답에 따라 경로가 갈라지고, 끝에서 유저는 네 개의 세그먼트 중 하나로 분류된다.
var segmentLabels = {
'A': '미인지 (이벤트 본 적 없음)',
'B': '참여자 (이벤트 참여함)',
'C': '비참여자 (이벤트 봤지만 미참여)',
'E': '저빈도 사용자'
};
각 질문의 ‘다음’은 단순 페이지 이동이 아니라, 응답값을 읽고 다음 경로를 정하는 핸들러로 연결된다. 예를 들어 연간 요청 횟수가 ‘없음’이면 저빈도 경로로, 아니면 일반 경로로 분기한다. 월별 이벤트 응답을 모두 받은 뒤에는, 그 사람이 어떤 유형인지에 따라 다음 질문 묶음 자체가 달라진다.
function handleQ0_1a() {
var value = getPageValue('q0-1a');
if (value === 'none') { // 저빈도(E) 경로
surveyAnswers.isLowFrequency = true;
goToPage('q0-1-1');
} else {
surveyAnswers.isLowFrequency = false;
goToPage('q0-1b');
}
}
function finishEventQuestions() {
if (seenEvents.length === 0) return goToPage('a1'); // 미인지(A)
if (participatedEvents.length > 0) return goToPage('b1'); // 참여자(B)
goToPage('c1'); // 비참여자(C)
}
이 분기는 화면 흐름을 매끄럽게 하려는 UX 장치가 아니라 가설을 검증하는 설계다. 미인지(A) 경로는 노출 가설을 — 접점이 아예 없었는지, 봤지만 무시했는지 — 가른다. 참여자(B)는 참여 과정의 마찰·가치·신뢰를, 비참여자(C)는 인지하고도 참여하지 않은 이유를 가치·피로·상황으로 나눠 받는다. 끝에서 모든 경로가 합류하는 공통 문항은 “11월 대비 의향이 어떻게 달라졌나”를 물어 계절성과 구조 문제를 분리한다. 응답자가 누구냐에 따라 다른 질문이 나가는 이유가 여기 있다.
| 세그먼트 | 정의 | 분기가 캐는 것 (가설) |
|---|---|---|
| A 미인지 | 이벤트를 본 적 없음 | 노출 — 접점이 없었나, 봤지만 무시했나 (H1) |
| B 참여자 | 이벤트에 참여함 | 참여 과정의 마찰·가치·신뢰 (H2) |
| C 비참여자 | 봤지만 미참여 | 미참여 이유 — 가치·피로·상황 (H2·H3) |
| E 저빈도 | 활동·요청 빈도가 낮음 | 저빈도 사용자 경로 |
유저 유형에 따라 갈라지는 전체 설문 플로우는 다음과 같다.
구현 · 동적 생성
하드코딩하지 않는다 — 월별 질문 동적 생성
이 설문은 11·12·1월 세 번의 프로모션을 각각 물어야 했다. “○월 이벤트 봤나 → 어떻게 알았나 → 참여했나 → (만족도 / 미참여 이유)”를 월마다 반복하면 정적 페이지만 수십 장이 된다. 그래서 질문 페이지는 단 하나(event-question)만 두고, 그 안을 템플릿 함수로 매번 다시 그렸다.
var eventList = ['nov_event', 'dec_event', 'jan_event'];
function showEventQuestion() {
var event = eventData[eventList[currentEventIndex]];
var modal = document.getElementById('event-question-modal');
if (currentQuestionType === 'seen') modal.innerHTML = buildSeenQuestion(event);
else if (currentQuestionType === 'channel') modal.innerHTML = buildChannelQuestion(event);
else if (currentQuestionType === 'participated') modal.innerHTML = buildParticipatedQuestion(event);
// …attract / non_participate_reason
goToPage('event-question');
}
이벤트가 늘어도 eventList 배열에 키 하나만 추가하면 된다. 질문 문구와 이미지는 eventData에서 주입되므로, 화면은 한 장인데 실제로는 월×질문유형만큼의 화면이 동적으로 생성된다.
여기까지 오면 라우터·분기·동적 생성·데이터 기록이 합쳐져 하나의 재사용 가능한 설문 엔진이 된다. 다음 설문은 이 구조를 처음부터 다시 짜는 게 아니라, 질문 정의(eventData)와 분기 규칙만 갈아끼우면 된다. 매번 인앱을 새로 구성하는 일회성 작업이 아니라, 반복 구현 가능한 자산으로 설계한 것이다.
구현 · 데이터 기록
응답을 데이터로 — 그리고 255자의 벽
설문 모달에서 가장 중요한 건 질문을 띄우는 게 아니라, 응답을 이후 캠페인에 쓸 수 있는 형태로 남기는 것이다. 두 갈래로 기록했다. 완료 시점에는 모든 응답을 하나의 Custom Event(promo_survey_completed)로 발화한다. 세그먼트(path)와 각 답변이 property로 함께 실린다.
※ 이 글에 등장하는 Custom Event·Attribute 명(promo_survey_completed, promo_survey_responses 등)은 구조를 보여주기 위한 예시다.
function saveSurveyData(status) {
var segment = surveyAnswers.path || 'screenout';
var eventProperties = {
survey_status: status,
segment: segment,
segment_label: segmentLabels[segment] || segment,
completed_at: new Date().toISOString()
};
Object.keys(surveyAnswers).forEach(function (key) {
if (key === 'path' || key === 'isLowFrequency') return;
var v = surveyAnswers[key];
eventProperties[key] = Array.isArray(v) ? v.join(',') : String(v);
});
var bridge = window.brazeBridge || window.appboyBridge;
bridge.logCustomEvent('promo_survey_completed', eventProperties);
bridge.requestImmediateDataFlush();
}
여기서 한 가지 디테일을 더 넣었다. 화면에 보이는 문구와 기록 값을 분리하되, 동시에 사람이 읽을 수 있는 형태도 같이 남겼다. 코드 값(q0_1a_request_count = none)만 쌓이면 나중에 리포트를 볼 때 매번 해석표가 필요하기 때문이다. 그래서 [Q] 질문 텍스트 = 답변 텍스트를 함께 저장했다.
surveyAnswers[key] = value; // 분석용 코드 값 surveyAnswers['[Q] ' + questionText] = answerText; // 사람이 읽는 값
문제는 중간 이탈이었다. 완료해야만 이벤트가 남는다면, 끝까지 안 푼 유저의 응답은 전부 사라진다. 그래서 페이지를 넘길 때마다 진행 상황을 Custom Attribute로 저장했다 — 이탈해도 그 시점까지의 응답이 프로필에 남는다. 이건 구글폼 같은 외부 설문툴로 응답을 받을 때 대비 분명한 장점이다. 외부 툴이라면 끝까지 제출하지 않은 응답은 통째로 사라지지만, 여기서는 한 페이지라도 넘긴 시점의 응답이 유저 프로필에 보존된다. 그런데 Braze Attribute의 문자열 값은 255자 제한이 있어, 응답이 쌓이면 JSON 한 덩어리가 한도를 넘는다. 250자 청크로 쪼개 여러 Attribute에 나눠 담고, 더는 안 쓰는 청크는 비워 정리했다.
var fullJson = JSON.stringify(responseData);
var CHUNK_SIZE = 250;
var totalChunks = Math.ceil(fullJson.length / CHUNK_SIZE);
var user = bridge.getUser();
for (var i = 0; i < totalChunks; i++) {
var chunk = fullJson.substring(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
var attrName = i === 0 ? 'promo_survey_responses' : 'promo_survey_responses_' + (i + 1);
user.setCustomUserAttribute(attrName, chunk);
}
// 이전 회차에 더 많았던 청크 정리
for (var j = totalChunks; j < 5; j++) {
var name = j === 0 ? 'promo_survey_responses' : 'promo_survey_responses_' + (j + 1);
user.setCustomUserAttribute(name, null);
}
실제로 응답은 이렇게 적립된다. 진행 중에는 Custom Attribute에 부분 응답이 누적되고, 완료된 응답은 이벤트로 쌓인다. 이 이벤트는 Braze에는 곧바로 기록되지만, 사내 분석 환경(Amplitude)에서는 Currents의 Custom Event 전송 옵션이 꺼져 있어 보이지 않았다. 전송을 켜면 이벤트 볼륨이 급증해 분석 도구의 수집 한도를 압박하기 때문에, 설문 응답만 추출하는 목적에는 Braze 쿼리 빌더가 더 맞는 선택이었다. 쿼리 빌더로 뽑으면 코드 값과 사람이 읽는 질문·답변이 함께 담긴 행으로 떨어진다.
// q0-0 진입 — 시작 {"current_page":"q0-0"} // 첫 응답 직후 {"current_page":"q0-1a","q0_0_first_use":"within_6m"} // 분기 응답 후 — 값이 계속 누적 {"current_page":"q0-1b","q0_0_first_use":"within_6m","q0_1a_request_count":"3_4"} // 완료 — 이탈해도 여기까지는 프로필에 보존 { …, "q0_1b_usage_period":"1week", "isLowFrequency":false }
어트리뷰트에 담기는 주요 키는 다음과 같다.
| key | 의미 | 예시값 |
|---|---|---|
| current_page | 마지막으로 머문 페이지 | q0-1b |
| q0_0_first_use | 첫 이용 시점 | within_6m · 1year_plus |
| q0_1a_request_count | 최근 1년 핵심 행동 횟수 | 3_4 · none |
| q0_1b_usage_period | 집중 이용 기간 | 1week · year_round |
| isLowFrequency | 저빈도 경로 진입 여부 | true · false |
SELECT
external_user_id AS external_id,
TO_TIMESTAMP(time) AS event_time_utc,
CONVERT_TIMEZONE('UTC', 'Asia/Seoul', TO_TIMESTAMP(time)) AS event_time_kst,
properties
FROM
USERS_BEHAVIORS_CUSTOMEVENT_SHARED
WHERE
name = 'promo_survey_completed'
AND time >= DATE_PART('epoch_second', CONVERT_TIMEZONE('Asia/Seoul', 'UTC', TO_TIMESTAMP('2026-02-01 00:00:00')))
ORDER BY
time DESC;
여기엔 한 가지 함정이 있었다. 처음 안내받은 초안 쿼리는 user_id 컬럼을 호출했는데, 이 값은 해싱된 형태로 떨어져 우리가 운영하는 유저 식별자와 매칭되지 않았다. external_user_id 컬럼을 직접 호출하도록 바꾸자 해싱되지 않은 실제 external_id가 정상적으로 추출됐다 — 위 쿼리가 그 최종 형태다.
| EXTERNAL_ID | EVENT_TIME_UTC | EVENT_TIME_KST | PROPERTIES · promo_survey_completed |
|---|---|---|---|
| u_10··abcd | 2026-02-23T03:19Z | 2026-02-23 12:19 | {"survey_status":"completed","segment":"A","segment_label":"미인지","q0_0_first_use":"6m_1year","[Q] 처음 이용 시점은?":"6개월~1년 전", …} |
| u_15··efgh | 2026-02-22T18:18Z | 2026-02-23 03:18 | { …,"segment":"B","segment_label":"참여자","[Q] 혜택은 매력적이었나요?":"3점", …} |
| u_99··wxyz | 2026-02-21T07:24Z | 2026-02-21 16:24 | { …,"segment":"E","segment_label":"저빈도","[Q] 선호 혜택은?":"확정형", …} |
[Q] 사람이 읽는 질문·답변이 한 행에 함께 담긴다.이렇게 응답과 프로모션 관련 기록을 유저 어트리뷰트에 계속 업데이트해두면, 각 유저의 프로필 자체가 세그먼트 조건이 된다. “확정형 혜택을 선호한다고 답한 유저”, “이벤트를 인지하지 못했다고 답한 유저”처럼 응답이 곧바로 타겟 조건으로 쓰이고, 후속 캠페인의 분기·발송 대상이 된다. 설문이 결과를 한 번 모으고 끝나는 게 아니라 세그먼테이션과 후속 타겟팅의 입력값이 되는 것이다 — 단순 응답 수집을 넘어, 응답을 CRM 자산으로 적립하도록 설계했다.
결과
응답을 행동 데이터와 교차검증하다
여기서 “왜 IAM 안인가”가 회수된다. 응답이 유저 프로필에 붙어 있으니, 설문 응답을 같은 유저의 실제 활동·참여·당첨 데이터와 교차검증할 수 있었다. 외부 설문툴이었다면 불가능한 분석이다. (구체 수치는 대외비로, 이하 정성 표현으로 대체한다.)
가장 강한 발견은 교차검증에서 나왔다.
실제로 11월 프로모션에 참여한 유저의 대다수가 그 이벤트를 “못 봤다”고 답했고, 실제 당첨자의 절반이 혜택을 “못 받았다”고 답했다.
행동은 했지만 경험으로 각인되지 않은 것이다 — 참여도, 당첨도 유저의 기억에는 남지 않았다.
그래서 처음의 질문 — 계절성이냐 구조냐 — 에 답이 닫혔다. 이탈자에게 의향이 왜 식었는지 묻자 “변함없음”과 “기억 안 남”이 대부분이었고, 피로를 꼽은 비율은 소수에 그쳤다. 효율 급감은 연말 분위기나 메시지 피로 때문이 아니라, 애초에 도달하지 않았고(미인지율이 80%를 넘고), 도달해도 각인되지 않은(참여자 대다수가 “못 봤다”) 구조의 문제였다. 계절성 가설은 기각됐고, 노출과 경험 각인이 핵심으로 남았다.
다음 실험
묻고, 기록하고, 다시 실험한다
교차검증이 가리킨 방향은 분명했다. 유저는 불확실한 추첨보다 즉시 체감되는 확정 보상에 반응하고, 무엇보다 행동이 결과로 각인되지 않으면 참여도 당첨도 의미가 없었다. 서베이 모달은 그래서 의견 수집 도구가 아니었다. 유저에게 먼저 묻고, 응답을 데이터로 남기고, 그 데이터로 다음 캠페인의 구조를 바꾸고, 다시 성과를 확인하는 — 순환의 출발점이었다. 마테크는 툴을 운영하는 일에서 끝나지 않는다. 묻고, 기록하고, 다시 실험하는 구조를 설계하는 일이다.
다음 아티클에서는 이 발견과 인사이트를 반영한 다음 프로모션 실험을 다뤄보려 한다.
관련 글 · 시리즈 다음 편 행동과 보상을 한 화면에 — 게이미피케이션 수신동의 IAM 이 설문이 가리킨 “즉시 체감·경험 각인”을, 약관 동의 → 게임 → 즉시 결과 확인 플로우로 구현한 기록.