# 핵클 사용 가이드

{% columns %}
{% column width="58.333333333333336%" %}
**하나의 플랫폼으로 해결하세요.**

실시간 행동 데이터 수집부터 분석, CRM 마케팅, A/B 테스트, 기능 플래그, AI 추천까지 비즈니스 성장을 위한 모든 기능을\
하나의 플랫폼으로 제공합니다.

<a href="https://dashboard.hackle.io/demo?_gl=1*1s9owup*_gcl_au*MTI4NDM1MTA3MS4xNzg0MTkyNzIw*_ga*NDMyMjI2Njc2LjE3ODQxOTI3MjA.*_ga_3WL6RVCBCQ*czE3ODQxOTI3MjAkbzEkZzEkdDE3ODQxOTI4ODAkajU5JGwwJGg2MjUzMDY4MDY." class="button primary">데모 둘러보기</a><a href="https://dashboard.hackle.io/login" class="button secondary">대시보드 방문</a>
{% endcolumn %}

{% column width="41.666666666666664%" %}

<figure><img src="/files/TVK4yN8YLgL4VnA8eNog" alt=""><figcaption></figcaption></figure>
{% endcolumn %}
{% endcolumns %}

### Quick Start

<table data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><i class="fa-chart-line">:chart-line:</i></td><td>핵클 서비스</td><td><a href="/pages/u25oCNoXTMtxlf99lsCT">/pages/u25oCNoXTMtxlf99lsCT</a></td></tr><tr><td><i class="fa-pen-line">:pen-line:</i></td><td>개발자 문서</td><td><a href="/pages/AbCR5ybaOSPHHVbuOYmw">/pages/AbCR5ybaOSPHHVbuOYmw</a></td></tr><tr><td><i class="fa-robot">:robot:</i></td><td>AI</td><td><a href="/pages/9pqHcE02DBstOhMP0hYd">/pages/9pqHcE02DBstOhMP0hYd</a></td></tr></tbody></table>

### 핵클로 전환율 10X 높이는 법

어디서부터 시작해야할지 막막하시다구요?

핵클을 활용한 전환율 10배 늘리기 제안 둘러보시기를 추천드려요.\
핵클의 데이터 분석을 통해 사용자 행동 데이터에 숨은 인사이트를 찾고, 맞춤형 CRM 마케팅을 통해 서비스 전환율을 높여보세요.

<table data-view="cards"><thead><tr><th align="center"></th><th align="center"></th><th data-hidden data-card-cover data-type="image">Cover image</th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td align="center"><strong>장바구니 이탈 사용자를</strong><br><strong>찾고 싶다면?</strong></td><td align="center">퍼널 분석으로 이탈 사용자를 찾고, 코호트로 관리하는 방법을 알려드려요.</td><td><a href="/files/6ftGsr0nLWr9iCzSmqkF">/files/6ftGsr0nLWr9iCzSmqkF</a></td><td><a href="/pages/ECrTvA3UZnGxcsMDFJRo">/pages/ECrTvA3UZnGxcsMDFJRo</a></td></tr><tr><td align="center"><strong>이탈 사용자의 재방문을</strong><br><strong>유도하는 법</strong></td><td align="center">떠난 사용자를 다시 사이트로 데려오는 방법 A-Z를 알려드려요.</td><td><a href="/files/BTZfSAL5h8CoKjxRQcng">/files/BTZfSAL5h8CoKjxRQcng</a></td><td><a href="/pages/tquTR1FNRJNRw1XvwYpt">/pages/tquTR1FNRJNRw1XvwYpt</a></td></tr><tr><td align="center"><strong>사용자에게 가장</strong><br><strong>효과적인 문구는?</strong></td><td align="center">인앱 메시지 MAB 테스트로 사용자의 반응을 확인하고 최적화 하는 방법을 알려드려요.</td><td><a href="/files/1ZsZBN2QruTYqL2EkdUO">/files/1ZsZBN2QruTYqL2EkdUO</a></td><td><a href="/pages/Ab7QbDCJHQzYoMxKRjy6">/pages/Ab7QbDCJHQzYoMxKRjy6</a></td></tr></tbody></table>


# 업데이트 소식

{% updates format="full" %}
{% update date="2026-08-06" tags="데이터 분석,SDK,인앱 메시지,사용자 관리,카카오 메시지,analytics,ab" %}

## 🔍 Hackle Event Explorer 출시

![Hackle Event Explorer 출시](/files/LCmvbJNpVNATOYJBr5Xt)

원하는 이벤트가 제대로 수집되는지, 이제 브라우저에서 바로 확인하세요.\
[Chrome 웹 스토어](https://chromewebstore.google.com/detail/hackle-event-explorer/lajjijmomildiipdfkohahkefimgjcpa)에서 설치하면 페이지에서 발생하는 이벤트와 속성을 눈으로 확인하며 연동이 잘 됐는지 바로 검증할 수 있습니다.&#x20;

개발자 도구를 뒤지지 않아도 되니 QA와 디버깅이 훨씬 빨라집니다.

**이렇게 활용해보세요**

* **연동 검증**\
  새 이벤트 연동 후 실제로 잘 들어오는지 바로 검증
* **수집 데이터 확인**\
  이벤트 이름·속성 값이 의도대로 찍히는지 화면에서 확인
* **QA·디버깅 시간 단축**\
  개발자 도구를 열지 않고도 수집 여부를 확인해 QA·디버깅에 드는 시간 단축

### ✨ 원격 평가

핵클 서버에 저장된 사용자 프로필을 활용하여 사용자 분배와 인앱 메시지 대상 여부를 결정할 수 있습니다.

기존에는 타겟팅에 쓸 사용자 속성을 SDK로 직접 전송해야 했습니다.&#x20;

이제는 핵클에 이미 쌓여 있는 프로필 정보를 별도 전송 없이 서버 평가에 활용할 수 있어, 클라이언트가 값을 들고 있지 않아도 서버에 저장된 속성만으로 사용자 분배와 인앱 메시지 타겟팅이 이뤄집니다.

**이렇게 활용해보세요**

* **서버 속성 기반 분배**\
  서버에서만 아는 CRM 등급·구매 이력 같은 속성으로 A/B 테스트·기능 플래그·원격 구성 분배
* **인앱 메시지 타겟 세분화**\
  결제 상태, 멤버십 등급 등 서버에서만 아는 정보로 캠페인 타겟 세분화
* **누락 없는 타겟팅**\
  속성 전달 누락으로 타겟에서 빠지던 사용자까지 정확히 포함

[**원격 평가 가이드 확인하기**](/development-guide/sdk/evaluation-mode/remote-evaluation)

### 📂 사용자 속성 업로드

![사용자 속성 업로드](/files/Z4ED71c7Bwi7rvl5KmQl)

개발 연동 없이도 스프레드시트로 정리한 오프라인 데이터나 외부 CRM 속성을 CSV 파일로 핵클 사용자 프로필에 바로 올릴 수 있습니다.\
원격 평가와 함께 사용하면 업로드한 속성이 그대로 타겟팅과 분배에 활용됩니다.

[**사용자 속성 업로드 가이드 확인하기**](/user-view/csv-import)

**개선**

* **080 수신 거부 목록 조회**\
  수신 거부 관리에서 080 수신 거부 번호와 목록을 직접 확인할 수 있습니다.
* **캐시 잔액 부족 알림톡**\
  메시지 발송용 캐시 잔액이 부족해지면 알림톡으로 알려드립니다.
* **메시지 발송 전 대상자 목록 조회**\
  발송하기 전에 실제 메시지를 받게 될 대상자 목록을 미리 확인할 수 있습니다.
* **알림톡 템플릿 첨부파일 등록**\
  알림톡 템플릿 검수를 요청할 때 첨부파일을 함께 등록할 수 있습니다.
* **스케줄 발송 Array 속성 타겟팅**\
  예약 발송에서 Array 형태 속성으로도 타겟팅할 수 있습니다.
  {% endupdate %}

{% update date="2026-06-15" tags="사용자 여정,외부 연동,데이터 분석,사용자 관리,AI,journey,ab" %}

## ✨ 사용자 여정 - A/B 테스트

![사용자 여정 - A/B 테스트](/files/RZAMnOr2AXeLBIXB90Ee)

이제 여정 흐름 안에서 직접 A/B 테스트를 설정하고, 어떤 메시지·채널·타이밍이 더 높은 전환을 만드는지 데이터로 확인할 수 있습니다.\
별도 실험을 따로 만들지 않아도 하나의 여정 안에 A/B 테스트 노드를 추가해 가설 검증부터 성과 분석까지 한 번에 이어갈 수 있습니다.

**이렇게 활용해보세요**

* **채널 비교**\
  알림톡 vs 푸시 vs 문자 중 어떤 채널이 더 잘 반응하는지 검증
* **메시지 소재 비교**\
  쿠폰을 바로 주는 그룹 vs 콘텐츠를 먼저 보내고 쿠폰을 주는 그룹 중 어떤 흐름의 전환율이 높은지 비교
* **타이밍 테스트**\
  동일 메시지를 1시간 후 발송 vs 24시간 후 발송해 최적 타이밍 찾기

### 🔗 Hackle MCP 원격 서버

<figure><img src="/files/jEQYKcMGzQIeP31gNY7Z" alt=""><figcaption></figcaption></figure>

이제 핵클을 Claude 같은 AI 클라이언트와 더 쉽게 연결할 수 있습니다.

Claude 외에도 Cursor, Gemini, GitHub Copilot, n8n, Zapier, Slack, Notion 등 MCP를 지원하는 다양한 앱에서 손쉽게 핵클을 바로 활용할 수 있습니다.

{% hint style="warning" %}
**기존 로컬 MCP 서버를 사용 중이신 분들은**

로컬 MCP 서버는 2026년 하반기 지원이 종료될 예정이며, 신규 도구는 원격 서버에만 추가됩니다.
{% endhint %}

또한 MCP가 원격 서버로 전환하면 기존 15개에서 24개로 도구가 늘어납니다.\
문자 메시지·카카오 메시지 조회가 새로 추가됐고, 인앱·푸시·문자·카카오 채널의 통계 분석도 이제 AI에게 바로 물어볼 수 있습니다.

**이렇게 활용해보세요**

* 지난주 시작한 A/B 테스트 결과를 요약해줘
* 최근 푸시 캠페인의 전환율을 채널별로 비교해줘
* 최근 30일 DAU 추이와 7일 리텐션을 차트로 그려줘
* 오늘 종료된 실험 중 통계적으로 유의한 것만 골라서 알려줘

### 📊 사용자 조회 - 전화번호 검색 지원

<figure><img src="/files/oJFWbXhYCwiRVNgw97tT" alt=""><figcaption></figcaption></figure>

이제 사용자 조회에서 전화번호로도 사용자를 검색할 수 있습니다.\
Hackle ID, User ID, Device ID 등 특정 ID로만 조회할 수 있었던 사용자 검색에 전화번호 검색이 추가되었습니다.
{% endupdate %}

{% update date="2026-05-15" tags="CRM,crm" %}

## ✨ 카페24 비회원 발송

![카페24 비회원 발송](/files/GJ3EtGFjONBnwaucEyGo)

이제 카페24 비회원 고객도 메시지 발송 대상으로 활용할 수 있습니다.\
기존에는 회원으로 등록된 고객에게만 메시지를 보낼 수 있었지만, 이제는 비회원 구매 고객까지 함께 관리해 더 넓은 고객군에 메시지를 전달할 수 있습니다.

주문 확인·배송 안내부터 재구매 유도, 혜택 안내까지 비회원 고객과의 접점을 놓치지 않고 챙길 수 있습니다.\
비회원 구매 고객에게 쿠폰 발행 메시지를 전달해 회원가입 → 재구매로 전환시켜 보세요. 회원 전환을 유도하고 싶은 팀에도 효과적으로 활용할 수 있습니다.

### **🔗 커넥티드 컨텐츠**

![커넥티드 컨텐츠](/files/h7u93zD9nYStGq0GJIxW)

메시지를 발송하는 순간 고객사가 세팅해둔 API를 자동으로 호출해 응답값을 메시지 개인화에 바로 활용할 수 있는 **커넥티드 컨텐츠**가 출시되었습니다.

실시간 재고·실시간 할인가·포인트·추천 상품 등 외부 데이터를 메시지에 녹여 고객 한 명 한 명에게 꼭 맞는 메시지를 보낼 수 있습니다.

커넥티드 컨텐츠에서 태그를 직접 입력하거나, **\[개인화 데이터 추가]** 모달에서 원하는 변수를 선택해 추가할 수 있습니다. 모달에서 API URL을 입력하면 응답 결과를 바로 확인할 수 있고, 활용할 값을 변수 형태로 클릭 한 번에 메시지에 삽입할 수 있습니다.
{% endupdate %}

{% update date="2026-04-27" tags="사용자 여정,CRM,journey" %}

## 🔁 사용자 여정 재진입

![사용자 여정 재진입](/files/wwtyvYFOJhIv6KEhWEI2)

이제 동일한 사용자가 진입 조건을 다시 충족했을 때 여정에 자동으로 재진입하도록 설정할 수 있습니다.\
재진입 주기는 초·분·시간·일·주·월 단위로 지정할 수 있어, 시간 흐름에 따른 반복 시나리오를 유연하게 설계할 수 있습니다.

**활용 시나리오**

* **재구매 유도**\
  구매 완료 → 7일 대기 → 리뷰 요청 알림 → 30일 대기 → 재구매 쿠폰 발송
* **결제 전환 자동화**\
  무료 체험 시작 → 3일 대기 → 핵심 기능 안내 → 7일 대기 → 유료 전환 유도 메시지
* **미완료 예약 이탈 방지**\
  예약 시작 → 1시간 대기 → 미완료 확인 → 알림톡 발송 → 1일 대기 → 추가 리마인드
  {% endupdate %}

{% update date="2026-04-09" tags="인앱 메시지,카카오 메시지,문자 메시지,CRM,사용자 여정,외부 연동,in-app-message,crm" %}

## 💌 자유로운 인앱 메시지를 위한 커스텀 HTML

![자유로운 인앱 메시지를 위한 커스텀 HTML](/files/KcNOFvKbey8LSUn4zqBU)

HTML 기반 커스텀 인앱 메시지를 지원합니다.\
정해진 템플릿을 벗어나 브랜드 아이덴티티와 캠페인 목적에 맞춘 인앱 메시지를 자유롭게 구성할 수 있습니다.\
룰렛·추첨 이벤트, 프로모션 배너, 별점 리뷰 유도, 설문, 영상형 메시지 등 기존에 개발자가 필요했던 형태도 직접 구성할 수 있습니다.

HTML 코드를 모르더라도 사전 준비된 템플릿을 선택해 즉시 적용할 수 있고, **\[AI에게 물어보기]** 기능으로 원하는 형태의 메시지를 빠르게 제작할 수 있습니다.

**💌 핵클 간편 발송 출시**

![핵클 간편 발송 — 전화번호만 있어도 발송](/files/Md8oU54P8mDa4F7ayFYK)

[**간편 발송 가이드 확인하기**](https://hackle.gitbook.io/hackle-docs)

별도 설치나 설정 없이 메시지 발송을 시작할 수 있는 '간편 발송' 기능이 출시되었습니다.\
고객 전화번호 리스트만 업로드하면 카카오 알림톡·브랜드 메시지·SMS를 즉시 발송하고 성과 확인까지 한 번에 이어집니다.

* SDK 설치 없이 즉시 사용
* 080 발신 기본 지원
* 발송 실패 시 문자로 자동 대체 발송
* 마케팅 수신 동의자 대상 타겟 발송
* 대량 발송 지원

**📊 CRM 캠페인 통계 다운로드**

![CRM 캠페인 통계 다운로드](/files/FtWMYcfGz6rgGsO1Sa5R)

CRM 캠페인 통계 페이지에서 원하는 기간의 데이터를 엑셀로 다운로드할 수 있습니다.\
화면에서 보던 성과를 일자별 수치로 내려받아 더 상세한 분석에 활용할 수 있습니다.

**개선**

* **사용자 여정 이탈 이벤트 N개 이상 설정**\
  단일 이탈 조건뿐 아니라 N개 이상 기준으로 이탈 이벤트를 설정해 사용자 흐름을 더 정교하게 분석할 수 있습니다.
  {% endupdate %}

{% update date="2026-03-10" tags="CRM,푸시 메시지,카카오 메시지,인앱 메시지,crm,analytics" %}

## 📊 CRM 캠페인 성과 기간 분석

![CRM 캠페인 성과 — 원하는 기간 기준으로 분석](/files/LxuVikqC52lL1Z5ktHQi)

CRM 캠페인 성과를 원하는 기간 단위로 조회할 수 있습니다.\
발송 직후 시점의 성과뿐 아니라 캠페인 후 일정 기간이 경과한 시점의 누적 성과까지 비교 분석할 수 있어, 단기·중기 효과를 함께 검증할 수 있습니다.

**개선**

* **메시지 발송 대상 설정 고도화**\
  푸시·카카오·인앱 메시지 캠페인 설정에서 발송 타겟으로 코호트와 속성 규칙을 여러 개 조합할 수 있습니다.
  {% endupdate %}

{% update date="2026-02-05" tags="CRM,데이터 분석,카카오 메시지,문자 메시지,외부 연동,crm,analytics" %}

## 📊 CRM 캠페인 발송 후 전환 성과 분석

![CRM 캠페인 발송 후 전환 성과 분석](/files/t0CuJAt9y6IQgo99551s)

CRM 캠페인 발송 후 메시지 도달뿐 아니라 전환까지 바로 확인할 수 있도록 성과 분석이 고도화되었습니다.\
클릭, 전환, 매출 등 캠페인이 비즈니스에 미친 영향을 단일 화면에서 추적할 수 있습니다.

**개선**

* **스케줄 기반 발송 메시지에 '발송 제한 시간' 적용**\
  정해진 시간대에만 메시지가 발송되어, 야간 발송 등 고객 경험을 해치는 발송을 방지할 수 있습니다.
  {% endupdate %}

{% update date="2026-01-07" tags="카카오 메시지,외부 연동,kakao" %}

## 💌 추가 개발 없이 카카오 브랜드 메시지 지원

![추가 개발 없이 카카오 브랜드 메시지 지원](/files/vm297aYHo8U5NQ6rtw75)

핵클이 카카오 브랜드 메시지를 추가 개발 없이 지원합니다.\
기존 카카오톡 채널 친구 대상 친구톡과 달리 브랜드 메시지는 카카오 수신 동의가 있는 모든 사용자에게 도달할 수 있어, 캠페인 도달 범위를 크게 확장할 수 있습니다.

**개선**

* **알림톡 템플릿 관리**\
  핵클 대시보드에서 알림톡 및 브랜드 메시지 발송에 필요한 템플릿을 직접 등록·관리할 수 있습니다.

{% hint style="warning" %}
**카카오 알림톡 마일리지·포인트·쿠폰 발송 기준 개편 (카카오 정책 변경)**

2026년 1월 1일부터 다음 조건의 메시지는 알림톡으로 발송할 수 없습니다.

* 명시적 거래·계약 관계 없이 자체 지급되는 쿠폰·포인트·마일리지
* 사용 활성화 목적의 혜택성 알림
* 혜택 구매 유도·홍보 목적이 포함된 메시지
  {% endhint %}
  {% endupdate %}

{% update date="2025-12-03" tags="데이터 분석,인앱 메시지,카카오 메시지,analytics" %}

## 📊 앱 성장 여정 트렌드 리포트 출시

![앱 성장 여정 트렌드 리포트 출시](/files/Aa9pm5gAX7BiWDkpSq6p)

자동 수집 이벤트(`$app_install`, `$app_update`, `$app_open`, `$app_background`)를 활용한 신규 트렌드 리포트가 추가되었습니다.\
앱의 설치·업데이트·세션 데이터를 기반으로 사용자 여정을 진단하고 개선 기회를 찾을 수 있습니다.

리포트 화면의 \[내 데이터로 확인해 보기]를 클릭하면 추천 차트 세트가 자동 생성됩니다.\
자동 수집 이벤트가 활성화되므로 필요한 차트만 선택해 사용하시기 바랍니다.

**개선**

* **인앱 메시지 스케줄 관리**\
  캘린더 타임테이블에서 노출 시간대(예: 매일 야간 20\~24시)와 요일을 지정할 수 있습니다.
* **카카오 알림톡 바로 연결 버튼**\
  알림톡 템플릿에 '바로 연결' 타입 버튼을 추가할 수 있습니다.
  {% endupdate %}

{% update date="2025-11-05" tags="CRM,카카오 메시지,푸시 메시지,SDK,crm,analytics" %}

## 📊 CRM 캠페인 보고서

![CRM 캠페인 보고서](/files/fklWg3hXvzZFtGtDVZ51)

푸시·카카오·문자 등 채널별 캠페인의 성과와 기여도를 한 화면에서 확인할 수 있는 보고서가 추가되었습니다.\
채널 간 비교와 핵심 지표(전환율 등)를 통합 조회해 다음 캠페인 전략을 빠르게 설계할 수 있습니다.

**개선**

* **카카오 메시지 반복 스케줄**\
  매일·매주·매월 단위로 자동 발송 일정을 설정할 수 있습니다. '2주마다 화·목요일' 같은 복합 주기도 지원합니다.
* **자동 수집 이벤트 추가**\
  `$app_install`, `$app_update`, `$app_open`, `$app_background` 이벤트가 SDK 연동만으로 자동 수집됩니다.
* **푸시 메시지 URL 이미지 첨부**\
  기존 파일 업로드 외에 URL로도 이미지를 첨부할 수 있어 동적 이미지 활용이 가능합니다.
  {% endupdate %}

{% update date="2025-10-15" tags="카카오 메시지,문자 메시지,AI,사용자 여정,웹훅,인앱 메시지,kakao,sms" %}

## 💌 카카오톡 발송 실패 시 문자로 자동 대체 발송

![카카오톡 발송 실패 시 문자로 자동 대체 발송](/files/6roIsPazxkkaqw1hfhn3)

카카오 메시지 발송이 실패한 경우(미가입·채널 친구 아님·수신 거부 등) 문자 메시지(SMS/LMS/MMS)로 자동 대체 발송할 수 있습니다.\
캠페인 설정에서 '대체 발송 여부' 토글로 활성화합니다.

**🤖 카카오 메시지에도 AI 적용**

![카카오 메시지에도 AI 적용](/files/MnocTdoAIrQutOKkxG6D)

기존 푸시 메시지에서만 지원되던 AI 카피 자동 생성 기능이 카카오 메시지로 확장되었습니다.

{% hint style="info" %}
AI 메시지 생성 기능은 일부 고객사에 베타로 제공됩니다. 적용을 원하시면 <support@hackle.io>로 문의해 주세요.
{% endhint %}

**개선**

* **사용자 여정에 웹훅 채널 추가**\
  사용자 여정에서 사내·외부 시스템과 연동할 수 있어, 메시지 발송 전 재고 확인이나 데이터베이스 갱신 등 후속 작업을 자동화할 수 있습니다.
* **데스크톱 인앱 메시지 사이즈 조정**\
  다양한 데스크톱 해상도에서 안정적으로 노출되도록 사이즈를 조정했습니다(비율은 동일).
  {% endupdate %}

{% update date="2025-09-03" tags="문자 메시지,카카오 메시지,푸시 메시지,데이터 분석,kakao,sms,ab" %}

## 💌 문자 메시지 직접 발송

![문자 메시지 직접 발송](/files/K4OAAbnHRJbNs0wZPobC)

핵클 대시보드에서 SMS/LMS를 직접 발송할 수 있습니다.\
앱 설치·푸시 토큰 보유 여부와 무관하게 모든 사용자에게 도달할 수 있으며, 행동 데이터 기반 타겟팅으로 발송 대상을 정밀하게 추릴 수 있습니다.

**🎯 카카오 메시지 A/B 테스트**

![카카오 메시지 A/B 테스트](/files/vlkCJqieatf1IMgOgtip)

카카오 비즈 메시지에서도 A/B 테스트를 진행할 수 있습니다.\
레이아웃 차이, 쿠폰 포함 여부, 콘텐츠 순서 등 다양한 변수를 비교 검증할 수 있습니다.

**신규 출시**

* **A/B 테스트 전사 영향도(Global Impact)**\
  일부 그룹에서 측정된 실험 결과를 전체 사용자에 적용했을 때의 영향(전환 수 증가량 등)을 수치로 확인할 수 있습니다.
* **Array 타입 속성값 지원**\
  여러 값을 하나의 속성으로 묶어 다룰 수 있습니다.
* **API 기반 카카오 메시지 발송**\
  스케줄 기반·이벤트 기반에 더해 API로 즉시 발송을 트리거할 수 있습니다.

**개선**

* **푸시 메시지 — Android 알림 채널 지원**\
  알림 채널을 추가해 앱 알림을 중요도·카테고리별로 구분 발송할 수 있습니다.
* **푸시 메시지 — 이미지 첨부 (iOS & Android)**\
  푸시 메시지에 이미지를 추가해 시각적 임팩트를 줄 수 있습니다.
  {% endupdate %}

{% update date="2025-08-05" tags="푸시 메시지,CRM,데이터 분석,사용자 여정,push-message,crm" %}

## ✅ 푸시 메시지 발송 체크리스트

![푸시 메시지 발송 체크리스트](/files/j7QE4HI86NxwfzjDZ0RQ)

푸시 메시지 발송 전 필수 점검 항목을 한 화면에서 확인할 수 있습니다.\
테스트 발송 결과(수신 가능 인원, 클릭 이벤트 수집), 링크 이동 경로, 시작 이벤트 수집 여부, 개인화 변수 적용 여부를 발송 직전에 검증할 수 있습니다.

**💌 CRM 수신 동의 상태 통합 관리**

![CRM 수신 동의 상태 통합 관리](/files/lPgJpne0P1H2cEmyXyh8)

푸시·카카오 메시지를 광고성/정보성 목적별로 수신 동의를 구분 설정하고, 발송 대상을 수신 동의 상태별로 조정할 수 있습니다.

* `SUBSCRIPTION`\
  명시적으로 수신 동의한 사용자만
* `UNKNOWN + SUBSCRIPTION`\
  수신 가능한 사용자 모두
* `UNKNOWN + SUBSCRIPTION + UNSUBSCRIPTION`\
  모든 사용자

**개선**

* **퍼널 차트 속성으로 나눠보기**\
  '전환율 추이' 차트를 캠페인·채널·디바이스 등 속성 기준으로 분할 비교할 수 있습니다.
* **사용자 여정 연산자 확장**\
  진입 조건·분기 설정에 '존재 여부' 연산자와 'OR' 조건을 사용할 수 있습니다.
* **메시지 리스트 화면 개선**\
  푸시·카카오·웹훅 리스트에서 발송 대상과 발송 유형을 한눈에 확인할 수 있습니다.
  {% endupdate %}

{% update date="2025-07-08" tags="카카오 메시지,사용자 관리,데이터 분석,문자 메시지,kakao,analytics" %}

## 💌 카카오 브랜드 메시지 정식 출시

![카카오 브랜드 메시지 정식 출시](/files/H1q4ei0JKGvYWiuvcEpd)

카카오톡 채널 친구가 아니더라도 카카오 수신 동의가 있는 사용자에게 메시지를 발송할 수 있습니다.\
친구톡 대비 도달 가능한 타겟이 넓어졌으며, 행동 데이터 기반 개인화 메시지를 함께 활용할 수 있습니다.

**👀 사용자 활동 타임라인**

![사용자 활동 타임라인](/files/g5rKe2RISH6XOjYMuvDU)

특정 사용자가 언제 어떤 행동을 했는지 시간순으로 확인할 수 있습니다. '사용자 조회' 탭에서 사용자 식별자로 검색하면 좌측에 사용자 속성, 우측에 발생 이벤트 타임라인이 표시됩니다.

**개선**

* **친구톡 캐러셀 — 피드형 & 커머스형 추가**\
  여러 상품·콘텐츠를 한 메시지에 묶어 보낼 수 있는 캐러셀 형식이 추가되었습니다.
* **퍼널 스텝 30개로 확장**\
  기존 10개에서 최대 30개까지 지원하며, 퍼널 복사 기능으로 유사 퍼널을 빠르게 구성할 수 있습니다.
  {% endupdate %}

{% update date="2025-06-09" tags="카카오 메시지,사용자 여정,외부 연동,kakao" %}

## 💌 카카오 알림톡 발송

![카카오 알림톡 발송](/files/tKUWhBvj5VyQH29wTpzN)

정보 전달 중심의 카카오 알림톡을 핵클에서 발송할 수 있습니다.\
행동 데이터 기반 여정 설계, 메시지 발송, 성과 측정까지 단일 플랫폼에서 처리할 수 있습니다.

**🤝 무료 딥링크 서비스 Supalink 인수**

![무료 딥링크 서비스 Supalink 인수](/files/x9aIQoZYCB89LDAG1kUY)

Firebase Dynamic Links 종료(2025년 8월 25일)에 대비해 핵클이 딥링킹 서비스 **Supalink**를 인수했습니다.\
Supalink는 무료로 제공되며, 종료 전 마이그레이션을 시작할 수 있습니다.

**개선**

* **사용자 여정에 카카오 메시지 추가**\
  사용자 여정 캠페인에서 카카오 알림톡/친구톡을 푸시와 함께 통합 운영할 수 있습니다.
* **MAB 테스트 지표 고도화**\
  최적화 지표 유형이 추가되었고, 보조 지표를 함께 설정해 다각도 분석이 가능합니다.
* **친구톡 쿠폰 버튼**\
  친구톡 메시지에 쿠폰 버튼을 추가할 수 있습니다.
  {% endupdate %}

{% update date="2025-04-08" tags="사용자 여정,카카오 메시지,외부 연동,푸시 메시지,journey,kakao" %}

## 🔁 사용자 여정 (Customer Journey)

![사용자 여정 (Customer Journey)](/files/LU55p0ab3gNhXrQD2aV8)

사용자의 행동 단계에 따라 여러 캠페인을 연결한 시나리오를 드래그 앤 드롭으로 설계할 수 있습니다.\
특정 조건의 고객에게 자동 맞춤 메시지를 발송하고, 시간 지연·분기 등을 시각적으로 구성할 수 있습니다.

**💌 카카오 친구톡 발송**

![카카오 친구톡 발송](/files/nbn1vbVBCdfmmhtHKF6h)

핵클에서 카카오 친구톡을 직접 발송할 수 있습니다.\
행동 데이터에 기반한 개인화 친구톡으로 채널 친구 대상 캠페인을 운영할 수 있습니다.

**신규 출시**

* **카페24 스토어앱 출시**\
  카페24 운영자는 스토어앱에서 핵클을 설치해 CRM 마케팅 기능을 사용할 수 있습니다.

**개선**

* **푸시 메시지 임시 저장 / 발송 중 중단**\
  발송 전 임시 저장과 발송 중 중단이 가능합니다.
* **지표 설정 속성값 복사·붙여넣기**\
  대량 속성값도 복사·붙여넣기로 입력할 수 있습니다.
  {% endupdate %}

{% update date="2025-03-06" tags="푸시 메시지,인앱 메시지,SDK,push-message,in-app-message,ab" %}

## 🎯 푸시 메시지 A/B 테스트 대조군 설정

![푸시 메시지 A/B 테스트 대조군 설정](/files/YQJtA6bubJvoVcmZtsWP)

A/B 테스트에 '대조군(Control Group)'이 추가되었습니다.\
메시지를 받지 않는 그룹과 발송 그룹을 비교해 메시지의 실질적 효과를 측정할 수 있습니다.

* **대조군 (Group A)**\
  푸시 메시지가 발송되지 않음 (기존 환경 유지)
* **실험군 (Group B, C, D…)**\
  새로운 푸시 메시지 발송

**🎯 인앱 메시지 이벤트 조건 타겟팅**

![인앱 메시지 이벤트 조건 타겟팅](/files/PJNFu9gjdyECaKKnBOB7)

특정 이벤트를 발생시킨 사용자에게 인앱 메시지를 노출할 수 있습니다.\
실시간 이벤트 기반 타겟팅으로 적시에 맞춤 메시지를 전달할 수 있습니다.

{% hint style="warning" %}
이벤트 조건 타겟팅 적용 시 추가 과금이 발생할 수 있습니다. 자세한 내용은 <support@hackle.io>로 문의해 주세요.
{% endhint %}

**개선**

* **Flutter SDK 웹앱 연동 지원**\
  Flutter SDK 2.13.0 이상 + JavaScript SDK 11.24.1 이상에서 웹앱 연동이 가능합니다.
  {% endupdate %}

{% update date="2025-02-04" tags="웹훅,데이터 분석,SDK,webhook" %}

## 💌 웹훅 발송 유형에 '이벤트 기반' 추가

![웹훅 발송 유형에 '이벤트 기반' 추가](/files/SrseC2P0BmLw9mgwTYWr)

고객 행동 이벤트를 트리거로 웹훅을 발송할 수 있습니다.\
활용 예시:

* 장바구니 담기 후 미구매 사용자에게 할인 쿠폰 발송
* 푸시 알림함 기능을 자체 서버 연동으로 구현
* 카카오 알림톡 발송 자동화

**개선**

* **데이터 분석 속성 필터**\
  콤마(,)로 구분해 여러 속성값을 한 번에 입력·선택할 수 있습니다.
* **이벤트 변경 내역 확인**\
  \[이벤트 관리] → 이벤트 → '변경 내역'에서 최근 변경 이력을 확인할 수 있습니다.
* **SDK 타겟팅 사용성 개선**\
  타겟팅 규칙을 문자열로 선택하고 불리언 값을 입력해 체크할 수 있습니다.
  {% endupdate %}

{% update date="2025-01-06" tags="푸시 메시지,웹훅,사용자 관리,인앱 메시지,데이터 분석,push-message,ab" %}

## 🎯 푸시 메시지 A/B 테스트

![푸시 메시지 A/B 테스트](/files/rRekDH2cw0b1CzT7ezco)

핵클 대시보드에서 푸시 메시지 A/B 테스트를 설정할 수 있습니다.\
여러 메시지 안을 동시에 발송해 성과가 가장 높은 안을 데이터로 검증할 수 있습니다.

**💌 웹훅으로 크로스 채널 연동**

![웹훅으로 크로스 채널 연동](/files/fXM8I3BxRPrg4JE9WkxN)

웹훅을 통해 알림톡·SMS 등 다양한 채널로 메시지를 보내거나, 자체 서버와 연동해 쿠폰 발급 등의 작업을 자동화할 수 있습니다.\
채널별 캠페인 일정·빈도 관리와 통합 성과 분석을 지원합니다.

**신규 출시**

* **사용자 조회 메뉴**\
  대시보드 사이드바의 '사용자 조회'에서 사용자 속성(플랫폼, OS, 디바이스, 타임존 등)과 푸시 토큰 정보를 한눈에 확인할 수 있습니다.
* **인앱 메시지 캐러셀 자동 전환 템플릿**\
  최대 10개 이미지를 등록해 지정한 시간 간격으로 자동 전환되는 인앱 메시지를 사용할 수 있습니다.

**개선**

* **데이터 분석/이벤트 조회 정규표현식 연산자**\
  정규식 기반 패턴 매칭이 가능합니다.
* **푸시 메시지 A/B 테스트 핵심 지표 직접 선택**\
  클릭률 외에 핵심 지표를 직접 지정할 수 있습니다.
* **코호트 이벤트 행동 조건 확장**\
  '속성 값 (Distinct)'과 '주기 (Count)' 조건이 추가되었습니다.
  {% endupdate %}
  {% endupdates %}


# 핵클 서비스

![](/files/ZqZcsmtI4bJXGTWyJaOY)

{% hint style="info" %}
궁금한 사항이 있을까요?

[핵클 슬랙 커뮤니티 ](https://join.slack.com/t/hackle-community/shared_invite/zt-1awrnygsh-U8VCHwN06ZDTF9yAzik5SA)에 가입하여 핵클의 프로덕트 매니저, 데이터 엔지니어 및 개발자와 함께 대화해보세요.

서비스 및 가격 관련 문의 사항은 <support@hackle.io>으로 해주세요.
{% endhint %}

<table data-view="cards"><thead><tr><th></th><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th><th data-hidden data-card-cover data-type="image">Cover image</th></tr></thead><tbody><tr><td><i class="fa-layer-group">:layer-group:</i></td><td><strong>A/B 테스트</strong></td><td>사용자를 여러 실험 그룹으로 나눠 테스트를 하고, 데이터를 기반으로 결정하세요</td><td><a href="/pages/npFJTKh6DlqN6s3XMHhU">/pages/npFJTKh6DlqN6s3XMHhU</a></td><td></td></tr><tr><td><i class="fa-building-flag">:building-flag:</i></td><td><strong>기능 플래그</strong></td><td>기능의 on/off를 배포없이 제어하고, 사용자 타겟팅으로 특정 사용자에게만 공개해보세요</td><td><a href="/pages/LecukLwsw3iph8JoxzJb">/pages/LecukLwsw3iph8JoxzJb</a></td><td></td></tr><tr><td><i class="fa-rotate">:rotate:</i></td><td><strong>원격 구성</strong></td><td>복잡한 설정값을 서비스를 배포 없이 대시보드에서 직접 변경하고 즉시 반영해보세요</td><td><a href="/pages/PLEHmUvXlrtuRB547Nrp">/pages/PLEHmUvXlrtuRB547Nrp</a></td><td></td></tr><tr><td><i class="fa-database">:database:</i></td><td><strong>데이터 분석 및 인사이트</strong></td><td>복잡한 쿼리 없이 사용자 행동<br>데이터를 시각화 하고 분석해<br>빠르게 의사결정 해보세요</td><td><a href="/pages/brL1T4FyJwsFAHDXMd2f">/pages/brL1T4FyJwsFAHDXMd2f</a></td><td></td></tr></tbody></table>

<table data-view="cards"><thead><tr><th></th><th></th><th></th><th data-hidden data-card-cover data-type="image">Cover image</th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><i class="fa-plane-departure">:plane-departure:</i></td><td><strong>사용자 여정</strong></td><td>고객의 유입부터 전환까지 전체 흐름을 시각적으로 설계해보세요</td><td></td><td><a href="/pages/FcrPneEfi15zDP83Mptl">/pages/FcrPneEfi15zDP83Mptl</a></td></tr><tr><td><i class="fa-inbox">:inbox:</i></td><td><strong>인앱 메시지</strong></td><td>실시간으로 팝업을 띄워 즉각적인 참여를 이끌어내보세요</td><td></td><td><a href="/pages/JnOIBvhfse6dKWRhqX78">/pages/JnOIBvhfse6dKWRhqX78</a></td></tr><tr><td><i class="fa-mobile-vibrate">:mobile-vibrate:</i></td><td>푸시 메시지</td><td>개인화된 문구와 이미지를 활용해 이탈 고객의 재방문을 유도하세요</td><td></td><td><a href="/pages/taji3Vq1TKBuQARMsubx">/pages/taji3Vq1TKBuQARMsubx</a></td></tr><tr><td><i class="fa-kakao-talk">:kakao-talk:</i></td><td><strong>카카오 메시지</strong><br><strong>(알림톡, 브랜드메시지)</strong></td><td>핵클의 타겟팅을 활용해 카카오톡 유저에게 메시지를 발송하세요</td><td></td><td><a href="/pages/GOVgPYiufqjnjzCUqkLE">/pages/GOVgPYiufqjnjzCUqkLE</a></td></tr><tr><td><i class="fa-webhook">:webhook:</i></td><td><strong>웹훅</strong></td><td>핵클 시스템과 비지니스 로직을 연동하여 확장성 있는 마케팅<br>시스템을 구축하세요</td><td></td><td><a href="/pages/ncpOaLMoWPK8dDUnGjyI">/pages/ncpOaLMoWPK8dDUnGjyI</a></td></tr><tr><td><i class="fa-message-sms">:message-sms:</i></td><td><strong>문자 메시지</strong></td><td>앱 미설치 고객을 포함해 모든 고객을 포괄하는 도달률 높은 채널로 활용됩니다.</td><td></td><td><a href="/pages/lBfCdyULVJQA3u1Mjb33">/pages/lBfCdyULVJQA3u1Mjb33</a></td></tr><tr><td><i class="fa-chart-simple-horizontal">:chart-simple-horizontal:</i></td><td><strong>캠페인 보고서</strong></td><td>다양한 CRM 캠페인이 실제 비즈니스에 얼마나 기여했는지<br>분석해보세요.</td><td></td><td><a href="/pages/IZW4mSvnxGsEl0aRcHZn">/pages/IZW4mSvnxGsEl0aRcHZn</a></td></tr><tr><td><i class="fa-square-check">:square-check:</i></td><td><strong>CRM 속성 관리</strong></td><td>이용자의 마케팅 수신동의, 전화번호 수집을 손쉽게 관리할 수 있습니다.</td><td></td><td><a href="/pages/Ny7PqNezk7zxVrZO9i8d">/pages/Ny7PqNezk7zxVrZO9i8d</a></td></tr></tbody></table>

### 비디오 가이드

문서보다 보는 영상을 선호하시나요? 아래의 서비스 가이드 영상을 확인해보세요.

{% embed url="<https://www.youtube.com/watch?v=ZEYyyXqXNgg>" %}


# 사용자

핵클을 활용하여 더 효과적인 마케팅 액션을 수행하기 위해 사용자를 어떻게 정의하고 관리하는지 이해하는 것은\
매우 중요합니다.

이 가이드는 마케터가 알아야 할 핵클의 사용자 개념과 활용 방안에 대한 간단한 가이드를 제공합니다.

### 사용자 식별

핵클은 사용자를 고유한 식별자를 기준으로 인식하고 행동을 추적합니다.\
이를 통해 한 명의 사용자가 어떤 행동 흐름을 보이는지 정확하게 파악할 수 있습니다.

사용자 식별자에 대한 자세한 설명은 [사용자 식별자](/getting-started/user-identifier)를 참고해주세요.

### 사용자 속성

사용자 속성은 각 사용자가 가진 구체적인 정보입니다.\
이 속성을 활용하여 고객을 더욱 정밀하게 분류하고, 개인화된 메시지를 전달할 수 있습니다.

사용자 속성은 특정 시점에 한 번 만 저장되거나(예: 최초 가입일), 지속적으로 업데이트될 수 있습니다(예: 멤버십 등급).

이 정보를 통해 "30대 여성이면서 VIP 등급인 사용자"와 같은 구체적인 타겟 그룹 생성이 가능합니다.

**사용자 속성 예시**

<table><thead><tr><th width="170.3671875">구분</th><th>예시</th></tr></thead><tbody><tr><td>인구 통계 정보</td><td>연령, 성별, 지역 등</td></tr><tr><td>멤버십 정보</td><td>등급(예: VIP, Gold), 멤버십 가입일</td></tr><tr><td>서비스 이용 정보</td><td>월 평균 구매 횟수, 최근 접속일, 최초 가입일</td></tr></tbody></table>

### 사용자 그룹 (코호트)

사용자 식별자와 속성 그리고 이벤트를 기반으로, 특정 조건을 충족하는 코호트를 만들 수 있습니다.\
코호트는 마케팅 캠페인의 핵심 타겟이 됩니다.

생성된 이탈 사용자 코호트를 대상으로 재방문을 유도하는 CRM 마케팅을 진행하거나, 핵심 고객에게는 특별 혜택을 제공하는 등 맞춤형 캠페인을 설계하고 실행할 수 있습니다.

코호트에 대한 자세한 설명은 [코호트 소개](/cohort/cohort)를 참고해주세요.

**코호트 생성 예시**

<table><thead><tr><th width="170.14453125">구분</th><th>예시</th></tr></thead><tbody><tr><td>이탈 사용자</td><td>특정 기간(예: 최근 14일) 동안 접속하지 않은 사용자 그룹</td></tr><tr><td>핵심 고객</td><td>특정 상품을 3회 이상 구매한 사용자 그룹</td></tr><tr><td>잠재 고객</td><td>무료 체험 종료 후 아직 유료로 전환하지 않은 사용자 그룹</td></tr></tbody></table>


# 사용자 식별자

각각의 개별 사용자를 식별하는 용도로 사용하는 값으로, 매우 중요한 값입니다.

핵클에서 제공하는 기능은 행동을 기반으로 합니다.\
따라서 사용자 행동을 정확히 분석하기 위해서는 **사용자**를 식별하는 기준이 명확해야 합니다.

특정 사용자가 누군지 명확히 식별할 수 있어야 해당 사용자의 구매, 검색, 상품 조회 등의 이력을 바탕으로 사용자당 구매 금액, 검색 전환율, 상품 페이지 조회 수와 같은 목표를 정확하게 계산할 수 있습니다.

{% hint style="info" %}
식별자는 중복이 없는 유일한 값(unique value)이어야 합니다.\
예를 들어 회원 번호, 사용자 기기 번호와 같이 사용자를 특정할 수 있는 값을 의미합니다.
{% endhint %}

### 기본 사용자 식별자

핵클에 다양한 사용자 식별자를 전송할 수 있지만 가장 기본적인 식별자는 User ID와 Device ID입니다.

<table><thead><tr><th width="146.5703125">식별자</th><th>설명</th></tr></thead><tbody><tr><td><strong>DeviceID</strong></td><td>사용자가 서비스를 사용할 때 쓰게 되는 핸드폰, PC, 태블릿과 같은 기기의 고유 식별자를 의미합니다.<br>UserID 설정에 관계없이 동일하게 유지됩니다.</td></tr><tr><td><strong>HackleDeviceId</strong></td><td>별도로 Device ID를 설정하지 않았다면 이 값으로 DeviceID가 세팅됩니다.</td></tr><tr><td><strong>UserID</strong></td><td>서비스 내의 사용자를 고유하게 식별할 수 있는 회원 번호를 의미합니다.<br>사용자가 로그인 혹은 회원가입 후 생성된 ID 값을 설정하는 것을 권장합니다.</td></tr><tr><td><strong>HackleID</strong></td><td><a href="/pages/bsthCgxvKp2Buvm98za0">핵클에서 제공하는 통합 식별자</a> 입니다.<br>User ID, Device ID가 전송되면 두 정보를 조합한 '핵클 통합 식별자'가 생성되어 더 정확하게 동일한 사용자를 식별할 수 있습니다.</td></tr><tr><td>SessionID</td><td>사용자의 연속된 행동동안 유지되는 <a href="/pages/qOxty6cGNEB0FuabMXDo">세션 ID</a> 입니다.</td></tr><tr><td><strong>ID</strong><br>(deprecated)</td><td>과거에 기본 식별자로 사용되던 식별자입니다.<br>Device ID 혹은 User ID 중에 프론트/서버에서 같은 값으로 전송할 수 있는 값을 보내주시면 됩니다.<br>프론트 SDK의 경우 별도 설정을 하지않으면 Device ID와 동일한 값이 그대로 ID로 전송됩니다. 즉 ID, Device ID, Hackle Device ID가 모두 같은 값을 가지게 됩니다.</td></tr></tbody></table>

### 추가로 보낼 수 있는 사용자 식별자

기본적으로 사용하는 사용자 식별자 외에 'Custom 유형'의 식별자를 추가로 전송할 수 있습니다.

#### Custom 유형

서비스에 맞게 직접 정의하신 사용자 식별자를 의미합니다.\
예를 들어, 커머스라면 Session ID, 주문 번호 등을 사용하실 수 있습니다. 단, Custom 유형은 핵클 통합 식별자를 사용할 수 없습니다.

{% hint style="danger" %}
광고 ID (GAID, IDFA) 사용 시 유의사항

해당 ID를 수집할 때에는 Google 및 Apple의 정책을 반드시 따라야 합니다. 각 사의 정책에 대해서는 아래 링크를 참고하시기 바랍니다.

* [Google 광고 ID 사용 시 유의사항](https://developer.android.com/training/articles/user-data-ids?hl=ko#advertising-ids)
* [Apple의 사용자 개인정보 보호 및 데이터 사용 안내](https://developer.apple.com/kr/app-store/user-privacy-and-data-use/)
  {% endhint %}


# 핵클 통합 식별자

사용자 데이터 분석은 고유한 사용자를 정확하게 식별하는게 중요합니다.\
서비스의 특성에 따라 사용자는 로그인 및 로그아웃 상태에서 서비스를 사용할 수도 있고 한명의 사용자가 여러 장치를 사용할 수 있습니다.

고유한 사용자를 정확하게 식별하기 위해 핵클은 User ID, Device ID, 핵클 통합 ID의 조합을 사용합니다.

이 문서에서는 핵클 통합 ID가 작동하는 방식에 대해 설명합니다.

### 핵클 통합 식별자

핵클 통합 식별자를 할당할때 발생할 수 있는 상황을 살펴보겠습니다.\
각 항목에서 User ID, Device ID를 사용하여 핵클 통합 ID를 생성하거나 재사용하는 방법에 대한 설명합니다.

#### 로그인하지 않은 사용자만 있는 경우

User ID가 없는경우(로그인하지 않은경우) 새로운 Device ID마다 새로운 핵클 통합 ID를 생성하여 할당합니다.

| Device ID | User ID | 핵클 통합 ID |
| --------- | ------- | -------- |
| A         | null    | 1        |
| B         | null    | 2        |
| A         | null    | 1        |
| A         | null    | 1        |
| C         | null    | 3        |
| B         | null    | 2        |
| C         | null    | 3        |

* 첫번째 이벤트는 A 기기에서 발생했고 이 기기에 대한 내역이 없었기 때문에 새로운 핵클 통합 ID 1을 할당합니다
* 두번째 이벤트는 B 기기에서 발생했고 이 디바이스에 대한 내역도 없어서 핵클 통합 ID 2를 할당합니다
* 세번째 이벤트는 A 기기에서 발생했고 A 기기에 이미 할당된 통합 ID 1이 할당됩니다.

#### 로그인하지 않은 상태로 활동하다 로그인 하는 경우

User ID가 없는 경우 이전에 같은 Device ID에서 발생한 User ID가 없는 이벤트는 같은 사용자라고 가정합니다. 이 경우 같은 핵클 통합 ID가 할당됩니다.

| Device ID | User ID | 핵클 통합 ID |
| --------- | ------- | -------- |
| D         | null    | 4        |
| D         | null    | 4        |
| D         | Jay     | 4        |

* 처음 두 이벤트는 D 기기에서 User ID없이 발생했고 통합 ID 4가 할당되었습니다.
* 세번째 이벤트는 D 기기에서 User ID가 같이 전송된 첫 번째 이벤트입니다. 해당 User ID에 이전 이벤트와 동일한 핵클 통합 ID를 할당합니다. 이 경우 세 이벤트 모두 Jay가 발생시켰다고 기록됩니다.

#### 로그인사용자가 여러 디바이스를 사용하는 경우

핵클 통합 ID를 할당 할 때 Device ID보다 Uesr ID가 우선순위가 높습니다.

| Device ID | User ID | 핵클 통합 ID |
| --------- | ------- | -------- |
| E         | Lyle    | 5        |
| F         | Lyle    | 5        |
| G         | Lyle    | 5        |

* E 기기에서 Lyle이 이벤트를 발생시켰고 핵클 통합 ID 5가 할당되었습니다.
* User ID가 Lyle인 경우 Device ID와 관계 없이 핵클 통합 ID 5가 할당됩니다.

#### 하나의 기기를 여러 사용자가 사용하는 경우

Device ID와 User ID가 같은 핵클 통합 ID로 할당된 이후 같은 기기에서 User ID가 없는 이벤트가 발생하면 마지막 사용자에 의해 이벤트가 발생되었다고 가정하고 해당 사용자의 핵클 통합 ID를 할당합니다.

| Device ID | User ID | 핵클 통합 ID |
| --------- | ------- | -------- |
| H         | Mark    | 6        |
| H         | null    | 6        |
| H         | Alley   | 7        |
| H         | null    | 7        |
| H         | null    | 7        |

* H 기기에서 Mark가 이벤트를 발생시키고 핵클 통합 ID 6이 할당됩니다.
* 다음 이벤트는 User ID가 없이 전송되었지만 해당 기기에서 Mark가 마지막 사용자였기 때문에 핵클 통합 ID 6이 할당 됩니다.
* 세 번째 이벤트는 Alley가 H 기기에서 발생했고 Alley에 해당하는 핵클 통합 ID 7이 할당됩니다.
* 다음 두 이벤트는 User ID가 없지만 H 기기의 마지막 사용자인 Alley에 의해 발생되었다고 가정합니다.

#### 한명의 사용자가 기기를 변경하고 로그인 하는 경우

| Device ID | User ID | 핵클 통합 ID |
| --------- | ------- | -------- |
| Y         | Jamie   | 8        |
| Z         | null    | 9        |
| Z         | Jamie   | 8        |
| Z         | null    | 8        |

* Jamie가 Y 기기에서 이벤트를 발생시키고 핵클 통합 ID 8이 할당됩니다.
* Z 기기에서 User ID가 없는 이벤트가 발생됐고 핵클 통합 ID 9가 할당됩니다.
* Jamie가 Z 기기에서 로그인후 User ID가 포함된 이벤트가 발생됐고 Jamie의 핵클 통합 ID인 8이 할당 됩니다.
* 이후 Z 기기에서 User ID 없이 이벤트가 발생되고 Jamie가 마지막 사용자여서 핵클 통합 ID 8이 할당 됩니다.


# 세션 관리

세션(Session)은 사용자의 연속된 앱 사용 구간을 의미하며, 특정 조건에 따라 시작되고 만료됩니다.

### 세션 시작 조건

세션은 기본적으로 아래 조건으로 시작됩니다.

* 식별자가 변경된 경우
* 세션 만료 시간이 지난 후 이벤트가 발생한 경우

### 세션 만료 조건

새로운 세션이 시작될 때 이전 세션은 만료됩니다.

* 세션 만료 시각은 마지막 이벤트 발생 시간과 동일합니다.

### 세션 정책 (Session Policy)

세션 정책은 세션의 유지 조건과 만료 조건을 세밀하게 제어할 수 있는 기능입니다. 세션 정책은 두 가지 조건으로 구성됩니다.

#### 세션 유지 조건 (Persist Condition)

식별자가 변경될 때 기존 세션을 유지할지, 새로운 세션을 시작할지 결정합니다.

<table><thead><tr><th width="186.40234375">구분</th><th width="404.59765625">설명</th><th width="150.9765625">기본값</th></tr></thead><tbody><tr><td>alwaysNewSession</td><td>식별자가 변경되면 항상 새로운 세션을 시작합니다.</td><td>default</td></tr><tr><td>nullToUserId</td><td>userId가 null에서 특정 값으로 변경될 때 (로그인 시) 기존 세션을 유지합니다. <br>그 외의 식별자 변경 시 새로운 세션을 시작합니다.</td><td>-</td></tr></tbody></table>

#### 세션 만료 조건 (Timeout Condition)

세션이 만료되는 조건을 설정합니다.

<table><thead><tr><th width="240.171875">설정</th><th width="370.02734375">설명</th><th>기본값</th></tr></thead><tbody><tr><td>timeout</td><td>세션 만료 시간입니다.</td><td>1800초 (30분)</td></tr><tr><td>onForeground</td><td>foreground에서 이벤트 발생 시 타임아웃 체크 여부입니다.</td><td>false</td></tr><tr><td>onBackground</td><td>background에서 이벤트 발생 시 타임아웃 체크 여부입니다.</td><td>true</td></tr><tr><td>onApplicationStateChange</td><td>앱 상태 변경 (<code>foreground &#x3C;-> background</code>) 시 타임아웃을 체크합니다.</td><td>true</td></tr></tbody></table>

{% hint style="info" %}
세션 정책의 구체적인 설정 코드는 각 SDK의 SDK 연동 문서를 참고하세요.
{% endhint %}


# 이벤트

정확한 데이터 분석과 성공적인 A/B 테스트를 위해서는 사용자의 행동 데이터, 즉 이벤트를 체계적으로 수집하고 관리해야 합니다.

이 가이드는 핵클에 최적화된 이벤트 설계 및 관리 가이드를 제공합니다.

### 이벤트

이벤트란 사용자가 서비스 내에서 발생시키는 모든 행동과 사건을 의미합니다. 핵클을 이용하면 다양한 이벤트를 수집하고, 이 이벤트를 기반으로 모든 데이터 분석과 실험의 성과를 측정합니다.

**주요 이벤트 예시**

<table><thead><tr><th width="199.52734375">구분</th><th>예시</th></tr></thead><tbody><tr><td><strong>행동 기반</strong></td><td>회원가입 완료, 검색 버튼 클릭, 상품 상세 페이지 진입, 구매 완료 등</td></tr><tr><td><strong>시스템 기반</strong></td><td>서버 응답 시간 등</td></tr></tbody></table>

위와 같은 이벤트 정보를 바탕으로 핵클에서 아래와 같이 활용 할 수 있습니다.

<table><thead><tr><th width="199.9296875">핵클 서비스</th><th>활용 방안 예시</th></tr></thead><tbody><tr><td><strong>A/B 테스트</strong></td><td>'구매 전환율'과 같은 핵심 지표(Metric) 계산</td></tr><tr><td><strong>데이터 분석</strong></td><td>특정 이벤트 발생 횟수, 빈도 등 사용자 행동 패턴 분석</td></tr><tr><td><strong>퍼널 분석</strong></td><td>사용자가 특정 목표(예: 구매)까지 도달하는 과정에서 이탈하는 지점 파악</td></tr></tbody></table>

{% hint style="info" %}
핵클을 통해 이벤트를 생성하고 관리하는 방법은 [이벤트 관리](/event-management/event-management)를 참고해주세요.
{% endhint %}

### 텍소노미

이벤트들을 특정 규칙에 따라 분류하는 작업을 택소노미(Taxonomy)라고 합니다.\
잘 설계된 택소노미는 조직 내 누구나 데이터를 명확하게 이해하고 활용할 수 있게 하여, 데이터 기반의 의사결정을 가속화합니다.

데이터 기반으로 효과적이고 효율적인 의사결정을 하기 위해서는 원하는 데이터를 정확하게 측정하고, 분석할 수 있어야 합니다.

이 과정의 첫 단계가 바로 이벤트를 정의하고 분류하는 것, 이벤트 택소노미 정의이기 때문에 이 작업은 매우 중요합니다.

#### 이벤트 네이밍 컨벤션

이벤트 네이밍 컨벤션이란 이벤트를 명확하게 식별하고, 분류하기 위해 조직 내 합의된 규칙을 말합니다. 이벤트를 잘 정의하고 분류하기 위해 네이밍 컨벤션을 정하는 것은 **매우 중요합니다**.

이벤트 네이밍 컨벤션을 잘 적립하여, 조직 내 커뮤니케이션을 능숙하게 진행해보세요!

**이벤트 네이밍 컨벤션 예**

이벤트를 통해 우리는 특정 사용자가 ‘어디서’ ‘무엇을 했는지’를 남기고 싶을 것 입니다.

<table><thead><tr><th width="185.46875">구분</th><th width="259.99609375">설명</th><th>예시</th></tr></thead><tbody><tr><td><strong>name</strong></td><td>'어디서' 일어났는지에 해당하는 <strong>위치</strong></td><td><code>home</code>, <code>gnb</code>, <code>login_page</code></td></tr><tr><td><strong>action</strong></td><td>'무엇을 했는지'에 해당하는 <strong>행동</strong></td><td><code>click</code>, <code>scroll</code>, <code>view</code>, <code>submit</code></td></tr><tr><td><strong>object</strong></td><td>행동의 대상이 되는 <strong>요소</strong></td><td><code>search_button</code>, <code>login_form</code>, <code>product_banner</code></td></tr></tbody></table>

위와 구분하면 아래와 같이 이벤트를 만들 수 있습니다.

<table><thead><tr><th width="185.0546875">텍소노미 구분</th><th width="259.80078125">예시</th><th>설명</th></tr></thead><tbody><tr><td><strong>action_name</strong></td><td><code>viewed_home</code></td><td>사용자가 <strong>홈(home) 화면</strong>에 <strong>진입(viewed)</strong>한 행동</td></tr><tr><td><strong>action_object_name</strong></td><td><code>clicked_search_home</code></td><td>사용자가 <strong>홈(home) 화면</strong>의 <strong>검색(search) 버튼</strong>을 <strong>클릭(clicked)</strong>한 행동</td></tr><tr><td><strong>action_object</strong></td><td><code>clicked_search</code></td><td>사용자가 <strong>검색(search) 버튼</strong>을 <strong>클릭(clicked)</strong>한 행동 (위치 무관)</td></tr></tbody></table>

gnb 영역처럼 공통 영역에서 발생하는 사용자 행동은 clicked\_search\_gnb와 같이 함께 남길 수 있습니다.\
gnb영역이 노출되는 개별 페이지를 별도로 남기고 싶다면, 공통 이벤트에 속성값으로 페이지를 남기는 방법을 추천합니다.

{% hint style="info" %}
[이벤트 택소노미 정의부터 실무 적용까지, A to Z 가이드 확인하기 👉](https://hackle.io/ko/post/event-taxonomy/?utm_source=blog)
{% endhint %}


# 알아두어야 할 용어

핵클에서 자주 사용하는 용어들에 대해 설명합니다.

{% hint style="info" %}
이 페이지의 용어는 핵클 대시보드 및 본 가이드 문서 전체에서 사용됩니다.
{% endhint %}

| 카테고리    | 용어                                                      |
| ------- | ------------------------------------------------------- |
| 일반      | 대시보드, 워크스페이스, 환경, 사용자 식별자, SDK, SDK 키                   |
| A/B 테스트 | 대조군/실험군, 목표, 테스트 그룹, 테스트 그룹 분배, 트래픽 할당, 수동할당, 타겟팅, 실험 키 |
| 데이터 분석  | 분석 기준, 분석 항목, 속성, 세션                                    |
| 기능 플래그  | 기능 키, 사용자 타겟팅, 개별 타겟팅                                   |
| 퍼널/이벤트  | 사용자 퍼널, 이벤트, 이벤트 키                                      |

### 대시보드 (Dashboard)

핵클 플랫폼 사용자를 위해 제공하는 공간으로, 하나의 워크스페이스를 제공하고 있습니다.

### 워크스페이스 (Workspace)

핵클 플랫폼을 사용하는 회사(또는 팀)가 공유하는 작업 공간을 의미합니다. 워크스페이스 안에서 A/B 테스트, 퍼널 및 이벤트를 생성하고 관리합니다. 같은 워크스페이스 안에서 A/B 테스트, 퍼널과 이벤트를 공유합니다.

일반적으로 하나의 회사가 하나의 워크스페이스를 사용합니다. 필요에 따라 하나의 팀이나 서비스 단위로 사용할 수도 있습니다.

### 환경 (Environment)

하나의 워크스페이스에 여러 개의 환경을 제공하고 있습니다. 예를 들어 실제 고객 대상으로 서비스하는 운영 환경(production)과 개발 및 QA 등을 위한 개발 환경(development) 등이 있습니다.

핵클 워크스페이스는 현재 두 가지 환경 - 운영 환경(production)과 개발 환경(development)을 제공하고 있습니다. 현재 선택된 환경은 파란색 글씨로 보입니다.

### 사용자 식별자

개별 사용자를 식별하는 용도로 사용하는 값으로, 중복값이 없는 유일값(unique value)이어야 합니다.

{% hint style="info" %}
보다 자세한 내용은 [사용자 식별자 관리하기](/getting-started/user-identifier) 문서를 참고하시기 바랍니다.
{% endhint %}

### SDK (Software Development Kit)

개발자가 쉽고 간편하게 핵클 플랫폼을 연동할 수 있도록 제공하는 개발 도구입니다. 사용하는 개발 언어에 맞게 선택하여 연동할 수 있습니다.

### SDK 키 (SDK Key)

SDK 연동 시 환경을 식별하기 위한 키입니다.

***

## A/B 테스트 (Experiment)

새로 출시하는 기능, 화면, 알고리즘 등을 현재 제공하는 기능, 화면, 알고리즘과 비교하여 어떤 것이 우수한지 확인할 수 있는 실험입니다. 실험 혹은 Experiment라는 용어 또한 A/B 테스트를 의미합니다.

### 대조군, 실험군

**대조군 (Control Group)**

현재 사용자에게 제공하는 기능, 화면 혹은 알고리즘을 의미합니다. A/B 테스트에서는 통상 테스트 그룹 A로 칭하게 됩니다.

**실험군 (Treatment Group)**

사용자에게 선보인 적 없는 기능, 화면 혹은 알고리즘을 의미합니다. 새로 출시할 혹은 변경할 내용으로 볼 수 있습니다. A/B 테스트에서는 통상 테스트 그룹 B, C, D 등을 의미합니다. (테스트 그룹 A를 제외한 나머지 그룹)

### 목표 (Goal)

목표는 실험군(그룹 B,C,D,...)인 경우에 대조군(그룹 A)인 경우보다 우수한 결과를 얻을 수 있는지 알아보기 위한 측정 기준(metric)입니다. 즉, A/B 테스트의 성과를 측정하기 위해 목표를 설정합니다. A/B 테스트마다 측정하고 싶은 목표를 하나 이상 자유롭게 생성할 수 있습니다.

<details>

<summary>예시: 구매하기 버튼 색상 변경 A/B 테스트의 목표</summary>

어떤 회사가 구매하기 버튼의 색상 변경을 검토하고 있고, 변경안이 효과가 있는지 A/B 테스트를 통해 알고 싶다면 목표는 다음과 같이 정할 수 있습니다.

* 구매하기 버튼 클릭수
* 구매 전환율

</details>

### 테스트 그룹 (Variation)

대조군, 실험군을 의미하며, 줄여서 **그룹**으로 칭합니다. A/B 테스트는 하나의 대조군(그룹 A)과 하나 이상의 실험군(그룹 B,C,D,...)을 갖습니다. 각 테스트 그룹은 A/B 테스트를 통해 비교하고자 하는 기능, 화면, 알고리즘 등을 포함합니다.

<details>

<summary>예시: 테스트 그룹 구성</summary>

* A/B 테스트 (구매하기 버튼 색상 변경)
  * 그룹 A : 기존 색상 (파란색)
  * 그룹 B : 변경 색상 (빨간색)
  * 그룹 C : 변경 색상 (초록색)
* A/B 테스트 (검색 알고리즘 변경)
  * 그룹 A : 기존 알고리즘
  * 그룹 B : 변경 알고리즘

</details>

핵클 A/B 테스트에서 테스트 그룹은 최대 10개(대조군 1개, 실험군 9개)까지 생성할 수 있습니다.

### 테스트 그룹 분배 (Variation Distribution)

A/B 테스트의 대상으로 선정된 사용자를 임의의 테스트 그룹에 배정합니다.

A/B 테스트의 조건이 변경되지 않는 한, 사용자는 항상 동일한 테스트 그룹에 할당됩니다. 즉, 구매하기 버튼의 색상을 실험하는 A/B 테스트에서 홍길동이라는 사용자가 그룹 B로 분배되었다면 홍길동은 계속 그룹 B에 속하게 됩니다.

어떤 테스트 그룹에 속하게 되는지는 일련의 분배 과정을 통해 결정됩니다.

{% hint style="info" %}
테스트 그룹 분배 원리에 대해서는 [테스트 그룹 분배 원리](/ab-test/group-distribution/ab-bucketing) 문서를 참고하시기 바랍니다.
{% endhint %}

### 트래픽 할당 (Traffic Allocation)

전체 사용자 중 A/B 테스트의 대상이 될 사용자의 비율입니다.

예를 들어 트래픽 할당 수준을 40%로 설정한다면 전체 사용자 중 40%만 A/B 테스트의 대상이 되고 나머지 60%는 A/B 테스트 대상에 포함되지 않습니다. A/B 테스트에 할당된 40% 사용자의 데이터만으로 A/B 테스트의 목표를 계산합니다.

![트래픽 할당 예시: 전체 사용자 중 40%만 A/B 테스트에 포함](/files/qLOfr3Q5oNO3srlzOU6l)

{% hint style="warning" %}
**트래픽 할당(Traffic Allocation)과 테스트 그룹 분배(Variation Distribution)는 다른 개념입니다.**

위의 예시에서 트래픽 할당 수준이 40%로 설정되었다면, 이 40%의 사용자를 대상으로 테스트 그룹 분배 비율에 맞게 각 테스트 그룹으로 사용자를 분배하게 됩니다.

<img src="/files/RlI70xuMN05WKdT7grUq" alt="트래픽 할당 후 테스트 그룹 분배 예시" data-size="original">
{% endhint %}

### 수동할당 (Override)

사용자 식별자 값을 사용해서 특정 사용자를 특정 테스트 그룹으로 강제 할당하는 기능입니다. 등록 대상이 된 사용자의 경우 타겟팅, 트래픽 할당, 테스트 그룹 분배 비율과 관계없이 지정된 테스트 그룹으로 할당됩니다.

개발 테스트, QA 등을 위해 특정 테스트 그룹에서의 동작을 확인할 때 사용합니다.

### 타겟팅 (Targeting)

특정 속성을 가진 사용자에 한정하여 A/B 테스트에 참여하도록 설정하는 기능입니다. 예를 들어 모바일 운영체제 중 안드로이드 사용자만을 대상으로 A/B 테스트에 참여하도록 설정할 수 있습니다. 단, 수동할당 대상이 된 사용자는 타겟 설정 대상이 될 수 없습니다.

### 실험 키 (Experiment Key)

개개의 A/B 테스트를 식별하기 위한 키입니다. 실험 키는 A/B 테스트를 생성할 때 자동으로 발급됩니다.

실험 키는 워크스페이스 내에서 고유한 값으로, 테스트 그룹 분배 과정에서 사용합니다.

***

## 데이터 세부 분석 (Data Segment Analytics)

사용자 객체에 속성을 포함하여 핵클에 전송한 경우, 속성을 기반으로 보다 상세한 분석을 할 수 있습니다. 예를 들어 '회원 등급' 이라는 속성을 보냈다면 사용자를 회원 등급에 따라 분류하여 데이터를 분석할 수 있습니다.

### 분석 기준

세부 분석할 대상을 의미하며, 동일한 성질을 가진 부류나 범위 등을 묶은 것으로 볼 수 있습니다. 예를 들어 "사용자가 사용하는 OS 별 분석을 진행한다"고 하면 분석 기준은 OS가 됩니다.

### 분석 항목

분석 기준이 가질 수 있는 값 중 분석을 위해 선택한 항목입니다. 예를 들어 분석 기준을 OS로 선택한 경우 분석 항목으로는 Android, iOS, Windows, MacOS 등을 넣을 수 있습니다.

### 속성

어떤 사용자가 가진 특징을 속성명(key)과 속성값(value)으로 표현하는 방법입니다. 예를 들어 운영체제를 속성으로 표현하고 싶은 경우, 속성명은 os가 될 수 있고, 속성값은 Android, iOS, Windows, MacOS 등이 포함될 수 있습니다.

### 세션 (Session)

세션이란 정해진 시간 내에 웹 또는 모바일 앱에서 사용자가 이벤트를 발생시키며 발생하는 상호작용의 집합을 의미합니다. 단일 세션에 페이지 방문, 버튼 클릭, 상품 구매 등 다양한 이벤트가 포함될 수 있습니다.

핵클에서 정의하는 세션 시간은 30분입니다. 즉, 가장 최근에 이벤트가 발생한 시점을 기준으로 30분이 경과했다면 해당 세션은 종료된 것으로 간주됩니다.

***

## 기능 플래그 (Feature Flag)

기능 플래그는 개발자가 코드를 배포하지 않고 원격으로 기능을 활성화하거나 비활성화할 수 있는 소프트웨어 개발 방법입니다. 코드 배포와 기능 출시를 분리할 때 유용하게 사용할 수 있습니다.

### 기능 키 (Feature Key)

개개의 기능 플래그를 식별하기 위한 키입니다. 기능 키는 기능 플래그를 생성할 때 자동으로 발급됩니다.

기능 키는 워크스페이스 내에서 고유한 값으로, 기능 플래그 결정 단계에서 사용합니다.

### 사용자 타겟팅 (Targeting)

특정 속성을 가진 사용자에게 기능 플래그 상태 값을 적용하는 기능입니다. 예를 들어 모바일 운영체제 중 안드로이드를 사용하는 모든 사용자에게 기능 플래그를 적용하는 것이 가능합니다.

단, 개별 타겟팅 대상이 된 사용자는 사용자 타겟팅 대상이 될 수 없습니다. 또한 개별 타겟팅 대상 및 사용자 타겟팅 대상이 된 사용자는 트래픽 할당 대상이 되지 않습니다.

### 개별 타겟팅 (Override)

특정 사용자가 기능 플래그 적용 대상이 되도록 사용자 식별자 값을 이용해서 강제 할당하는 기능입니다. 개별 타겟팅에 등록된 사용자의 경우 사용자 타겟팅, 트래픽 할당 비율과 관계없이 기능 플래그 상태 값이 적용됩니다.

이 기능은 내부에서만 테스트하고 싶을 때 유용하게 사용할 수 있습니다.

***

## 사용자 퍼널 (Funnel)

사용자 퍼널은 서비스 내에서의 사용자 여정을 가시적으로 확인하기 위한 분석 수단입니다. 서비스의 메인화면 대비 상세화면에 얼마나 많은 사용자들이 진입하였으며, 일/주/월/분기 단위로 어떻게 변화하고 있는지를 사용자 퍼널 분석을 통해 확인할 수 있습니다.

***

## 이벤트 (Event)

\*\*이벤트(Event)\*\*는 사용자가 웹 사이트, 모바일 애플리케이션, 시스템 등을 사용할 때 하는 행동, 혹은 행동으로 인해 발생하는 사건입니다. 사용자의 행동 중에는 검색 버튼 클릭, 상품 페이지 진입, 구매완료 등이 있으며, 그 외 주문서 로딩 시간, 서버 API 처리 시간, 모바일 앱 크래시 등도 이벤트로 볼 수 있습니다.

핵클의 경우 다음의 처리를 위해 이벤트가 필요합니다.

* A/B 테스트의 목표(Goal)를 계산
* 사용자 퍼널 분석

### 이벤트 키 (Event Key)

이벤트 생성 시 이벤트 키를 입력해야 합니다. 이벤트 키는 핵클에서 이벤트를 식별하기 위해 필요합니다. 핵클이 이벤트를 수집하고 기록할 때 이벤트 키를 통해 수행합니다.

이벤트 키는 워크스페이스 내에서 고유한 값으로, SDK 연동 이후 사용자 이벤트 전송을 위해 파라미터로 사용합니다.


# 자주 묻는 질문

핵클 서비스별 자주 묻는 질문을 확인하세요.

### 핵클을 이용하는 기업에는 어떤 곳들이 있나요?

올리브영, 현대카드, 카카오페이증권, 라이나생명, 신한라이프 등 다양한 기업이 핵클을 이용하고 있습니다.

***

### 여러 제품을 하나의 계정에서 관리할 수 있나요?

하나의 이메일 계정으로 여러 워크스페이스를 이용할 수 있습니다.

***

### 서로 다른 도메인을 하나의 워크스페이스에서 관리할 수 있나요?

가능합니다. 사용자 흐름이 연결되면 통합을 검토하세요. 제품과 고객층이 다르면 분리를 권장합니다.

***

### 노코드 툴로 제작된 서비스도 핵클을 이용할 수 있나요?

가능합니다. HTML 또는 스크립트 편집 기능이 있으면 데이터 수집 스크립트를 삽입할 수 있습니다.

***

### ATT 동의 없이도 데이터를 수집할 수 있나요?

네. 핵클은 퍼스트파티 데이터 기반이므로 ATT 동의 여부와 관계없이 데이터를 수집할 수 있습니다.

***

### 과금 사용량과 트래픽은 어떻게 측정되나요?

분배 이벤트와 행동 이벤트로 측정합니다. 준비 중이거나 종료된 실험은 비용에서 제외됩니다.

***

<table data-view="cards"><thead><tr><th>서비스</th><th data-card-target data-type="content-ref">FAQ</th></tr></thead><tbody><tr><td><strong>A/B 테스트</strong><br>실험 설계, 운영, 결과 해석</td><td><a href="/spaces/LHRZjjknm5KdNgPtDsbl/pages/1YTcwYg4abcryLgWEwcZ">/spaces/LHRZjjknm5KdNgPtDsbl/pages/1YTcwYg4abcryLgWEwcZ</a></td></tr><tr><td><strong>데이터 분석</strong><br>퍼널, 리텐션, 이벤트 집계</td><td><a href="/spaces/LHRZjjknm5KdNgPtDsbl/pages/mTSWJpDeDX2WSreuiWCj">/spaces/LHRZjjknm5KdNgPtDsbl/pages/mTSWJpDeDX2WSreuiWCj</a></td></tr><tr><td><strong>CRM 캠페인</strong><br>인앱 메시지, 푸시, 카카오 메시지</td><td><a href="/spaces/LHRZjjknm5KdNgPtDsbl/pages/wOogErJtpNej0NWK9ken">/spaces/LHRZjjknm5KdNgPtDsbl/pages/wOogErJtpNej0NWK9ken</a></td></tr><tr><td><strong>커넥티드 콘텐츠</strong><br>외부 API 기반 메시지 개인화</td><td><a href="/spaces/LHRZjjknm5KdNgPtDsbl/pages/8C6YCUoz2YOAZAsdaJVe">/spaces/LHRZjjknm5KdNgPtDsbl/pages/8C6YCUoz2YOAZAsdaJVe</a></td></tr><tr><td><strong>MCP</strong><br>MCP와 AI 기능 연동</td><td><a href="/spaces/LHRZjjknm5KdNgPtDsbl/pages/Bz7zIFbuqKDAPGEG5Rls">/spaces/LHRZjjknm5KdNgPtDsbl/pages/Bz7zIFbuqKDAPGEG5Rls</a></td></tr><tr><td><strong>개발자 문서</strong><br>SDK 연동과 개발 환경 문제 해결</td><td><a href="/spaces/LHRZjjknm5KdNgPtDsbl/pages/4NR4ZFJznWu1EvrGE4Fm">/spaces/LHRZjjknm5KdNgPtDsbl/pages/4NR4ZFJznWu1EvrGE4Fm</a></td></tr><tr><td><strong>카페24 연동</strong><br>카페24 서비스 연동과 활용</td><td><a href="/spaces/LHRZjjknm5KdNgPtDsbl/pages/J3KQNryCBeipzXZtaiFL">/spaces/LHRZjjknm5KdNgPtDsbl/pages/J3KQNryCBeipzXZtaiFL</a></td></tr></tbody></table>


# Hackle vs Google Optimize

### 목적

핵클 A/B 테스트가 Google Optimize와 비교하여 제공하는 주요 차별점과 그를 통해 사용자가 얻을 수 있는 이점을 소개합니다.

핵클 A/B 테스트는 Google Optimize 대비 향상된 성능, 유연성, 정확성을 제공하여 사용자가 더 신속하고 효과적인 데이터 기반 의사결정을 내릴 수 있도록 지원합니다.

### 주요 차이점 요약

#### 성능 및 데이터

| **구분**  | **핵클 A/B 테스트**            | **Google Optimize**            |
| ------- | ------------------------- | ------------------------------ |
| 서비스 속도  | AB 테스트 갯수에 관계 없이 속도 지연 없음 | AB 테스트 수가 많아질수록 속도 지연이 생길 수 있음 |
| 데이터 반영  | 실시간에 가까운 설정 변경 반영         | 설정 변경 반영에 수 시간 소요 가능           |
| 데이터 최신성 | 1시간 이내 업데이트               | 최대 24시간 지연                     |
| 데이터 정확성 | 전체 데이터 기반의 정확한 분석         | 샘플링 데이터 사용으로 오차 발생 가능          |
| 사용자 식별  | 커스텀 ID로 정확한 사용자 추적        | 세션(Session) 기반으로 사용자 중복 집계     |

#### 기능 및 유연성

| **구분** | **핵클 A/B 테스트**               | **Google Optimize**   |
| ------ | ---------------------------- | --------------------- |
| 지원 환경  | 웹, 모바일 앱, 서버, SPA 등 모든 환경    | 웹 브라우저 중심, SPA 지원 제한적 |
| 지표 설정  | 비즈니스 맞춤형 지표 생성 (AOV, ARPU 등) | 기본 지표 위주, 커스텀 지표 제한적  |
| 타겟팅    | 다중 조건을 조합한 정교한 타겟팅           | 기본적인 타겟팅 조건만 제공       |
| 심층 분석  | 세그먼트별(플랫폼, 회원 등급 등) 분석       | 제한적인 세그먼트 분석 기능       |

#### 운영 및 지원

| **구분** | **핵클 A/B 테스트**             | **Google Optimize**     |
| ------ | -------------------------- | ----------------------- |
| 실험 개수  | 동시 실험 및 목표 지표 개수 무제한       | 동시 실험 5개, 실험당 목표 3개로 제한 |
| 개발 편의성 | 개발/운영 환경 완벽 분리로 안전한 배포     | 환경 분리 미제공으로 인한 운영 실수 위험 |
| 기술 지원  | 전문가의 1:1 실시간 기술 지원 (Slack) | 커뮤니티 기반의 제한적인 지원        |

### 상세 설명: 핵클 A/B 테스트의 14가지 장점

#### 1. 사용자 경험을 지키는 빠른 속도

핵클은 SDK 방식으로 작동하여 서비스 로딩 속도를 저하시키지 않습니다.\
반면, Google Optimize의 비주얼 에디터(Visual Editor) 혹은 위지윅 에디터(WYSIWYG editor)를 기반으로 수행한 AB테스트는 테스트가 늘어날수록 렌더링을 지연시켜 사용자 경험을 해칠 수 있습니다.

#### 2. 기다림 없는 실시간 실험 제어

대시보드에서 실험 설정을 변경하면 거의 즉시 적용됩니다.\
설정 변경에 수 시간이 걸릴 수 있는 Google Optimize와 달리, 원하는 타이밍에 신속하게 실험을 제어할 수 있습니다.

#### 3. 신속한 의사결정을 위한 최신 데이터

실험 데이터가 최소 1시간에 한 번씩 업데이트됩니다.\
최대 24시간까지 시차가 발생하는 Google Optimize에 비해, 변화하는 상황을 빠르게 파악하고 즉각 대응할 수 있습니다.

#### 4. SPA 환경에서도 안정적인 테스트

최신 웹 기술인 SPA(Single Page Application) 환경에서도 안정적으로 테스트를 수행합니다.\
Google Optimize는 SPA 환경에서 데이터 수집 오류가 발생하는 경우가 잦습니다.

#### 5. 비즈니스에 꼭 맞는 자유로운 지표 설정

AOV, ARPU 등 비즈니스에 필수적인 모든 지표를 자유롭게 설정하고 측정할 수 있습니다.\
이는 제한된 기본 목표만 제공하는 Google Optimize와의 큰 차이점입니다.

#### 6. 숨겨진 인사이트를 찾는 심층 분석

실험 결과를 플랫폼, 회원 등급 등 다양한 기준으로 나누어 분석할 수 있습니다.\
Google Optimize의 제한적인 세분화 기능으로는 발견하기 어려운 깊이 있는 인사이트를 제공합니다.

#### 7. 원하는 고객에게만 정확하게, 고도화된 타겟팅

여러 속성을 조합하여 원하는 고객 그룹을 정교하게 타겟팅할 수 있습니다.\
이는 기본적인 조건만 제공하는 Google Optimize보다 훨씬 강력한 기능입니다.

#### 8. 모든 플랫폼을 지원하는 폭넓은 환경

클라이언트(iOS, Android)와 서버 SDK를 모두 제공합니다.\
웹 브라우저 환경에 국한된 Google Optimize와 달리, 모바일 앱과 서버를 포함한 모든 환경에서 테스트가 가능합니다.

#### 9. 정확한 데이터의 기반, 일관된 사용자 식별

유저 ID, 디바이스 ID 등 고유 식별자로 사용자를 추적합니다.\
세션 기반으로 사용자를 식별하는 Google Optimize는 동일 사용자를 중복 집계하는 문제가 발생할 수 있습니다.

#### 10. 오차 없는 결과, 100% 데이터 분석

데이터를 샘플링하지 않고 전체 트래픽을 기반으로 통계 결과를 계산합니다.\
샘플링된 데이터를 사용하는 Google Optimize에 비해 더 정확하고 신뢰도 높은 의사결정이 가능합니다.

#### 11. 막힐 때마다 전문가가 바로, 밀착 기술 지원

고객사별 Slack 채널을 통해 핵클 전문가의 1:1 기술 지원을 받을 수 있습니다.\
커뮤니티 기반으로 지원이 이루어지는 Google Optimize와는 비교할 수 없는 이점입니다.

#### 12. 제한 없는 목표 설정으로 다각적인 성과 측정

하나의 실험에서 측정하고 싶은 목표 지표의 수에 제한이 없습니다.\
실험당 목표가 3개로 제한되는 Google Optimize보다 훨씬 다각적인 성과 분석이 가능합니다.

#### 13. 빠른 성장 속도, 무제한 동시 실험

동시에 진행할 수 있는 실험 개수에 제한이 없습니다.\
최대 5개의 실험만 허용하는 Google Optimize의 한계를 넘어, 다양한 아이디어를 마음껏 테스트할 수 있습니다.

#### 14. 개발 실수를 원천 차단하는 안전한 실험 환경

개발 환경과 운영 환경을 완벽하게 분리하여 제공합니다.\
환경이 분리되지 않은 Google Optimize에서 발생할 수 있는 운영상의 실수를 원천적으로 방지합니다.


# Hackle vs Firebase

### 목적

본 문서는 핵클의 A/B Testing(실험) 및 Remote Config(원격 구성) 기능이 Firebase와 비교하여 제공하는 주요 차별점과 그를 통해 사용자가 얻을 수 있는 이점을 소개합니다.

핵클은 Firebase 대비 더 정교한 분석 기능과 운영 효율성을 제공하여, 데이터 기반의 빠른 제품 개선을 지원합니다.

### 주요 차이점

#### 성능 및 데이터

| 구분      | 핵클                       | Firebase                    |
| ------- | ------------------------ | --------------------------- |
| 데이터 반영  | 실시간에 가까운 설정 변경 반영        | 최대 12시간 지연 (Fetch Interval) |
| 데이터 최신성 | 1시간 이내 결과 업데이트           | 최대 24시간 지연 (GA 데이터 기준)      |
| 데이터 정확성 | 전체 데이터 기반 분석             | 샘플링 데이터 사용으로 오차 발생 가능       |
| 전환율 계산  | 노출 시점 기준으로 정확한 계산        | 분배/노출 시점 불일치로 오류 가능성        |
| SDK 지원  | 클라이언트 SDK + 서버 SDK 모두 제공 | 클라이언트 SDK만 제공               |

#### 기능 및 유연성

| 구분     | 핵클                           | Firebase         |
| ------ | ---------------------------- | ---------------- |
| 지표 설정  | 비즈니스 맞춤형 지표 생성 (AOV, ARPU 등) | 기본 제공 지표 위주로 제한적 |
| 심층 분석  | 속성 기반의 상세 세그먼트 분석            | 제한적인 세그먼트 분석 기능  |
| 타겟팅    | 다중 조건을 조합한 정교한 타겟팅           | 기본적인 타겟팅 조건만 제공  |
| 플랫폼 지원 | 단일 실험으로 Android, iOS 동시 지원   | 플랫폼별 별도 실험 생성 필요 |

#### 운영 및 지원

| 구분     | 핵클                         | Firebase                |
| ------ | -------------------------- | ----------------------- |
| 실험 제약  | 목표 지표 개수 제한 없음             | 목표 6개로 제한 (성공 1, 보조 5)  |
| 개발 편의성 | 개발/운영 환경 완벽 분리로 안전한 배포     | 환경 분리 미제공으로 인한 운영 실수 위험 |
| 기술 지원  | 전문가의 1:1 실시간 기술 지원 (Slack) | 커뮤니티 기반의 제한적인 지원        |

### 상세 설명: 핵클의 12가지 장점

#### 1. 비즈니스에 꼭 맞는 자유로운 지표 설정

AOV, ARPU 등 비즈니스 성장에 필수적인 모든 지표를 자유롭게 설정하고 측정할 수 있습니다.\
이는 제한된 기본 목표만 제공하는 Firebase와의 큰 차이점입니다.

#### 2. 숨겨진 인사이트를 찾는 심층 분석

실험 결과를 플랫폼, 회원 등급 등 다양한 기준으로 나누어 분석할 수 있습니다.\
Firebase의 제한적인 세분화 기능으로는 발견하기 어려운 깊이 있는 인사이트를 제공합니다.

#### 3. 원하는 고객에게만 정확하게, 고도화된 타겟팅

여러 속성을 조합하여 원하는 고객 그룹을 정교하게 타겟팅할 수 있습니다.\
이는 기본적인 조건만 제공하는 Firebase보다 훨씬 강력한 기능입니다.

#### 4. 하나의 실험으로 Android, iOS 동시 지원

하나의 실험으로 Android와 iOS를 동시에 테스트하여 개발 리소스를 절약할 수 있습니다.\
Firebase에서는 두 플랫폼을 위해 각각 별도의 실험을 만들어야 하는 번거로움이 있습니다.

#### 5. 기다림 없는 실시간 설정 변경

실험 및 원격 구성 설정을 변경하면 거의 즉시 반영됩니다.\
Firebase의 설정 업데이트 주기는 12시간으로, 즉각적인 제어에 한계가 있습니다.

#### 6. 신속한 의사결정을 위한 최신 데이터

실험 데이터가 최소 1시간에 한 번씩 업데이트됩니다.\
Google Analytics 데이터를 기준으로 최대 24시간까지 시차가 발생하는 Firebase에 비해, 변화하는 상황에 빠르게 대응할 수 있습니다.

#### 7. 서버 로직까지 효율적으로, 클라이언트 및 서버 SDK 모두 제공

서버 SDK를 통해 검색, 추천 로직 등 서버 단의 A/B 테스트를 효율적으로 진행할 수 있습니다.\
클라이언트 SDK만 제공하는 Firebase로 서버 로직을 테스트하려면 클라이언트와 서버 양쪽에서 복잡한 작업을 해야 합니다.

#### 8. 오차 없는 결과, 100% 데이터 분석

데이터를 샘플링하지 않고 전체 트래픽을 기반으로 통계 결과를 계산합니다.\
샘플링된 데이터를 사용하는 Firebase에 비해 더 정확하고 신뢰도 높은 의사결정이 가능합니다.

#### 9. 정확한 전환율 계산 로직

사용자가 실제 실험 화면에 노출되는 시점을 기준으로 전환율을 계산하여 결과값의 오염을 방지합니다.\
Firebase는 그룹 분배 시점과 노출 시점이 달라 전환율 계산에 오류가 발생할 수 있습니다.

#### 10. 막힐 때마다 전문가가 바로, 밀착 기술 지원

고객사별 Slack 채널을 통해 핵클 전문가의 1:1 기술 지원을 받을 수 있습니다.\
커뮤니티 기반으로 지원이 이루어지는 Firebase와는 비교할 수 없는 이점입니다.

#### 11. 제한 없는 목표 설정으로 다각적인 성과 측정

하나의 실험에서 측정하고 싶은 목표 지표의 수에 제한이 없습니다.\
실험당 목표가 6개로 제한되는 Firebase보다 훨씬 다각적인 성과 분석이 가능합니다.

#### 12. 개발 실수를 원천 차단하는 안전한 실험 환경

개발 환경과 운영 환경을 완벽하게 분리하여 제공합니다.\
환경이 분리되지 않은 Firebase에서 발생할 수 있는 운영상의 실수를 원천적으로 방지합니다.


# Hackle vs Optimizely

## 목적

이 문서는 Hackle A/B 테스트와 Optimizely의 주요 차이점을 기술적인 관점에서 비교하고, Hackle이 제공하는 추가적인 이점을 설명하기 위해 작성되었습니다.

핵클은 Optimizely 대비하여 합리적인 가격으로 빠르게 지표를 분석할 수 있습니다.

## 주요 차이점 요약

| 구분          | 핵클 (Hackle)                                        | Optimizely                    |
| ----------- | -------------------------------------------------- | ----------------------------- |
| **지표 설정**   | 사용자 정의 지표(분모/분자) 설정, 세그먼트 필터링, AOV/ARPU 등 고급 지표 지원 | 사전 정의된 지표 위주로 설정              |
| **분석 기능**   | 세그먼트 기반 심층 분석, 실시간 퍼널 분석 기능 통합 제공                  | 기본적인 A/B 테스트 결과 분석 기능 제공      |
| **가격 정책**   | 무료 플랜 및 사용량 기반 과금(MAU), 월 단위 계약 지원                 | 고정된 MAU 기반의 연 단위 계약           |
| **기술 지원**   | 국내 기술 지원팀의 한국어 지원 (실시간 소통 가능)                      | 해외 오피스를 통한 지원 (시차 및 언어 장벽 존재) |
| **테스트 안정성** | MAU 할당량 초과 시에도 진행 중인 실험 유지                         | MAU 할당량 초과 시 모든 실험 자동 중단      |
| **결과 업데이트** | 데이터 볼륨과 무관하게 시간 단위로 결과 업데이트                        | 데이터 볼륨 증가 시 결과 업데이트 지연 가능     |

***

## 상세 설명

#### 1. 유연한 지표 설정

Hackle은 지표의 분모와 분자를 원하는 이벤트로 커스텀 설정할 수 있으며, 특정 사용자 세그먼트를 기준으로 지표를 필터링하는 기능을 제공합니다.\
이를 통해 기본 전환율 외에도 AOV(평균 주문 금액), ARPU(사용자당 평균 수익) 등 비즈니스에 핵심적인 지표를 정밀하게 측정할 수 있습니다.

#### 2. 세그먼트 기반 심층 분석

플랫폼(iOS, Android, Web), 앱 버전 및 사용자 속성(예: 멤버십 등급, 첫 구매 여부) 등 다양한 기준으로 지표를 세분화하여 분석할 수 있습니다.\
SDK를 통해 전달된 속성 정보를 기반으로 별도 설정 없이 특정 세그먼트가 실험 결과에 미치는 영향을 확인할 수 있습니다.

#### 3. 통합 데이터 분석 기능

A/B 테스트 기능 외에도 데이터 인사이트, 사용자 퍼널 분석 등 고도화된 데이터 분석 기능을 함께 제공합니다.\
이를 활용하여 가설 수립 단계부터 결과 해석에 이르기까지 데이터에 기반한 의사결정이 가능합니다.

#### 4. 합리적인 가격 정책

Hackle은 사용량 기반의 유연한 과금 정책과 월 단위 계약 옵션을 제공하여 도입 장벽을 낮추고 합리적인 비용으로 운영할 수 있도록 지원합니다.\
Optimizely는 일반적으로 연간 계약 기반의 높은 고정 비용 정책을 가지고 있습니다.

#### 5. 신속한 기술 지원

Hackle은 국내에 상주하는 기술 지원팀이 한국어로 신속하게 이슈를 해결하여 원활한 커뮤니케이션과 안정적인 서비스 운영을 보장합니다.\
Optimizely는 해외 오피스를 통해 기술 지원을 제공하므로 시차 및 언어 장벽이 발생할 수 있습니다.

#### 6. 중단 없는 테스트 환경

Hackle은 할당량을 초과하더라도 진행 중인 실험을 중단시키지 않아 데이터 손실 없이 안정적으로 테스트를 완료할 수 있습니다.\
Optimizely는 플랜에 따라 설정된 월간 활성 사용자(MAU) 할당량을 초과하면 진행 중인 모든 A/B 테스트가 중단됩니다.

#### 7. 빠른 결과 업데이트 주기

Hackle은 데이터 볼륨과 관계없이 일관된 시간 단위로 A/B 테스트 결과를 빠르게 업데이트합니다. 이를 통해 신속한 결과 확인과 의사결정이 가능합니다.\
Optimizely는 누적 데이터 양이 증가함에 따라 결과 업데이트가 지연될 수 있습니다.


# Hackle vs VWO

## 목적

핵클에서 경험하실 수 있는 VWO (Visual Web Optimizer) Web 와의 차별점에 대해 소개합니다. 이 문서를 통해 다음 두 가지 사항을 이해하실 수 있습니다.

1. 핵클이 VWO 대비 추가로 제공하는 기능
2. 해당 기능을 통해 핵클 사용자가 얻을 수 있는 이점

## 주요 차이점

### 성능 및 데이터

| 구분        | 핵클                     | VWO                          |
| --------- | ---------------------- | ---------------------------- |
| 서비스 속도 영향 | SDK 기반으로 서비스 속도에 영향 없음 | 비주얼 에디터 기반 테스트 증가 시 로딩 지연 가능 |
| 설정 반영 속도  | 실시간에 가깝게 변경 사항 반영      | 수 시간 소요 가능                   |
| 결과 업데이트   | 1시간당 최소 1회 이상          | 최대 24시간 지연                   |
| SPA 지원    | 정상 지원                  | 정상 동작하지 않는 경우 있음             |
| 데이터 정확성   | 샘플링 없이 전체 데이터 기반       | 별도 언급 없음                     |

### 기능 및 유연성

| 구분      | 핵클                            | VWO        |
| ------- | ----------------------------- | ---------- |
| 지표 설정   | 분모/분자 자유 설정, AOV/ARPU/ARPPU 등 | 제한적        |
| 세그먼트 분석 | 플랫폼, 브라우저, 앱 버전, 커스텀 속성 기반    | 제한적        |
| 타겟팅     | 다중 조건 조합의 커스텀 타겟팅             | 기본 타겟팅 조건  |
| SDK 지원  | 클라이언트 + 서버 SDK (모바일/웹/서버)     | 웹 브라우저 기반만 |
| 사용자 식별  | 디바이스 ID 등 원하는 기준으로 식별         | 세션 기반      |

### 운영 및 지원

| 구분       | 핵클                | VWO      |
| -------- | ----------------- | -------- |
| 기술 지원    | Slack 상시 Hotline  | 별도 언급 없음 |
| 실험/결과 제한 | 동시 실험 수, 결과 수 무제한 | 제한 있음    |
| 환경 분리    | 개발환경/운영환경 모두 제공   | 환경 분리 없음 |

## 상세 설명

#### 1. 핵클 A/B 테스트는 사용자의 서비스 이용 속도를 지연시키지 않습니다.

핵클 A/B 테스트를 도입하면 고객 플랫폼 서비스의 속도가 느려지지 않습니다. 반면, 비주얼 에디터(Visual Editor) 혹은 위지윅 에디터(WYSIWYG editor)를 기반으로 수행한 A/B 테스트가 많아질수록 사용자에게 보여질 화면을 띄우기 위한 로딩시간이 점점 느려질 수 있습니다. 이는 사용자가 어떤 화면을 봐야할지를 에디터에서 판단하게 되어 발생하는 현상입니다.

#### 2. 핵클 A/B 테스트는 실시간에 가깝게 실험 설정의 변경 사항을 반영합니다.

핵클 SDK 연동후에는 대시보드에서 변경된 설정 정보를 주기적으로 수신하여 코드에 반영합니다.

A/B 테스트를 시작하는데 수 시간이 소요될 수 있는 VWO와 비교하여 핵클 SDK는 구성 정보 변경 사항을 업데이트하는 주기가 짧아, 실시간에 가깝게 A/B 테스트의 진행 상황을 제어할 수 있습니다.

#### 3. 핵클 A/B 테스트는 실험 결과 데이터를 자주 업데이트 합니다. (1시간 당 최소 1회 이상)

VWO에서 전날까지 취합된 데이터를 기반으로 실험 결과를 제공하기 때문에 경우에 따라 24시간까지 시차가 나타납니다. 이 경우 사용자는 현재 상황에 대한 즉각적인 판단과 대응을 하기 어렵습니다.

핵클에서는 실험 결과를 1시간에 최소 1회 이상 업데이트하기 때문에 시차가 1시간 이내로 관리됩니다.

#### 4. 핵클 A/B 테스트는 SPA(Single Page Application)를 지원합니다.

VWO는 SPA(Single Page Application) 방식으로 구현된 서비스, 즉 기존의 웹 브라우저에서 새 웹 페이지를 로드하는 대신 현재 웹 페이지에서 변경이 필요한 부분만 갱신하는 방식으로 동작하는 서비스에서는 정상적으로 A/B 테스트 구현이 되지 않거나 사용자 데이터 수집이 되지 않는 경우가 있습니다. 반면, 핵클은 SPA 방식으로 구현된 서비스 상에서도 A/B 테스트를 수행하는데 문제가 없습니다.

#### 5. 핵클 A/B 테스트는 원하는 지표를 자유롭게 설정할 수 있습니다.

핵클에서는 지표의 분모/분자를 원하는 이벤트로 선택할 수 있으며, 다양한 계산유형 제공을 통해 전환율 뿐만 아니라 평균 주문금액(AOV), 사용자당 평균 구매금액(ARPU), 구매자당 평균 구매금액(ARPPU) 등의 지표를 측정할 수 있습니다.

또한, 특정 유저 세그먼트를 대상으로 지표를 측정할 수 있는 필터 설정 기능도 제공하고 있습니다.

#### 6. 핵클 A/B 테스트에서는 지표를 세그먼트 단위로 분석할 수 있습니다.

실험에서 측정하고자 하는 지표의 결과를 플랫폼(iOS, Android, Web 등), 브라우저, 앱 버전 혹은 고객사에서 내부적으로 관리하는 속성정보(ex. 멤버십 회원여부, 첫구매여부, 성별, 연령대 등) 를 기준으로 세그먼트 분석을 할 수 있습니다.

이를 통해, A/B 테스트의 지표가 특정 세그먼트에만 영향을 주고 있는지 확인할 수 있습니다.

#### 7. 핵클 A/B 테스트는 더욱 고도화된 타겟팅 기능을 제공합니다.

핵클은 VWO에서 제공하는 사용자 타겟팅 조건보다 더욱 다양하고 원하는 타겟팅 조건을 Custom 하게 설정할 수 있도록 지원하며, 타겟팅 시 여러 조건을 설정할 수 있도록하여 원하는 유저를 대상으로 A/B 테스트를 진행할 수 있도록 지원하고 있습니다.

#### 8. 핵클은 클라이언트 SDK 와 서버 SDK 를 모두 제공합니다.

VWO Web은 웹 브라우저 기반 A/B 테스트만 지원합니다. 반면, 핵클은 클라이언트 및 서버 SDK를 제공하여 모바일, 데스크탑, 서버 환경과 플랫폼을 만들기 위해 사용된 모든 프로그래밍 언어에서의 A/B 테스트 및 테스트 그룹 생성, 구현을 지원합니다.

#### 9. 핵클 A/B 테스트는 세션뿐만 아니라 디바이스 ID 등의 원하는 기준으로 사용자를 식별할 수 있습니다.

핵클의 사용자 식별자 ([사용자 식별자 관리하기](/getting-started/user-identifier))문서에 게시된 것 처럼 A/B 테스트에 있어서 사용자 식별자를 정확하게 정의하는 것이 매우 중요합니다. 핵클에서는 고객이 직접 원하는 측정기준을 사용해서 사용자 식별자를 정의할 수 있으므로 세션 기반 사용자 식별자에서 나타나는 한계점을 극복할 수 있습니다.

#### 10. 핵클 A/B 테스트는 샘플링 없이 전체 데이터를 기반으로 결과값을 제공합니다.

핵클 A/B 테스트는 결과를 계산할 때 전체 데이터를 사용하기 때문에 정확한 계산 결과를 제공합니다.

#### 11. 핵클은 상시 Hotline을 통해 고객을 기술적으로 지원합니다.

핵클에서는 A/B 테스트 코드 구현을 포함한 SDK 연동 과정이나 실험을 진행하면서 궁금하셨던 점들을 편하게 질문하실 수 있도록 [**Slack 메신저**](https://hackle-community.slack.com/)에서 고객사별 private channel을 지원해 드리고 있습니다.

#### 12. 핵클 A/B 테스트는 하나의 실험에 대해 개발환경과 운영환경을 모두 제공하기 때문에 실험 진행이 용이합니다.

VWO는 운영환경과 개발환경의 분리가 되어있지 않아, 개발자가 환경을 오해할 가능성이 있습니다. A/B 테스트를 코드로 구현하고자 할 때 미리 개발환경에서 테스트를 진행해야 하는데, 환경을 오해하여 실제로 사용자에게 노출되는 운영환경의 설정을 수정하는 문제가 발생할 가능성이 높습니다.

핵클에서는 하나의 실험을 진행할 때 운영환경과 개발환경을 모두 제공하여, 이와 같은 문제의 발생 가능성이 없습니다.


# PoC 안내 가이드

PoC는 실제 서비스에서 핵클 솔루션의 사용성을 검증하기 위해 일정 기간 동안 제한적으로 제공되는 프로그램입니다.

내부 정책에 따라 다음과 같은 조건 하에 운영됩니다.

## PoC 정책

{% hint style="danger" %}
PoC는 법인당 1회만 제공됩니다.

비정상적인 이용 및 무분별한 사용으로 판단되는 경우, PoC 진행이 중단될 수 있습니다.
{% endhint %}

<table><thead><tr><th width="160.16796875">구분</th><th>설명</th></tr></thead><tbody><tr><td>PoC 기간</td><td>SDK 연동기간 포함 <strong>워크스페이스 제공시점부터 최대 3주간 진행 가능</strong>합니다.</td></tr><tr><td>제공 트래픽</td><td>테스트 트래픽 포함 <strong>최대 1,000만 건까지 무상으로 제공</strong>됩니다.</td></tr><tr><td>과금 정책</td><td>무상 제공량 초과분에 대해서는 당사 표준 정책에 따라 건당 0.3원이 발생합니다.<br>단, 해당 비용은 향후 본 계약 전환 시 계약 조건에 포함하여 협의 가능합니다.</td></tr><tr><td>제공 기능 / 진행 범위</td><td>Raw Data의 업로드 &#x26; 추출 등 일부 기능은 제공되지 않을 수 있습니다.<br>진행 범위에 대한 사전 협의가 필요합니다.</td></tr></tbody></table>

## PoC 범위

PoC 기간 동안에는 실제 데이터를 통한 가설 검증이 아닌, SDK 연동·이벤트 수집·실험 구현 등 기술적 동작의 정상 여부를 확인하는 데 초점을 둡니다.

본 계약 전환 전에 핵클 솔루션이 실제 서비스 환경에서 안정적으로 작동하는지 사전에 점검하실 수 있습니다.

{% hint style="warning" %}
네이티브 앱에서는 심사 및 업데이트 기간(1\~2주) 등 기술적 제약이 있어, 3주간의 PoC 일정 내 유의미한 검증이 가능한 웹(Web) 영역 실험 진행을 권고드립니다.

부득이하게 모바일 앱 Native 영역에서 진행하시는 경우, 일정 리스크 방지를 위해 Android 또는 iOS 중 1개 플랫폼을 선택하여 진행하시는 것을 권고드립니다.
{% endhint %}

## PoC 진행 (지원체계)

{% hint style="info" %}
PoC 기간 커뮤니케이션과 기술지원은 슬랙 채널을 통해 진행하고 있습니다.

워크스페이스 및 슬랙 채널 초대는 최초 담당자 메일로 발송하며, 추가 인원 초대는 고객사에서 자유롭게 진행할 수 있습니다.
{% endhint %}

* PoC 전용 워크스페이스 및 슬랙채널 개설
* 킥오프 미팅 주관
* 주요 지원 사항
  * 실험 및 분석 설계는 고객사에서 직접 수행하심을 원칙으로 하며, 핵클은 원활한 온보딩을 위한 가이드를 제공드립니다.
  * 핵클 지원 사항: SDK 연동 검수, 설정 가이드 제공, 주기적 사용 현황 모니터링 및 이슈 대응
* PoC 종료 후 결과 리뷰 및 후속 논의 미팅 주관

## PoC 마무리

{% hint style="success" %}
PoC 리뷰 미팅은 PoC 종료시점에 맞추어 진행됩니다.
{% endhint %}

* PoC 종료 후, 고객사와 결과를 리뷰하고 도입 여부 및 추가 협업 방향에 대해 논의하는 미팅을 진행합니다.
  * PoC 검증 포인트에 대한 의견 공유
  * PoC 기간동안 고객사의 이용현황을 핵클에서 리뷰한 내용 공유
  * 계약 검토를 위한 예상 트래픽 산정 지원 및 이용견적 제안
* PoC 리뷰 미팅은 PoC 종료일 기준으로 5영업일 이내에 진행합니다.
* PoC 기간이 종료되면 대시보드 이용이 제한됩니다.


# 핵클 AI 기능 한눈에 보기

핵클 AI 기능은 데이터를 빠르게 해석하고, 캠페인 제작 시간을 줄여줍니다.

{% columns %}
{% column width="41.66666666666667%" %}
![Hackle MCP 원격 서버](/files/jEQYKcMGzQIeP31gNY7Z)
{% endcolumn %}

{% column width="58.33333333333333%" %}

#### 캠페인 성과를 빠르게 파악하세요

AI에 실험과 메시지 성과를 질문하세요.\
분석 결과를 바탕으로 다음 액션을 정할 수 있습니다.
{% endcolumn %}
{% endcolumns %}

{% columns %}
{% column width="58.333333333333336%" %}

#### 채널별 메시지를 준비하세요

AI로 메시지 초안을 만들고 채널에 맞게 다듬으세요.\
푸시와 카카오 메시지의 카피 제작 시간을 줄일 수 있습니다.
{% endcolumn %}

{% column width="41.666666666666664%" %}
![카카오 메시지 AI 카피 자동 생성](/files/MnocTdoAIrQutOKkxG6D)
{% endcolumn %}
{% endcolumns %}

{% columns %}
{% column width="41.66666666666667%" %}
![커스텀 HTML 인앱 메시지](/files/KcNOFvKbey8LSUn4zqBU)
{% endcolumn %}

{% column width="58.33333333333333%" %}

#### 인앱 경험을 확장하세요

AI를 활용해 캠페인 목적에 맞는 인앱 메시지를 제작하세요.\
프로모션, 설문 등 다양한 화면을 빠르게 구성할 수 있습니다.
{% endcolumn %}
{% endcolumns %}


# MCP 연동

핵클 MCP(*Model Context Protocol*)는 Claude, ChatGPT 등 AI 클라이언트가 자연어로 데이터를 조회하고 분석할 수 있도록 지원합니다.

대시보드에서 클릭으로 확인하던 실험·메시지·분석 데이터를, AI 와의 대화로 묻고 해석하고 시각화까지 받을 수 있습니다.

{% hint style="success" %}
핵클 MCP를 활용하면 아래와 예제와 같은 작업을 AI와 함께 확인할 수 있습니다.

* **실험 결과 해석**: *"지난주 시작한 A/B 테스트 결과를 요약해줘"*
* **메시지 효과 측정**: *"최근 푸시 캠페인의 전환률을 비교해줘"*
* **사용자 동향 분석**: *"최근 30일 DAU 추이와 7일 리텐션을 같이 보여줘"*
* **퍼널 분석**: *"회원가입에서 첫 결제까지 단계별 이탈률을 차트로 그려줘"*
* **운영 자동화**: *"오늘 종료된 실험 중 통계적으로 유의한 것만 골라서 알려줘"*
  {% endhint %}

## 동작 방식

```mermaid
flowchart LR
    client["AI 클라이언트"]
    server["핵클 MCP 서버"]
    workspace["핵클 워크스페이스"]

    client --> server --> workspace
```

1. AI 클라이언트에 자연어로 질문을 하면
2. 핵클 인프라에서 운영되는 MCP 서버를 통해
3. 핵클 워크스페이스에 접근하여 데이터를 조회할 수 있습니다.

{% hint style="success" %}
Hackle MCP 서버는 Hackle 인프라에서 운영되므로 별도 설치가 필요하지 않습니다.
{% endhint %}

## 도입

[Hackle 대시보드](https://dashboard.hackle.io/) 의 **MCP 연동** 메뉴에서 API 키 발급을 신청하실 수 있습니다.


# Claude 연동

클로드 커스텀 커넥터 기능을 이용하면 핵클 MCP와 쉽게 연동할 수 있습니다.

### 클라이언트의 커스텀 커넥터 사용 조건

커스텀 커넥터의 경우 Claude 플랜(요금제) 별 사용 제약이 있습니다.

{% hint style="warning" %}
Claude 정책에 따라 커스텀 커넥터 사용 조건이 변경될 수 있습니다.
{% endhint %}

<table><thead><tr><th width="216.12890625">Claude 플랜</th><th>조건</th></tr></thead><tbody><tr><td>무료 플랜</td><td>커스텀 커넥터를 <strong>1개까지</strong> 추가할 수 있습니다.</td></tr><tr><td>유료 플랜</td><td>제한 없이 추가할 수 있습니다.</td></tr><tr><td>Team / Enterprise</td><td><p>워크스페이스 관리자가 커스텀 커넥터 추가를 막아두면 <code>커스텀 커넥터 추가</code> <strong>메뉴 가 표시되지 않습니다.</strong></p><p>이 경우 워크스페이스 관리자에게 문의해주세요.</p></td></tr></tbody></table>

### 연동하기

{% columns %}
{% column %}
{% content-ref url="/pages/2jPEBlZl631L2jqc4lk0" %}
[Claude 웹 & 데스크탑 연동](/ai/model-context-protocol/claude/claude-ai-web)
{% endcontent-ref %}
{% endcolumn %}

{% column %}
{% content-ref url="/pages/Uy5gaumePO8bEEEISfNA" %}
[Claude Code 연동](/ai/model-context-protocol/claude/claude-code)
{% endcontent-ref %}
{% endcolumn %}
{% endcolumns %}


# Claude 웹 & 데스크탑 연동

[claude.ai](https://claude.ai) 에서 커스텀 커넥터를 통한 Hackle MCP 연동을 설명합니다.

{% stepper %}
{% step %}

### 커넥터 페이지 열기

[claude.ai](https://claude.ai) 에 로그인합니다.

좌측 사이드바에서 `사용자 지정 > 커넥터` 를 클릭합니다.

<figure><img src="/files/C4mdfEg7gYIU8sH8Tquv" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### 커스텀 커넥터 추가

커넥터 페이지 우측 상단의 `+` 버튼을 클릭하고 `커스텀 커넥터 추가` 를 선택합니다. [바로가기](https://claude.ai/customize/connectors?modal=add-custom-connector)

표시되는 다이얼로그에 아래 내용을 입력하고 `추가` 를 누릅니다.

```
이름: Hackle
원격 MCP 서버 URL: https://mcp.hackle.io/mcp
```

<figure><img src="/files/03nR7p9tFC2dn1nnFTt1" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Hackle API 키 입력

URL 추가 후 연결 버튼을 클릭하면 인증 화면이 열립니다.

**API 키 입력란**에 Hackle API 키를 붙여넣고 진행합니다.

<figure><img src="/files/VcAG6IwY9HdXngzYrb4P" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### (선택) 도구 권한 확인

연결 후 커넥터 페이지에서 Hackle 커넥터를 클릭하면 노출되는 도구 목록과 권한을 볼 수 있습니다.

필요 시 일부 도구만 허용하도록 조정할 수 있습니다.

<figure><img src="/files/RcTGKOVh47UCd2xx23Hy" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### 연결 확인

새 대화에서 아래 메시지를 입력해보세요.

```
사용 가능한 Hackle 도구를 알려줘
```

응답에 `experiment-list`, `in-app-message-list` 같은 도구 이름이 포함되면 연결 성공입니다.
{% endstep %}
{% endstepper %}


# Claude Code 연동

Anthropic 의 공식 CLI 인 [Claude Code](https://code.claude.com) 에서 Hackle MCP 를 연결하는 방법입니다.

{% stepper %}
{% step %}

### MCP 서버 등록

```bash
claude mcp add --transport http hackle-mcp https://mcp.hackle.io/mcp
```

기본적으로 현재 프로젝트 로컬에만 등록됩니다. 모든 프로젝트에서 사용하려면:

```bash
claude mcp add --transport http --scope user hackle-mcp https://mcp.hackle.io/mcp
```

{% endstep %}

{% step %}

### 인증

Claude Code 세션 안에서:

```
/mcp
```

`hackle-mcp` 옆에 인증이 필요하다는 표시가 보입니다. 항목을 선택하면 브라우저가 열리고 Hackle 인증 화면이 표시됩니다. **API 키 입력란**에 Hackle API 키를 붙여넣고 진행합니다.

<figure><img src="/files/VcAG6IwY9HdXngzYrb4P" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### 연결 확인

```
/mcp
```

`hackle-mcp` 상태가 connected 로 표시되면 완료입니다. 새 프롬프트에서 도구를 사용할 수 있습니다.
{% endstep %}
{% endstepper %}

### 로컬 서버에서 이전

기존에 로컬 MCP 서버(npm 패키지 형태)를 Claude Code 에 등록해 사용하고 계셨다면 다음 단계를 따라 원격 서버로 옮길 수 있습니다.

#### 1. 기존 로컬 서버 제거

```bash
claude mcp list                          # 등록 이름 확인
claude mcp remove hackle-mcp             # 이름이 다르면 그에 맞게 변경
```

#### 2. 원격 서버 등록

위의 **방법 A** 또는 **방법 B** 를 따라 등록합니다. API 키 값은 기존에 쓰던 것 그대로 사용하실 수 있습니다.

#### 3. 로컬 서버 패키지 정리 (선택)

전역 설치한 npm 패키지가 있다면 제거합니다.

```bash
npm uninstall -g @hackle-io/hackle-mcp
```


# 로컬 서버에서 마이그레이션

{% hint style="danger" %}
로컬 MCP 서버는 더이상 업데이트 되지 않으므로 2026년 하반기내에 지원이 종료될 예정입니다.\
가이드를 참고하여 마이그레이션을 진행해주세요.
{% endhint %}

{% hint style="info" %}
**원격 서버로 옮기면 데이터가 외부로 전송되나요?**\
원격 서버는 Hackle 인프라에서 운영됩니다. AI 클라이언트의 요청은 Hackle MCP 서버를 통해 워크스페이스 데이터를 조회하고 응답하며, 별도의 외부 시스템을 거치지 않습니다.
{% endhint %}

기존에 로컬 MCP 서버(npm 패키지 형태)를 사용하고 계셨다면 이 가이드를 따라 원격 서버로 옮길 수 있습니다.

### 왜 옮겨야 하나요?

* **설치 / 업데이트가 필요 없습니다.** npm 패키지 관리, Node.js 버전, 의존성 충돌에서 자유로워집니다.
* **신규 도구는 원격 서버에만 추가됩니다.** CRM 통계 분석 등 최근 툴은 로컬 서버에서 사용할 수 없습니다.
* **항상 최신 상태입니다.** 서버 측 개선과 버그 수정이 클라이언트 업데이트 없이 즉시 적용됩니다.

### 핵심 변화

원격 서버는 **클라이언트 내 커스텀 커넥터 UI** 로 추가합니다. 로컬 서버처럼 `claude_desktop_config.json` 에 `command` / `args` 를 적는 방식이 아니며, 같은 파일에 `url` 키를 적는 것도 동작하지 않습니다.

| 항목       | 로컬 서버                                                                       | 원격 서버                    |
| -------- | --------------------------------------------------------------------------- | ------------------------ |
| 추가 방식    | `claude_desktop_config.json` 의 `mcpServers` 에 `command` / `args` / `env` 등록 | 클라이언트의 **커스텀 커넥터 추가** UI |
| API 키 위치 | 환경 변수 (`env.API_KEY`)                                                       | 클라이언트의 인증 화면에 입력         |

### 마이그레이션 절차

{% stepper %}
{% step %}

### MCP 키 새로 발급

로컬 서버에서 사용하던 api key는 원격 서버의 일부 기능을 사용할 수 없습니다.\
[Hackle 대시보드](https://dashboard.hackle.io/) 의 **MCP 연동** 메뉴에서 API 키 발급을 신청하실 수 있습니다.
{% endstep %}

{% step %}

### 기존 로컬 서버 설정 제거 (Claude Desktop)

Claude Desktop -> 설정 -> 개발자 -> 구성 편집

* Mac: \~/Library/Application Support/Claude/claude\_desktop\_config.json
* Windows: %APPDATA%\Claude\claude\_desktop\_config.json
* Linux: \~/.config/Claude/claude\_desktop\_config.json

`claude_desktop_config.json` 파일을 문서 편집기에서 열고 로컬 Hackle 서버 항목을 제거합니다.

**Before — 로컬 서버**:

```json
{
  "mcpServers": {
    "hackle-mcp": {
      "command": "npx",
      "args": ["-y", "@hackle-io/hackle-mcp@latest"],
      "env": {
        "API_KEY": "YOUR_API_KEY"
      }
    }
  }
}
```

위 `hackle-mcp` 항목을 삭제하고 저장합니다. `mcpServers` 객체에 다른 서버가 있다면 그것들은 유지합니다.

**After — 로컬 서버**:

```json
{
  "mcpServers": {
    
  }
}
```

{% endstep %}

{% step %}

### 원격 서버를 커넥터로 추가

* 좌측 사이드바 **사용자 지정** → **커넥터** → **+** → **커스텀 커넥터 추가** → 원격 MCP 서버 URL `https://mcp.hackle.io/mcp` → 인증 화면에서 Hackle API 키 입력.
* 자세한 절차는 [claude.ai 웹 연동](https://docs.hackle.io/external-link/model-context-protocol/claude-ai-web) 참고하세요.
* [바로 연결](https://claude.ai/customize/connectors?modal=add-custom-connector)
  {% endstep %}

{% step %}

### 연동 확인

새 대화에서 아래 메시지를 입력해보세요.

```
사용 가능한 Hackle 도구를 알려줘
```

응답에 도구 이름이 포함되면 마이그레이션 완료입니다.
{% endstep %}

{% step %}

### 로컬 서버 패키지 정리 (선택)

전역 설치한 npm 패키지가 있다면 제거합니다.

```bash
npm uninstall -g @hackle-io/hackle-mcp
```

`npx -y` 로 실행하던 경우는 캐시 정리만 해도 됩니다.

```bash
npx clear-npx-cache
```

{% endstep %}

{% step %}

### 도구 호환성

원격 서버는 로컬 서버의 모든 도구를 포함하며, 신규 도메인이 추가되어 있습니다.

{% hint style="info" %}
기존 프롬프트는 그대로 사용할 수 있습니다. 도구 이름은 변경되지 않았습니다.
{% endhint %}

| 도메인                                         | 로컬 서버 | 원격 서버 |
| ------------------------------------------- | ----- | ----- |
| Experiment (A/B 테스트)                        | ✅     | ✅     |
| In-App Message                              | ✅     | ✅     |
| Push Message                                | ✅     | ✅     |
| Auto Metrics (DAU / Retention / Stickiness) | ✅     | ✅     |
| Analytics (데이터 리포트 / 차트)                    | ✅     | ✅     |
| Remote Config                               | ✅     | ✅     |
| Text Message                                | —     | ✅     |
| Kakao Message                               | —     | ✅     |
| In-App Message 통계                           | —     | ✅     |
| Push Message 통계                             | —     | ✅     |
| Text Message 통계                             | —     | ✅     |
| Kakao Message 통계                            | —     | ✅     |
| {% endstep %}                               |       |       |
| {% endstepper %}                            |       |       |


# ChatGPT 연동

ChatGPT 플러그인 기능을 이용하면 핵클 MCP와 쉽게 연동할 수 있습니다.

### 플러그인 사용 조건

플러그인을 통한 핵클 MCP 이용은 유료 플랜에서만 사용 가능합니다.

* Plus
* Pro
* Business
* Enterprise
* Edu

{% hint style="warning" %}
ChatGPT 정책에 따라 플러그인 및 커스텀 MCP 사용 조건이 변경될 수 있습니다.
{% endhint %}

### 연동하기

{% content-ref url="/pages/bZeBT8pFps1yRsRTJx3q" %}
[ChatGPT 웹 & 데스크탑 연동](/ai/model-context-protocol/chatgpt/chatgpt-web)
{% endcontent-ref %}


# ChatGPT 웹 & 데스크탑 연동

[chatgpt.com](https://chatgpt.com/) 에서 플러그인을 통한 Hackle MCP 연동을 설명합니다.

{% stepper %}
{% step %}

### 개발자 모드 활성화

chatgpt에 로그인합니다.

좌측 하단 프로필을 눌러 설정 창을 연 뒤 `보안 및 로그인 > 개발자 모드`에서 개발자 모드를 활성화 합니다.

<figure><img src="/files/60E35C6syH4Xv7JHn8GF" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### 플러그인 추가

좌측 사이드바에서 플러그인 메뉴를 찾아 플러그인 페이지로 진입한 뒤 `+` 버튼을 누릅니다.

플러그인 추가 다이얼로그 표시되면 아래 정보를 입력한 뒤 `만들기` 버튼을 클릭합니다.

<pre><code>이름: Hackle
<strong>연결: https://mcp.hackle.io/mcp
</strong>인증: OAuth
</code></pre>

<figure><img src="/files/ZXLNjwR4aqkAJYdviBEw" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Hackle 계정으로 로그인

아래와 같은 다이얼로그가 표시되면 `Hackle 계정으로 로그인` 버튼을 클릭합니다.

<figure><img src="/files/PgyUpUFIIzvaXjMI87W1" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Hackle API 키 입력

URL 추가 후 연결 버튼을 클릭하면 인증 화면이 열립니다.

**API 키 입력란**에 Hackle API 키를 붙여넣고 진행합니다.

<figure><img src="/files/VcAG6IwY9HdXngzYrb4P" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### 연결 확인

새 대화에서 아래 메시지를 입력해보세요.

```
사용 가능한 Hackle 도구를 알려줘
```

아래와 같이 사용 가능 한 도구 목록 응답이 오면 정상적으로 연결이 된 것 입니다.

```
현재 사용할 수 있는 Hackle 도구는 총 38개입니다.
...
중략
```

{% endstep %}
{% endstepper %}


# MCP FAQ

MCP 연동과 AI 도구 사용에 관한 자주 묻는 질문입니다.

핵클 MCP를 AI 도구와 연결해 사용할 때 자주 묻는 질문입니다.

#### MCP API 키를 다른 것으로 바꾸고 싶습니다.

커넥터에서 **Hackle**을 연결 해제한 뒤 다시 추가하세요. 새 API 키로 인증할 수 있습니다.

***

#### 다른 워크스페이스의 데이터를 보고 싶습니다.

MCP API 키는 계정별로 발급됩니다. 여러 워크스페이스에 접근 가능한 계정은 키 하나로 접근할 수 있습니다.

프롬프트에서 조회할 워크스페이스를 지정하세요. 지정하지 않으면 AI 도구가 워크스페이스를 확인합니다.


# 활용 사례

{% hint style="info" %}
결과 링크의 차트와 응답은 예시이며, 실제 응답은 워크스페이스 데이터와 시점에 따라 달라집니다.
{% endhint %}

{% hint style="success" %}
**간단히 물어본 뒤 후속 질문으로 깊이를 더해보세요.**

처음부터 모든 조건을 담은 긴 질문보다, 결과를 보고 *"왜 이 시점에 떨어졌나요?"*, *"이 코호트만 따로 보여줘"*, *"차트로 그려줘"* 같은 후속 질문을 던지는 방식이 더 깊이 있는 분석을 이끌어냅니다.
{% endhint %}

{% hint style="success" %}
**시각화가 필요하면 명시적으로 요청해주세요.**

*"차트로 그려줘"*, *"시각화해서 분석해줘"* 와 같이 요청하면 적절한 형태의 차트를 생성합니다.
{% endhint %}

핵클 MCP 로 어떤 분석이 가능한지 실제 사례로 확인하실 수 있습니다.\
각 사례에는 사용한 프롬프트와 Claude 가 생성한 결과 링크가 함께 있습니다.

<table data-view="cards"><thead><tr><th align="center"></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td align="center"><strong>일간 활성 사용자 수</strong><br><strong>시각화</strong></td><td>활성 사용자 추이를 차트로 확인하고,<br>변동 구간과 원인을 분석합니다.</td><td><a href="https://claude.ai/public/artifacts/cb661fe3-8816-4c83-9ad6-5d1547e68ac1">https://claude.ai/public/artifacts/cb661fe3-8816-4c83-9ad6-5d1547e68ac1</a></td></tr><tr><td align="center"><strong>주간 리텐션 시각화</strong></td><td>코호트별 리텐션을 비교해,<br>차이가 나타난 구간과 원인 가설을 살펴봅니다.</td><td><a href="https://claude.ai/public/artifacts/61051380-d824-4163-b3f9-c0eb2ff004c9">https://claude.ai/public/artifacts/61051380-d824-4163-b3f9-c0eb2ff004c9</a></td></tr><tr><td align="center"><strong>지난 3개월간</strong><br><strong>활성 사용자 변화</strong></td><td>특정 기간 사용자 변화를 요약하고,<br>추세를 파악합니다.</td><td><a href="https://claude.ai/share/e1e433c5-d88a-45ec-b2a9-d8e893f07b72">https://claude.ai/share/e1e433c5-d88a-45ec-b2a9-d8e893f07b72</a></td></tr><tr><td align="center"><strong>사용자 경로별</strong><br><strong>전환율 비교</strong></td><td>경로별 전환율을 비교하고,<br>전환에 유리한 경로의 특징을 찾습니다.</td><td><a href="https://claude.ai/share/b31b7296-1a44-4e06-b6c7-1e9a0eb2c132">https://claude.ai/share/b31b7296-1a44-4e06-b6c7-1e9a0eb2c132</a></td></tr><tr><td align="center"><strong>신규 기능이</strong><br><strong>스티키니스에 미친 영향</strong></td><td>신규 기능 전후 스티키니스를 비교해,<br>사용자 재방문에 미친 영향을 살펴봅니다.</td><td><a href="https://claude.ai/share/d0d78742-7807-4b92-9daf-79944d1f6ce2">https://claude.ai/share/d0d78742-7807-4b92-9daf-79944d1f6ce2</a></td></tr><tr><td align="center"><strong>신규 기능 출시 후</strong><br><strong>리텐션 변화</strong></td><td>출시 전후 리텐션을 비교해,<br>신규 기능이 잔존율에 미친 변화를 확인합니다.</td><td><a href="https://claude.ai/share/9b9aaf56-016b-4c59-8f85-7e30f45e50be">https://claude.ai/share/9b9aaf56-016b-4c59-8f85-7e30f45e50be</a></td></tr><tr><td align="center"><strong>푸시 메시지 참여율 분석</strong></td><td>푸시 캠페인의 참여율을 비교하고,<br>성과가 높은 메시지의 공통점을 찾습니다.</td><td><a href="https://claude.ai/share/302b1a85-b865-4252-b7c7-a63fca220218">https://claude.ai/share/302b1a85-b865-4252-b7c7-a63fca220218</a></td></tr><tr><td align="center"><strong>인앱 메시지 디자인 변경 효과</strong></td><td>디자인 변경 전후 클릭률을 비교해,<br>변경 효과를 확인합니다.</td><td><a href="https://claude.ai/share/22b502eb-32af-4e05-a2fd-e47690b5b0b6">https://claude.ai/share/22b502eb-32af-4e05-a2fd-e47690b5b0b6</a></td></tr></tbody></table>


# 핵클이 추천하는 전환율 10X 늘리기 제안

### 핵클을 활용한 전환율 10배 늘리기 제안을 소개합니다!

**데이터 분석**을 통해 사용자 행동 데이터에 숨은 인사이트를 찾고, **맞춤형 CRM 마케팅으로** 서비스 전환율을 높여보세요.

![](/files/nbCwL8iWFGvri0u7hCbh)

### 핵클팀이 추천하는 USE CASE

<table data-view="cards"><thead><tr><th align="center"></th><th align="center"></th><th data-hidden data-card-cover data-type="image">Cover image</th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td align="center"><strong>장바구니 이탈 사용자를</strong><br><strong>찾고 싶다면?</strong></td><td align="center">퍼널 분석으로 이탈 사용자를 찾고, 코호트로 관리하는 방법을 알려드려요.</td><td><a href="/files/6ftGsr0nLWr9iCzSmqkF">/files/6ftGsr0nLWr9iCzSmqkF</a></td><td><a href="/pages/ECrTvA3UZnGxcsMDFJRo">/pages/ECrTvA3UZnGxcsMDFJRo</a></td></tr><tr><td align="center"><strong>이탈 사용자의 재방문을</strong><br><strong>유도하는 법</strong></td><td align="center">떠난 사용자를 다시 사이트로 데려오는 방법 A-Z를 알려드려요.</td><td><a href="/files/BTZfSAL5h8CoKjxRQcng">/files/BTZfSAL5h8CoKjxRQcng</a></td><td><a href="/pages/tquTR1FNRJNRw1XvwYpt">/pages/tquTR1FNRJNRw1XvwYpt</a></td></tr><tr><td align="center"><strong>사용자에게 가장</strong><br><strong>효과적인 문구는?</strong></td><td align="center">인앱 메시지 MAB 테스트로 사용자의 반응을 확인하고 최적화 하는 방법을 알려드려요.</td><td><a href="/files/1ZsZBN2QruTYqL2EkdUO">/files/1ZsZBN2QruTYqL2EkdUO</a></td><td><a href="/pages/Ab7QbDCJHQzYoMxKRjy6">/pages/Ab7QbDCJHQzYoMxKRjy6</a></td></tr></tbody></table>


# Step 1. 이탈 사용자 찾아보기

![](/files/e18WYPwcX3NBie48u8Ci)

### 서비스 주요 이탈 시점

서비스 주요 이탈 시점은 고객이 서비스를 떠나는 경향이 높은 특정 시기를 말합니다. 이 시점을 이해하는 것은 이탈을 줄이고 리텐션을 높이는 데 매우 중요합니다.

* **가입 후 초기 단계**
  * 고객이 서비스에 가입한 후 첫 며칠이나 몇 주 동안 이탈할 가능성이 큽니다. 이 시점에 고객이 서비스 가치를 빠르게 이해하지 못하거나 초기 설정 과정이 복잡하면 이탈할 확률이 높습니다.
* **무료 체험 기간 종료 시점**
  * 많은 서비스가 무료 체험 기간을 제공합니다. 이 체험 기간이 끝나고 유료 구독으로 전환되는 시점에서 많은 사용자가 이탈할 수 있습니다. 이는 사용자가 서비스 가치를 충분히 느끼지 못했거나, 가격이 부담스러울 때 발생합니다.
* **첫 결제 시점**
  * 첫 결제를 할 때 사용자는 서비스를 계속 이용할지 결정합니다. 결제 과정이 번거롭거나, 기대했던 가치 대비 가격이 높다고 느낄 때 이탈할 수 있습니다.

{% hint style="info" %}
핵클을 활용하여 사용자의 이탈 시점을 확인하고, 이탈 사용자 [코호트](/cohort/cohort)를 만들어보세요.
{% endhint %}

### 1. 퍼널 차트에서 만들기

***특정 기간 내에 이탈한 사용자*****를 코호트로 만들고 싶을때 활용할 수 있는 방법입니다.**

{% stepper %}
{% step %}
**퍼널 차트 설정**

데이터 분석에서 원하는 이벤트를 설정하여 퍼널 차트를 설정해주세요. [퍼널 분석 더 알아보기](/data/chart-types/funnel-analysis)
{% endstep %}

{% step %}
**이탈 영역에서 코호트 저장**

사용자가 이탈한 영역을 호버하여 해당 코호트를 저장해주세요.

![](/files/67yrLjg0nW16w6bZ3G5w)
{% endstep %}

{% step %}
**코호트 저장**

해당 조건으로 코호트를 저장할 수 있습니다.

![](/files/m9Gga8fAMnSZB8dBlMgC)
{% endstep %}

{% step %}
**코호트 상세 사용자 확인**

\[확인하러 가기]를 클릭하여 코호트 내 상세 사용자를 확인할 수 있습니다.

![](/files/2MQPaUL9OHYY2E3n7Eaz)
{% endstep %}

{% step %}
**기간별 이탈 사용자 코호트 생성**

직접 선택한 기간 내에 이탈한 사용자의 코호트를 만들 수 있습니다.

![](/files/oTEFRrZUNooyOJpoOxrC)
{% endstep %}
{% endstepper %}

{% hint style="info" %}
Tip. 상황에 따라 알맞은 코호트를 만들어 활용해보세요
{% endhint %}

특정 조건에 해당하는 사용자 코호트를 만들고 싶으시면, 코호트 규칙의 **기간**을 수정해보세요.

* 최근 N일 이내 이탈한 사용자 코호트를 만들면 기간 동안 조건에 해당하는 사용자가 새롭게 업데이트됩니다.
  * 코호트 상세 화면의 연결 관리 탭에서 업데이트 주기를 설정할 수 있어요. (일별, 시간별)

![](/files/zUhys80hLsFZMgG61BeD)

### 2. 직접 만들기

코호트 상세 화면에서 원하는 조건을 직접 설정하여 코호트를 만들 수 있습니다.

#### 최근 14일 이내 미접속 사용자

{% stepper %}
{% step %}
**이벤트 조건 설정**

조건 설정 영역에서 `이벤트로 시작하기`를 눌러주세요.
{% endstep %}

{% step %}
**활성 사용자 기준 설정**

`Any Event` 를 최근 90일 이내 발생시킨 사용자를 설정합니다.
{% endstep %}

{% step %}
**미접속 사용자 필터링**

규칙을 추가하여 홈 진입을 측정할 수 있는 이벤트 `view_home` 을 선택한 뒤, 1번 이상 **발생시키지 않은** 사용자로 설정합니다.

![](/files/m0LcpjQFvqcXTpnLfUG0)
{% endstep %}
{% endstepper %}

#### 무료 체험 종료 후 이탈한 사용자

{% stepper %}
{% step %}
**퍼널 조건 설정**

조건 설정 영역에서 `퍼널로 시작하기`를 눌러주세요.
{% endstep %}

{% step %}
**멤버십 구독 사용자 설정**

무료 체험 기간 내에 멤버십을 구독한 사용자를 설정합니다. 멤버십 구독을 측정할 수 있는 이벤트 `membership_subscription`를 선택해주세요.
{% endstep %}

{% step %}
**미결제 이탈 사용자 필터링**

단계를 추가하여 결제 완료를 측정할 수 있는 이벤트 `complete_purchase` 를 선택한 뒤, **발생시키지 않은** 사용자로 설정합니다.

![](/files/n6D4y6Ev6DmoxgUMHfgn)
{% endstep %}
{% endstepper %}

이탈 사용자의 서비스 재진입율을 개선하기 위해 어떻게 해야할지 막막하신가요?

다음 문서를 통해 사용자의 상황에 맞는 CRM 마케팅을 진행해보세요!


# Step 2. 이탈 사용자에게 재방문 푸시 메시지 보내기

### 서비스 또는 장바구니 이탈 고객이 늘었다면

서비스에 더 이상 진입하지 않거나, 주문서에서 이탈한 고객이 있다면 해당 사용자에게 보내는 푸시만으로도 구매율을 높일 수 있어요.\
이전 단계에서 코호트를 만드셨다면, 해당 코호트를 사용해서 푸시메시지를 보내보세요.

![](/files/KttQn801XgV8f4nckhxT)

{% stepper %}
{% step %}
**푸시 메시지 화면으로 이동하고, 생성하기 버튼을 누르세요.**

![](/files/WxghMJgc7VX1img6tDHc)
{% endstep %}

{% step %}
**푸시 메시지로 보낼 내용을 설정하세요.**

주문서에서 이탈한 사용자에게 할인 쿠폰을 발급하는 내용의 푸시 메시지를 보내보겠습니다.

![](/files/mvReWARjkmjUiSrgwdJY)

<table><thead><tr><th width="212.3359375">종류</th><th>제목</th><th>본문</th></tr></thead><tbody><tr><td>1. 할인 쿠폰 / 프로모션 제공</td><td>특별 할인 쿠폰이 도착했어요!</td><td>(광고)지금 쿠폰 다운로드하면 내 장바구니에 있는 제품 50%까지 할인!</td></tr><tr><td></td><td>재방문 감사 쿠폰을 확인해보세요 :)</td><td>(광고)<code>000</code>님만을 위한 특별한 할인 혜택! 지금 로그인해서 쿠폰을 확인해보세요.</td></tr><tr><td></td><td>다시 돌아오신다면, 20% 할인 쿠폰을 드려요!</td><td>(광고)오래 기다리셨죠? 지금 재방문하면 <code>20</code>% 할인 쿠폰을 드려요. 지금 바로 사용해보세요!</td></tr><tr><td></td><td>재방문 감사 쿠폰을 받으세요!</td><td>(광고)다시 돌아와 주셔서 감사해요! 특별 할인 쿠폰을 사용해보세요.</td></tr><tr><td></td><td>단 3일! 한정 프로모션 놓치지 마세요</td><td>(광고)<code>00</code>시간 동안만 진행되는 특별 할인 행사, 지금 바로 확인하세요!</td></tr><tr><td></td><td>이번 주말 한정 특별 프로모션!</td><td>(광고)이번 주말에만 제공되는 특별 프로모션! 놓치지 말고 참여하세요!</td></tr><tr><td>2. 신규 기능 업데이트 알림</td><td><code>000</code>에서 사용할 수 있는 새로운 기능이 업데이트 되었어요</td><td>새롭게 업데이트된 기능으로 더욱 편리해진 서비스를 이용해보세요!</td></tr><tr><td>3. 생일/ 기념일 축하 및 혜택</td><td><code>000</code>님 생일 축하드려요! 특별 혜택이 기다리고 있습니다.</td><td>(광고)생일을 맞아 저희가 준비한 특별 혜택을 받아보세요! 지금 바로 확인하세요.</td></tr><tr><td></td><td><code>000</code>고객님의 특별한 날을 축하드립니다!</td><td>(광고)저희가 특별 혜택을 준비했어요.</td></tr><tr><td>4. 추천 상품 및 서비스 알림</td><td>맞춤형 추천 상품이 도착했습니다</td><td>(광고)<code>000</code>고객님을 위해 준비한 <code>추천 상품</code>을 만나보세요! 지금 확인하세요.</td></tr><tr><td></td><td><code>000</code>고객님께 어울리는 상품을 찾았습니다!</td><td>(광고)<code>000</code>고객님을 위해 특별히 추천하는 <code>상품</code>을 확인하세요! 지금 바로 확인해보세요.</td></tr></tbody></table>

{% content-ref url="/pages/piG9u8nlnsFuHnvkBqig" %}
[푸시 메시지 추천 템플릿](/crm-marketing/push-message-guide/recommended-templates)
{% endcontent-ref %}

**개인화 변수 추가하기**

개인화된 마케팅은 고객의 관심을 끌고 참여를 유도하는 데 효과적입니다.\
맞춤형 콘텐츠와 제안을 통해 고객은 더 많은 관심을 가지게 되고, 이는 참여도를 높이는 결과로 이어져요.

사용자에게 맞춤형 메시지를 보내 푸시 메시지의 오픈 전환율을 올려보세요.

![](/files/arxrJh9YyxYOn6zdZRjg)

<table><thead><tr><th width="160.15234375">종류</th><th>메시지</th></tr></thead><tbody><tr><td>이전 구매 기반</td><td>[고객 이름]님, 이전에 구매하신 [구매한 상품]을 좋아하셨나요?<br>지금 [관련 상품]을 할인된 가격으로 만나보세요!</td></tr><tr><td>장바구니 기반</td><td>[고객 이름]님, 장바구니에 담아두신 [상품명]이 아직 기다리고 있어요!<br>지금 구매하시면 할인 혜택을 드려요.</td></tr><tr><td>맞춤형 추천</td><td>[고객 이름]님, 당신이 좋아할 만한 새로운 상품을 준비했어요!<br>[추천 상품]을 확인하고 할인 혜택을 받아보세요.</td></tr><tr><td>특별 혜택 제공</td><td>[고객 이름]님, 저희가 준비한 특별 혜택을 놓치지 마세요!<br>지금 재방문하시면 다음 구매에 사용할 수 있는 [할인 금액/비율]% 할인 쿠폰을 드립니다.</td></tr></tbody></table>
{% endstep %}

{% step %}
**푸시 메시지를 보낼 코호트를 선택하세요**

이전 단계에서 생성한 코호트를 선택해주세요.

![](/files/SG8aWdpVfi9I9lC9XjlA)

{% content-ref url="/pages/ECrTvA3UZnGxcsMDFJRo" %}
[Step 1. 이탈 사용자 찾아보기](/use-cases/bounced-users)
{% endcontent-ref %}
{% endstep %}

{% step %}
**푸시 토큰으로 테스트를 미리 확인해보세요**

테스트 발송 팝업에 플랫폼을 선택하고, 테스트 메시지를 받아볼 기기의 푸시 토큰 값을 입력하여 고객이 실제로 받아 볼 푸시를 먼저 확인해볼 수 있어요.

![](/files/ZlgYkfLi3HU1BrKhZZWg)
{% endstep %}

{% step %}
**푸시 발송하기**

여기까지 모든 설정을 마치셨다면 설정한 코호트를 대상으로 메시지를 발송할 수 있습니다.\
주문서에서 이탈한 사용자가 특정 행동을 했을때 맞춤형 메시지를 보내 서비스 재진입율과 구매전환율을 높일 수 있습니다.

**장바구니에 상품을 추가했지만, 구매하지 않은 경우 메시지 발송**

사용자가 장바구니에서 이탈한 경우 개인화 메시지를 알맞은 시간에 전송하여 재방문을 유도할 수 있습니다.

* 발송 유형 중 **이벤트 기반**을 선택하세요.
* 시작 이벤트로 장바구니 담기를 측정할 수 있는 이벤트 `add_to_cart` 를 선택해주세요.
* 대기 시간에서 시작 이벤트가 발생하고 메시지가 발송될 시간 텀 (ex. 24시간) 을 설정해주세요.

![](/files/YhKGAHhb2T489utPGsKg)

**주말마다 특별 할인 쿠폰 메시지 발송**

결제 단계에서 이탈한 사용자에게 정해진 시간에 특별 할인 쿠폰 메시지를 전송하여 구매 전환율을 개선해보세요.

* 발송 유형 중 **스케줄 기반**을 눌러 **반복 발송**을 선택해주세요.
* 주말에 사용할 수 있는 쿠폰을 발급 메시지를 매주 금요일 저녁에 발송해보겠습니다.
* 메시지를 반복적으로 보낼 주기를 선택하고, 발송할 요일과 시간을 설정해주세요.

![](/files/Ps1oTmfOFnnZSFBwlGQP)

{% content-ref url="/pages/wG6ahDyBTPCCOaMxGjg9" %}
[푸시 메시지 발송하기](/crm-marketing/push-message-guide/send)
{% endcontent-ref %}
{% endstep %}
{% endstepper %}

푸시 메시지를 확인한 사용자가 서비스에 다시 돌아왔나요?

핵클을 활용하여 단순 방문이 아니라 이후 전환율까지 개선할 수 있는 방법을 추천드려요!


# Step 3. 재방문 사용자에게 쿠폰 발급 인앱 메시지 노출하기

### 다시 돌아온 고객을 잃지 않으려면?

이탈 사용자에게 푸시 메시지를 보내 사용자가 서비스에 다시 진입했나요?

새로운 고객을 획득하는 비용은 기존 고객을 유지하는 비용보다 훨씬 높으며 이탈한 사용자를 다시 활성화하는 것은 마케팅 비용을 절감할 수 있는 효율적인 방법입니다. 다시 돌아온 사용자를 놓치지 않고, 활성화하기 위해 다른 혜택이나 정보를 제공하는 추가 메시지를 전달하여 기타 서비스 전환율을 높여보세요!

#### 아래 시나리오를 바탕으로 인앱 메시지를 노출하여 전환율을 높여봐요

> 1. 장바구니 이탈 사용자에게 푸시 메시지를 보냈고,
> 2. 해당 사용자들이 푸시를 눌러 서비스에 진입한 경우,
> 3. 쿠폰을 발급하여 구매전환율을 개선하고 싶어요.
> 4. 이때 어떤 메시지가 가장 효과가 좋은지 쿠폰 발급 인앱 메시지에 대한 [A/B (MAB) 테스트](/ab-test/mab-test/create-mab-test)를 해보고 싶어요.<br>

![](/files/LTM05RC6ZJN1TrBNfi1x)

#### 가장 좋은 시안으로 효과를 극대화해보세요!

어떤 문구가 가장 효과가 좋을지 궁금하다면, 인앱 메시지 A/B테스트를 통해 알아볼 수 있습니다.

{% content-ref url="/pages/3o5GFarW27yqMDSa5Kkj" %}
[인앱 메시지 A/B 테스트](/crm-marketing/in-app-message-guide/in-app-message-ab-test)
{% endcontent-ref %}

{% stepper %}
{% step %}
**테스트할 시안을 제작합니다.**

디자인 리소스가 없다면, 핵클에서 준비한 인앱 메시지 이미지 템플릿을 사용할 수 있어요.

{% hint style="info" %}
여러 시안을 준비할 시간이 부족하다면 간단하게 버튼명만 바꿔서 사용자들의 반응을 살펴보세요!
{% endhint %}

{% content-ref url="/pages/xrSeKU9BSGYjQjzdFYtq" %}
[인앱 메시지 이미지 템플릿](/crm-marketing/in-app-message-guide/create-campaign/in-app-message-template)
{% endcontent-ref %}
{% endstep %}

{% step %}
**인앱 메시지를 생성해주세요.**

![](/files/oxBMxXvg2pSQexmynNyX)

**+** 버튼을 눌러 테스트 할 시안을 세팅합니다.\
실험 설정에 대한 구체적인 내용은 아래의 Docs를 참고해주세요!

푸시를 통해 재진입한 사용자에게 아래의 세가지 문구를 노출하여 더 효과가 좋은 메시지를 찾아보겠습니다.

* 그룹 A : 긴급성 강조
* 그룹 B : 혜택 강조
* 그룹 C : 개인화된 접근

![](/files/exPP5qzFVDQ5FTJ9Di3h)
{% endstep %}

{% step %}
**인앱 메시지를 노출할 화면과 대상을 설정해주세요.**

* 노출 설정에서 인앱 메시지가 노출될 이벤트를 선택해주세요.
  * 푸시를 통해 진입한 사용자가 홈에 진입했을때 `view_home` 인앱 메시지를 노출합니다.
* 대상에서 인앱 메시지를 노출할 사용자를 선택해주세요. [핵클 활용팁 - 이탈 사용자 찾아보기](https://dash.readme.com/project/hackle/v4.1.0/docs/%EC%9D%B4%ED%83%88-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%B0%BE%EC%95%84%EB%B3%B4%EA%B8%B0)에서 생성한 코호트를 등록합니다.

{% hint style="info" %}
푸시 메시지 설정시 터치 액션의 링크와 인앱 메시지가 노출되는 화면이 일치하면 더 좋습니다.

실제 고객에게 어떻게 보여지는지 테스트를 해보고 싶다면, 테스트 디바이스에 원하는 식별자 값을 입력해보세요.
{% endhint %}

![](/files/BATfjdGvLF8UU96gTQOU)
{% endstep %}

{% step %}
**캠페인을 시작합니다.**

인앱 메시지의 성과를 확인해보세요. 전환율이 높은 시안으로 테스트를 종료하여, 반응이 가장 좋은 메시지를 노출할 수 있습니다.

{% content-ref url="/pages/GOKGrPrNAAe1kSqQuLq5" %}
[인앱 메시지 캠페인 성과 측정하기](/crm-marketing/in-app-message-guide/analyze-in-app-message)
{% endcontent-ref %}

![](/files/5gescSuDn5oLug7Nb0Gm)
{% endstep %}
{% endstepper %}

상황에 맞는 CRM 마케팅을 진행했다면, 이제 사용자를 분석하고 다음의 Action Item을 도출해보세요!


# Developer Guide

한 번의 SDK 연동으로 핵클이 제공하고 있는 모든 기능을 사용해보실 수 있습니다.

{% columns %}
{% column width="41.66666666666667%" %}
**Quick Start**

SDK를 설치하고, A/B 테스트 그룹을 분배하는 것까지 간단하게 완료할 수 있습니다.

<a href="/pages/bq9qVTIMR8DGPLYU0UtX" class="button secondary">Get Started →</a>
{% endcolumn %}

{% column width="58.33333333333333%" %}

```javascript
// SDK 초기화
const hackleClient = Hackle.createInstance("YOUR_SDK_KEY");

// A/B 테스트 그룹 분배
const variation = hackleClient.variation(EXPERIMENT_KEY, userId);
if (variation === "A") {
  // 그룹 A
} else if (variation === "B") {
  // 그룹 B
}
```

{% endcolumn %}
{% endcolumns %}

{% columns %}
{% column %}
**지원 SDK**

핵클은 다양한 플랫폼 SDK를 제공합니다.
{% endcolumn %}

{% column %}

<p align="right"><a href="/pages/Sjnvwr2AesQNL638eysb" class="button secondary">모든 SDK 확인</a></p>
{% endcolumn %}
{% endcolumns %}

<table data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-cover data-type="image">Cover image</th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><i class="fa-android">:android:</i></td><td>Android SDK</td><td></td><td><a href="/pages/wq9wbNk6qIyD5rwHAAd6">/pages/wq9wbNk6qIyD5rwHAAd6</a></td></tr><tr><td><i class="fa-apple">:apple:</i></td><td>iOS SDK</td><td></td><td><a href="/pages/X2mKRmkyMPA7w2NYYHvV">/pages/X2mKRmkyMPA7w2NYYHvV</a></td></tr><tr><td><i class="fa-react">:react:</i></td><td>React SDK</td><td></td><td><a href="/pages/AqWeW0XpuWGXtOenWvvF">/pages/AqWeW0XpuWGXtOenWvvF</a></td></tr></tbody></table>

{% hint style="info" %}
카페 24를 사용중이신가요?

[카페 24 연동 가이드](/cafe-24/integration)를 바로 확인해보세요.
{% endhint %}

{% hint style="success" %}
궁금한 사항이 있을까요?

연동 과정에서 나오는 질문은 [핵클 슬랙 커뮤니티](https://join.slack.com/t/hackle-community/shared_invite/zt-1awrnygsh-U8VCHwN06ZDTF9yAzik5SA)에 언제든지 문의하세요.
{% endhint %}


# 빠른 시작

핵클 SDK를 연동하여 A/B 테스트를 실행하고 이벤트를 수집하는 방법을 안내합니다.

{% hint style="info" %}
SDK 키는 핵클 대시보드 > **설정 > SDK 연동 정보**에서 확인할 수 있습니다.
{% endhint %}

{% tabs %}
{% tab title="Android" %}
{% stepper %}
{% step %}
**SDK 설치**

`build.gradle`에 의존성을 추가합니다.

```gradle
repositories {
    mavenCentral()
}

dependencies {
    implementation 'io.hackle:hackle-android-sdk:2+'
}
```

{% endstep %}

{% step %}
**SDK 초기화**

```kotlin
import io.hackle.android.Hackle
import io.hackle.android.initialize

Hackle.initialize(applicationContext, "YOUR_APP_SDK_KEY") {
    // SDK 초기화 완료
}
```

{% endstep %}

{% step %}
**A/B 테스트 분배**

실험 키를 전달하여 사용자의 테스트 그룹을 확인합니다.

```kotlin
val hackleApp = Hackle.app()
val variation = hackleApp.variation(EXPERIMENT_KEY)

if (variation == Variation.A) {
    // 그룹 A
} else if (variation == Variation.B) {
    // 그룹 B
}
```

{% endstep %}

{% step %}
**이벤트 전송**

사용자 행동 이벤트를 전송합니다.

```kotlin
hackleApp.track("EVENT_KEY")
```

{% endstep %}
{% endstepper %}

각 단계의 상세 옵션은 [Android SDK 문서](/development-guide/android)를 참고하세요.
{% endtab %}

{% tab title="iOS" %}
{% stepper %}
{% step %}
**SDK 설치**

Swift Package Manager 또는 CocoaPods로 설치합니다.

**Swift Package Manager**

```
https://github.com/hackle-io/hackle-ios-sdk.git
```

**CocoaPods**

```ruby
pod 'Hackle'
```

{% endstep %}

{% step %}
**SDK 초기화**

```swift
import Hackle

Hackle.initialize(sdkKey: "YOUR_APP_SDK_KEY") {
    // SDK 초기화 완료
}
```

{% endstep %}

{% step %}
**A/B 테스트 분배**

실험 키를 전달하여 사용자의 테스트 그룹을 확인합니다.

```swift
let hackleApp = Hackle.app()
let variation = hackleApp.variation(experimentKey: EXPERIMENT_KEY)

if variation == "A" {
    // 그룹 A
} else if variation == "B" {
    // 그룹 B
}
```

{% endstep %}

{% step %}
**이벤트 전송**

사용자 행동 이벤트를 전송합니다.

```swift
hackleApp.track(eventKey: "EVENT_KEY")
```

{% endstep %}
{% endstepper %}

각 단계의 상세 옵션은 [iOS SDK 문서](/development-guide/ios)를 참고하세요.
{% endtab %}

{% tab title="JavaScript" %}
{% stepper %}
{% step %}
**SDK 설치**

npm으로 설치합니다.

```bash
npm install @hackler/javascript-sdk
```

{% endstep %}

{% step %}
**SDK 초기화**

```javascript
import * as Hackle from "@hackler/javascript-sdk";

const hackleClient = Hackle.createInstance("YOUR_BROWSER_SDK_KEY");
```

{% endstep %}

{% step %}
**A/B 테스트 분배**

실험 키를 전달하여 사용자의 테스트 그룹을 확인합니다.

```javascript
hackleClient.onReady(function () {
    const variation = hackleClient.variation(EXPERIMENT_KEY);

    if (variation === "A") {
        // 그룹 A
    } else if (variation === "B") {
        // 그룹 B
    }
});
```

{% endstep %}

{% step %}
**이벤트 전송**

사용자 행동 이벤트를 전송합니다.

```javascript
hackleClient.track({ key: "EVENT_KEY" });
```

{% endstep %}
{% endstepper %}

각 단계의 상세 옵션은 [JavaScript SDK 문서](/development-guide/javascript)를 참고하세요.
{% endtab %}
{% endtabs %}


# SDK

핵클 SDK를 이용해 핵클에서 제공하는 다양한 기능을 손쉽게 서비스에 통합할 수 있습니다.

### Client SDK

<table data-view="cards"><thead><tr><th></th><th></th><th></th><th></th><th data-hidden data-card-cover data-type="image">Cover image</th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><i class="fa-android">:android:</i></td><td>Android SDK</td><td><img src="https://img.shields.io/maven-central/v/io.hackle/hackle-android-sdk" alt="" data-size="original"></td><td><ul><li><a href="https://github.com/hackle-io/hackle-android-sdk">Github</a></li><li><a href="https://github.com/hackle-io/hackle-android-sdk/releases">Release</a></li></ul></td><td></td><td><a href="/pages/wq9wbNk6qIyD5rwHAAd6">/pages/wq9wbNk6qIyD5rwHAAd6</a></td></tr><tr><td><i class="fa-apple">:apple:</i></td><td>iOS SDK</td><td><img src="https://img.shields.io/github/v/release/hackle-io/hackle-ios-sdk?label=spm" alt=""> <img src="https://img.shields.io/cocoapods/v/Hackle" alt=""></td><td><ul><li><a href="https://github.com/hackle-io/hackle-ios-sdk">Github</a></li><li><a href="https://github.com/hackle-io/hackle-ios-sdk/releases">Release</a></li></ul></td><td></td><td><a href="/pages/X2mKRmkyMPA7w2NYYHvV">/pages/X2mKRmkyMPA7w2NYYHvV</a></td></tr><tr><td><i class="fa-js">:js:</i></td><td>JavaScript SDK</td><td><img src="https://img.shields.io/npm/v/%40hackler%2Fjavascript-sdk" alt=""></td><td><ul><li><a href="https://www.npmjs.com/package/@hackler/javascript-sdk">npm</a></li><li><a href="https://hackle-io.github.io/hackle-javascript-sdk/documents/release-javascript-sdk.html">ChangeLog</a></li></ul></td><td></td><td><a href="/pages/sx8wtBKW4Oqe9uDjzQ7Y">/pages/sx8wtBKW4Oqe9uDjzQ7Y</a></td></tr><tr><td><i class="fa-react">:react:</i></td><td>React SDK</td><td><img src="https://img.shields.io/npm/v/%40hackler%2Freact-sdk" alt=""></td><td><ul><li><a href="https://www.npmjs.com/package/@hackler/react-sdk">npm</a></li><li><a href="https://hackle-io.github.io/hackle-javascript-sdk/documents/release-react-sdk.html">ChangeLog</a></li></ul></td><td></td><td><a href="/pages/AqWeW0XpuWGXtOenWvvF">/pages/AqWeW0XpuWGXtOenWvvF</a></td></tr><tr><td><i class="fa-react">:react:</i></td><td>React Native SDK</td><td><img src="https://img.shields.io/npm/v/%40hackler%2Freact-native-sdk" alt=""></td><td><ul><li><a href="https://www.npmjs.com/package/@hackler/react-native-sdk">npm</a></li><li><a href="https://hackle-io.github.io/hackle-react-native-sdk/documents/CHANGELOG.html">ChangeLog</a></li></ul></td><td></td><td><a href="/pages/lYZqDcwULsR56dc949LH">/pages/lYZqDcwULsR56dc949LH</a></td></tr><tr><td><i class="fa-flutter">:flutter:</i></td><td>Flutter SDK</td><td><img src="https://img.shields.io/pub/v/hackle" alt=""></td><td><ul><li><a href="https://pub.dev/packages/hackle">pub.dev</a></li><li><a href="https://pub.dev/packages/hackle/changelog">ChangeLog</a></li></ul></td><td></td><td><a href="/pages/K8nkEx69lBExq0a4DZYr">/pages/K8nkEx69lBExq0a4DZYr</a></td></tr><tr><td><i class="fa-unity">:unity:</i></td><td>Unity SDK</td><td><img src="https://img.shields.io/github/v/release/hackle-io/unity-sdk" alt=""></td><td><ul><li><a href="https://github.com/hackle-io/unity-sdk">Github</a></li><li><a href="https://github.com/hackle-io/unity-sdk/releases">Release</a></li></ul></td><td></td><td><a href="/pages/wvO4DhkJTRuS47UoP5vw">/pages/wvO4DhkJTRuS47UoP5vw</a></td></tr></tbody></table>

### Server SDK

<table data-view="cards"><thead><tr><th></th><th></th><th></th><th></th><th data-hidden data-card-cover data-type="image">Cover image</th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><i class="fa-java">:java:</i></td><td>Java/Kotlin SDK</td><td><img src="https://img.shields.io/maven-central/v/io.hackle/hackle-server-sdk" alt=""></td><td><ul><li><a href="https://github.com/hackle-io/hackle-java-sdk">Github</a></li><li><a href="https://github.com/hackle-io/hackle-java-sdk/releases">Release</a></li></ul></td><td></td><td><a href="/pages/PgjjQYCDXjOY2guT7ANE">/pages/PgjjQYCDXjOY2guT7ANE</a></td></tr><tr><td><i class="fa-node-js">:node-js:</i></td><td>Node.js SDK</td><td><img src="https://img.shields.io/npm/v/%40hackler%2Fjavascript-sdk" alt=""></td><td><ul><li><a href="https://www.npmjs.com/package/@hackler/javascript-sdk">npm</a></li><li><a href="https://hackle-io.github.io/hackle-javascript-sdk/documents/release-javascript-sdk.html">ChangeLog</a></li></ul></td><td></td><td><a href="/pages/Ip1fxvyA6ENMnwozPDGA">/pages/Ip1fxvyA6ENMnwozPDGA</a></td></tr><tr><td><i class="fa-python">:python:</i></td><td>Python SDK</td><td><img src="https://img.shields.io/pypi/v/hackle-sdk" alt=""></td><td><ul><li><a href="https://pypi.org/project/hackle-sdk/">pypi</a></li></ul></td><td></td><td><a href="/pages/RF21vHjedlXfyzvtou2Z">/pages/RF21vHjedlXfyzvtou2Z</a></td></tr><tr><td><i class="fa-golang">:golang:</i></td><td>Go SDK</td><td><img src="https://img.shields.io/github/v/tag/hackle-io/hackle-go-sdk" alt=""></td><td><ul><li><a href="https://github.com/hackle-io/hackle-go-sdk">Github</a></li><li><a href="https://github.com/hackle-io/hackle-go-sdk/releases">Release</a></li></ul></td><td></td><td><a href="/pages/8nPffHFrQ0jsWAJaiJTD">/pages/8nPffHFrQ0jsWAJaiJTD</a></td></tr><tr><td><i class="fa-php">:php:</i></td><td>PHP SDK</td><td><img src="https://img.shields.io/packagist/v/hackle/hackle-php-sdk" alt=""></td><td><ul><li><a href="https://github.com/hackle-io/hackle-php-sdk">Github</a></li><li><a href="https://github.com/hackle-io/hackle-php-sdk/releases">Release</a></li></ul></td><td></td><td><a href="/pages/JjqaCt4zy1G5tAAKu8Ze">/pages/JjqaCt4zy1G5tAAKu8Ze</a></td></tr><tr><td><i class="fa-gem">:gem:</i></td><td>Ruby SDK</td><td><img src="https://img.shields.io/gem/v/hackle-ruby-sdk" alt=""></td><td><ul><li><a href="https://github.com/hackle-io/hackle-ruby-sdk">Github</a></li><li><a href="https://github.com/hackle-io/hackle-ruby-sdk/releases">Release</a></li></ul></td><td></td><td><a href="/pages/8ZqwlHeYsqDPrJSDU5ck">/pages/8ZqwlHeYsqDPrJSDU5ck</a></td></tr></tbody></table>

### 기능별 SDK 최소 지원 버전

{% tabs %}
{% tab title="Android" %}

| 기능                  | 최소 버전   |
| ------------------- | ------- |
| 원격 평가               | 4.0.0+  |
| 인앱 HTML 개인화 강화      | 2.67.0+ |
| 인앱 HTML             | 2.66.0+ |
| HackleSessionPolicy | 2.65.0+ |
| opt-out tracking    | 2.65.0+ |
| 인앱 메시지 TimeTable    | 2.63.0+ |
| 웹앱 페이지 이벤트 자동 수집    | 2.62.0+ |
| 앱 자동 수집 이벤트         | 2.61.0+ |
| 인앱 메시지 딜레이          | 2.59.0+ |
| 푸시 이미지 / 푸시 채널      | 2.58.0+ |
| 웹뷰 브라우저 프로퍼티        | 2.58.0+ |
| 푸시 아이콘 변경           | 2.57.0+ |
| 마케팅 수신 동의           | 2.55.0+ |
| 푸시 메시지              | 2.33.0+ |
| 웹앱 연동               | 2.29.0+ |
| 인앱 메시지              | 2.24.0+ |
| 원격 구성               | 2.11.0+ |
| 파라미터 설정             | 2.9.0+  |
| 상호 배타적 설정           | 2.6.0+  |
| 타겟팅                 | 2.1.0+  |
| 데이터 세부 분석           | 2.0.0+  |
| 기능 플래그              | 2.0.0+  |
| {% endtab %}        |         |

{% tab title="iOS" %}

| 기능                  | 최소 버전   |
| ------------------- | ------- |
| 원격 평가               | 4.0.0+  |
| 인앱 HTML 개인화 강화      | 3.3.0+  |
| 인앱 HTML             | 3.2.0+  |
| HackleSessionPolicy | 3.1.0+  |
| opt-out tracking    | 3.1.0+  |
| 인앱 메시지 TimeTable    | 2.58.0+ |
| 웹앱 페이지 이벤트 자동 수집    | 2.57.0+ |
| 앱 자동 수집 이벤트         | 2.56.1+ |
| 인앱 메시지 딜레이          | 2.54.0+ |
| 푸시 이미지 / 푸시 채널      | 2.53.0+ |
| 웹뷰 브라우저 프로퍼티        | 2.53.0+ |
| 마케팅 수신 동의           | 2.50.0+ |
| 푸시 메시지              | 2.28.0+ |
| 웹앱 연동               | 2.27.0+ |
| 인앱 메시지              | 2.24.0+ |
| 원격 구성               | 2.11.0+ |
| 파라미터 설정             | 2.9.0+  |
| 상호 배타적 설정           | 2.6.0+  |
| 타겟팅                 | 2.0.0+  |
| 데이터 세부 분석           | 2.0.0+  |
| 기능 플래그              | 2.0.0+  |
| {% endtab %}        |         |

{% tab title="JavaScript" %}

| 기능                  | 최소 버전    |
| ------------------- | -------- |
| 원격 평가               | 12.0.0+  |
| 인앱 HTML 개인화 강화      | 11.56.0+ |
| 인앱 HTML             | 11.55.0+ |
| HackleSessionPolicy | 11.54.0+ |
| opt-out tracking    | 11.54.0+ |
| 인앱 메시지 TimeTable    | 11.52.0+ |
| 웹앱 페이지 이벤트 자동 수집    | 11.51.0+ |
| PC 환경 인앱 메시지 사이즈 조정 | 11.49.2+ |
| 인앱 메시지 딜레이          | 11.47.0+ |
| 웹뷰 브라우저 프로퍼티        | 11.46.0+ |
| 마케팅 수신 동의           | 11.45.0+ |
| 인앱 메시지              | 11.17.0+ |
| 원격 구성               | 11.7.3+  |
| 파라미터 설정             | 11.3.0+  |
| 상호 배타적 설정           | 3.5.0+   |
| 타겟팅                 | 2.1.0+   |
| 데이터 세부 분석           | 2.0.0+   |
| 기능 플래그              | 2.0.0+   |
| {% endtab %}        |          |

{% tab title="React" %}

| 기능                  | 최소 버전    |
| ------------------- | -------- |
| 원격 평가               | 12.0.0+  |
| 인앱 HTML 개인화 강화      | 11.56.0+ |
| 인앱 HTML             | 11.55.0+ |
| HackleSessionPolicy | 11.54.0+ |
| opt-out tracking    | 11.54.0+ |
| 인앱 메시지 TimeTable    | 11.52.0+ |
| 웹앱 페이지 이벤트 자동 수집    | 11.51.0+ |
| PC 환경 인앱 메시지 사이즈 조정 | 11.49.2+ |
| 인앱 메시지 딜레이          | 11.47.0+ |
| 웹뷰 브라우저 프로퍼티        | 11.46.0+ |
| 마케팅 수신 동의           | 11.45.0+ |
| 인앱 메시지              | 11.17.0+ |
| 원격 구성               | 11.7.3+  |
| 파라미터 설정             | 11.3.0+  |
| 상호 배타적 설정           | 3.5.0+   |
| 타겟팅                 | 2.1.0+   |
| 데이터 세부 분석           | 2.0.0+   |
| 기능 플래그              | 2.0.0+   |
| {% endtab %}        |          |

{% tab title="React Native" %}

| 기능                  | 최소 버전   |
| ------------------- | ------- |
| 인앱 HTML 개인화 강화      | 3.35.0+ |
| 인앱 HTML             | 3.33.0+ |
| HackleSessionPolicy | 3.32.0+ |
| opt-out tracking    | 3.32.0+ |
| 인앱 메시지 TimeTable    | 3.29.0+ |
| 웹앱 페이지 이벤트 자동 수집    | 3.28.0+ |
| 앱 자동 수집 이벤트         | 3.28.0+ |
| 웹앱 연동               | 3.28.0+ |
| 푸시 이미지 / 푸시 채널      | 3.26.0+ |
| 인앱 메시지 딜레이          | 3.26.0+ |
| 푸시 아이콘 변경           | 3.25.0+ |
| 마케팅 수신 동의           | 3.24.0+ |
| 푸시 메시지              | 3.10.0+ |
| 인앱 메시지              | 3.8.0+  |
| 원격 구성               | 3.3.0+  |
| 파라미터 설정             | 3.3.0+  |
| 상호 배타적 설정           | 3.3.0+  |
| 타겟팅                 | 2.0.0+  |
| 데이터 세부 분석           | 2.0.0+  |
| 기능 플래그              | 2.0.0+  |
| {% endtab %}        |         |

{% tab title="Flutter" %}

| 기능                  | 최소 버전   |
| ------------------- | ------- |
| 인앱 HTML 개인화 강화      | 2.31.0+ |
| 인앱 HTML             | 2.30.0+ |
| HackleSessionPolicy | 2.29.0+ |
| opt-out tracking    | 2.29.0+ |
| 인앱 메시지 TimeTable    | 2.27.0+ |
| 웹앱 페이지 이벤트 자동 수집    | 2.26.0+ |
| 앱 자동 수집 이벤트         | 2.25.1+ |
| 인앱 메시지 딜레이          | 2.23.0+ |
| 푸시 이미지 / 푸시 채널      | 2.23.0+ |
| 웹뷰 브라우저 프로퍼티        | 2.23.0+ |
| 푸시 아이콘 변경           | 2.22.0+ |
| 마케팅 수신 동의           | 2.20.0+ |
| 웹앱 연동               | 2.13.0+ |
| 푸시 메시지              | 2.8.0+  |
| 인앱 메시지              | 2.5.0+  |
| 원격 구성               | 모든 버전   |
| 파라미터 설정             | 모든 버전   |
| 상호 배타적 설정           | 모든 버전   |
| 타겟팅                 | 모든 버전   |
| 데이터 세부 분석           | 모든 버전   |
| 기능 플래그              | 모든 버전   |
| {% endtab %}        |         |

{% tab title="Unity" %}

| 기능                  | 최소 버전  |
| ------------------- | ------ |
| 원격 평가               | 미지원    |
| 인앱 HTML             | 미지원    |
| HackleSessionPolicy | 미지원    |
| opt-out tracking    | 미지원    |
| 인앱 메시지 TimeTable    | 미지원    |
| 웹앱 연동               | 미지원    |
| 푸시 메시지              | 미지원    |
| 인앱 메시지              | 미지원    |
| 원격 구성               | 1.4.0+ |
| 파라미터 설정             | 1.3.0+ |
| 상호 배타적 설정           | 1.2.0+ |
| 타겟팅                 | 모든 버전  |
| 데이터 세부 분석           | 모든 버전  |
| 기능 플래그              | 모든 버전  |
| {% endtab %}        |        |

{% tab title="Java/Kotlin" %}

| 기능           | 최소 버전  |
| ------------ | ------ |
| 원격 구성        | 2.9.0+ |
| 파라미터 설정      | 2.8.0+ |
| 상호 배타적 설정    | 2.6.0+ |
| 타겟팅          | 2.1.0+ |
| 데이터 세부 분석    | 2.0.0+ |
| 기능 플래그       | 2.0.0+ |
| {% endtab %} |        |

{% tab title="Python" %}

| 기능           | 최소 버전  |
| ------------ | ------ |
| 원격 구성        | 3.2.0+ |
| 파라미터 설정      | 3.1.0+ |
| 상호 배타적 설정    | 2.3.0+ |
| 타겟팅          | 2.1.0+ |
| 데이터 세부 분석    | 2.0.0+ |
| 기능 플래그       | 2.0.0+ |
| {% endtab %} |        |

{% tab title="Node.js" %}

| 기능           | 최소 버전   |
| ------------ | ------- |
| 원격 구성        | 11.5.0+ |
| 파라미터 설정      | 11.3.0+ |
| 상호 배타적 설정    | 3.5.0+  |
| 타겟팅          | 2.1.0+  |
| 데이터 세부 분석    | 2.0.0+  |
| 기능 플래그       | 2.0.0+  |
| {% endtab %} |         |

{% tab title="Go" %}

| 기능           | 최소 버전  |
| ------------ | ------ |
| 원격 구성        | 3.2.0+ |
| 파라미터 설정      | 3.1.0+ |
| 상호 배타적 설정    | 2.3.0+ |
| 타겟팅          | 2.1.0+ |
| 데이터 세부 분석    | 2.0.0+ |
| 기능 플래그       | 2.0.0+ |
| {% endtab %} |        |

{% tab title="PHP" %}

| 기능           | 최소 버전  |
| ------------ | ------ |
| 원격 구성        | 1.0.0+ |
| 파라미터 설정      | 1.0.0+ |
| 상호 배타적 설정    | 1.0.0+ |
| 타겟팅          | 1.0.0+ |
| 데이터 세부 분석    | 1.0.0+ |
| 기능 플래그       | 1.0.0+ |
| {% endtab %} |        |

{% tab title="Ruby" %}

| 기능            | 최소 버전  |
| ------------- | ------ |
| 원격 구성         | 2.0.0+ |
| 파라미터 설정       | 2.0.0+ |
| 상호 배타적 설정     | 2.0.0+ |
| 타겟팅           | 2.0.0+ |
| 데이터 세부 분석     | 2.0.0+ |
| 기능 플래그        | 2.0.0+ |
| {% endtab %}  |        |
| {% endtabs %} |        |


# 클라이언트 SDK와 서버 SDK

이 문서는 클라이언트 및 서버 측 SDK에 대해 설명하고 사용할 SDK 유형을 결정하는데 도움을 줍니다. SDK가 제공하는 기능을 클라이언트 또는 서버 중 어디에 구현하는 것이 적합할지 이해하는 것이 중요합니다.

<table><thead><tr><th width="158.19921875">유형</th><th>설명</th></tr></thead><tbody><tr><td>클라이언트 측</td><td><p>브라우저, 모바일 앱 등 사용자 디바이스에서 SDK 기능이 실행됩니다.</p><p><code>JavaScript</code>, <code>Android</code>, <code>iOS</code>, <code>React</code>, <code>React Native</code>, <code>Flutter</code>, <code>Unity</code> SDK 등이 해당됩니다.</p></td></tr><tr><td>서버 측</td><td><p>서비스를 제공하는 서버에서 SDK 기능이 실행됩니다.</p><p><code>Java/Kotlin</code>, <code>Python</code>, <code>Node.js</code>, <code>PHP</code>, <code>Ruby</code> SDK 등이 해당됩니다.</p></td></tr></tbody></table>

### 클라이언트 측 SDK

![](/files/3ppYJPm6G4G33JUOuAFy)

웹 브라우저, 모바일 앱 등 사용자의 디바이스에서 SDK의 기능이 실행되며 디바이스와 핵클 서버가 직접 통신합니다.

다음과 같은 경우 클라이언트 측 SDK의 사용을 권장합니다.

* 버튼 색상, 레이아웃 변경 등 시각적인 요소의 변경을 테스트하기 위해 테스트 그룹을 분배하는 경우
* 클릭, 스크롤 등 서버와 통신 없이 클라이언트에서 수행되는 사용자 이벤트를 전송해야 하는 경우
* 비즈니스 로직이 클라이언트에 집중되어 있는 경우

### 서버 측 SDK

![2264](/files/6UfhF1DkLQFs3kbB2f0F)

서버에서 SDK의 기능이 실행되며 서버 대 서버로 통신을 합니다.

다음과 같은 경우 서버 측 SDK의 사용을 권장합니다.

* 검색 알고리즘 개선, 추천 로직 변경 등 백엔드 시스템의 변경을 테스트하기 위해 테스트 그룹을 분배하는 경우
* 회원가입, 구매완료 등 서버에서 상태를 확정하는 사용자의 이벤트를 전송해야 하는 경우


# SDK에서 사용되는 키

핵클 SDK를 연동하고 활용하기 위해 개발자가 알아야 할 몇 가지 키(key)가 있습니다.

## SDK 키

SDK를 연동하기 위해 필요한 값으로, **SDK 초기화 단계**에서 사용합니다.

해당 값은 대시보드의 `SDK 연동 정보`에서 제공하고 있습니다.

![](/files/b7TKm67VNx6CPYSeHHab)

환경 및 개발 플랫폼에 따라 SDK 키 값이 다르므로, 이 점 유의하여 원하는 유형의 SDK 키 값을 선택합니다.

* 환경은 운영 환경(Production) 및 개발 환경(Development)을 의미합니다.
* 개발 플랫폼 별 SDK 키의 유형은 아래와 같이 분류할 수 있습니다.

| Server                                                                                                                                                                           | App/Browser                                                                                                                                                                                                          |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| <ul><li><code>Java / Kotlin</code></li><li><code>Node.js</code></li><li><code>Python</code></li><li><code>Go</code></li><li><code>PHP</code></li><li><code>Ruby</code></li></ul> | <ul><li><code>Android</code></li><li><code>iOS</code></li><li><code>JavaScript</code></li><li><code>React</code></li><li><code>React Native</code></li><li><code>Flutter</code></li><li><code>Unity</code></li></ul> |

## 실험 키

실험 키는 Experiment Key로도 불리며, 개별 A/B 테스트를 구분하기 위해 핵클에서 부여하는 값입니다.\
이 값은 **테스트 그룹 분배** 시 필요합니다.

대시보드의 A/B 테스트 메뉴에 진입한 경우 목록의 첫 번째 열에서 확인할 수 있으며, 각 테스트 상세 화면에서도 볼 수 있습니다.

![A/B 테스트 목록의 첫 번째 열에서 각 A/B 테스트의 실험 키를 확인할 수 있습니다.](/files/FGksed3W2VAT3wd7sBGL)

![A/B 테스트 목록에서 특정 실험을 선택했을 경우에도 실험 키를 확인할 수 있습니다.](/files/0yMrrbEmsC14uT3hwcbL)

## 기능 키

기능 키는 각 기능 플래그를 식별하는 키로, **기능 플래그 결정** 시 필요합니다.

대시보드의 기능 플래그 메뉴에 진입한 경우 목록의 첫 번째 열에서 확인할 수 있으며, 각 기능 플래그 상세 화면에서도 볼 수 있습니다.

![기능 플래그 목록의 첫 번째 열에서 각 기능 플래그의 기능 키를 확인할 수 있습니다.](/files/PAiyU7ICW0GGpgHUDKxh)

![기능 플래그 목록에서 특정 기능 플래그를 선택했을 경우에도 기능 키를 확인할 수 있습니다.](/files/cQIabWzYlpWDS1XLKYGK)

## 이벤트 키

이벤트 키는 각 이벤트를 식별하는 키로, **이벤트 키 전송** 시 필요합니다.

대시보드의 이벤트 메뉴를 통해 이벤트 목록을 볼 수 있으며, 해당 목록의 첫 번째 열에 이벤트 키가 있습니다.\
일부 이벤트 키는 핵클에서 제공하며, 용도에 따라 직접 생성하고 관리할 수 있습니다.

이벤트 키 생성에 대해서는 [이벤트 생성하기](/event-management/create-event) 문서를 참고 바랍니다.

![이벤트 리스트 첫 번째 열에서 이벤트 키를 확인할 수 있습니다.](/files/eIXljaxXBlOSu9aIPBij)


# 평가 방식

핵클 SDK는 사용자가 A/B 테스트의 어떤 그룹에 속하는지, 기능 플래그가 켜져 있는지, 어떤 원격 구성 값을 사용하는지를 **평가(Evaluation)** 과정을 통해 결정합니다.\
평가는 사용자 정보와 워크스페이스 설정 정보를 입력으로 받아 분배 결과를 출력합니다.

핵클 SDK는 평가를 수행하는 위치에 따라 두 가지 평가 방식을 제공합니다.\
평가 방식은 SDK 초기화 시 선택하며, 별도로 설정하지 않으면 로컬 평가로 동작합니다.

<table data-card-size="large" data-view="cards"><thead><tr><th></th><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><i class="fa-mobile">:mobile:</i></td><td><strong>로컬 평가</strong></td><td>SDK가 사용자 디바이스에서 직접 평가합니다.<br>기본 평가 방식입니다.</td><td><a href="/pages/TzEazcgoPvplv6yx9JR5">/pages/TzEazcgoPvplv6yx9JR5</a></td></tr><tr><td><i class="fa-server">:server:</i></td><td><strong>원격 평가</strong></td><td>핵클 서버가 미리 평가한 결과를 SDK가 조회합니다.</td><td><a href="/pages/SHlv75VEpmA22qytjROJ">/pages/SHlv75VEpmA22qytjROJ</a></td></tr></tbody></table>

### 두 방식의 차이

<table><thead><tr><th width="191.1796875">구분</th><th>로컬 평가</th><th>원격 평가</th></tr></thead><tbody><tr><td>평가 위치</td><td>사용자 디바이스 (SDK)</td><td>핵클 서버</td></tr><tr><td>대시보드 변경사항 반영</td><td><p>SDK 초기화 시,</p><p>SDK fetch 시</p></td><td>SDK 초기화 시,<br>사용자 정보가 업데이트 될 시</td></tr><tr><td>분배에 사용되는 정보</td><td>디바이스에 저장된 사용자 정보 기반</td><td>서버에 저장된 사용자 정보 기반</td></tr><tr><td>분배/결정 호출 시 동작</td><td>저장된 설정 정보를 기반으로<br>SDK가 직접 평가</td><td>저장된 평가 결과를 조회</td></tr><tr><td>레이턴시</td><td>네트워크 통신 없이 평가</td><td>사용자 정보가 바뀌지 않았다면 네트워크 통신 없이 처리<br>사용자 정보가 바뀌면 서버와 통신해 다시 평가</td></tr><tr><td>지원 SDK</td><td>모든 SDK</td><td>일부 클라이언트 SDK</td></tr></tbody></table>

{% hint style="success" %}
두 방식 모두 분배/결정 호출 시점에는 네트워크 호출이 발생하지 않으므로 속도 저하 없이 처리됩니다.

자세한 내용은 [SDK 레이턴시](/development-guide/sdk/sdk-latency) 문서를 참고 바랍니다.
{% endhint %}

{% hint style="info" %}

### 어떤 방식을 선택해야 하나요?

기본 평가 방식은 **로컬 평가**이며, 대부분의 경우 로컬 평가가 적합합니다.

* 앱 버전, 디바이스 OS, 클라이언트에서 설정한 사용자 속성 등 디바이스가 가진 정보로 타겟팅하는 경우
* 로그인 직후 화면처럼 사용자 정보가 바뀌는 시점에 분배 결과가 필요한 경우
* pagePath 등 화면 기반으로 타겟팅이 필요한 경우

핵클 서버에 저장된 사용자 정보를 기준으로 분배해야 하는 경우 **원격 평가**를 사용합니다.

* 서버에서 HTTP API를 이용해 사용자 속성을 업데이트하고 해당 정보로 타겟팅하는 경우
* 구독 정보, 멤버십 등급 등 핵클 서버에 1회성으로 속성을 업데이트하고 해당 정보로 타겟팅하는 경우
  {% endhint %}


# 로컬 평가

로컬 평가는 SDK가 사용자 디바이스에서 직접 평가를 수행하는 방식입니다.

평가를 위한 서버 통신이 없어 사용자 정보가 변경되어도 서버와 다시 동기화하지 않고 분배를 처리합니다.\
모든 핵클 SDK의 기본 평가 방식으로, 별도 설정 없이 사용할 수 있습니다.

### 기능 지원

로컬 평가는 디바이스에 저장된 사용자 정보를 기반으로 평가합니다.

{% hint style="info" %}
로컬 평가는 핵클 서버에 저장된 사용자 정보와 동기화하지 않습니다.

* 사용자 속성 기반 타겟팅을 사용하려면 필요한 속성이 디바이스 또는 브라우저에 저장되어 있어야 합니다.
* 로그인하는 경우 사용자 속성을 설정하는 것을 권장합니다.
* 앱에서 설정한 사용자 속성은 기기에 저장됩니다. 앱을 삭제하기 전까지 유지됩니다.
  {% endhint %}

<table><thead><tr><th width="469.87890625">기능</th><th align="center">로컬 평가</th><th align="center">원격 평가</th></tr></thead><tbody><tr><td>A/B 테스트 분배</td><td align="center">✅</td><td align="center">✅</td></tr><tr><td>기능 플래그 결정</td><td align="center">✅</td><td align="center">✅</td></tr><tr><td>원격 구성 확인</td><td align="center">✅</td><td align="center">✅</td></tr><tr><td>디바이스에 저장된 사용자 정보 기반 평가</td><td align="center">✅</td><td align="center">❌</td></tr><tr><td>서버에 저장된 사용자 정보 기반 평가</td><td align="center">❌</td><td align="center">✅</td></tr><tr><td>사용자 정보 변경 시 서버 통신 없이 재평가</td><td align="center">✅</td><td align="center">❌</td></tr></tbody></table>

### 동작 방식

SDK가 초기화 과정에서 A/B 테스트, 기능 플래그 등 워크스페이스의 설정 정보를 받아와 내부에 저장합니다.\
이후 분배가 필요한 시점에 저장된 설정 정보를 기반으로 SDK가 직접 평가합니다.

```mermaid
sequenceDiagram
    participant App as 클라이언트
    participant SDK as SDK (로컬 평가 엔진)
    participant Hackle as 핵클 서버

    App->>SDK: SDK 초기화
    SDK->>Hackle: 워크스페이스 설정 정보 요청
    activate Hackle
    Hackle-->>SDK: 워크스페이스 설정 정보
    deactivate Hackle
    Note over SDK: 설정 정보를 내부에 저장

    App->>SDK: 분배/결정 호출
    SDK->>SDK: 저장된 설정 정보로 직접 평가<br/>(네트워크 호출 없음)

    opt 사용자 정보 변경 (setUser, 속성 업데이트 등)
        App->>SDK: 사용자 정보 변경
        SDK->>SDK: 저장된 설정 정보로 다시 평가<br/>(서버와 다시 동기화할 필요 없음)
    end

    opt 설정 정보 갱신 (fetch 호출)
        App->>SDK: fetch 호출
        SDK->>Hackle: 최신 설정 정보 요청
        activate Hackle
        Hackle-->>SDK: 워크스페이스 설정 정보
        deactivate Hackle
        Note over SDK: 저장된 설정 정보 갱신
    end

```

1. SDK 초기화 시 핵클 서버로부터 A/B 테스트, 기능 플래그 등 워크스페이스의 설정 정보를 받아와 내부에 저장합니다.
2. 분배/결정 호출 시 저장된 설정 정보를 기반으로 SDK가 직접 평가합니다.\
   이 과정에서 네트워크 호출이 발생하지 않습니다.

{% hint style="success" %}
로컬 평가는 워크스페이스 설정 정보를 받아올 때를 제외하면 평가를 위한 서버 통신/네트워크 지연 없이 분배를 처리할 수 있습니다.
{% endhint %}

### 사용자 정보가 변경되는 경우

사용자 식별자가 변경되거나(`setUser`, `setUserId`, `setDeviceId`, `resetUser`) 사용자 속성이 업데이트되면 SDK 내부 저장소에 사용자 정보를 저장합니다.

A/B 테스트 등 함수가 호출되면 사용자 정보를 이용하여 실시간으로 분배처리를 합니다.

### 대시보드 변경사항 반영

로컬 평가는 SDK 내부에 저장된 설정 정보를 기반으로 평가합니다.\
분배/결정을 호출한 시점의 대시보드 설정이 아니라, **설정 정보를 받아온 시점**을 기반으로 평가합니다.

SDK에 저장된 설정 정보는 다음 시점에 갱신됩니다.

* SDK 초기화 시
* SDK의 `fetch` 함수 호출 시

{% hint style="warning" %}
대시보드에서 A/B 테스트의 트래픽 분배율을 조정하거나 기능 플래그를 켜고 끄더라도, SDK가 설정 정보를 다시 받아오기 전까지는 기존 설정 정보로 평가합니다.

변경된 설정을 반영해야 하는 경우 SDK를 다시 초기화하거나 `fetch` 함수를 호출해야 합니다.
{% endhint %}


# 원격 평가

{% hint style="warning" %}
서버 SDK는 원격 평가를 지원하지 않습니다.
{% endhint %}

원격 평가는 핵클 서버가 사용자에 대한 평가를 미리 수행하고, SDK는 그 결과를 조회하는 방식입니다.

핵클 서버에 저장된 사용자 정보를 기반으로 평가하므로, 서버에 저장된 최신 사용자 정보를 기준으로 분배해야 하는 경우 사용합니다.

### 원격 평가 사용하기

원격 평가는 SDK 초기화 시 `evaluationMode` 설정을 원격 평가로 지정하면 사용할 수 있습니다.\
플랫폼별 설정 방법은 [평가 방식](/development-guide/sdk/evaluation-mode)의 **설정 방법**을 참고 바랍니다.

{% hint style="info" %}
[기능 별 SDK 최소 지원 버전](/development-guide/sdk#sdk)에서 지원 SDK와 버전을 확인하세요.
{% endhint %}

### 기능 지원

원격 평가는 핵클 서버에 저장된 사용자 정보를 기반으로 평가합니다. 평가가 서버에서 수행되므로, 사용자 정보가 변경되면 서버와 통신해 다시 평가합니다.

{% hint style="info" %}
원격 평가를 사용하면 기기 또는 브라우저에 사용자 정보를 저장하지 않습니다.

기존에 로컬 평가를 사용하며 저장된 사용자 정보가 있는 경우 삭제 처리합니다.
{% endhint %}

<table><thead><tr><th width="470.140625">기능</th><th align="center">로컬 평가</th><th align="center">원격 평가</th></tr></thead><tbody><tr><td>A/B 테스트 분배</td><td align="center">✅</td><td align="center">✅</td></tr><tr><td>기능 플래그 결정</td><td align="center">✅</td><td align="center">✅</td></tr><tr><td>원격 구성 확인</td><td align="center">✅</td><td align="center">✅</td></tr><tr><td>디바이스에 저장된 사용자 정보 기반 평가</td><td align="center">✅</td><td align="center">❌</td></tr><tr><td>서버에 저장된 사용자 정보 기반 평가</td><td align="center">❌</td><td align="center">✅</td></tr><tr><td>사용자 정보 변경 시 서버 통신 없이 재평가</td><td align="center">✅</td><td align="center">❌</td></tr></tbody></table>

### 동작 방식

핵클 서버가 사용자에 대한 A/B 테스트, 기능 플래그 등의 평가를 미리 수행하고, SDK는 그 평가 결과만 받아와 내부에 저장합니다.\
이후 분배가 필요한 시점에는 저장된 평가 결과를 조회합니다.

```mermaid
sequenceDiagram
    participant App as 클라이언트
    participant SDK as SDK
    participant Hackle as 핵클 서버

    App->>SDK: SDK 초기화
    SDK->>Hackle: 사용자 정보 전달
    activate Hackle
    Note over Hackle: 사용자에 대한<br/>A/B 테스트, 기능 플래그 등을 평가
    Hackle-->>SDK: 평가 결과
    deactivate Hackle
    Note over SDK: 평가 결과를 내부에 저장

    App->>SDK: 분배/결정 호출
    SDK->>SDK: 저장된 평가 결과 조회<br/>(네트워크 호출 없음)

    opt 사용자 정보 변경 (setUser, 속성 업데이트 등)
        App->>SDK: 사용자 정보 변경
        SDK->>Hackle: 변경된 사용자 정보 전달
        activate Hackle
        Note over Hackle: 다시 평가
        Hackle-->>SDK: 최신 평가 결과
        deactivate Hackle
        Note over SDK: 저장된 평가 결과 갱신
    end
```

1. SDK 초기화 시 사용자 정보를 핵클 서버로 전달합니다.
2. 핵클 서버가 사용자에 대한 A/B 테스트, 기능 플래그 등을 평가하고, SDK는 평가 결과를 받아와 내부에 저장합니다.
3. 분배/결정 호출 시 저장된 평가 결과를 조회합니다. 이 과정에서 네트워크 호출이 발생하지 않습니다.

{% hint style="success" %}
사용자 정보가 바뀌지 않았다면 저장된 평가 결과를 조회하므로 네트워크 지연 없이 분배를 처리할 수 있습니다.
{% endhint %}

### 사용자 정보가 변경되는 경우

사용자 식별자가 변경되거나(`setUser`, `setUserId`, `setDeviceId`, `resetUser`) 사용자 속성이 업데이트되면, SDK가 변경된 사용자 정보를 핵클 서버로 전달하고 최신 평가 결과를 받아와 갱신합니다.

{% hint style="warning" %}
원격 평가는 사용자 정보가 바뀔 때마다 서버와 통신해 평가를 다시 수행합니다.

사용자 정보 수정 → 서버 평가 → 결과 반영 과정을 거치므로, 최신 결과가 반영되기까지 레이턴시가 존재합니다.
{% endhint %}

### 대시보드 변경사항 반영

원격 평가는 핵클 서버가 미리 수행한 평가 결과를 SDK가 저장해두고 조회합니다.\
따라서 분배/결정을 호출한 시점의 대시보드 설정이 아니라, **평가가 수행된 시점**을 기반으로 한 결과를 반환합니다.

평가는 다음 시점에 수행되며, 이때 SDK에 저장된 평가 결과가 갱신됩니다.

* SDK 초기화 시
* 사용자 정보가 업데이트될 시 (`setUser`, `setUserId`, `setDeviceId`, `resetUser`, 사용자 속성 업데이트)

{% hint style="warning" %}
원격 평가는 평가를 핵클 서버에서 수행하지만, 분배 호출할 때마다 서버와 통신해 최신 설정으로 다시 평가하지는 않습니다.

대시보드에서 A/B 테스트의 트래픽 분배율을 조정하거나 기능 플래그를 켜고 끄더라도, SDK가 평가 결과를 다시 받아오기 전까지는 기존 평가 결과를 조회합니다.
{% endhint %}


# SDK 레이턴시

핵클 SDK는 저장된 설정을 사용해 A/B 테스트, 기능 플래그, 원격 구성의 분배를 로컬에서 처리합니다. \
분배 과정에서는 네트워크를 호출하지 않습니다.

{% hint style="info" %}

* SDK는 초기화 시 핵클 서버에서 설정을 가져옵니다.&#x20;
* 이벤트는 내부에 저장한 뒤, 정해진 주기로 비동기 일괄 전송합니다.&#x20;
* `track()` 호출 자체는 네트워크를 호출하지 않습니다.
  {% endhint %}

클라이언트, 서버 SDK는 각 환경의 특성에 맞춰 설계되었으며, 이에 따라 동작 로직에 미세한 차이가 있습니다.

### Client-side SDK

```mermaid
sequenceDiagram
    participant App as 클라이언트 앱
    participant SDK as Hackle SDK
    participant Server as 핵클 서버

    App->>SDK: SDK 초기화
    alt 로컬 평가
        SDK->>Server: 워크스페이스 설정 정보 요청
        Server-->>SDK: 워크스페이스 설정 정보 반환
        SDK->>SDK: 설정 정보 저장
        App->>SDK: 분배·결정 호출
        SDK->>SDK: 저장된 설정으로 직접 평가
        SDK-->>App: 평가 결과 반환
    else 원격 평가
        SDK->>Server: 사용자 정보 전달
        Server->>Server: 사용자 평가
        Server-->>SDK: 평가 결과 반환
        SDK->>SDK: 평가 결과 저장
        App->>SDK: 분배·결정 호출
        SDK->>SDK: 저장된 평가 결과 조회
        SDK-->>App: 평가 결과 반환
    end
    App->>SDK: track() 호출
    SDK->>Server: 이벤트 비동기 일괄 전송
```

1. SDK 초기화시 대시보드에 설정된 정보를 SDK로 가져와서 저장합니다. 주기적으로 가져오게 하거나, 직접 가져올 수도 있습니다
2. A/B 테스트, 기능플래그, 원격구성 호출시 네트워크 호출 없이 SDK 내부에 저장된 설정정보만 가지고 처리가 됩니다. 이벤트 전송 호출시 서버로 즉시 전송하지 않고 내부 저장소에만 저장해 놓습니다.
3. 주기적으로 백그라운드 작업을 통해 수집된 이벤트들을 핵클 서버로 비동기로 전송합니다. 앱이 종료되거나 웹사이트가 닫힐 때 남아있는 이벤트를 핵클 서버로 전송합니다.

### Server-side SDK

```mermaid
sequenceDiagram
    participant App as 서버 애플리케이션
    participant SDK as Hackle SDK
    participant Server as 핵클 서버

    App->>SDK: SDK 초기화
    SDK->>Server: 워크스페이스 설정 정보 요청
    Server-->>SDK: 워크스페이스 설정 정보 반환
    SDK->>SDK: 설정 정보 저장
    loop 10초마다
        SDK->>Server: 최신 설정 정보 요청
        Server-->>SDK: 최신 설정 정보 반환
        SDK->>SDK: 설정 정보 갱신
    end
    App->>SDK: 분배·결정 호출
    SDK->>SDK: 저장된 설정으로 직접 평가
    SDK-->>App: 평가 결과 반환
    App->>SDK: track() 호출
    SDK->>Server: 이벤트 비동기 일괄 전송
```

1. SDK 초기화시 대시보드에 설정된 정보를 SDK로 가져와서 저장합니다. 이후 10초마다 최신 설정 정보를 가져옵니다
2. A/B 테스트, 기능플래그, 원격구성 호출시 네트워크 호출 없이 SDK 내부에 저장된 설정정보만 가지고 처리가 됩니다. 이벤트 전송 호출시 서버로 즉시 전송하지 않고 내부 저장소에만 저장해 놓습니다.
3. 주기적으로 백그라운드 작업을 통해 수집된 이벤트들을 핵클 서버로 비동기로 전송합니다.


# 사용자 식별자와 속성

## 식별자

{% hint style="info" %}
사용자 식별자의 의미와 중요성, 선택하는 기준 등에 대해서는 [사용자 식별자 관리하기](/getting-started/user-identifier) 문서를 참고하시기 바랍니다.
{% endhint %}

사용자 식별자는 사용자를 고유하게 식별하는 목적으로 사용합니다.\
핵클 SDK를 통해 사용자 식별자를 관리할 수 있습니다.

<table><thead><tr><th width="199.671875">구분</th><th>설명</th></tr></thead><tbody><tr><td><a href="/pages/X9yrGMTqRfkKtIwBnmIT">클라이언트</a></td><td>클라이언트에서는 내부적으로 사용자 식별자를 관리합니다.<br>클라이언트에서는 크게 2가지 식별자를 사용합니다.<br>- 핵클 SDK에서 제공하는 디바이스 ID<br>- 사용자 지정 식별자</td></tr><tr><td><a href="/pages/HgToCUd3Yp2eaeQvKnxd">서버</a></td><td>서버에서는 내부적으로 사용자 식별자를 관리하지 않습니다.<br>서버의 모든 요청에 대해서 사용자 식별자 항상 전달해야 합니다.</td></tr></tbody></table>

## 속성(Property)

핵클 SDK는 사용자(User) 객체에 속성을 추가할 수 있도록 지원합니다.\
사용자 속성으로 사용자별 정보를 전송하면 반복적인 코드 작업을 줄이면서도 A/B테스트나 데이터 분석에서 다양하게 활용할 수 있습니다.

* 속성은 속성명(key)과 속성값(value)을 한 쌍으로 보내야 합니다.
* 추가 가능한 속성 개수는 최대 128개입니다.

<table><thead><tr><th width="133.04296875">구분</th><th width="127.60546875">타입</th><th>제약사항</th></tr></thead><tbody><tr><td>속성 명(key)</td><td><code>string</code></td><td><ul><li>글자수 제한은 128자입니다. (128 characters)</li><li>대소문자를 구분하지 않습니다.</li><li>예를 들어 AGE와 age는 동일한 속성명으로 인식합니다.</li></ul></td></tr><tr><td>속성 값(value)</td><td><code>boolean</code>, <code>string</code>, <code>number</code>, <code>array</code></td><td><ul><li>string 타입인 경우 글자수 제한은 1024자입니다.<br>(1024 characters)</li><li>string 타입은 대소문자를 구분합니다.</li><li>예를 들어 APPLE과 apple은 서로 다른 속성값으로 인식합니다.</li><li>number 타입인 경우 정수 최대 15자리, 소수점 최대 6자리를<br>지원합니다.</li></ul></td></tr></tbody></table>

### 예시

사용자(User) 객체는 테스트 그룹 분배, 기능 플래그 결정, 사용자 이벤트 전송에서 파라미터로 사용됩니다. 아래 예시에서는 세 가지 속성(age, grade, is\_paying\_user)을 추가한 것을 확인할 수 있습니다.

```java
import io.hackle.android.HackleApp
import io.hackle.sdk.common.User
import io.hackle.sdk.common.Variation
  
User user = User.builder()
  	.userId(userId)
    .property("age", 30)
    .property("grade", "GOLD")
    .property("is_paying_user", false)
    .build();
    
// 테스트 그룹 분배  
Variation variation = hackleApp.variation(experimentKey, user);

// 기능 플래그 결정
boolean featureOn = hackleApp.isFeatureOn(featureKey, user);

// 사용자 이벤트 전송
hackleApp.track(event, user);
```

```kotlin
import io.hackle.android.Hackle
import io.hackle.sdk.common.User
import io.hackle.sdk.common.Variation
  
val user = Hackle.user() {
  	userId(userId)
    property("age", 30)
    property("grade", "GOLD")
    property("is_paying_user", false)
}
    
// 테스트 그룹 분배
val variation = hackleApp.variation(experimentKey, user)

// 기능 플래그 결정
val featureOn = hackleApp.isFeatureOn(featureKey, user)

// 사용자 이벤트 전송
hackleApp.track(event, user)
```


# 클라이언트 사용자 식별자

클라이언트에서는 두 가지 방법을 통해 사용자 식별자를 사용할 수 있습니다.

* SDK 내부적으로 관리하는 디바이스 식별자 사용
* 사용자 지정 식별자 사용

## SDK 내부적으로 관리하는 디바이스 식별자 사용

{% hint style="warning" %}
클라이언트 측 SDK에 한해 사용 가능합니다.
{% endhint %}

클라이언트 측 SDK는 디바이스의 식별자를 관리하는 기능을 포함하고 있습니다.\
따라서 사용자 식별자를 별도로 전달하지 않아도 사용자를 자동으로 식별할 수 있습니다.

JavaScript, Android, iOS의 경우 SDK가 관리하는 디바이스 식별자를 얻을 수 있으니 아래 예제 코드를 참고하시기 바랍니다.

```javascript
// 테스트 그룹 분배
const experimentKey = 42
const variation = hackleClient.variation(experimentKey)

// 사용자 이벤트 전송
hackleClient.track("purchase")

// 내부적으로 관리되는 디바이스 식별자 가져오기
const userId = Hackle.getUserId()
```

```java
// 테스트 그룹 분배
int experimentKey = 42;
Variation variation = hackleApp.variation(experimentKey);

// 사용자 이벤트 전송
hackleApp.track("purchase");

// 내부적으로 관리되는 디바이스 식별자 가져오기
String deviceId = hackleApp.getDeviceId();
```

```swift
// 테스트 그룹 분배
let variation = hackleApp.variation(experimentKey: 42)

// 사용자 이벤트 전송
hackleApp.track(eventKey: "purchase")

// 내부적으로 관리되는 디바이스 식별자 가져오기
let deviceId = hackleApp.deviceId
```

```javascript
<HackleProvider hackleClient={hackleClient}>
  <YourApp />
</HackleProvider>
```

## 사용자 지정 식별자 사용

SDK는 파라미터를 통해 받은 식별자를 통해 사용자를 식별합니다.\
전달하는 식별자는 직접 관리하는 Primary Key, 디바이스 식별자, 회원 아이디, 이메일, 해시값 등이 될 수 있습니다.

```javascript
// 테스트 그룹 분배
const experimentKey = 42;
const user = { userId: "ae2182e0" };
const variation = hackleClient.variation(experimentKey, user);

// 사용자 이벤트 전송
hackleClient.track("purchase", user);
```

```java
// 테스트 그룹 분배
long experimentKey = 42L;
String userId = "ae2182e0";
Variation variation = hackleApp.variation(experimentKey, userId);

// 사용자 이벤트 전송
hackleApp.track("purchase", userId);
```

```swift
// 테스트 그룹 분배
let variation = hackleApp.variation(experimentKey: 42, userId: "ae2182e0")

// 사용자 이벤트 전송
hackleApp.track(eventKey: "purchase", userId: "ae2182e0")
```

```javascript
const user = { 
    userId: "ae2182e0"
}

<HackleProvider hackleClient={hackleClient} user={user}>
  <YourApp />
```

### 사용자 지정 식별자 만들기 예시

로그인 사용자만을 대상으로 데이터가 필요한 경우에는 로그인 시 사용하는 회원 아이디나 이메일 주소 등을 식별자로 사용할 수 있습니다.

그러나 비로그인 사용자를 포함할 경우에는 앱, 기기 혹은 브라우저를 기반으로 구분할 수 있는 값을 활용하는 것을 권장하고 있습니다.\
(모바일 앱의 경우에는 UUID 혹은 ADID 값을, PC/Mobile 웹의 경우에는 쿠키 값)

아래에 쿠키를 기반으로 사용자 식별자를 생성하는 예시를 소개합니다.

```javascript
import Cookies from "js-cookie"
import uuid4 from "uuid4"
function getUserId() {
  const key = "PCID" // 원하는 이름을 입력
  const id = Cookies.get(key)
  if (id) {
    return id
  } else {
    const id = uuid4()
    const [top, second] = window.location.hostname.split(".").reverse()
    const domain = `.${second}.${top}`
    Cookies.set(key, id, { expires: 99999, domain: domain, path: "/" })
    return id
  }
}
```

```javascript
const app = require("express")()
const bodyParser = require("body-parser")
const cookieParser = require("cookie-parser")
const {v4: uuidv4} = require("uuid")

app.use(bodyParser.urlencoded({extended: false}))
app.use(cookieParser())
app.set("view engine", "ejs")
app.set("views", "views")

function extractDomain(hostname) {
    const DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]+\.[a-z.]{2,6}$/i;
    const SIMPLE_DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]*\.[a-z]+$/i;
    let domain_regex = DOMAIN_MATCH_REGEX;
    const parts = hostname.split(".");
    const tld = parts[parts.length - 1];
    if (tld.length > 4 || tld === "com" || tld === "org") {
        domain_regex = SIMPLE_DOMAIN_MATCH_REGEX;
    }
    const matches = hostname.match(domain_regex);
    return matches ? matches[0] : "";
}

app.use((req, res, next) => {
    const domain = extractDomain(req.headers.host)

    if (!req.cookies.deviceId) {
        const deviceId = uuidv4()
        res.cookie("deviceId", deviceId, {
            maxAge: 365 * 10 * 365 * 24 * 60 * 60,
            domain: domain,
            path: "/"
        })
        req.cookies.deviceId = deviceId
    }
    next()
});

app.get("/", (req, res) => {
    console.log(req.cookies.deviceId)
    res.render("index")
});

app.listen(3000, () => {
    console.log("App Start")
});
```


# 서버 사용자 식별자

서버에서는 사용자를 특정할 수 없기 때문에 **항상 사용자 지정 식별자를 파라미터로 전달**해야 합니다.

```java
// 테스트 그룹 분배
long experimentKey = 42L;
User user = User.builder().userId("ae2182e0");
Variation variation = hackleClient.variation(experimentKey, user);

// 사용자 이벤트 전송
hackleClient.track("purchase", user);
```

```python
# 테스트 그룹 분배
uesr = Hackle.user(user_id='ae2182e0')
variation = hackle_client.variation(experiment_key=42, user=user)

# 사용자 이벤트 전송
event = Hackle.event(key='purchase')
hackle_client.track(event=event, user=user)
```

```javascript
// 테스트 그룹 분배
const experimentKey = 42;
const user = { userId: "ae2182e0" };
hackleClient.variation(experimentKey, user);

// 사용자 이벤트 전송
hackleClient.track("purchase", user);
```

```php
// 테스트 그룹 분배
$experimentKey = 42;
$userId = 'ae2182e0'
$user = \Hackle\Common\HackleUser::builder()->userId($userId)->build();
$variation = $hackleClient->variation($experimentKey, $user);

// 사용자 이벤트 전송
$hackleClient->track('purchase', $user);
```

```ruby
# 테스트 그룹 분배
user = Hackle.user(user_id: 'ae2182e0')
variation = hackle_client.variation(experiment_key: 42, user: user)

# 사용자 이벤트 전송
event = Hackle.event(key: 'purchase')
hackle_client.track(event: event, user: user)
```

## 클라이언트, 서버 간 식별자 맞추기

클라이언트, 서버 SDK 를 동시에 사용하는 경우 각 환경에서 동일한 유저에 대해서 동일한 식별자로 맞추어 하여 사용할 수 있습니다.

이러한 경우에는 클라이언트, 서버간 API 통신시에 아래와 같이 HTTP 헤더를 통해서 식별자를 전달하는 방식을 권장드립니다.

```http
X-DEVICE-ID: a3017217-3d46-4d7e-8d88-a0a905709fe4
```


# CRM 속성

핵클 SDK에서는 CRM에 활용되는 속성은 별도로 관리합니다.

CRM 속성은 클라이언트에 저장되지 않고 서버에만 안전하게 저장됩니다.

## 전화번호 관리

{% hint style="info" %}
기존 가입자의 전화번호를 수집해 이미 보유하고 있다면, [일괄 반영을 검토](/crm-marketing/crm-properties/collect-phone-number#import)해보세요.
{% endhint %}

문자메시지 수신, 카카오톡 메시지 수신을 위한 전화번호를 수집할 수 있습니다.

## 수신 동의 관리

핵클 SDK는 사용자별 CRM 마케팅 메시지 수신 동의 상태를 관리하기 위해 별도의 속성을 관리합니다.

핵클에서 제공하는 CRM 마케팅 메시지는 아래와 같습니다.

* 푸시 메시지
* 카카오 메시지
* 문자 메시지

### 메시지 목적 분류

메시지 목적 별로 수신 동의 상태를 다르게 관리할 수 있습니다.

* 광고성
* 정보성

{% hint style="warning" %}
광고성, 정보성 이외의 특수한 목적의 메시지 분류는 추후 추가 될 예정입니다.
{% endhint %}

### 메시지 수신 동의 상태

<table><thead><tr><th width="215.3671875">HackleSubscriptionStatus</th><th>설명</th></tr></thead><tbody><tr><td><code>UNKNOWN</code></td><td>수신 동의/거부를 하지 않음. <em><strong>(default)</strong></em></td></tr><tr><td><code>SUBSCRIPTION</code></td><td>명시적으로 수신 동의</td></tr><tr><td><code>UNSUBSCRIPTION</code></td><td>명시적으로 수신 거부</td></tr></tbody></table>

{% hint style="danger" %}
**유저를 새로 생성한 경우 수신 동의는 UNKNOWN 상태입니다.**

핵클 대시보드에서 유저의 수신 동의 상태별로 메시지를 송신할 수 있습니다.

* 명시적으로 수신 동의 한 유저 대상 (SUBSCRIPTION)
* 메시지를 수신 할 수 있는 유저 대상 (UNKNOWN + SUBSCRIPTION)
  {% endhint %}


# SDK 옵트아웃

옵트아웃은 사용자의 데이터 수집을 중단하는 기능입니다. 개인정보 보호 규정 준수나 사용자의 데이터 수집 거부 요청에 대응하기 위해 사용합니다.

{% hint style="info" %}
옵트아웃 상태에서도 A/B 테스트, 기능 플래그, 인앱 메시지 등 SDK 기능은 정상적으로 동작합니다.
{% endhint %}

### 옵트아웃 설정 방법

옵트아웃은 두 가지 방법으로 설정할 수 있습니다:

* **초기화 시 설정**: SDK 초기화 Config에서 `optOutTracking`을 설정합니다.
* **런타임 설정**: `setOptOutTracking()` 메서드를 통해 앱 실행 중에 옵트아웃 상태를 변경합니다.

### 상태 관리

{% hint style="warning" %}
SDK는 옵트아웃 상태를 메모리에서만 관리합니다. 앱이 재시작되면 초기화 Config에 설정된 값으로 리셋됩니다.
{% endhint %}

`setOptOutTracking()`을 통해 변경한 옵트아웃 상태는 앱 재시작 시 유지되지 않습니다. 앱이 다시 시작되면 `HackleConfig`에 설정된 `optOutTracking` 값이 적용됩니다.

#### 영속성 관리

사용자의 옵트아웃 설정을 앱 재시작 후에도 유지하려면, **앱에서 직접 영속적으로 관리**해야 합니다.

**권장 구현 흐름**

1. 사용자가 옵트아웃 설정을 변경하면, 앱의 영속 저장소에 상태를 저장합니다.
2. 앱 시작 시, 저장소에서 옵트아웃 상태를 읽어 SDK 초기화 Config에 설정합니다.
3. 앱 실행 중 상태가 변경되면, 저장소와 SDK 양쪽 모두 업데이트합니다.

**플랫폼별 영속 저장소 예시**

| 플랫폼                | 영속 저장소                                      |
| ------------------ | ------------------------------------------- |
| iOS                | UserDefaults                                |
| Android            | SharedPreferences                           |
| JavaScript / React | localStorage                                |
| React Native       | AsyncStorage 또는 MMKV                        |
| Flutter            | SharedPreferences (shared\_preferences 패키지) |

{% hint style="info" %}
옵트아웃 API의 구체적인 사용법과 영속성 관리 코드 예시는 각 SDK의 옵트아웃 문서를 참고하세요.
{% endhint %}


# 예제 실습: 간단한 A/B 테스트

이 문서에서는 핵클에서 제공하는 SDK 중 JavaScript SDK를 이용한 예제를 소개합니다. 소개 목적은 다음과 같습니다.

1. 어떤 코드 작업이 필요한지 알 수 있다.
2. SDK의 기능을 경험할 수 있다.

## 예제: 간단한 A/B 테스트

예제가 수행하는 내용은 다음과 같습니다.

1. 임의의 사용자 식별자를 입력 받습니다.
2. **테스트 그룹 분배** 버튼을 누릅니다.
3. 테스트 그룹 분배를 수행하고 `hackle_test_event_key` 이벤트 키를 전송합니다.

{% hint style="info" %}
예제 코드 확인 및 실습 링크

[Hackle JavaScript SDK 연동 및 기능 적용 예제](https://codepen.io/hackle-example/pen/wvdedja)

Tip. 코드에 있는 주석과 함께 [JavaScript](/development-guide/javascript) 문서를 참고하면 빠른 이해에 도움이 됩니다.
{% endhint %}

## 유의사항

1. **예제는 핵클에서 생성한 예제용 A/B 테스트를 기반으로 실행됩니다. 예제용 A/B 테스트는 다음과 같은 특징을 갖고 있습니다.**
   * 분배 사유를 여러 종류 경험할 수 있도록 설정했습니다.
   * 사용자 식별자에 `apple` 입력 시 테스트 그룹 A, `banana` 입력 시 테스트 그룹 B를 반환하도록 설정했습니다.
   * 사용자 식별자에 그 외 값을 입력할 경우에는 테스트 그룹 분배 원리에 따라 테스트 그룹이 결정됩니다.
2. **핵클 대시보드에서 직접 생성한 A/B 테스트로 해당 예제를 수행하기 위해서는 JS 탭에서 SDK 키와 실험 키를 수정해야 합니다.**

![](/files/fzF0077QfoIYwrWMeUjC)


# Android

{% hint style="info" %}
Hackle Android SDK 는 Android API 24 이상 (7.0 Nougat)을 지원합니다.

* Hackle Android SDK 4.0.0 버전부터 최소 지원 버전을 Android API 24 로 변경하였습니다.
* Hackle Android SDK 3.0.0 버전부터 최소 지원 버전을 Android API 21 로 변경하였습니다.
* Hackle Android SDK 2.x 버전의 최소 지원 버전은 Android API 16 이상입니다.
  {% endhint %}

## 의존성 추가

[![](https://img.shields.io/maven-central/v/io.hackle/hackle-android-sdk)](https://central.sonatype.com/artifact/io.hackle/hackle-android-sdk)

```groovy
repositories {
  mavenCentral()
}
```

`build.gradle` 파일에 의존성을 추가합니다.

```groovy
dependencies {
  implementation 'io.hackle:hackle-android-sdk:2.66.1'
}
```

#### ProGuard

ProGuard를 사용하는 경우, aar 아티팩트에 난독화 규칙이 자동으로 포함됩니다. 이 경우가 아니라면 아래 규칙을 포함시켜야 합니다.

```
-keep class io.hackle.android.** { *; }
-keep class io.hackle.sdk.** { *; }
```

#### Third-party 의존성

핵클 Android SDK는 아래 third-party 의존성을 가지고 있습니다

<table><thead><tr><th width="450.45703125">라이브러리</th><th>설명</th></tr></thead><tbody><tr><td><code>com.squareup.okhttp3:okhttp:3.12.2</code></td><td>HTTP/HTTPS 클라이언트</td></tr><tr><td><code>com.google.code.gson:gson:2.8.6</code></td><td>JSON 직렬화/역직렬화</td></tr><tr><td><code>com.google.android.gms:play-services-base:17.3.0</code></td><td>Google Play Services 공통 모듈</td></tr></tbody></table>

## SDK 초기화

SDK를 사용하기 위해서 반드시 `HackleApp`을 초기화 해야 합니다.

* `HackleApp`을 초기화 하기 위해 SDK 키가 필요합니다.
* `HackleApp`은 SDK의 기능을 사용하기 위한 메소드들을 제공하는 클래스입니다.
* SDK 키는 핵클 대시보드 내 [SDK 연동 정보](https://dashboard.hackle.io/config/sdk-setting)에서 확인하실 수 있습니다.

초기화는 **비동기로 실행**되며, 핵클 서버로부터 필요한 정보들을 가져와서 SDK에 저장합니다.

{% hint style="warning" %}
초기화가 완료 되기 전에 A/B 테스트, 기능 플래그를 호출하면 기본 그룹(A), 꺼짐(false)을 리턴합니다.
{% endhint %}

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import io.hackle.android.Hackle
import io.hackle.android.initialize

Hackle.initialize(applicationContext, YOUR_APP_SDK_KEY) {
  // SDK ready to use.
}
```

{% endtab %}

{% tab title="Java" %}

```java
import io.hackle.android.HackleApp;

HackleApp.initializeApp(getApplicationContext(), YOUR_APP_SDK_KEY);
```

{% endtab %}
{% endtabs %}

#### 권장 초기화 전략: 스플레시 화면을 통한 초기화

앱을 즉시 시작하지 않고 스플레시 화면을 표시하고 SDK를 초기화합니다. 이후 콜백을 통해 스플레시 화면을 닫고 사용자가 앱과 상호작용을 시작할 수 있도록 합니다.

### 초기화 시 사용자 주입

유저 정보를 포함하여 SDK를 초기화 할 수 있습니다.

* 유저 정보를 포함하지 않으면 로컬 스토리지에 저장된 유저 정보를 사용합니다.
* 유저 정보를 포함하는 경우 로컬 스토리지에 저장된 유저 정보는 사용하지 않습니다.
* 사용자 주입을 하지 않고, 로컬 스토리지에 저장된 유저 정보도 없는 경우 [Hackle Device ID](/getting-started/user-identifier)를 device id로 가지고 유저를 사용합니다.

{% hint style="info" %}
유저 정보는 SDK 초기화 이후에도 유저 정보 설정 함수를 통해 자유롭게 수정 할 수 있습니다.
{% endhint %}

{% hint style="warning" %}
초기화 시 주입한 유저 정보와 로컬 스토리지에 저장된 유저 정보는 병합하지 않습니다.

ex) 스토리지에 `userId: A` 가 저장된 상태에서 초기화 시 `deviceId: B` 를 주입하는 경우,\
`userId: null, deviceId: B`인 유저로 설정됩니다.
{% endhint %}

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import io.hackle.android.Hackle
import io.hackle.android.initialize
import io.hackle.sdk.common.User

val user = User.builder()
    .userId("142")                  // 사용자 ID
    .deviceId("ae2182e0")           // 디바이스 ID
    .build()

Hackle.initialize(applicationContext, YOUR_APP_SDK_KEY, user) {
  // SDK ready to use.
}
```

{% endtab %}

{% tab title="Java" %}

```java
import io.hackle.android.HackleApp;
import io.hackle.sdk.common.User

User user = User.builder()
    .userId("142")                  // 사용자 ID
    .deviceId("ae2182e0")           // 디바이스 ID
    .build()

HackleApp.initializeApp(getApplicationContext(), YOUR_APP_SDK_KEY, user, () -> {
  // SDK ready to use.
});
```

{% endtab %}
{% endtabs %}

### 초기화 설정정보

설정정보를 포함하여 SDK를 초기화 할 수 있습니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import io.hackle.android.Hackle
import io.hackle.android.HackleConfig
import io.hackle.android.initialize

val config = HackleConfig.builder()
  .build()

Hackle.initialize(applicationContext, YOUR_APP_SDK_KEY, config) {
  // SDK ready to use.
}
```

{% endtab %}

{% tab title="Java" %}

```java
import io.hackle.android.HackleApp;
import io.hackle.android.HackleConfig;

HackleConfig config = HackleConfig.builder()
  .build();

HackleApp.initializeApp(getApplicationContext(), YOUR_APP_SDK_KEY, config, () -> {
  // SDK ready to use.
});
```

{% endtab %}
{% endtabs %}

#### 설정 옵션

<table data-full-width="false"><thead><tr><th width="222.29296875">설정</th><th width="295.48828125">기능</th><th width="125.8125">기본값</th><th width="101.90234375">지원 버전</th></tr></thead><tbody><tr><td><code>exposureEventDedupIntervalMillis</code><sup>*</sup></td><td><p>동일한 사용자가 연속으로 발생시킨 동일한 A/B 테스트, 기능플래그 분배결과에 대한 노출 이벤트를 제거합니다.</p><ul><li>최솟값: 1000 (1초)</li><li>최댓값: 86400000(24시간)</li></ul></td><td>60000 (1분)</td><td>2.7.0+</td></tr><tr><td><code>eventFlushIntervalMillis</code></td><td><p>수집된 이벤트를 서버로 전송하는 주기입니다.</p><ul><li>최솟값: 1000 (1초)</li><li>최댓값: 60000 (1분)</li></ul></td><td>10000 (10초)</td><td>2.10.0+</td></tr><tr><td><code>pollingIntervalMillis</code></td><td><p>대시보드에서 설정한 정보를 주기적으로 업데이트 할 수 있습니다.</p><ul><li>최솟값 : 60000 (60초)</li></ul></td><td>-1<br>(주기적으로 업데이트하지 않음)</td><td>2.19.0+</td></tr><tr><td><code>automaticScreenTracking</code></td><td>화면 자동 추적 활성화 여부</td><td><code>true</code></td><td>2.39.0+</td></tr><tr><td><code>automaticAppLifecycleTracking</code></td><td>앱 시작 / 종료 자동 추적 활성화 여부</td><td><code>true</code></td><td>2.64.0+</td></tr><tr><td><code>sessionPolicy</code></td><td>세션 유지 조건과 만료 조건을 설정합니다.</td><td><p><code>ALWAYS_NEW_SESSION</code> ,</p><p><code>1800000</code></p></td><td>2.65.0+</td></tr><tr><td><code>optOutTracking</code></td><td>옵트아웃 활성화 여부.</td><td><code>false</code></td><td>2.65.0+</td></tr><tr><td><code>evaluationMode</code></td><td>평가 방식을 설정합니다.</td><td><code>LOCAL</code></td><td>4.0.0+</td></tr><tr><td><code>sessionTimeoutMillis</code><br>(deprecated)</td><td>세션만료 시간을 설정합니다. <code>sessionPolicy</code>를 사용해 주세요.</td><td>1800000<br>(30분)</td><td>2.13.0+</td></tr></tbody></table>

{% hint style="info" %} <sup>\*</sup> 2.47.0 이후부터 앱 종료 후 재시작 시에도 지원합니다.\ <sup>\*</sup> 2.47.0 미만버전의 경우 최댓값은 : 3600000 (1시간) 입니다.
{% endhint %}

#### 평가 방식 설정

평가를 SDK에서 직접 수행할지, 핵클 서버가 미리 수행한 결과를 조회할지 선택할 수 있습니다.\
설정하지 않으면 기본값인 `EvaluationMode.LOCAL`(로컬 평가)로 동작합니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import io.hackle.sdk.common.EvaluationMode

val config = HackleConfig.builder()
    .evaluationMode(EvaluationMode.REMOTE)
    .build()

Hackle.initialize(applicationContext, YOUR_APP_SDK_KEY, config) {
    // SDK ready to use.
}
```

{% endtab %}

{% tab title="Java" %}

```java
import io.hackle.sdk.common.EvaluationMode;

HackleConfig config = HackleConfig.builder()
    .evaluationMode(EvaluationMode.REMOTE)
    .build();

HackleApp.initializeApp(getApplicationContext(), YOUR_APP_SDK_KEY, config, () -> {
    // SDK ready to use.
});
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
원격 평가를 사용하면 기기 내부에 사용자 정보를 저장하지 않습니다. 기존에 로컬 평가를 사용하며 저장된 사용자 정보가 있는 경우 삭제 처리합니다.

두 방식의 차이와 선택 기준은 [평가 방식](/development-guide/sdk/evaluation-mode) 문서를 참고 바랍니다.
{% endhint %}

#### 세션 정책 설정

세션 정책을 설정하여 세션의 유지 조건과 만료 조건을 제어할 수 있습니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
val sessionPolicy = HackleSessionPolicy.builder()
    .persistCondition(HackleSessionPersistCondition.NULL_TO_USER_ID)
    .timeoutCondition(
        HackleSessionTimeoutCondition.builder()
            .millis(3600000)
            .onForeground(false)
            .onBackground(true)
            .onApplicationStateChange(true)
            .build()
    )
    .build()

val config = HackleConfig.builder()
    .sessionPolicy(sessionPolicy)
    .build()

Hackle.initialize(applicationContext, YOUR_APP_SDK_KEY, config) {
    // SDK ready to use.
}
```

{% endtab %}

{% tab title="Java" %}

```java
HackleSessionTimeoutCondition timeoutCondition = HackleSessionTimeoutCondition.builder()
    .millis(3600000)
    .onForeground(false)
    .onBackground(true)
    .onApplicationStateChange(true)
    .build();

HackleSessionPolicy sessionPolicy = HackleSessionPolicy.builder()
    .persistCondition(HackleSessionPersistCondition.NULL_TO_USER_ID)
    .timeoutCondition(timeoutCondition)
    .build();

HackleConfig config = HackleConfig.builder()
    .sessionPolicy(sessionPolicy)
    .build();

HackleApp.initializeApp(getApplicationContext(), YOUR_APP_SDK_KEY, config, () -> {
    // SDK ready to use.
});
```

{% endtab %}
{% endtabs %}

### 인스턴스 가져오기

초기화 이후 아래 코드를 통해 `HackleApp` 인스턴스를 가져올 수 있습니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import io.hackle.android.Hackle
import io.hackle.android.app

val hackleApp = Hackle.app
```

{% endtab %}

{% tab title="Java" %}

```java
import io.hackle.android.HackleApp;

HackleApp hackleApp = HackleApp.getInstance();
```

{% endtab %}
{% endtabs %}

### 대시보드 설정 정보 갱신

초기화 이후 대시 보드 설정 정보 갱신이 필요한 경우 명시적으로 갱신 할 수 있습니다.

{% hint style="warning" %}
해당 함수는 60초에 한번 제한적으로 호출할 수 있습니다.
{% endhint %}

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
hackleApp.fetch {
  // done
}
```

{% endtab %}

{% tab title="Java" %}

```java
hackleApp.fetch(new Runnable() {
    @Override
    public void run() {
        // done
    }
});
```

{% endtab %}
{% endtabs %}


# 사용자 식별자와 속성

{% hint style="info" %}
사용자 식별자 관리

사용자 식별자는 사용자를 고유하게 식별하는 목적으로 사용합니다. 사용자 식별자의 의미와 중요성, 선택하는 기준 등에 대해서는 [사용자 식별자 관리하기](/getting-started/user-identifier) 문서를 참고하시기 바랍니다.
{% endhint %}

{% hint style="warning" %}
사용자 정보를 변경하는 함수(`setUser`, `setUserId`, `setDeviceId`, `updateUserProperties`, `resetUser` 등)는 callback이 있는 함수 사용을 권장합니다.&#x20;

callback은 변경된 사용자 정보가 SDK에 반영된 이후 호출됩니다.&#x20;
{% endhint %}

## 사용자 식별자

### 핵클에서 제공하는 기본 식별자

Android SDK는 디바이스의 식별자를 관리하는 기능을 포함하고 있습니다. 따라서 사용자 식별자를 별도로 전달하지 않아도 사용자를 자동으로 식별할 수 있습니다.

SDK에서 관리하는 식별자를 조회하는 방법은 다음과 같습니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
// 디바이스ID 가져오기
val deviceId = hackleApp.deviceId

// 세션 ID 가져오기
val sessionId = hackleApp.sessionId

// 사용자 정보 모두 가지고 오기
val user = hackleApp.user
```

{% endtab %}

{% tab title="Java" %}

```java
// 디바이스ID 가져오기
String deviceId = hackleApp.getDeviceId();

// 세션 ID 가져오기
String sessionId = hackleApp.getSessionId();

// 사용자 정보 모두 가지고 오기
User user = hackleApp.getUser();
```

{% endtab %}
{% endtabs %}

#### 디바이스 ID 수정

핵클에서 제공하는 디바이스 ID를 사용하지 않고 직접 디바이스 ID를 주입할 수 있습니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
// 디바이스 ID 변경
hackleApp.setDeviceId("CUSTOM_DEVICE_ID") {
    // 적용 완료
}

// 빌더 패턴 사용
val user = User.builder()
    .deviceId("CUSTOM_DEVICE_ID") // 디바이스 ID
    .build()

hackleApp.setUser(user) {
    // 적용 완료
}
```

{% endtab %}

{% tab title="Java" %}

```java
// 디바이스 ID 변경
hackleApp.setDeviceId("CUSTOM_DEVICE_ID", () -> {
    // 적용 완료
});
```

{% endtab %}
{% endtabs %}

#### 사용자 식별자(User ID) 설정

로그인 한 사용자의 식별자를 설정하실 수 있습니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
// 로그인 한 사용자 ID 추가
hackleApp.setUserId("LOGIN_ID") {
    // 적용 완료
}

// 빌더 패턴 사용
val user = User.builder()
    .userId("LOGIN_ID") // 사용자 ID
    .build()

hackleApp.setUser(user) {
    // 적용 완료
}
```

{% endtab %}

{% tab title="Java" %}

```java
// 로그인 한 사용자 ID 추가
hackleApp.setUserId("LOGIN_ID", () -> {
    // 적용 완료
});
```

{% endtab %}
{% endtabs %}

### 추가 식별자

기본 식별자(deviceid, userid) 외의 식별자 타입을 추가할 경우 아래와 같이 설정할 수 있습니다.

{% hint style="info" %}
추가 식별자는 [핵클 통합 식별자](/getting-started/user-identifier/hackle-id)로 통합되지 않습니다.
{% endhint %}

{% hint style="danger" %}
`setUser` 를 하는 경우 현재 디바이스의 유저 정보를 덮어씁니다.

* 현재 A userId를 사용중인데 setUser 시 A userId 를 전달하지 않으면 userId가 A -> null로 변경됩니다.
* 현재 custom deviceId를 사용중인데 setUser 시 사용중인 deviceId를 전달하지 않으면 custom deviceId -> hackle deviceId로 변경됩니다.
* 추가 식별자를 사용하는 경우에도 setUser 시 전달하지 않으면 추가 식별자가 초기화가 됩니다.
* 프로퍼티의 경우 아래 케이스로 디바이스 내 캐싱된 프로퍼티가 유지 or 초기화 될 수 있습니다.
  * setUser 전 / 후 userId와 deviceId가 동일하다면 캐시된 프로퍼티 유지됩니다.
  * setUser 전 / 후 userId 혹은 deviceId가 변경된다면 캐시된 프로퍼티가 삭제됩니다.
    {% endhint %}

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import io.hackle.sdk.common.User

val user = User.builder()
    .userId("143")                   // 사용자 ID (핵클 통합 식별자 사용가능)
    .deviceId("ae2182e0")            // 디바이스 ID (핵클 통합 식별자 사용가능)
    .identifier("myCustomId", "42")  // Custom ID
    .build()

hackleApp.setUser(user) {
    // 적용 완료
}
```

{% endtab %}

{% tab title="Java" %}

```java
import io.hackle.sdk.common.User

User user = User.builder()
    .userId("143")                   // 사용자 ID (핵클 통합 식별자 사용가능)
    .deviceId("ae2182e0")            // 디바이스 ID (핵클 통합 식별자 사용가능)
    .identifier("myCustomId", "42")  // Custom ID
    .build();

hackleApp.setUser(user, () -> {
    // 적용 완료
});
```

{% endtab %}
{% endtabs %}

## 사용자 속성(Property)

핵클 SDK는 사용자 속성을 추가할 수 있도록 지원합니다.

* 속성은 속성명(key)과 속성값(value)을 한 쌍으로 보내야 합니다.
* 추가 가능한 속성 개수는 최대 128개입니다.

<table><thead><tr><th width="133.04296875">구분</th><th width="127.60546875">타입</th><th>제약사항</th></tr></thead><tbody><tr><td>속성 명(key)</td><td><code>string</code></td><td><ul><li>글자수 제한은 128자입니다. (128 characters)</li><li>대소문자를 구분하지 않습니다.</li><li>예를 들어 AGE와 age는 동일한 속성명으로 인식합니다.</li></ul></td></tr><tr><td>속성 값(value)</td><td><code>boolean</code>, <code>string</code>, <code>number</code>, <code>array</code></td><td><ul><li>string 타입인 경우 글자수 제한은 1024자입니다.<br>(1024 characters)</li><li>string 타입은 대소문자를 구분합니다.</li><li>예를 들어 APPLE과 apple은 서로 다른 속성값으로 인식합니다.</li><li>number 타입인 경우 정수 최대 15자리, 소수점 최대 6자리를<br>지원합니다.</li></ul></td></tr></tbody></table>

### 사용자 속성 추가

`PropertyOperations` 객체에 `set` 을 이용하여 속성을 추가한 뒤 `updateUserProperties` 를 호출하면 사용자 속성을 간단하게 추가할 수 있습니다.

{% hint style="warning" %}
`setUserProperty` 함수는 Android SDK 4.0.0 부터 deprecated 되었습니다. `updateUserProperties` 를 사용해 주세요.
{% endhint %}

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import io.hackle.sdk.common.PropertyOperations

// 속성 설정
val operations = PropertyOperations.builder()
    .set("gender", "female")
    .build()

hackleApp.updateUserProperties(operations) {
    // 적용 완료
}
```

{% endtab %}

{% tab title="Java" %}

```java
import io.hackle.sdk.common.PropertyOperations;

// 속성 설정
PropertyOperations operations = PropertyOperations.builder()
    .set("gender", "female")
    .build();

hackleApp.updateUserProperties(operations, () -> {
    // 적용 완료
});
```

{% endtab %}
{% endtabs %}

### 사용자 속성 설정

사용자 속성을 추가, 제거 할 수 있습니다.

<table><thead><tr><th width="150">지원하는 함수</th><th>설명</th></tr></thead><tbody><tr><td><code>set</code></td><td>사용자 속성을 설정합니다. 속성 키에 이미 설정한 속성값이 있는 경우 덮어씁니다</td></tr><tr><td><code>setOnce</code></td><td><p>사용자 속성 값을 한번만 설정합니다. 속성키에 대한 속성이 이미 있는 경우 무시됩니다.</p><p>예를 들어 사용자에 대한 가입일, 초기 가입 위치 등을 설정할 수 있습니다.</p></td></tr><tr><td><code>unset</code></td><td>사용자 속성을 제거합니다.</td></tr><tr><td><code>clearAll</code></td><td>사용자의 모든 속성을 제거합니다.</td></tr></tbody></table>

설정하고 싶은 사용자 속성으로 `PropertyOperations` 객체를 인스턴스화 합니다. 다음 `updateUserProperties` 를 호출하여 사용자 속성을 업데이트 합니다. 한 번에 여러개의 속성을 설정할 수도 있습니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import io.hackle.sdk.common.PropertyOperations

// 속성 설정
val operations = PropertyOperations.builder()
    .set("age", 42)
    .set("grade", "GOLD")
    .setOnce("sign_up_date", "2020-07-03")
    .build()

hackleApp.updateUserProperties(operations) {
    // 적용 완료
}

// 속성 지우기
val clearOperations = PropertyOperations.clearAll()

hackleApp.updateUserProperties(clearOperations) {
    // 적용 완료
}
```

{% endtab %}

{% tab title="Java" %}

```java
import io.hackle.sdk.common.PropertyOperations;

// 속성 설정
PropertyOperations operations = PropertyOperations.builder()
    .set("age", 42)
    .set("grade", "GOLD")
    .setOnce("sign_up_date", "2020-07-03")
    .build();

hackleApp.updateUserProperties(operations, () -> {
    // 적용 완료
});

// 속성 지우기
PropertyOperations clearOperations = PropertyOperations.clearAll();

hackleApp.updateUserProperties(clearOperations, () -> {
    // 적용 완료
});
```

{% endtab %}
{% endtabs %}

## 사용자 초기화

기존에 설정한 정보를 초기화해야 합니다. 초기화를 하는 경우 기존에 설정했던 식별자, 속성이 모두 초기화됩니다.

{% hint style="danger" %}
사용자 초기화를 하는 경우 서버에 저장된 사용자 속성까지 모두 초기화가 됩니다. 로그아웃 처리를 원하는 경우 `hackleApp.setUserId(null)`을 사용해주세요.

userId에 null을 대입하면 클라이언트 상에서 로그아웃 처리가 됩니다.
{% endhint %}

```kotlin
hackleApp.resetUser {
    // 적용 완료
}
```


# CRM 속성

CRM 속성은 핵클 서버에만 안전하게 저장되며 SDK를 통해 값을 직접 조회할 수는 없습니다.

{% hint style="danger" %}
CRM 속성은 별도로 관리되며, `resetUser()`를 호출하거나 `updateUserProperties`에서 `clearAll`을 호출해도 삭제되거나 초기화되지 않습니다.

사용자가 회원 탈퇴 등을 한 경우, 반드시 별도로 제공되는 함수를 호출하여 정보를 삭제해야 합니다.
{% endhint %}

## 전화번호 수집

{% hint style="info" %}
Android SDK 2.52.0 버전 이상에서 지원하는 기능입니다.
{% endhint %}

{% hint style="success" %}
카카오 / 문자 메시지 권장 사항

이 기능을 이용하여 사용자 식별자와 전화번호를 매핑하면 핵클을 통한 카카오 / 문자 메시지를 더욱 원활히 이용할 수 있습니다.
{% endhint %}

#### setPhoneNumber

사용자의 전화번호를 등록합니다.

전화번호는 올바른 E.164 포맷일 경우에만 저장됩니다. 국가코드를 지정하지 않았다면 대한민국(+82)이 기본값으로 사용됩니다.

이미 저장된 전화번호가 있는 사용자의 경우, 이 함수를 호출하면 기존 전화번호가 새로운 값으로 교체됩니다..

```kotlin
val myPhoneNumber = "+821012341234"
hackleApp.setPhoneNumber(myPhoneNumber) {
    // 적용 완료
}
```

#### unsetPhoneNumber

사용자에게 등록되어 있는 전화번호를 삭제합니다.

```kotlin
hackleApp.unsetPhoneNumber {
    // 적용 완료
}
```

## CRM 마케팅 메시지 수신 동의

{% hint style="info" %}
Android SDK 2.55.0 버전 이상에서 지원하는 기능입니다.

수신 동의 상태에 대한 자세한 내용은 [CRM 메시지 수신 동의 관리](/development-guide/sdk/user-identifier/crm-subscription) 문서를 참고해주세요.
{% endhint %}

### 수신 동의 속성

메시지 목적 별로 수신 동의/거부를 할 수 있습니다.

`HackleSubscriptionOperations.Builder`를 사용해 원하는 속성의 동의 상태를 설정한 후, `updatePushSubscriptions()` 같은 메서드로 최종 업데이트를 진행합니다.

메시지 채널 별로 동의 상태를 업데이트할 수 있습니다.

```kotlin
import io.hackle.sdk.common.subscription.HackleSubscriptionOperations
import io.hackle.sdk.common.subscription.HackleSubscriptionStatus

val subscriptions = HackleSubscriptionOperations.builder()
  .marketing(HackleSubscriptionStatus.SUBSCRIBED)
  .information(HackleSubscriptionStatus.SUBSCRIBED)
  .build()

hackle.app.updatePushSubscriptions(subscriptions)
```

#### 광고성 메시지

광고성 메시지 수신 동의 속성을 설정합니다.

```kotlin
import io.hackle.sdk.common.subscription.HackleSubscriptionOperations
import io.hackle.sdk.common.subscription.HackleSubscriptionStatus

val subscriptions = HackleSubscriptionOperations.builder()
  .marketing(HackleSubscriptionStatus.SUBSCRIBED)
  .build()

hackle.app.updatePushSubscriptions(subscriptions)
```

#### 정보성 메시지

정보성 메시지 수신 동의 속성을 설정합니다.

```kotlin
import io.hackle.sdk.common.subscription.HackleSubscriptionOperations
import io.hackle.sdk.common.subscription.HackleSubscriptionStatus

val subscriptions = HackleSubscriptionOperations.builder()
  .information(HackleSubscriptionStatus.SUBSCRIBED)
  .build()

hackle.app.updatePushSubscriptions(subscriptions)
```

#### HackleSubscriptionStatus

<table><thead><tr><th width="224.09765625">HackleSubscriptionStatus</th><th>설명</th></tr></thead><tbody><tr><td><code>UNKNOWN</code></td><td>수신 동의/거부를 하지 않음 (<code>default</code>)</td></tr><tr><td><code>SUBSCRIPTION</code></td><td>명시적으로 수신 동의</td></tr><tr><td><code>UNSUBSCRIPTION</code></td><td>명시적으로 수신 거부</td></tr></tbody></table>

### 푸시 수신 동의 상태 업데이트

사용자의 푸시 메시지 수신 동의 상태를 업데이트 합니다.

```kotlin
val subscriptions = HackleSubscriptionOperations.builder()
  .marketing(HackleSubscriptionStatus.SUBSCRIBED)
  .information(HackleSubscriptionStatus.SUBSCRIBED)
  .build()

hackle.app.updatePushSubscriptions(subscriptions)
```

### 카카오 메시지 수신 동의 상태 업데이트

사용자의 카카오 메시지 수신 동의 상태를 업데이트 합니다.

```kotlin
val subscriptions = HackleSubscriptionOperations.builder()
  .marketing(HackleSubscriptionStatus.SUBSCRIBED)
  .information(HackleSubscriptionStatus.SUBSCRIBED)
  .build()

hackle.app.updateKakaoSubscriptions(subscriptions)
```

### 문자 메시지 수신 동의 상태 업데이트

사용자의 문자 메시지 수신 동의 상태를 업데이트 합니다.

```kotlin
val subscriptions = HackleSubscriptionOperations.builder()
  .marketing(HackleSubscriptionStatus.SUBSCRIBED)
  .information(HackleSubscriptionStatus.SUBSCRIBED)
  .build()

hackle.app.updateSmsSubscriptions(subscriptions)
```


# 테스트 그룹 분배

A/B 테스트를 진행할 때, 테스트 그룹을 대상으로 사용자를 분배하고 각 테스트 그룹에 해당하는 로직을 작성해야 합니다. 이 때 사용자 분배를 핵클 SDK를 통해 진행할 수 있습니다.

## variation

`variation()` 메소드에 **실험 키**를 전달하면 사용자를 분배하고 결과를 전달받을 수 있습니다.

<table><thead><tr><th width="150">파라미터</th><th width="120">타입</th><th>필수</th></tr></thead><tbody><tr><td>실험 키 (key)</td><td><code>int</code></td><td>필수</td></tr></tbody></table>

#### 예제

아래 예제 코드에서는 실험 키 42를 전달하고 있으며, 테스트 그룹은 A와 B 두 개가 존재합니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import io.hackle.android.HackleApp
import io.hackle.sdk.common.Variation

// 실험 키가 42인 A/B 테스트에서 사용자에게 노출할 테스트 그룹을 결정합니다.
// 결정하지 못하는 상황인 경우 테스트 그룹 A를 반환합니다.
val variation = hackleApp.variation(42)

// 할당받은 그룹에 대한 로직
if (variation == Variation.A) {
  // 그룹 A 로직
} else if (variation == Variation.B) {
  // 그룹 B 로직
}
```

{% endtab %}

{% tab title="Java" %}

```java
import io.hackle.android.HackleApp;
import io.hackle.sdk.common.Variation;

// 실험 키가 42인 A/B 테스트에서 사용자에게 노출할 테스트 그룹을 결정합니다.
// 결정하지 못하는 상황인 경우 테스트 그룹 A를 반환합니다.
Variation variation = hackleApp.variation(42);

// 할당받은 그룹에 대한 로직
if (variation == Variation.A) {
  // 그룹 A 로직
} else if (variation == Variation.B) {
  // 그룹 B 로직
}
```

{% endtab %}
{% endtabs %}

## variationDetail

`variationDetail()` 메소드는 `variation()` 메소드와 동일하게 동작하고 분배된 사유를 같이 제공합니다. 이 메소드는 분배가 잘 되고 있는지 살펴볼 때 유용하게 사용할 수 있습니다.

<table><thead><tr><th width="150">파라미터</th><th width="120">타입</th><th>필수</th></tr></thead><tbody><tr><td>실험 키 (key)</td><td><code>int</code></td><td>필수</td></tr></tbody></table>

#### 예제

아래 예제 코드의 경우 실험 키 42를 전달하고 있습니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import io.hackle.android.HackleApp
import io.hackle.sdk.common.Variation
import io.hackle.sdk.common.decision.Decision
import io.hackle.sdk.common.decision.DecisionReason

// 분배 결정 상세
val decision = hackleApp.variationDetail(42)

// 분배 그룹
val variation = decision.variation

// 분배 결정 사유
val reason = decision.reason
```

{% endtab %}

{% tab title="Java" %}

```java
import io.hackle.android.HackleApp;
import io.hackle.sdk.common.Variation;
import io.hackle.sdk.common.decision.Decision;
import io.hackle.sdk.common.decision.DecisionReason;

// 분배 결정 상세
Decision decision = hackleApp.variationDetail(42);

// 분배 그룹
Variation variation = decision.getVariation();

// 분배 결정 사유
DecisionReason reason = decision.getReason();
```

{% endtab %}
{% endtabs %}

### 분배 사유

분배 결정 사유는 **`SDK_NOT_READY`** 와 같은 형태로 받게 됩니다. 자세한 내용은 아래 표를 참고해주세요.

<table><thead><tr><th width="319.12109375">분배 사유</th><th width="305.35546875">설명</th><th>분배 결과</th></tr></thead><tbody><tr><td><code>SDK_NOT_READY</code></td><td><p>SDK 사용 준비가 되지 않았습니다.</p><p>(예: 잘못된 SDK 키로 초기화 시도)</p></td><td>A (기본 그룹)</td></tr><tr><td><code>EXPERIMENT_NOT_FOUND</code></td><td>전달한 실험 키에 대한 A/B 테스트를 찾을 수 없습니다. 실험 키가 잘못되었거나 해당 실험이 보관 상태일 수 있습니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>NOT_IN_MUTUAL_EXCLUSION_EXPERIMENT</code></td><td>실험이 상호 배타적 설정에 포함되어 있지만<br>해당 상호 배타적 그룹에 할당되지 않은 경우</td><td>A (기본 그룹)</td></tr><tr><td><code>EXPERIMENT_DRAFT</code></td><td>A/B 테스트가 준비 상태입니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>EXPERIMENT_PAUSED</code></td><td>A/B 테스트가 일시 정지 상태입니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>EXPERIMENT_COMPLETED</code></td><td>A/B 테스트가 종료되었습니다.</td><td>종료 시 선택한승리 그룹</td></tr><tr><td><code>OVERRIDDEN</code></td><td>사용자가 수동할당에 의해<br>특정 그룹으로 결정되었습니다.</td><td>수동 할당한<br>그룹</td></tr><tr><td><code>NOT_IN_EXPERIMENT_TARGET</code></td><td>사용자가 A/B 테스트 타겟이 아닙니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>TRAFFIC_NOT_ALLOCATED</code></td><td>A/B 테스트가 실행 중이지만<br>사용자가 테스트에 할당되지 않았습니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>TRAFFIC_ALLOCATED</code></td><td>사용자가 A/B 테스트에 할당되었습니다.</td><td>할당된 그룹</td></tr><tr><td><code>VARIATION_DROPPED</code></td><td>원래 할당된 그룹이 테스트에서 제외되었습니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>INVALID_INPUT</code></td><td>입력값이 유효하지 않습니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>EXCEPTION</code></td><td>알 수 없는 오류가 발생했습니다.</td><td>A (기본 그룹)</td></tr></tbody></table>

### 파라미터

{% hint style="info" %}
Android SDK 2.9.0 이상 버전에서 지원하는 기능입니다.
{% endhint %}

* `variationDetail()` 메소드를 통해 분배된 그룹의 파라미터 값도 같이 제공받을 수 있습니다.
* `variationDetail()` 메소드를 통해 전달받은 Decision 인스턴스에는 전체 파라미터 설정 정보가 담긴 `ParameterConfig` 객체가 존재합니다.
* 핵클의 A/B 테스트 화면에서 설정한 파라미터 값이 key, value 형태로 존재하기 때문에, 설정한 파라미터 유형에 따라 아래 메소드를 사용하여 설정한 파라미터 값을 받아 활용할 수 있습니다.
* 분배된 그룹의 설정된 값은 아래 함수를 이용하여 조회할 수 있습니다.

<table><thead><tr><th width="150">타입</th><th>설명</th></tr></thead><tbody><tr><td><code>getString</code></td><td>STRING, JSON 유형으로 설정된 parameter값을 반환합니다.</td></tr><tr><td><code>getInt</code></td><td>NUMBER 유형으로 설정된 parameter 값을 int 타입으로 반환합니다.</td></tr><tr><td><code>getDouble</code></td><td>NUMBER 유형으로 설정된 parameter 값을 double 타입으로 반환합니다</td></tr><tr><td><code>getLong</code></td><td>NUMBER 유형으로 설정된 parameter 값을 long 타입으로 반환합니다</td></tr><tr><td><code>getBoolean</code></td><td>Boolean 유형으로 설정된 parameter값을 반환합니다.</td></tr></tbody></table>

#### 예제

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import io.hackle.android.HackleApp
import io.hackle.sdk.common.decision.Decision
import io.hackle.sdk.common.ParameterConfig

val decision: Decision = hackleApp.variationDetail(42)
val config: ParameterConfig = decision.config

val strValue: String = decision.getString("parameter_key_string_type", "defaultValue")
val strValueInConfig: String = config.getString("parameter_key_string_type", "defaultValue")

val jsonValue: String = decision.getString("parameter_key_json_type", "defaultValue")
val jsonValueInConfig: String = config.getString("parameter_key_json_type", "defaultValue")

val intValue: Int = decision.getInt("parameter_key_number_type", 0)
val intValueInConfig: Int = config.getInt("parameter_key_number_type", 0)

val doubleValue: Double = decision.getDouble("parameter_key_number_type", 0.0)
val doubleValueInConfig: Double = config.getDouble("parameter_key_number_type", 0.0)

val longValue: Long = decision.getLong("parameter_key_number_type", 0L)
val longValueInConfig: Long = config.getLong("parameter_key_number_type", 0L)

val booleanValue: Boolean = decision.getBoolean("parameter_key_boolean_type", false)
val booleanValueInConfig: Boolean = config.getBoolean("parameter_key_boolean_type", false)
```

{% endtab %}

{% tab title="Java" %}

```java
import io.hackle.android.HackleApp
import io.hackle.sdk.common.decision.Decision
import io.hackle.sdk.common.ParameterConfig

Decision decision = hackleApp.variationDetail(42);
ParameterConfig config = decision.getConfig();

String strValue = decision.getString("parameter_key_string_type", "defaultValue");
String strValueInConfig = config.getString("parameter_key_string_type", "defaultValue");

String jsonValue = decision.getString("parameter_key_json_type", "defaultValue");
String jsonValueInConfig = config.getString("parameter_key_json_type", "defaultValue");

int intValue: Int = decision.getInt("parameter_key_number_type", 0)
int intValueInConfig: Int = config.getInt("parameter_key_number_type", 0)

double doubleValue: Double = decision.getDouble("parameter_key_number_type", 0.0)
double doubleValueInConfig: Double = config.getDouble("parameter_key_number_type", 0.0)

long longValue: Long = decision.getLong("parameter_key_number_type", 0L)
long longValueInConfig: Long = config.getLong("parameter_key_number_type", 0L)

boolean booleanValue: Boolean = decision.getBoolean("parameter_key_boolean_type", false)
boolean booleanValueInConfig: Boolean = config.getBoolean("parameter_key_boolean_type", false)
```

{% endtab %}
{% endtabs %}


# 기능 플래그 결정

{% hint style="info" %}
Android SDK 2.0.0 이상 버전에서 지원하는 기능입니다.
{% endhint %}

기능 플래그는 켜짐(on) 상태와 꺼짐(off) 상태가 있습니다. 각 상태에 따라 다른 기능을 설정하게 됩니다. 기능 플래그를 적용한 기능에 어떤 사용자가 접근할 경우 켜짐 혹은 꺼짐 상태를 받을 수 있어야 합니다. 이 상태 결정을 핵클 SDK를 통해 진행할 수 있습니다.

## isFeatureOn

`isFeatureOn()` 메소드에 **기능 키**를 전달하면 사용자에 대한 상태 결과를 전달받을 수 있습니다. 이후 상태에 따른 로직을 구현합니다.

아래 예제 코드에서는 기능 키 42를 전달하고 있습니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import io.hackle.android.HackleApp

// 기능 키가 42인 기능 플래그에서 사용자의 상태를 결정합니다.
// 결정하지 못하는 상황인 경우 false(꺼짐 상태)를 반환합니다.
val isFeatureOn: Boolean = hackleApp.isFeatureOn(42)

if (isFeatureOn) {
    // ON 기능
} else {
    // OFF 기능
}
```

{% endtab %}

{% tab title="Java" %}

```java
import io.hackle.android.HackleApp

// 기능 키가 42인 기능 플래그에서 사용자의 상태를 결정합니다.
// 결정하지 못하는 상황인 경우 false(꺼짐 상태)를 반환합니다.
boolean featureOn = hackleApp.isFeatureOn(42);

if (featureOn) {
    // ON 기능
} else {
    // OFF 기능
}
```

{% endtab %}
{% endtabs %}

## featureFlagDetail

`featureFlagDetail()` 메소드는 `isFeatureOn()` 메소드와 동일하게 동작하고 추가로 상태 결정에 대한 사유를 같이 제공합니다. 수동할당이 잘 되고 있는지 알아보거나 설정한 트래픽 할당 대비 결과 비중이 이상하다고 여길 때 유용하게 활용할 수 있습니다.

파라미터로 기능 키를 전달해야 합니다. 아래 예제 코드의 경우 기능 키 42를 전달하고 있습니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import io.hackle.android.HackleApp
import io.hackle.sdk.common.decision.FeatureFlagDecision

// 상태 결정 상세
val decision = hackleApp.featureFlagDetail(42)

// 기능 on/off 여부
val featureOn = decision.isOn()

// 상태 결정 사유
val reason = decision.getReason()
```

{% endtab %}

{% tab title="Java" %}

```java
import io.hackle.android.HackleApp
import io.hackle.sdk.common.decision.FeatureFlagDecision

// 상태 결정 상세
FeatureFlagDecision decision = hackleApp.featureFlagDetail(42);

// 기능 on/off 여부
boolean featureOn = decision.isOn();

// 상태 결정 사유
DecisionReason reason = decision.getReason();
```

{% endtab %}
{% endtabs %}

상태 결정 사유는 **`SDK_NOT_READY`** 와 같은 형태로 받게 됩니다. 자세한 내용은 아래 표를 참고해주세요.

<table><thead><tr><th width="233.71875">결정 사유</th><th width="370.8046875">설명</th><th>분배 결과</th></tr></thead><tbody><tr><td><code>SDK_NOT_READY</code></td><td>SDK 사용 준비가 되지 않았습니다.<br>(예: 잘못된 SDK 키로 초기화 시도)</td><td>기본 상태<br>(꺼짐/off)</td></tr><tr><td><code>FEATURE_FLAG_NOT_FOUND</code></td><td>전달한 기능 키에 대한 기능 플래그를 찾을 수 없습니다.<br>기능 키가 잘못되었거나 해당 기능 플래그가 보관 상태일 수 있습니다.</td><td>기본 상태<br>(꺼짐/off)</td></tr><tr><td><code>FEATURE_FLAG_INACTIVE</code></td><td>기능 플래그가 꺼짐 상태입니다</td><td>기본 상태<br>(꺼짐/off)</td></tr><tr><td><code>INDIVIDUAL_TARGET_MATCH</code></td><td>개별 타겟팅에 매치 되었습니다.</td><td>개별 타겟팅으로<br>설정한 상태</td></tr><tr><td><code>TARGET_RULE_MATCH</code></td><td>사용자 타겟팅에 매치 되었습니다.</td><td>사용자 타겟팅으로 설정한 상태</td></tr><tr><td><code>DEFAULT_RULE</code></td><td>개별 타겟팅, 사용자 타겟팅 중 어디에도 매치 되지 않았습니다.</td><td>기본 룰로<br>설정한 상태</td></tr><tr><td><code>EXCEPTION</code></td><td>알 수 없는 오류가 발생했습니다.</td><td>기본 상태<br>(꺼짐/off)</td></tr></tbody></table>

## 기능 플래그 파라미터

{% hint style="info" %}
Android SDK 2.9.0 이상 버전에서 지원하는 기능입니다.
{% endhint %}

* `featureFlagDetail()` 메소드를 통해 상태 결정에 대한 파라미터 값도 같이 제공받을 수 있습니다.
* `featureFlagDetail()` 메소드를 통해 전달받은 FeatureFlagDecision 인스턴스에는 전체 파라미터 설정 정보가 담긴 `ParameterConfig` 객체가 존재합니다.
* 핵클의 기능 플래그 화면에서 설정한 파라미터 값이 key, value 형태로 존재하기 때문에, 설정한 파라미터 유형에 따라 아래 메소드를 사용하여 설정한 파라미터 값을 받아 활용할 수 있습니다.
* 기능 플래그의 파라미터 설정화면에서 값을 유동적으로 변경할 수 있습니다.

### getString

* STRING, JSON 유형으로 설정된 parameter값을 반환합니다.
* 상태 결정에 따라 False 또는 True에 설정된 값을 반환합니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import io.hackle.android.HackleApp
import io.hackle.sdk.common.decision.FeatureFlagDecision
import io.hackle.sdk.common.ParameterConfig

val decision: FeatureFlagDecision = hackleApp.featureFlagDetail(42)
val config: ParameterConfig = decision.config

val strValue: String = decision.getString("parameter_key_string_type", "defaultValue")
val strValueInConfig: String = config.getString("parameter_key_string_type", "defaultValue")

val jsonValue: String = decision.getString("parameter_key_json_type", "defaultValue")
val jsonValueInConfig: String = config.getString("parameter_key_json_type", "defaultValue")
```

{% endtab %}

{% tab title="Java" %}

```java
import io.hackle.android.HackleApp
import io.hackle.sdk.common.decision.FeatureFlagDecision
import io.hackle.sdk.common.ParameterConfig

FeatureFlagDecision decision = hackleApp.featureFlagDetail(42);
ParameterConfig config = decision.getConfig();

String strValue = decision.getString("parameter_key_string_type", "defaultValue");
String strValueInConfig = config.getString("parameter_key_string_type", "defaultValue");

String jsonValue = decision.getString("parameter_key_json_type", "defaultValue");
String jsonValueInConfig = config.getString("parameter_key_json_type", "defaultValue");
```

{% endtab %}
{% endtabs %}

### getInt

* Number 유형으로 설정된 parameter 값을 Int 타입으로 반환합니다.
* 상태 결정에 따라 False 또는 True에 설정된 값을 반환합니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import io.hackle.android.HackleApp
import io.hackle.sdk.common.decision.FeatureFlagDecision
import io.hackle.sdk.common.ParameterConfig

val decision: FeatureFlagDecision = hackleApp.featureFlagDetail(42)
val config: ParameterConfig = decision.config

val intValue: Int = decision.getInt("parameter_key_number_type", 0)
val intValueInConfig: Int = config.getInt("parameter_key_number_type", 0)
```

{% endtab %}

{% tab title="Java" %}

```java
import io.hackle.android.HackleApp
import io.hackle.sdk.common.decision.FeatureFlagDecision
import io.hackle.sdk.common.ParameterConfig

FeatureFlagDecision decision = hackleApp.featureFlagDetail(42);
ParameterConfig config = decision.getConfig();

int intValue = decision.getInt("parameter_key_number_type", 0);
int intValueInConfig = config.getInt("parameter_key_number_type", 0);
```

{% endtab %}
{% endtabs %}

### getDouble

* Number 유형으로 설정된 parameter 값을 Double 타입으로 반환합니다.
* 상태 결정에 따라 False 또는 True에 설정된 값을 반환합니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import io.hackle.android.HackleApp
import io.hackle.sdk.common.decision.FeatureFlagDecision
import io.hackle.sdk.common.ParameterConfig

val decision: FeatureFlagDecision = hackleApp.featureFlagDetail(42)
val config: ParameterConfig = decision.config

val doubleValue: Double = decision.getDouble("parameter_key_number_type", 0.0)
val doubleValueInConfig: Double = config.getDouble("parameter_key_number_type", 0.0)
```

{% endtab %}

{% tab title="Java" %}

```java
import io.hackle.android.HackleApp
import io.hackle.sdk.common.decision.FeatureFlagDecision
import io.hackle.sdk.common.ParameterConfig

FeatureFlagDecision decision = hackleApp.featureFlagDetail(42);
ParameterConfig config = decision.getConfig();

double doubleValue = decision.getDouble("parameter_key_number_type", 0.0);
double doubleValueInConfig = config.getDouble("parameter_key_number_type", 0.0);
```

{% endtab %}
{% endtabs %}

### getLong

* Number 유형으로 설정된 parameter 값을 Long 타입으로 반환합니다.
* 상태 결정에 따라 False 또는 True에 설정된 값을 반환합니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import io.hackle.android.HackleApp
import io.hackle.sdk.common.decision.FeatureFlagDecision
import io.hackle.sdk.common.ParameterConfig

val decision: FeatureFlagDecision = hackleApp.featureFlagDetail(42)
val config: ParameterConfig = decision.config

val longValue: Long = decision.getLong("parameter_key_number_type", 0L)
val longValueInConfig: Long = config.getLong("parameter_key_number_type", 0L)
```

{% endtab %}

{% tab title="Java" %}

```java
import io.hackle.android.HackleApp
import io.hackle.sdk.common.decision.FeatureFlagDecision
import io.hackle.sdk.common.ParameterConfig

FeatureFlagDecision decision = hackleApp.featureFlagDetail(42);
ParameterConfig config = decision.getConfig();

long longValue = decision.getLong("parameter_key_number_type", 0L);
long longValueInConfig = config.getLong("parameter_key_number_type", 0L);
```

{% endtab %}
{% endtabs %}

### getBoolean

* Boolean 유형으로 설정된 parameter값을 반환합니다.
* 상태 결정에 따라 False 또는 True에 설정된 값을 반환합니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import io.hackle.android.HackleApp
import io.hackle.sdk.common.decision.FeatureFlagDecision
import io.hackle.sdk.common.ParameterConfig

val decision: FeatureFlagDecision = hackleApp.featureFlagDetail(42)
val config: ParameterConfig = decision.config

val booleanValue: Boolean = decision.getBoolean("parameter_key_boolean_type", false)
val booleanValueInConfig: Boolean = config.getBoolean("parameter_key_boolean_type", false)
```

{% endtab %}

{% tab title="Java" %}

```java
import io.hackle.android.HackleApp
import io.hackle.sdk.common.decision.FeatureFlagDecision
import io.hackle.sdk.common.ParameterConfig

FeatureFlagDecision decision = hackleApp.featureFlagDetail(42);
ParameterConfig config = decision.getConfig();

boolean booleanValue = decision.getBoolean("parameter_key_boolean_type", false);
boolean booleanValueInConfig = config.getBoolean("parameter_key_boolean_type", false);
```

{% endtab %}
{% endtabs %}


# 원격 구성 적용

{% hint style="info" %}
Android SDK 2.11.0 이상 버전에서 지원하는 기능입니다.
{% endhint %}

원격 구성은 애플리케이션에서 관리되고 있는 값, 또는 속성들을 핵클 대시보드에서 정의한 파라미터 값들로 대체하여 실시간으로 애플리케이션의 동작 및 설정 값들을 제어할 수 있는 기능입니다.

핵클의 대시보드의 원격 구성 화면으로 이동하여 파라미터 정보들을 설정하고, 사용자 식별 규칙에 따른 값들을 설정할 수 있습니다.

## remoteConfig

`remoteConfig()` 메소드를 호출하면 사용자에 대한 원격 구성 정보(설정한 파라미터 및 규칙 정보)를 담고 있는 `HackleRemoteConfig` 인스턴스를 얻을 수 있습니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import io.hackle.android.HackleApp
import io.hackle.sdk.common.HackleRemoteConfig

// 원격 구성 정보 담은 인스턴스를 반환합니다.
val remoteConfig: HackleRemoteConfig = hackleApp.remoteConfig()
```

{% endtab %}

{% tab title="Java" %}

```java
import io.hackle.android.HackleApp
import io.hackle.sdk.common.HackleRemoteConfig

// 원격 구성 정보 담은 인스턴스를 반환합니다.
HackleRemoteConfig remoteConfig = hackleApp.remoteConfig();
```

{% endtab %}
{% endtabs %}

## 원격 구성 파라미터 조회

핵클의 원격 구성 화면에서 설정한 파라미터 값이 key, value 형태로 존재하기 때문에, 설정한 파라미터 유형에 따라 아래 메소드를 사용하여 설정한 파라미터 값을 반환받을 수 있습니다.

{% hint style="warning" %}
보관 후 원격 구성과 관련된 코드를 제거하세요.

원격 구성 파라미터를 보관한 경우 더이상 파라미터 정보에 접근 할 수 없습니다. 원격 구성 파라미터 보관 후에는 반드시 관련된 코드를 정리해주시기 바랍니다.
{% endhint %}

### getString

* STRING, JSON 유형으로 설정된 파라미터 값을 반환합니다.
* 상태 결정에 따라 기본 값 혹은 규칙에 설정된 값을 반환합니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import io.hackle.android.HackleApp
import io.hackle.sdk.common.HackleRemoteConfig

val remoteConfig: HackleRemoteConfig = hackleApp.remoteConfig()

val strValue: String = remoteConfig.getString("parameter_key_string_type", "defaultValue")

val jsonValue: String = remoteConfig.getString("parameter_key_json_type", "defaultValue")
```

{% endtab %}

{% tab title="Java" %}

```java
import io.hackle.android.HackleApp
import io.hackle.sdk.common.HackleRemoteConfig

HackleRemoteConfig remoteConfig = hackleApp.remoteConfig();

String strValue = remoteConfig.getString("parameter_key_string_type", "defaultValue");

String jsonValue = remoteConfig.getString("parameter_key_json_type", "defaultValue");
```

{% endtab %}
{% endtabs %}

### getInt

* Number 유형으로 설정된 parameter 값을 Int 타입으로 반환합니다.
* 상태 결정에 따라 기본 값 혹은 규칙에 설정된 값을 반환합니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import io.hackle.android.HackleApp
import io.hackle.sdk.common.HackleRemoteConfig

val remoteConfig: HackleRemoteConfig = hackleApp.remoteConfig()

val intValue: Int = remoteConfig.getInt("parameter_key_int_type", 0)
```

{% endtab %}

{% tab title="Java" %}

```java
import io.hackle.android.HackleApp
import io.hackle.sdk.common.HackleRemoteConfig

HackleRemoteConfig remoteConfig = hackleApp.remoteConfig();

int intValue = remoteConfig.getInt("parameter_key_int_type", 0);
```

{% endtab %}
{% endtabs %}

### getDouble

* Number 유형으로 설정된 parameter 값을 Double 타입으로 반환합니다.
* 상태 결정에 따라 기본 값 혹은 규칙에 설정된 값을 반환합니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import io.hackle.android.HackleApp
import io.hackle.sdk.common.HackleRemoteConfig

val remoteConfig: HackleRemoteConfig = hackleApp.remoteConfig()

val doubleValue: Double = remoteConfig.getDouble("parameter_key_double_type", 0.0)
```

{% endtab %}

{% tab title="Java" %}

```java
import io.hackle.android.HackleApp
import io.hackle.sdk.common.HackleRemoteConfig

HackleRemoteConfig remoteConfig = hackleApp.remoteConfig();

double doubleValue = remoteConfig.getDouble("parameter_key_double_type", 0.0);
```

{% endtab %}
{% endtabs %}

### getLong

* Number 유형으로 설정된 parameter 값을 Long 타입으로 반환합니다.
* 상태 결정에 따라 기본 값 혹은 규칙에 설정된 값을 반환합니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import io.hackle.android.HackleApp
import io.hackle.sdk.common.HackleRemoteConfig

val remoteConfig: HackleRemoteConfig = hackleApp.remoteConfig()

val longValue: Long = remoteConfig.getLong("parameter_key_long_type", 0L)
```

{% endtab %}

{% tab title="Java" %}

```java
import io.hackle.android.HackleApp
import io.hackle.sdk.common.HackleRemoteConfig

HackleRemoteConfig remoteConfig = hackleApp.remoteConfig();

long longValue = remoteConfig.getLong("parameter_key_long_type", 0L);
```

{% endtab %}
{% endtabs %}

### getBoolean

* Boolean 유형으로 설정된 parameter값을 반환합니다.
* 상태 결정에 따라 기본 값 혹은 규칙에 설정된 값을 반환합니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import io.hackle.android.HackleApp
import io.hackle.sdk.common.HackleRemoteConfig

val remoteConfig: HackleRemoteConfig = hackleApp.remoteConfig()

val booleanValue: Boolean = remoteConfig.getBoolean("parameter_key_boolean_type", false)
```

{% endtab %}

{% tab title="Java" %}

```java
import io.hackle.android.HackleApp
import io.hackle.sdk.common.HackleRemoteConfig

HackleRemoteConfig remoteConfig = hackleApp.remoteConfig();

boolean booleanValue = remoteConfig.getBoolean("parameter_key_boolean_type", false);
```

{% endtab %}
{% endtabs %}


# 이벤트 전송

핵클 SDK는 사용자 이벤트를 핵클로 전송하는 기능을 제공합니다.

사용자 행동의 변화가 일어나는 지점마다 이 기능을 활용하면 사용자 행동에 대한 유의미한 데이터를 얻을 수 있으며, 그렇게 모인 데이터를 통해 사용자 행동 분석을 할 수 있습니다.

{% hint style="info" %}
대시보드 [이벤트관리](https://dashboard.hackle.io/event-management) 메뉴에서 전송한 이벤트를 확인할 수 있습니다.

이벤트 전송 후 대시보드에 표시되기까지 일반적으로 \~60초가 걸립니다.
{% endhint %}

## track

`track()` 메소드에 **이벤트 키**를 전달하여 사용자 이벤트를 전송할 수 있습니다.

<table><thead><tr><th width="150">파라미터</th><th width="120">타입</th><th width="120">필수</th><th>제약사항</th></tr></thead><tbody><tr><td>이벤트 명(key)</td><td><code>string</code></td><td>필수</td><td>글자수 제한은 128자입니다. (128 characters)</td></tr></tbody></table>

#### 예시

사용자가 구매하기 버튼을 눌렀을 때 이벤트를 수집하기 위해 `purchase` 라는 이벤트 키를 정의했다고 가정합니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import io.hackle.android.HackleApp
import io.hackle.sdk.common.Event

hackleApp.track("purchase")
```

{% endtab %}

{% tab title="Java" %}

```java
import io.hackle.android.HackleApp
import io.hackle.sdk.common.Event

hackleApp.track("purchase");
```

{% endtab %}
{% endtabs %}

### 속성(Property)

핵클 SDK는 이벤트(Event) 객체에 속성을 추가할 수 있도록 지원합니다.

* 속성은 속성명(key)과 속성값(value)을 한 쌍으로 보내야 합니다.
* 이벤트 객체에 추가 가능한 속성 개수는 최대 64개입니다.

<table><thead><tr><th width="133.04296875">구분</th><th width="127.60546875">타입</th><th>제약사항</th></tr></thead><tbody><tr><td>속성 명(key)</td><td><code>string</code></td><td><ul><li>글자수 제한은 128자입니다. (128 characters)</li><li>대소문자를 구분하지 않습니다.</li><li>예를 들어 AGE와 age는 동일한 속성명으로 인식합니다.</li></ul></td></tr><tr><td>속성 값(value)</td><td><code>boolean</code>, <code>string</code>, <code>number</code>, <code>array</code></td><td><ul><li>string 타입인 경우 글자수 제한은 1024자입니다.<br>(1024 characters)</li><li>string 타입은 대소문자를 구분합니다.</li><li>예를 들어 APPLE과 apple은 서로 다른 속성값으로 인식합니다.</li><li>number 타입인 경우 정수 최대 15자리, 소수점 최대 6자리를<br>지원합니다.</li></ul></td></tr></tbody></table>

#### 예시

아래 예시에서는 세 가지 속성(`pay_method`, `discount_amount`, `is_discount`)을 추가한 것을 확인할 수 있습니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import io.hackle.android.HackleApp
import io.hackle.sdk.common.Event

val event = Event.builder("purchase")
    .property("pay_method", "CARD")
    .property("discount_amount", 800)
    .property("is_discount", true)
    .build()


hackleApp.track(event)
```

{% endtab %}

{% tab title="Java" %}

```java
import io.hackle.android.HackleApp
import io.hackle.sdk.common.Event

Event event = Event.builder("purchase")
    .property("pay_method", "CARD")
    .property("discount_amount", 800)
    .property("is_discount", true)
    .build();

hackleApp.track(event);
```

{% endtab %}
{% endtabs %}


# 사용자 화면 추적

Hackle SDK는 Activity 단위로 화면 정보를 수집합니다.\
SAA(Single Activity Architecture) 구조의 앱이거나 [Compose UI](https://developer.android.com/develop/ui/compose/architecture) 를 사용하는 경우 자동으로 화면 정보를 수집하기 어렵습니다.

`$page_view` 및 `$engagement` 이벤트를 정상적으로 수집하려면, 화면이 변경될 때마다 setCurrentScreen 메소드를 직접 호출해야 합니다.

## setCurrentScreen

{% hint style="info" %}
Android SDK 2.64.0 이상 버전에서 정식으로 지원하는 기능입니다.
{% endhint %}

`setCurrentScreen(screen)` 메소드로 화면을 추적합니다.

* 화면 추적의 최소 단위 시간은 1초 입니다.
* 1초 이내에 변경된 페이지의 `$engagement` 는 측정되지 않습니다.

<table><thead><tr><th width="150">파라미터</th><th width="120">타입</th><th width="110">필수</th><th>설명</th></tr></thead><tbody><tr><td>name</td><td><code>string</code></td><td>필수</td><td>현재 화면의 명입니다.</td></tr><tr><td>className</td><td><code>string</code></td><td>필수</td><td>별도의 클래스 명을 남기지 않을 경우 name과 동일한 값을 기입하면 됩니다.</td></tr></tbody></table>

{% hint style="warning" %}
동일 화면에 대한 화면 추적은 할 수 없습니다.

이전 화면과 동일한(name과 className이 모두 동일한) Screen으로 `setCurrentScreen`을 호출한 경우 화면 전환이 되지 않은 것으로 인식되어 아무런 동직을 하지 않습니다.

* `$page_view`, `$engagement`가 호출되지 않습니다.
  {% endhint %}

#### 예시

Compose UI를 사용하는 경우 `ON_RESUME` 이벤트가 발생할 때 화면 추적을 하는 것을 추천합니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
val hackleApp = Hackle.app

val screen = Screen.builder("screenName", "className")
    .build()

Hackle.app.setCurrentScreen(screen)
```

{% endtab %}

{% tab title="Java" %}

```java
HackleApp hackleApp = HackleApp.getInstance();

Screen screen = Screen.builder("screenName", "className")
    .build();

hackleApp.setCurrentScreen(screen);
```

{% endtab %}

{% tab title="Compose UI" %}

```kotlin
@Composable
fun TrackScreen(screenName: String, className: String) {
    val lifecycleOwner = LocalLifecycleOwner.current

    DisposableEffect(lifecycleOwner, screenName, className) {
        val observer = LifecycleEventObserver { _, event ->
            // 뷰가 나타날 때 & 백그라운드 -> 포그라운드 전환될 때 화면 추적
            if (event == Lifecycle.Event.ON_RESUME) {
                val screen = Screen.builder(screenName, className).build()
                hackleApp.setCurrentScreen(screen)
            }
        }
        lifecycleOwner.lifecycle.addObserver(observer)

        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }
}

@Composable
fun MainScreen() {
    TrackScreen("Main", "MainScreen")
}
```

{% endtab %}
{% endtabs %}

### 속성 (Property)

핵클 SDK는 이벤트(Event) 객체에 속성을 추가할 수 있도록 지원합니다.

* 이벤트 객체에 추가 가능한 속성 개수는 최대 64개입니다.

<table><thead><tr><th width="133.04296875">구분</th><th width="127.60546875">타입</th><th>제약사항</th></tr></thead><tbody><tr><td>속성 명(key)</td><td><code>string</code></td><td><ul><li>글자수 제한은 128자입니다. (128 characters)</li><li>대소문자를 구분하지 않습니다.</li><li>예를 들어 AGE와 age는 동일한 속성명으로 인식합니다.</li></ul></td></tr><tr><td>속성 값(value)</td><td><code>boolean</code>, <code>string</code>, <code>number</code>, <code>array</code></td><td><ul><li>string 타입인 경우 글자수 제한은 1024자입니다.<br>(1024 characters)</li><li>string 타입은 대소문자를 구분합니다.</li><li>예를 들어 APPLE과 apple은 서로 다른 속성값으로 인식합니다.</li><li>number 타입인 경우 정수 최대 15자리, 소수점 최대 6자리를<br>지원합니다.</li></ul></td></tr></tbody></table>

#### 예시

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
val hackleApp = Hackle.app

val screen = Screen.builder("screenName", "className")
    .property("key", "value")
    .build()

Hackle.app.setCurrentScreen(screen)
```

{% endtab %}

{% tab title="Java" %}

```java
HackleApp hackleApp = HackleApp.getInstance();

Screen screen = Screen.builder("screenName", "className")
    .property("key", "value")
    .build();

hackleApp.setCurrentScreen(screen);
```

{% endtab %}
{% endtabs %}

## automaticScreenTracking 비활성화

SDK에서는 기본적으로 `automaticScreenTracking`이 활성화되어 있습니다.\
Activity 단위의 자동 화면 정보 수집을 원하지 않는 경우 `automaticScreenTracking`를 비활성화 해야 합니다.

SDK를 초기화 할 때 `automaticScreenTracking`을 설정할 수 있습니다.

{% hint style="danger" %}
`automaticScreenTracking`이 활성화 된 상태에서 `setCurrentScreen`를 통해 수동으로 화면 수집을 하는 경우, `$page_view`와 `$engagement`가 과수집 되거나 의도하지 않은 속성으로 수집될 수 있습니다.

**수동으로 화면 정보를 수집하는 경우 `automaticScreenTracking` 비활성화를 추천합니다.**
{% endhint %}

#### 예시

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import io.hackle.android.Hackle
import io.hackle.android.HackleConfig
import io.hackle.android.initialize

val config = HackleConfig.builder()
  .automaticScreenTracking(false)
  .build()

Hackle.initialize(applicationContext, YOUR_APP_SDK_KEY, config) {
  // SDK ready to use.
}
```

{% endtab %}

{% tab title="Java" %}

```java
import io.hackle.android.HackleApp;
import io.hackle.android.HackleConfig;

HackleConfig config = HackleConfig.builder()
  .automaticScreenTracking(false)
  .build();

HackleApp.initializeApp(getApplicationContext(), YOUR_APP_SDK_KEY, config, () -> {
  // SDK ready to use.
});
```

{% endtab %}
{% endtabs %}


# 사용자 탐색

{% hint style="info" %}
**Debug 빌드에서만 사용하는 것을 권장합니다**
{% endhint %}

{% hint style="warning" %}
원격 평가 방식에서는 사용자 탐색 기능을 사용할 수 없습니다.
{% endhint %}

사용자 식별자를 확인하고 A/B 테스트, 기능플래그에 강제할당 하는 방법을 설명합니다.

아래 코드를 추가합니다.

```java
@Override
public void onCreate(Bundle savedInstanceState) {
  //...
  hackleApp.showUserExplorer();
  //...
}
```

화면 하단에 핵클 로고 버튼이 표시됩니다. 버튼 클릭시 설정 화면으로 진입 할 수 있습니다.

<div data-full-width="false"><img src="/files/QJcsrZweauOdE4h4ecOS" alt="" width="375"></div>

## 사용자 식별자 확인하기

화면 상단에서 사용자 식별자를 확인 및 복사 할 수 있습니다.

푸시메시지를 지원하는 SDK 버전인 경우 이 기기의 푸시 토큰 정보도 확인할 수 있습니다.

![](/files/EIJ0M6R9V9TSGonpB4yv)

## 사용자 강제 할당

* 화면하단에서 A/B 테스트, 기능플래그의 분배 결과를 확인할 수 있습니다.
* SelectBox 클릭 시 특정 그룹으로 강제할당 할 수 있습니다.
* `RESET` 버튼 클릭 시 강제할당이 해제됩니다.
* `RESET ALL` 버튼 클릭시 모든 강제할당이 해제됩니다.
* 앱에서 강제할당한 경우 앱에서 분배하는 경우에만 적용됩니다. (대시보드 테스트기기에 등록되지 않습니다)
* 강제할당이 적용되지 않는경우 앱을 완전 종료 후 재실행 해주세요.

![](/files/Gi8crcYvmV0iTc3dcwL6)

![](/files/EVtJjrdHzdsNtRipbLnu)


# 웹앱 연동

{% hint style="info" %}
Android SDK 2.29.0 이상, JavaScript SDK 11.24.1 이상 버전에서 지원하는 기능입니다.

웹앱에 대해서는 [문서](/development-guide/faq/web-app-intergration)를 참고해주세요.
{% endhint %}

`WebView` 를 통해 자사 웹사이트를 랜더링하는 경우, 다음 같은 설정을 통해 웹사이트에 포함된 핵클 JavaScript SDK를 웹사이트 코드 변경없이 핵클 Android SDK 기능과 동일하게 사용할 수 있습니다.

브릿지 설정을 하면 웹뷰에서 발생하는 핵클 이벤트는 Android SDK를 통해 수집됩니다.

{% hint style="info" %}
웹뷰 브릿지는 Hackle JavaScript SDK에서 브릿지로 전달한 데이터만 Hackle Android SDK의 JavascriptInterface로 처리합니다.
{% endhint %}

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
...
Hackle.app.setWebViewBridge(webView)
...
```

{% endtab %}

{% tab title="Java" %}

```java
...
HackleApp.getInstance().setWebViewBridge(webView, HackleWebViewConfig.DEFAULT)
...
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
해당 기능을 사용하기 위해서는 **JavaScript 웹페이지에서 동일한 App SDK 키를 사용**해야 합니다.

핵클 안드로이드 웹뷰 설정은 안드로이드 `Javascript Interface`를 통해 핵클 JavaScript SDK와 상호작용하게 됩니다. 반드시 `WebView::loadUrl` 함수 호출 이전에 해당 설정이 완료될 수 있도록 코드를 위치시켜 주세요.
{% endhint %}

### 웹뷰에서 발생하는 자동 수집 이벤트 연동

{% hint style="info" %}
Android SDK 2.62.0 이상, JavaScript SDK 11.51.0 이상 버전에서 지원하는 기능입니다.
{% endhint %}

웹뷰 내 웹사이트에서 발생하는 `$page_view`와 `$engagement`는 비활성화 상태입니다. 웹뷰 브릿지를 설정할 때 `HackleWebViewConfig`를 설정하여 자동 수집 이벤트를 각각 활성화할 수 있습니다.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
val webViewConfig = HackleWebViewConfig
	.builder()
	.automaticScreenTracking(true)
	.automaticEngagementTracking(true)
	.build()
Hackle.app.setWebViewBridge(webView, webViewConfig)
```

{% endtab %}

{% tab title="Java" %}

```java
HackleWebViewConfig webViewConfig = HackleWebViewConfig
	.builder()
	.automaticScreenTracking(true)
	.automaticEngagementTracking(true)
	.build();

HackleApp.getInstance().setWebViewBridge(webView, webViewConfig);
```

{% endtab %}
{% endtabs %}

#### 설정 옵션

<table><thead><tr><th width="260.20703125">Option</th><th width="110">Default</th><th>Description</th></tr></thead><tbody><tr><td><code>automaticScreenTracking</code></td><td><code>false</code></td><td>웹사이트에서 발생해는 <code>$page_view</code> 수집 여부</td></tr><tr><td><code>automaticEngagementTracking</code></td><td><code>false</code></td><td>웹사이트에서 발생하는 <code>engagement</code> 수집 여부</td></tr><tr><td><code>automaticRouteTracking</code></td><td><code>true</code></td><td>웹사이트에서 발생하는 페이지 정보 자동 수집 여부</td></tr></tbody></table>

{% hint style="info" %}
웹페이지 이동 시 `$page_view`와 `$engagement` 를 자동 수집하려면 `automaticScreenTracking`, `automaticEngagementTracking`, `automaticRouteTracking` 를 모두 `true`로 설정하세요.

웹페이지 이동 시 `$page_view`와 `$engagement` 를 [수동 수집](/development-guide/javascript/event-tracking/js-track-page)하는 경우 `automaticScreenTracking`, `automaticEngagementTracking`는 `true`로 설정하고, `automaticRouteTracking` 를 `false`로 설정하세요.
{% endhint %}


# 푸시 메시지 연동

{% stepper %}
{% step %}
**Firebase 프로젝트 연동하기**

안드로이드 앱에서 푸시 메시지를 사용하기 위해서는 핵클 워크스페이스와 Firebase 프로젝트 연동 설정이 필요합니다.

{% content-ref url="/pages/Y3ElrX93z1lmQQsVCQgx" %}
[Android FCM 연동](/external-link/crm-channels/fcm-integration)
{% endcontent-ref %}
{% endstep %}

{% step %}
**Firebase Cloud Messaging SDK 연동하기**

[Firebase Cloud Messaging 설치 가이드](https://firebase.google.com/docs/cloud-messaging/android/client) 를 참고하여 안드로이드 앱 설정을 완료해주세요.
{% endstep %}

{% step %}
**핵클 SDK와 연동하기**

[SDK 연동](/development-guide/android) 를 참고해서 핵클 SDK 의존성을 추가하고 SDK를 초기화 합니다.

앱 빌드, 실행 시 자동으로 푸시 토큰이 등록 됩니다.

{% hint style="info" %}
정상적으로 SDK 연동이 완료되면 자동으로 푸시 토큰 수집이 되고, 푸시 수신, 푸시 클릭 처리가 가능합니다.
{% endhint %}
{% endstep %}

{% step %}
**푸시 메시지 테스트**

**토큰 확인**

* [사용자 식별자 확인하기 가이드](/development-guide/android/android-user-explorer) 를 통해 안드로이드 기기에 설정된 토큰을 확인할 수 있습니다.
* [사용자 조회 가이드](/user-view/user-profile) 를 통해 특정 사용자에 할당 된 안드로이드 푸시 토큰을 확인할 수 있습니다.

**발송 테스트**

* [푸시 메시지 테스트 발송 가이드](/crm-marketing/push-message-guide/create-campaign#id-3-1)를 참고하여 푸시 메시지를 안드로이드 기기에서 확인합니다.
  {% endstep %}

{% step %}
**푸시 메시지 수신**

푸시 수신 시 status bar와 알림센터에 아이콘이 표시됩니다.

* 갤럭시 안드로이드 스마트폰 환경에서는 앱 아이콘이 표시됩니다.
* 일반 안드로이드 스마트폰 or 시뮬레이터 환경에서는 흰색 원 아이콘이 표시됩니다.
* 푸시메시지 아이콘 변경 기능을 이용한 경우 변경한 아이콘이 표시됩니다.
  {% endstep %}
  {% endstepper %}

## 딥링크 이동

핵클 푸시 메시지는 클릭 시 딥링크 이동을 지원합니다. 푸시 메시지를 통해 해당 액티비티가 열리는 경우 아래와 같은 방법으로 열린 딥링크 정보를 확인할수 있습니다.

{% hint style="info" %}
안드로이드 딥링크에 대한 자세한 사항은 [안드로이드 딥링크 가이드](https://developer.android.com/training/app-links/deep-linking) 에서 확인 가능합니다.
{% endhint %}

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import android.app.Activity
import android.content.Intent
import android.os.Bundle

class ExampleActivity : Activity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstance)
        if (intent != null && !intent.dataString.isNullOrEmpty()) {
            //  Do something ...
            println("link : ${intent?.dataString}")
        }
    }

    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        setIntent(intent)
        if (intent != null && !intent.dataString.isNullOrEmpty()) {
            //  Do something ...
            println("link : ${intent?.dataString}")
        }
    }
}
```

{% endtab %}

{% tab title="Java" %}

```java
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;

public class ExampleActivity extends Activity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Intent intent = getIntent();
        if (intent != null && intent.getDataString() != null) {
            String text = String.format("link : %s", intent.getDataString());
            System.out.println(text);
        }
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        setIntent(intent);
        if (intent != null && intent.getDataString() != null) {
            String text = String.format("link : %s", intent.getDataString());
            System.out.println(text);
        }
    }
}
```

{% endtab %}
{% endtabs %}

## 푸시 메시지 아이콘 변경

{% hint style="info" %}
Android SDK 2.57.0 버전 이상에서 지원하는 기능입니다.
{% endhint %}

핵클 푸시 메시지는 푸시 아이콘 변경을 지원합니다. `AndroidManifest.xml`에 미리 예약된 key에 리소스를 할당하면 앱 내 로컬 리소스를 이용해서 푸시 아이콘을 변경할 수 있습니다.

{% hint style="warning" %}
구글 정책으로 일반 안드로이드 스마트폰에서는 색상이 포함된 아이콘을 푸시 아이콘으로 설정할 수 없습니다.
{% endhint %}

{% hint style="info" %}
small\_icon, large\_icon, color에 대한 자세한 사항은 [안드로이드 푸시 디자인 가이드](https://developer.android.com/design/ui/mobile/guides/home-screen/notifications?hl=ko#notification-header) 에서 확인 가능합니다.
{% endhint %}

<table><thead><tr><th width="451.35546875">key</th><th>설명</th></tr></thead><tbody><tr><td><code>io_hackle_android_default_notification_small_icon</code></td><td>small icon을 설정합니다.</td></tr><tr><td><code>io_hackle_android_default_notification_large_icon</code></td><td>large icon을 설정합니다.</td></tr><tr><td><code>io_hackle_android_default_notification_color</code></td><td>small icon background 색상을 설정합니다.</td></tr></tbody></table>

#### Example

```xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
    <application
        android:name=".MainApplication"
        >
       ...
       <meta-data
            android:name="io_hackle_android_default_notification_small_icon"
            android:resource="@drawable/ic_push_small"/>

       <meta-data
            android:name="io_hackle_android_default_notification_large_icon"
            android:resource="@drawable/ic_push_large"/>

       <meta-data
            android:name="io_hackle_android_default_notification_color"
            android:resource="@color/pie" />
       ...
    </application>
</manifest>
```

#### 대시보드에서 푸시 메시지 아이콘 수정

대시보드에서 푸시 아이콘을 설정 한 경우 푸시의 large icon을 수정합니다.

* 로컬 리소스로 푸시 아이콘을 설정하고 대시보드에서도 푸시 아이콘을 송신 한 경우 대시보드에 설정 한 아이콘을 사용합니다.

## 푸시 채널 지원

{% hint style="info" %}
Android SDK 2.58.0 버전 이상에서 지원하는 기능입니다.
{% endhint %}

핵클 푸시 메시지를 이용하면 앱 내 미리 선언한 채널로 푸시를 수신받을 수 있습니다. 앱에 존재하지 않는 채널 ID로 푸시 수신 시 핵클에서 제공하는 기본 채널로 푸시를 수신받습니다.

{% hint style="info" %}
앱에 푸시 채널을 추가하는 방법은 [안드로이드 개발자 문서](https://developer.android.com/develop/ui/views/notifications/channels?hl=ko) 에서 확인 가능합니다.

만약 iOS와 같이 푸시 수신 시 알림 팝업을 띄우고 싶다면 푸시 채널의 중요도를 `IMPORTANCE_HIGH`로 설정해주세요.
{% endhint %}


# 옵트아웃

옵트아웃이 활성화되면 SDK는 모든 이벤트 전송을 중단합니다.

## 초기화 시 설정

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
val config = HackleConfig.builder()
    .optOutTracking(true)
    .build()

Hackle.initialize(applicationContext, YOUR_APP_SDK_KEY, config) {
    // SDK ready to use.
}
```

{% endtab %}

{% tab title="Java" %}

```java
HackleConfig config = HackleConfig.builder()
    .optOutTracking(true)
    .build();

HackleApp.initializeApp(getApplicationContext(), YOUR_APP_SDK_KEY, config, () -> {
    // SDK ready to use.
});
```

{% endtab %}
{% endtabs %}

## 런타임 옵트아웃 제어

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
Hackle.setOptOutTracking(true)
Hackle.setOptOutTracking(false)
val isOptOut = Hackle.isOptOutTracking
```

{% endtab %}

{% tab title="Java" %}

```java
hackleApp.setOptOutTracking(true);
hackleApp.setOptOutTracking(false);
boolean isOptOut = hackleApp.isOptOutTracking();
```

{% endtab %}
{% endtabs %}

## 영속성 관리

{% hint style="warning" %}
런타임에 변경된 옵트아웃 상태는 앱 재시작 시 초기화 Config에 설정된 값으로 리셋됩니다.

앱 재시작 후에도 상태를 유지하려면 직접 저장 및 복원 로직을 구현해야 합니다.
{% endhint %}

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
fun saveOptOutState(context: Context, optOut: Boolean) {
    context.getSharedPreferences("hackle_prefs", Context.MODE_PRIVATE)
        .edit()
        .putBoolean("hackle_opt_out", optOut)
        .apply()
    Hackle.setOptOutTracking(optOut)
}

fun getOptOutConfig(context: Context): HackleConfig {
    val optOut = context.getSharedPreferences("hackle_prefs", Context.MODE_PRIVATE)
        .getBoolean("hackle_opt_out", false)
    return HackleConfig.builder()
        .optOutTracking(optOut)
        .build()
}
```

{% endtab %}

{% tab title="Java" %}

```java
void saveOptOutState(Context context, boolean optOut) {
    context.getSharedPreferences("hackle_prefs", Context.MODE_PRIVATE)
        .edit()
        .putBoolean("hackle_opt_out", optOut)
        .apply();
    hackleApp.setOptOutTracking(optOut);
}

HackleConfig getOptOutConfig(Context context) {
    boolean optOut = context.getSharedPreferences("hackle_prefs", Context.MODE_PRIVATE)
        .getBoolean("hackle_opt_out", false);
    return HackleConfig.builder()
        .optOutTracking(optOut)
        .build();
}
```

{% endtab %}
{% endtabs %}


# iOS

{% hint style="info" %}
Hackle iOS SDK 는 iOS 13 이상을 지원합니다.

* Hackle iOS SDK 3.0.0 버전부터 최소 지원 버전을 iOS 13으로 변경하였습니다.
* Hackle iOS SDK 2.x 버전의 최소 지원 버전은 iOS 10 입니다.
  {% endhint %}

## 의존성 추가

Hackle iOS SDK는 Swift Package Manager와 CocoaPods를 지원합니다.

[![](https://img.shields.io/github/v/release/hackle-io/hackle-ios-sdk?label=spm)](https://github.com/hackle-io/hackle-ios-sdk) [![](https://img.shields.io/cocoapods/v/Hackle)](https://cocoapods.org/pods/Hackle)

{% tabs %}
{% tab title="Swift Package Manager" %}

```swift
// ...
dependencies: [
    .package(url: "https://github.com/hackle-io/hackle-ios-sdk.git", from: "3.2.1")
],
targets: [
    .target(
        name: "YOUR_TARGET",
        dependencies: ["Hackle"]
    )
],
// ...
```

{% endtab %}

{% tab title="CocoaPods" %}

```
pod 'Hackle', '3.2.1'
```

{% endtab %}
{% endtabs %}

## SDK 초기화

SDK를 사용하기 위해서 반드시 `HackleApp`을 초기화 해야 합니다. `HackleApp`을 초기화 하기 위해 SDK 키가 필요합니다.

* `HackleApp`은 SDK의 기능을 사용하기 위한 메소드들을 제공하는 클래스입니다.
* SDK 키는 핵클 서비스의 대시보드 안에 위치한 [SDK 연동 정보](https://dashboard.hackle.io/config/sdk-setting)에서 확인하실 수 있습니다.

초기화는 **비동기로 실행**되며, 핵클 서버로부터 필요한 정보들을 가져와서 SDK에 저장합니다.

{% hint style="warning" %}
초기화가 완료 되기 전에 A/B 테스트, 기능 플래그를 호출하면 기본 그룹(A), 꺼짐(false)을 리턴합니다.
{% endhint %}

{% tabs %}
{% tab title="Swift" %}

```swift
import Hackle

Hackle.initialize(sdkKey: YOUR_APP_SDK_KEY) {
    // SDK ready to use.
}

await Hackle.initialize(sdkKey: YOUR_APP_SDK_KEY)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
@import Hackle;

[Hackle initializeWithSdkKey:@"YOUR_APP_SDK_KEY" config:[HackleConfig DEFAULT] completion:^{
    // SDK ready to use.
}];
```

{% endtab %}
{% endtabs %}

#### 권장 초기화 전략: 로딩 화면을 통한 초기화

앱을 즉시 시작하지 않고 스플레시 화면을 표시하고 SDK를 초기화합니다. 이후 콜백을 통해 스플레시 화면을 닫고 사용자가 앱과 상호작용을 시작할 수 있도록 합니다.

### 초기화 시 사용자 주입

유저 정보를 포함하여 SDK를 초기화 할 수 있습니다.

* 유저 정보를 포함하지 않으면 로컬 스토리지에 저장된 유저 정보를 사용합니다.
* 유저 정보를 포함하는 경우 로컬 스토리지에 저장된 유저 정보는 사용하지 않습니다.
* 사용자 주입을 하지 않고, 로컬 스토리지에 저장된 유저 정보도 없는 경우 [Hackle Device ID](/getting-started/user-identifier)를 device id로 가지고 유저를 사용합니다.

{% hint style="info" %}
유저 정보는 SDK 초기화 이후에도 유저 정보 설정 함수를 통해 자유롭게 수정 할 수 있습니다.
{% endhint %}

{% hint style="warning" %}
초기화 시 주입한 유저 정보와 로컬 스토리지에 저장된 유저 정보는 병합하지 않습니다.

ex) 스토리지에 `userId: A` 가 저장된 상태에서 초기화 시 `deviceId: B` 를 주입하는 경우, `userId: null, deviceId: B`인 유저로 설정됩니다.
{% endhint %}

{% tabs %}
{% tab title="Swift" %}

```swift
let user = User.builder()
    .userId("142")                  // 사용자 ID
    .deviceId("ae2182e0")           // 디바이스 ID
    .build()

Hackle.initialize(sdkKey: YOUR_APP_SDK_KEY, user: user) {
    // SDK ready to use.
}

await Hackle.initialize(sdkKey: YOUR_APP_SDK_KEY, user: user)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
HackleUserBuilder *builder = [HackleUser builder];
[builder userId:@"142"];                   // 사용자 ID
[builder deviceId:@"ae2182e0"];            // 디바이스 ID
HackleUser *user = [builder build];

[Hackle initializeWithSdkKey:@"YOUR_APP_SDK_KEY" user:user completion:^{
    // SDK ready to use.
}];
```

{% endtab %}
{% endtabs %}

### 초기화 설정정보

설정정보를 포함하여 SDK를 초기화 할 수 있습니다

{% tabs %}
{% tab title="Swift" %}

```swift
let config = HackleConfigBuilder()
  .build()

Hackle.initialize(sdkKey: YOUR_APP_SDK_KEY, config: config) {
    // SDK ready to use.
}

await Hackle.initialize(sdkKey: YOUR_APP_SDK_KEY, config: config)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
HackleConfigBuilder *builder = [[HackleConfigBuilder alloc] init];
[builder exposureEventDedupIntervalSeconds:1];
HackleConfig *config = [builder build];

[Hackle initializeWithSdkKey:@"YOUR_APP_SDK_KEY" config:config completion:^{
    // SDK ready to use.
}];
```

{% endtab %}
{% endtabs %}

#### 설정 옵션

<table data-full-width="false"><thead><tr><th width="215.02734375">설정</th><th width="295.48828125">기능</th><th width="125.8125">기본값</th><th width="108.09765625">지원 버전</th></tr></thead><tbody><tr><td><code>exposureEventDedupIntervalSeconds</code></td><td><p>동일한 사용자가 연속으로 발생시킨 동일한 A/B 테스트, 기능플래그 분배결과에 대한 노출 이벤트를 제거합니다.</p><ul><li>최솟값: 1</li><li>최댓값: 86400(24시간)</li></ul></td><td>60 (1분)</td><td>2.7.0+</td></tr><tr><td><code>eventFlushInterval</code></td><td><p>수집된 이벤트를 서버로 전송하는 주기입니다.</p><ul><li>최솟값: 1</li><li>최댓값: 60 (1분)</li></ul></td><td>10</td><td>2.10.0+</td></tr><tr><td><code>pollingIntervalSeconds</code></td><td><p>대시보드에서 설정한 정보를 주기적으로 업데이트 할 수 있습니다.</p><ul><li>최솟값 : 60 (1분)</li></ul></td><td>-1<br>(주기적으로 업데이트하지 않음)</td><td>2.18.0+</td></tr><tr><td><code>automaticScreenTracking</code></td><td>화면 자동 추적 활성화 여부</td><td><code>true</code></td><td>2.34.0+</td></tr><tr><td><code>automaticAppLifecycleTracking</code></td><td>앱 시작 / 종료 자동 추적 활성화 여부</td><td><code>true</code></td><td>2.59.0+</td></tr><tr><td><code>sessionPolicy</code></td><td>세션 유지 조건과 만료 조건을 설정합니다.</td><td><p><code>ALWAYS_NEW_SESSION</code> ,</p><p><code>1800000</code></p></td><td>3.1.0+</td></tr><tr><td><code>optOutTracking</code></td><td>옵트아웃 활성화 여부.</td><td><code>false</code></td><td>3.1.0+</td></tr><tr><td><code>evaluationMode</code></td><td>평가 방식을 설정합니다.</td><td><code>local</code></td><td>4.0.0+</td></tr><tr><td><code>sessionTimeoutIntervalSeconds</code><br>(deprecated)</td><td>세션만료 시간을 설정합니다. <code>sessionPolicy</code>를 사용해 주세요.</td><td>1800 (30분)</td><td>2.13.0+</td></tr></tbody></table>

{% hint style="info" %} <sup>\*</sup> 2.41.0 이후부터 앱 종료 후 재시작 시에도 지원합니다.\ <sup>\*</sup> 2.41.0 미만버전의 경우 최댓값은 : 3600 (1시간) 입니다.
{% endhint %}

#### 평가 방식 설정

평가를 SDK에서 직접 수행할지, 핵클 서버가 미리 수행한 결과를 조회할지 선택할 수 있습니다.\
설정하지 않으면 기본값인 `.local`(로컬 평가)로 동작합니다.

{% tabs %}
{% tab title="Swift" %}

```swift
let config = HackleConfigBuilder()
    .evaluationMode(.remote)
    .build()

Hackle.initialize(sdkKey: YOUR_APP_SDK_KEY, config: config) {
    // SDK ready to use.
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
HackleConfigBuilder *configBuilder = [[HackleConfigBuilder alloc] init];
[configBuilder evaluationMode:EvaluationModeRemote];
HackleConfig *config = [configBuilder build];

[Hackle initializeWithSdkKey:@"YOUR_APP_SDK_KEY" config:config completion:^{
    // SDK ready to use.
}];
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
원격 평가를 사용하면 기기 내부에 사용자 정보를 저장하지 않습니다. 기존에 로컬 평가를 사용하며 저장된 사용자 정보가 있는 경우 삭제 처리합니다.

두 방식의 차이와 선택 기준은 [평가 방식](/development-guide/sdk/evaluation-mode) 문서를 참고 바랍니다.
{% endhint %}

#### 세션 정책 설정

세션 정책을 설정하여 세션의 유지 조건과 만료 조건을 제어할 수 있습니다.

{% tabs %}
{% tab title="Swift" %}

```swift
let sessionPolicy = HackleSessionPolicy.builder()
    .persistCondition(.nullToUserId)
    .timeoutCondition(
        HackleSessionTimeoutCondition.builder()
            .timeoutIntervalSeconds(3600)
            .onForeground(false)
            .onBackground(true)
            .onApplicationStateChange(true)
            .build()
    )
    .build()

let config = HackleConfigBuilder()
    .sessionPolicy(sessionPolicy)
    .build()

Hackle.initialize(sdkKey: YOUR_APP_SDK_KEY, config: config) {
    // SDK ready to use.
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
HackleSessionTimeoutCondition *timeoutCondition = [[[[[[HackleSessionTimeoutCondition builder]
    timeoutIntervalSeconds:3600]
    onForeground:NO]
    onBackground:YES]
    onApplicationStateChange:YES]
    build];

HackleSessionPolicy *sessionPolicy = [[[[HackleSessionPolicy builder]
    persistCondition:[HackleSessionPersistCondition nullToUserId]]
    timeoutCondition:timeoutCondition]
    build];

HackleConfigBuilder *configBuilder = [[HackleConfigBuilder alloc] init];
[configBuilder sessionPolicy:sessionPolicy];
HackleConfig *config = [configBuilder build];

[Hackle initializeWithSdkKey:@"YOUR_APP_SDK_KEY" config:config completion:^{
    // SDK ready to use.
}];
```

{% endtab %}
{% endtabs %}

### 인스턴스 가져오기

초기화 이후 아래 코드를 통해 `HackleApp` 인스턴스를 가져올 수 있습니다. 초기화 이전에 호출하면 nil을 리턴합니다. 초기화 이후 호출해야 합니다.

{% tabs %}
{% tab title="Swift" %}

```swift
let hackleApp = Hackle.app()
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
HackleApp *hackleApp = [Hackle app];
```

{% endtab %}
{% endtabs %}

### 대시보드 설정 정보 갱신

대시보드 설정 정보를 명시적으로 갱신 할 수 있습니다.

{% hint style="warning" %}
해당 함수는 60초에 한번 제한적으로 호출할 수 있습니다.
{% endhint %}

{% tabs %}
{% tab title="Swift" %}

```swift
hackleApp.fetch {
  // done
}

await hackleApp.fetch()
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
[hackleApp fetch:^{
    // done
}];
```

{% endtab %}
{% endtabs %}


# 사용자 식별자와 속성

{% hint style="info" %}
사용자 식별자 관리

사용자 식별자는 사용자를 고유하게 식별하는 목적으로 사용합니다. 사용자 식별자의 의미와 중요성, 선택하는 기준 등에 대해서는 [사용자 식별자 관리하기](/getting-started/user-identifier) 문서를 참고하시기 바랍니다.
{% endhint %}

{% hint style="warning" %}
사용자 정보를 변경하는 함수(`setUser`, `setUserId`, `setDeviceId`, `updateUserProperties`, `resetUser` 등)는 completion 핸들러가 있는 함수 또는 async 함수 사용을 권장합니다.&#x20;

completion 핸들러는 변경된 사용자 정보가 SDK에 반영된 이후 호출됩니다.&#x20;
{% endhint %}

## 사용자 식별자

### SDK에서 관리하는 식별자

iOS SDK는 디바이스의 식별자를 관리하는 기능을 포함하고 있습니다. 따라서 사용자 식별자를 별도로 전달하지 않아도 사용자를 자동으로 식별할 수 있습니다.

SDK에서 관리하는 식별자를 조회하는 방법은 다음과 같습니다.

{% tabs %}
{% tab title="Swift" %}

```swift
// 디바이스ID 가져오기
let deviceId = hackleApp.deviceId

// 세션 ID 가져오기
let sessionId = hackleApp.sessionId

// 사용자 정보 모두 가지고 오기
let user = hackleApp.user
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
// 디바이스ID 가져오기
NSString *deviceId = [hackleApp deviceId];

// 세션 ID 가져오기
NSString *sessionId = [hackleApp sessionId];

// 사용자 정보 모두 가지고 오기
HackleUser *user = [hackleApp user];
```

{% endtab %}
{% endtabs %}

#### 디바이스 ID 수정

핵클에서 제공하는 디바이스 ID를 사용하지 않고 직접 디바이스 ID를 주입할 수 있습니다.

{% tabs %}
{% tab title="Swift" %}

```swift
// 디바이스 ID 변경
hackleApp.setDeviceId(deviceId: "CUSTOM_DEVICE_ID") {
    // 적용 완료
}

await hackleApp.setDeviceId(deviceId: "CUSTOM_DEVICE_ID")

// 빌더 패턴 사용
let user = User.builder()
    .deviceId("CUSTOM_DEVICE_ID") // 디바이스 ID
    .build()

hackleApp.setUser(user: user) {
    // 적용 완료
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
// 디바이스 ID 변경
[hackleApp setDeviceIdWithDeviceId:@"CUSTOM_DEVICE_ID" completion:^{
    // 적용 완료
}];
```

{% endtab %}
{% endtabs %}

#### 사용자 식별자(User ID) 설정

로그인 한 사용자의 식별자를 설정하실 수 있습니다.

{% tabs %}
{% tab title="Swift" %}

```swift
// 로그인 한 사용자 ID 추가
hackleApp.setUserId(userId: "LOGIN_ID") {
    // 적용 완료
}

await hackleApp.setUserId(userId: "LOGIN_ID")

// 빌더 패턴 사용
let user = User.builder()
    .userId("LOGIN_ID") // 사용자 ID
    .build()

hackleApp.setUser(user: user) {
    // 적용 완료
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
// 로그인 한 사용자 ID 추가
[hackleApp setUserIdWithUserId:@"LOGIN_ID" completion:^{
    // 적용 완료
}];
```

{% endtab %}
{% endtabs %}

### 추가 식별자

기본 식별자(deviceid, userid) 외의 식별자 타입을 추가할 경우 아래와 같이 설정할 수 있습니다.

{% hint style="info" %}
추가 식별자는 [핵클 통합 식별자](/getting-started/user-identifier/hackle-id)로 통합되지 않습니다.
{% endhint %}

{% hint style="danger" %}
`setUser` 를 하는 경우 현재 디바이스의 유저 정보를 덮어씁니다.

* 현재 A userId를 사용중인데 setUser 시 A userId 를 전달하지 않으면 userId가 A -> null로 변경됩니다.
* 현재 custom deviceId를 사용중인데 setUser 시 사용중인 deviceId를 전달하지 않으면 custom deviceId -> hackle deviceId로 변경됩니다.
* 추가 식별자를 사용하는 경우에도 setUser 시 전달하지 않으면 추가 식별자가 초기화가 됩니다.
* 프로퍼티의 경우 아래 케이스로 디바이스 내 캐싱된 프로퍼티가 유지 or 초기화 될 수 있습니다.
  * setUser 전 / 후 userId와 deviceId가 동일하다면 캐시된 프로퍼티 유지됩니다.
  * setUser 전 / 후 userId 혹은 deviceId가 변경된다면 캐시된 프로퍼티가 삭제됩니다.
    {% endhint %}

{% tabs %}
{% tab title="Swift" %}

```swift
import Hackle

let user = User.builder()
    .userId("142")                  // 사용자 ID (핵클 통합 식별자 사용가능)
    .deviceId("ae2182e0")           // 디바이스 ID (핵클 통합 식별자 사용가능)
    .identifier("myCustomId", "42") // Custom ID
    .build()

hackleApp.setUser(user: user) {
    // 적용 완료
}

await hackleApp.setUser(user: user)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
@import Hackle;

HackleUserBuilder *builder = [HackleUser builder];
[builder userId:@"142"];                   // 사용자 ID (핵클 통합 식별자 사용가능)
[builder deviceId:@"ae2182e0"];            // 디바이스 ID (핵클 통합 식별자 사용가능)
[builder identifier:@"myCustomId" :@"42"]; // Custom ID
HackleUser *user = [builder build];

[hackleApp setUserWithUser:user completion:^{
    // 적용 완료
}];
```

{% endtab %}
{% endtabs %}

## 사용자 속성 (Property)

핵클 SDK는 사용자 속성을 추가할 수 있도록 지원합니다.

* 속성은 속성명(key)과 속성값(value)을 한 쌍으로 보내야 합니다.
* 추가 가능한 속성 개수는 최대 128개입니다.

<table><thead><tr><th width="133.04296875">구분</th><th width="127.60546875">타입</th><th>제약사항</th></tr></thead><tbody><tr><td>속성 명(key)</td><td><code>string</code></td><td><ul><li>글자수 제한은 128자입니다. (128 characters)</li><li>대소문자를 구분하지 않습니다.</li><li>예를 들어 AGE와 age는 동일한 속성명으로 인식합니다.</li></ul></td></tr><tr><td>속성 값(value)</td><td><code>boolean</code>, <code>string</code>, <code>number</code>, <code>array</code></td><td><ul><li>string 타입인 경우 글자수 제한은 1024자입니다.<br>(1024 characters)</li><li>string 타입은 대소문자를 구분합니다.</li><li>예를 들어 APPLE과 apple은 서로 다른 속성값으로 인식합니다.</li><li>number 타입인 경우 정수 최대 15자리, 소수점 최대 6자리를<br>지원합니다.</li></ul></td></tr></tbody></table>

### 사용자 속성 추가

`PropertyOperations` 객체에 `set` 을 이용하여 속성을 추가한 뒤 `updateUserProperties` 를 호출하면 사용자 속성을 간단하게 추가할 수 있습니다.

{% hint style="warning" %}
`setUserProperty` 함수는 iOS SDK 4.0.0 부터 deprecated 되었습니다. `updateUserProperties` 를 사용해 주세요.
{% endhint %}

{% tabs %}
{% tab title="Swift" %}

```swift
import Hackle

let operations = PropertyOperations.builder()
    .set("gender", "female")
    .build()

hackleApp.updateUserProperties(operations: operations) {
    // 적용 완료
}

await hackleApp.updateUserProperties(operations: operations)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
@import Hackle;

PropertyOperationsBuilder *builder = [PropertyOperations builder];
[builder set:@"gender" :@"female"];
PropertyOperations *operations = [builder build];

[hackleApp updateUserPropertiesWithOperations:operations completion:^{
    // 적용 완료
}];
```

{% endtab %}
{% endtabs %}

### 사용자 속성 설정

사용자 속성을 추가, 제거 할 수 있습니다.

<table><thead><tr><th width="150">지원하는 함수</th><th>설명</th></tr></thead><tbody><tr><td><code>set</code></td><td>사용자 속성을 설정합니다. 속성 키에 이미 설정한 속성값이 있는 경우 덮어씁니다</td></tr><tr><td><code>setOnce</code></td><td><p>사용자 속성 값을 한번만 설정합니다. 속성키에 대한 속성이 이미 있는 경우 무시됩니다.</p><p>예를 들어 사용자에 대한 가입일, 초기 가입 위치 등을 설정할 수 있습니다.</p></td></tr><tr><td><code>unset</code></td><td>사용자 속성을 제거합니다.</td></tr><tr><td><code>clearAll</code></td><td>사용자의 모든 속성을 제거합니다.</td></tr></tbody></table>

설정하고 싶은 사용자 속성으로 `PropertyOperations` 객체를 인스턴스화 합니다. 다음 `updateUserProperties` 를 호출하여 사용자 속성을 업데이트 합니다. 한 번에 여러개의 속성을 설정할 수도 있습니다.

{% tabs %}
{% tab title="Swift" %}

```swift
import Hackle

let operations = PropertyOperations.builder()
    .set("age", 42)
    .set("grade", "GOLD")
    .setOnce("sign_up_date", "2020-07-03")
    .build()

hackleApp.updateUserProperties(operations: operations) {
    // 적용 완료
}

await hackleApp.updateUserProperties(operations: operations)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
@import Hackle;

PropertyOperationsBuilder *builder = [PropertyOperations builder];
[builder set:@"age" :@42];
[builder set:@"grade": @"GOLD"];
[builder setOnce:@"sign_up_date" :@"2020-07-03"];
PropertyOperations *operations = [builder build];

[hackleApp updateUserPropertiesWithOperations:operations completion:^{
    // 적용 완료
}];
```

{% endtab %}
{% endtabs %}

## 사용자 초기화

기존에 설정한 정보를 초기화해야 합니다. 초기화를 하는 경우 기존에 설정했던 식별자, 속성이 모두 초기화됩니다.

{% hint style="danger" %}
사용자 초기화를 하는 경우 서버에 저장된 사용자 속성까지 모두 초기화가 됩니다. 로그아웃 처리를 원하는 경우 `hackleApp.setUserId(userId: nil)`을 사용해주세요.

userId에 null을 대입하면 클라이언트 상에서 로그아웃 처리가 됩니다.
{% endhint %}

{% tabs %}
{% tab title="Swift" %}

```swift
hackleApp.resetUser {
    // 적용 완료
}

await hackleApp.resetUser()
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
[hackleApp resetUserWithCompletion:^{
    // 적용 완료
}];
```

{% endtab %}
{% endtabs %}

`resetUser()` 를 호출하는 경우 기존에 설정했던 식별자, 속성이 모두 초기화됩니다.


# CRM 속성

CRM 속성은 핵클 서버에만 안전하게 저장되며 SDK를 통해 값을 직접 조회할 수는 없습니다.

{% hint style="danger" %}
CRM 속성은 별도로 관리되며, `resetUser()`를 호출하거나 `updateUserProperties`에서 `clearAll`을 호출해도 삭제되거나 초기화되지 않습니다.

사용자가 회원 탈퇴 등을 한 경우, 반드시 별도로 제공되는 함수를 호출하여 정보를 삭제해야 합니다.
{% endhint %}

## 전화번호 수집

{% hint style="info" %}
iOS SDK 2.46.0 버전 이상에서 지원하는 기능입니다.
{% endhint %}

{% hint style="success" %}
카카오 / 문자 메시지 권장 사항

이 기능을 이용하여 사용자 식별자와 전화번호를 매핑하면 핵클을 통한 카카오 / 문자 메시지를 더욱 원활히 이용할 수 있습니다.
{% endhint %}

#### setPhoneNumber

사용자의 전화번호를 등록합니다.

전화번호는 올바른 E.164 포맷일 경우에만 저장됩니다. 국가코드를 지정하지 않았다면 대한민국(+82)이 기본값으로 사용됩니다.

이미 저장된 전화번호가 있는 사용자의 경우, 이 함수를 호출하면 기존 전화번호가 새로운 값으로 교체됩니다.

{% tabs %}
{% tab title="Swift" %}

```swift
let myPhoneNumber = "+821012341234"
hackleApp.setPhoneNumber(phoneNumber: myPhoneNumber) {
    // 적용 완료
}

await hackleApp.setPhoneNumber(phoneNumber: myPhoneNumber)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
NSString *myPhoneNumber = @"+821012341234";
[hackleApp setPhoneNumberWithPhoneNumber:myPhoneNumber completion:^{
    // 적용 완료
}];
```

{% endtab %}
{% endtabs %}

#### unsetPhoneNumber

사용자에게 등록되어 있는 전화번호를 삭제합니다.

{% tabs %}
{% tab title="Swift" %}

```swift
hackleApp.unsetPhoneNumber {
    // 적용 완료
}

await hackleApp.unsetPhoneNumber()
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
[hackleApp unsetPhoneNumberWithCompletion:^{
    // 적용 완료
}];
```

{% endtab %}
{% endtabs %}

## CRM 마케팅 메시지 수신 동의

{% hint style="info" %}
iOS SDK 2.50.0 버전 이상에서 지원하는 기능입니다.

수신 동의 상태에 대한 자세한 내용은 [CRM 메시지 수신 동의 관리](/development-guide/sdk/user-identifier/crm-subscription) 문서를 참고해주세요.
{% endhint %}

### 수신 동의 속성

메시지 목적 별로 수신 동의/거부를 할 수 있습니다.

`HackleSubscriptionOperationsBuilder`를 사용해 원하는 속성의 동의 상태를 설정한 후, `updatePushSubscriptions()` 같은 메서드로 최종 업데이트를 진행합니다.

메시지 채널 별로 동의 상태를 업데이트할 수 있습니다.

{% tabs %}
{% tab title="Swift" %}

```swift
import Hackle

let subscriptions = HackleSubscriptionOperationsBuilder()
    .marketing(.subscribed)
    .information(.subscribed)
    .build()

hackle.app.updatePushSubscriptions(subscriptions)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
HackleSubscriptionOperationsBuilder *builder = [[HackleSubscriptionOperationsBuilder alloc] init];
[builder marketing:HackleSubscriptionStatusSubscribed];
[builder information:HackleSubscriptionStatusSubscribed];
HackleSubscriptionOperations *subscriptions = [builder build];

[[Hackle app] updatePushSubscriptions:subscriptions];
```

{% endtab %}
{% endtabs %}

#### 광고성 메시지

광고성 메시지 수신 동의 속성을 설정합니다.

{% tabs %}
{% tab title="Swift" %}

```swift
import Hackle

let subscriptions = HackleSubscriptionOperationsBuilder()
    .marketing(.subscribed)
    .build()

hackle.app.updatePushSubscriptions(subscriptions)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
HackleSubscriptionOperationsBuilder *builder = [[HackleSubscriptionOperationsBuilder alloc] init];
[builder marketing:HackleSubscriptionStatusSubscribed];
HackleSubscriptionOperations *subscriptions = [builder build];

[[Hackle app] updatePushSubscriptions:subscriptions];
```

{% endtab %}
{% endtabs %}

#### 정보성 메시지

정보성 메시지 수신 동의 속성을 설정합니다.

{% tabs %}
{% tab title="Swift" %}

```swift
import Hackle

let subscriptions = HackleSubscriptionOperationsBuilder()
    .information(.subscribed)
    .build()

hackle.app.updatePushSubscriptions(subscriptions)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
HackleSubscriptionOperationsBuilder *builder = [[HackleSubscriptionOperationsBuilder alloc] init];
[builder information:HackleSubscriptionStatusSubscribed];
HackleSubscriptionOperations *subscriptions = [builder build];

[[Hackle app] updatePushSubscriptions:subscriptions];
```

{% endtab %}
{% endtabs %}

<table><thead><tr><th width="224.09765625">HackleSubscriptionStatus</th><th>설명</th></tr></thead><tbody><tr><td><code>UNKNOWN</code></td><td>수신 동의/거부를 하지 않음 (<code>default</code>)</td></tr><tr><td><code>SUBSCRIPTION</code></td><td>명시적으로 수신 동의</td></tr><tr><td><code>UNSUBSCRIPTION</code></td><td>명시적으로 수신 거부</td></tr></tbody></table>

### 푸시 수신 동의 상태 업데이트

사용자의 푸시 메시지 수신 동의 상태를 업데이트 합니다.

{% tabs %}
{% tab title="Swift" %}

```swift
let subscriptions = HackleSubscriptionOperationsBuilder()
    .marketing(.subscribed)
    .information(.subscribed)
    .build()

hackle.app.updatePushSubscriptions(subscriptions)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
HackleSubscriptionOperationsBuilder *builder = [[HackleSubscriptionOperationsBuilder alloc] init];
[builder marketing:HackleSubscriptionStatusSubscribed];
[builder information:HackleSubscriptionStatusSubscribed];
HackleSubscriptionOperations *subscriptions = [builder build];

[[Hackle app] updatePushSubscriptions:subscriptions];
```

{% endtab %}
{% endtabs %}

### 카카오 메시지 수신 동의 상태 업데이트

사용자의 카카오 메시지 수신 동의 상태를 업데이트 합니다.

{% tabs %}
{% tab title="Swift" %}

```swift
let subscriptions = HackleSubscriptionOperationsBuilder()
    .marketing(.subscribed)
    .information(.subscribed)
    .build()

hackle.app.updateKakaoSubscriptions(subscriptions)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
HackleSubscriptionOperationsBuilder *builder = [[HackleSubscriptionOperationsBuilder alloc] init];
[builder marketing:HackleSubscriptionStatusSubscribed];
[builder information:HackleSubscriptionStatusSubscribed];
HackleSubscriptionOperations *subscriptions = [builder build];

[[Hackle app] updateKakaoSubscriptions:subscriptions];
```

{% endtab %}
{% endtabs %}

### 문자 메시지 수신 동의 상태 업데이트

사용자의 문자 메시지 수신 동의 상태를 업데이트 합니다.

{% tabs %}
{% tab title="Swift" %}

```swift
let subscriptions = HackleSubscriptionOperationsBuilder()
    .marketing(.subscribed)
    .information(.subscribed)
    .build()

hackle.app.updateSmsSubscriptions(subscriptions)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
HackleSubscriptionOperationsBuilder *builder = [[HackleSubscriptionOperationsBuilder alloc] init];
[builder marketing:HackleSubscriptionStatusSubscribed];
[builder information:HackleSubscriptionStatusSubscribed];
HackleSubscriptionOperations *subscriptions = [builder build];

[[Hackle app] updateSmsSubscriptions:subscriptions];
```

{% endtab %}
{% endtabs %}


# 테스트 그룹 분배

A/B 테스트를 진행할 때, 테스트 그룹을 대상으로 사용자를 분배하고 각 테스트 그룹에 해당하는 로직을 작성해야 합니다. 이 때 사용자 분배를 핵클 SDK를 통해 진행할 수 있습니다.

## variation

`variation()` 메소드에 **실험 키**를 전달하면 사용자를 분배하고 결과를 전달받을 수 있습니다.

<table><thead><tr><th width="150">파라미터</th><th width="120">타입</th><th width="110">필수</th><th>제약사항</th></tr></thead><tbody><tr><td>실험 키 (key)</td><td><code>int</code></td><td>필수</td><td>-</td></tr></tbody></table>

#### 예제

아래 예제 코드에서는 실험 키 42를 전달하고 있으며, 테스트 그룹은 A와 B 두 개가 존재합니다.

{% tabs %}
{% tab title="Swift" %}

```swift
// 실험 키가 42인 A/B 테스트에서 사용자에게 노출할 테스트 그룹을 결정합니다.
// 결정하지 못하는 상황인 경우 테스트 그룹 A를 반환합니다.
let variation = hackleApp.variation(experimentKey: 42)

// 할당받은 그룹에 대한 로직
if variation == "A" {
  // 그룹 A 로직
} else if variation == "B" {
  // 그룹 B 로직
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
// 실험 키가 42인 A/B 테스트에서 사용자에게 노출할 테스트 그룹을 결정합니다.
// 결정하지 못하는 상황인 경우 테스트 그룹 A를 반환합니다.
NSString *variation = [app variationWithExperimentKey:42];

// 할당받은 그룹에 대한 로직
if([variation isEqual:@"A"]) {
    // 그룹 A 로직
} else if ([variation isEqual:@"B"]) {
    // 그룹 B 로직
}
```

{% endtab %}
{% endtabs %}

## variationDetail

`variationDetail()` 메소드는 `variation()` 메소드와 동일하게 동작하고 분배된 사유를 같이 제공합니다. 이 메소드는 분배가 잘 되고 있는지 살펴볼 때 유용하게 사용할 수 있습니다.

<table><thead><tr><th width="150">파라미터</th><th width="120">타입</th><th width="110">필수</th><th>제약사항</th></tr></thead><tbody><tr><td>실험 키 (key)</td><td><code>int</code></td><td>필수</td><td>-</td></tr></tbody></table>

#### 예제

파라미터로 실험 키를 전달해야 합니다. 아래 예제 코드의 경우 실험 키 42를 전달하고 있습니다.

{% tabs %}
{% tab title="Swift" %}

```swift
// 분배 결정 상세
let decision: Decision = hackleApp.variationDetail(experimentKey: 42)

// 분배 그룹
let variation: String = decision.variation

// 분배 결정 사유
let reason: String = decision.reason
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
// 분배 결정 상세
HackleDecision *decision = [hackleApp variationDetailWithExperimentKey:42];

// 분배 그룹
NSString *variation = [decision variation];

// 분배 결정 사유
NSString *reason = [decision reason];
```

{% endtab %}
{% endtabs %}

### 분배 사유

분배 결정 사유는 **`SDK_NOT_READY`** 와 같은 형태로 받게 됩니다. 자세한 내용은 아래 표를 참고해주세요.

<table><thead><tr><th width="319.12109375">분배 사유</th><th width="305.35546875">설명</th><th>분배 결과</th></tr></thead><tbody><tr><td><code>SDK_NOT_READY</code></td><td><p>SDK 사용 준비가 되지 않았습니다.</p><p>(예: 잘못된 SDK 키로 초기화 시도)</p></td><td>A (기본 그룹)</td></tr><tr><td><code>EXPERIMENT_NOT_FOUND</code></td><td>전달한 실험 키에 대한 A/B 테스트를 찾을 수 없습니다. 실험 키가 잘못되었거나 해당 실험이 보관 상태일 수 있습니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>NOT_IN_MUTUAL_EXCLUSION_EXPERIMENT</code></td><td>실험이 상호 배타적 설정에 포함되어 있지만<br>해당 상호 배타적 그룹에 할당되지 않은 경우</td><td>A (기본 그룹)</td></tr><tr><td><code>EXPERIMENT_DRAFT</code></td><td>A/B 테스트가 준비 상태입니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>EXPERIMENT_PAUSED</code></td><td>A/B 테스트가 일시 정지 상태입니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>EXPERIMENT_COMPLETED</code></td><td>A/B 테스트가 종료되었습니다.</td><td>종료 시 선택한승리 그룹</td></tr><tr><td><code>OVERRIDDEN</code></td><td>사용자가 수동할당에 의해<br>특정 그룹으로 결정되었습니다.</td><td>수동 할당한<br>그룹</td></tr><tr><td><code>NOT_IN_EXPERIMENT_TARGET</code></td><td>사용자가 A/B 테스트 타겟이 아닙니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>TRAFFIC_NOT_ALLOCATED</code></td><td>A/B 테스트가 실행 중이지만<br>사용자가 테스트에 할당되지 않았습니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>TRAFFIC_ALLOCATED</code></td><td>사용자가 A/B 테스트에 할당되었습니다.</td><td>할당된 그룹</td></tr><tr><td><code>VARIATION_DROPPED</code></td><td>원래 할당된 그룹이 테스트에서 제외되었습니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>INVALID_INPUT</code></td><td>입력값이 유효하지 않습니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>EXCEPTION</code></td><td>알 수 없는 오류가 발생했습니다.</td><td>A (기본 그룹)</td></tr></tbody></table>

### 파라미터

{% hint style="info" %}
iOS SDK 2.9.0 이상 버전에서 지원하는 기능입니다.
{% endhint %}

* `variationDetail()` 메소드를 통해 분배된 그룹의 파라미터 값도 같이 제공받을 수 있습니다.
* `variationDetail()` 메소드를 통해 전달받은 Decision 인스턴스에는 전체 파라미터 설정 정보가 담긴 `ParameterConfig` 객체가 존재합니다.
* 핵클의 A/B 테스트 화면에서 설정한 파라미터 값이 key, value 형태로 존재하기 때문에, 설정한 파라미터 유형에 따라 아래 메소드를 사용하여 설정한 파라미터 값을 받아 활용할 수 있습니다.
* 분배된 그룹의 설정된 값은 아래 함수를 이용하여 조회할 수 있습니다.

<table data-header-hidden><thead><tr><th width="136.12109375">구분</th><th>설명</th></tr></thead><tbody><tr><td><code>getString</code></td><td>STRING, JSON 유형으로 설정된 parameter값을 반환합니다.</td></tr><tr><td><code>getInt</code></td><td>NUMBER 유형으로 설정된 parameter 값을 Int 타입으로 반환합니다.</td></tr><tr><td><code>getDouble</code></td><td>NUMBER 유형으로 설정된 parameter 값을 Double 타입으로 반환합니다.</td></tr><tr><td><code>getBool</code></td><td>Bool 유형으로 설정된 parameter값을 반환합니다.</td></tr></tbody></table>

#### 예제

{% tabs %}
{% tab title="Swift" %}

```swift
let decision: Decision = hackleApp.variationDetail(experimentKey: 42)
let config: ParameterConfig = decision.config

let strValue: String = decision.getString(forKey: "parameter_key_string_type", defaultValue: "defaultValue")
let strValueInConfig: String = config.getString(forKey: "parameter_key_string_type", defaultValue: "defaultValue")

let jsonValue: String = decision.getString(forKey: "parameter_key_json_type", defaultValue: "defaultValue")
let jsonValueInConfig: String = config.getString(forKey: "parameter_key_json_type", defaultValue: "defaultValue")

let intValue: Int = decision.getInt(forKey: "parameter_key_number_type", defaultValue: 0)
let intValueInConfig: Int = config.getInt(forKey: "parameter_key_number_type", defaultValue: 0)

let doubleValue: Double = decision.getDouble(forKey: "parameter_key_number_type", defaultValue: 0.0)
let doubleValueInConfig: Double = config.getDouble(forKey: "parameter_key_number_type", defaultValue: 0.0)

let boolValue: Bool = decision.getBool(forKey: "parameter_key_boolean_type", defaultValue: false)
let boolValueInConfig: Bool = config.getBool(forKey: "parameter_key_boolean_type", defaultValue: false)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
HackleDecision *decision = [hackleApp variationDetailWithExperimentKey:42];

NSString *stringValue = [decision getStringForKey:@"parameter_key_string_type" defaultValue:@"defaultValue"];

NSString *jsonValue = [decision getStringForKey:@"parameter_key_json_type" defaultValue:@"defaultValue"];
```

{% endtab %}
{% endtabs %}


# 기능 플래그 결정

{% hint style="info" %}
iOS SDK 2.0.0 이상 버전에서 지원하는 기능입니다.
{% endhint %}

기능 플래그는 켜짐(on) 상태와 꺼짐(off) 상태가 있습니다. 각 상태에 따라 다른 기능을 설정하게 됩니다. 기능 플래그를 적용한 기능에 어떤 사용자가 접근할 경우 켜짐 혹은 꺼짐 상태를 받을 수 있어야 합니다. 이 상태 결정을 핵클 SDK를 통해 진행할 수 있습니다.

## isFeatureOn

`isFeatureOn()` 메소드에 **기능 키**를 전달하면 사용자에 대한 상태 결과를 전달받을 수 있습니다. 이후 상태에 따른 로직을 구현합니다.

아래 예제 코드에서는 기능 키 42를 전달하고 있습니다.

{% tabs %}
{% tab title="Swift" %}

```swift
// 기능 키가 42인 기능 플래그에서 사용자의 상태를 결정합니다.
// 결정하지 못하는 상황인 경우 false(꺼짐 상태)를 반환합니다.
let isFeatureOn: Bool = hackleApp.isFeatureOn(featureKey: 42)

if isFeatureOn {
    // ON 기능
} else {
    // OFF 기능
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
// 기능 키가 42인 기능 플래그에서 사용자의 상태를 결정합니다.
// 결정하지 못하는 상황인 경우 false(꺼짐 상태)를 반환합니다.
bool isFeatureOn = [hackleApp isFeatureOnFeatureKey:@42];

if (isFeatureOn) {
    // ON 기능
} else {
    // OFF 기능
}
```

{% endtab %}
{% endtabs %}

## featureFlagDetail

`featureFlagDetail()` 메소드는 `isFeatureOn()` 메소드와 동일하게 동작하고 추가로 상태 결정에 대한 사유를 같이 제공합니다. 수동할당이 잘 되고 있는지 알아보거나 설정한 트래픽 할당 대비 결과 비중이 이상하다고 여길 때 유용하게 활용할 수 있습니다.

파라미터로 기능 키를 전달해야 합니다. 아래 예제 코드의 경우 기능 키 42를 전달하고 있습니다.

{% tabs %}
{% tab title="Swift" %}

```swift
// 상태 결정 상세
let decision: FeatureFlagDecision = hackleApp.featureFlagDetail(featureKey: 42)

// 기능 on/off 여부
let isFeatureOn: Bool = decision.isOn

// 상태 결정 사유
let reason: String = decision.reason
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
// 상태 결정 상세
HackleFeatureFlagDecision *decision = [hackleApp featureFlagDetailWithFeatureKey:@42];

// 기능 on/off 여부
bool isFeatureOn = [decision isOn];

// 상태 결정 사유
NSString *reason = [decision reason];
```

{% endtab %}
{% endtabs %}

상태 결정 사유는 **`SDK_NOT_READY`** 와 같은 형태로 받게 됩니다. 자세한 내용은 아래 표를 참고해주세요.

<table><thead><tr><th width="233.71875">결정 사유</th><th width="370.8046875">설명</th><th>분배 결과</th></tr></thead><tbody><tr><td><code>SDK_NOT_READY</code></td><td>SDK 사용 준비가 되지 않았습니다.<br>(예: 잘못된 SDK 키로 초기화 시도)</td><td>기본 상태<br>(꺼짐/off)</td></tr><tr><td><code>FEATURE_FLAG_NOT_FOUND</code></td><td>전달한 기능 키에 대한 기능 플래그를 찾을 수 없습니다.<br>기능 키가 잘못되었거나 해당 기능 플래그가 보관 상태일 수 있습니다.</td><td>기본 상태<br>(꺼짐/off)</td></tr><tr><td><code>FEATURE_FLAG_INACTIVE</code></td><td>기능 플래그가 꺼짐 상태입니다</td><td>기본 상태<br>(꺼짐/off)</td></tr><tr><td><code>INDIVIDUAL_TARGET_MATCH</code></td><td>개별 타겟팅에 매치 되었습니다.</td><td>개별 타겟팅으로<br>설정한 상태</td></tr><tr><td><code>TARGET_RULE_MATCH</code></td><td>사용자 타겟팅에 매치 되었습니다.</td><td>사용자 타겟팅으로 설정한 상태</td></tr><tr><td><code>DEFAULT_RULE</code></td><td>개별 타겟팅, 사용자 타겟팅 중 어디에도 매치 되지 않았습니다.</td><td>기본 룰로<br>설정한 상태</td></tr><tr><td><code>EXCEPTION</code></td><td>알 수 없는 오류가 발생했습니다.</td><td>기본 상태<br>(꺼짐/off)</td></tr></tbody></table>

## 기능 플래그 파라미터

{% hint style="info" %}
iOS SDK 2.9.0 이상 버전에서 지원하는 기능입니다.
{% endhint %}

* `featureFlagDetail()` 메소드를 통해 상태 결정에 대한 파라미터 값도 같이 제공받을 수 있습니다.
* `featureFlagDetail()` 메소드를 통해 전달받은 FeatureFlagDecision 인스턴스에는 전체 파라미터 설정 정보가 담긴 `ParameterConfig` 객체가 존재합니다.
* 핵클의 기능 플래그 화면에서 설정한 파라미터 값이 key, value 형태로 존재하기 때문에, 설정한 파라미터 유형에 따라 아래 메소드를 사용하여 설정한 파라미터 값을 받아 활용할 수 있습니다.
* 기능 플래그의 파라미터 설정화면에서 값을 유동적으로 변경할 수 있습니다.

### getString

* STRING, JSON 유형으로 설정된 parameter값을 반환합니다.
* 상태 결정에 따라 False 또는 True에 설정된 값을 반환합니다.

{% tabs %}
{% tab title="Swift" %}

```swift
let decision: FeatureFlagDecision = hackleApp.featureFlagDetail(featureKey: 42)
let config: ParameterConfig = decision.config

let strValue: String = decision.getString(forKey: "parameter_key_string_type", defaultValue: "defaultValue")
let strValueInConfig: String = config.getString(forKey: "parameter_key_string_type", defaultValue: "defaultValue")

let jsonValue: String = decision.getString(forKey: "parameter_key_json_type", defaultValue: "defaultValue")
let jsonValueInConfig: String = config.getString(forKey: "parameter_key_json_type", defaultValue: "defaultValue")
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
HackleFeatureFlagDecision *decision = [[Hackle app] featureFlagDetailWithFeatureKey:@42];

NSString *stringValue = [decision getStringForKey:@"string" defaultValue:@"defaultValue"];

NSString *jsonValue = [decision getStringForKey:@"parameter_key_json_type" defaultValue:@"defaultValue"];
```

{% endtab %}
{% endtabs %}

### getInt

* Number 유형으로 설정된 parameter 값을 Int 타입으로 반환합니다.
* 상태 결정에 따라 False 또는 True에 설정된 값을 반환합니다.

{% tabs %}
{% tab title="Swift" %}

```swift
let decision: FeatureFlagDecision = hackleApp.featureFlagDetail(featureKey: 42)
let config: ParameterConfig = decision.config

let intValue: Int = decision.getInt(forKey: "parameter_key_number_type", defaultValue: 0)
let intValueInConfig: Int = config.getInt(forKey: "parameter_key_number_type", defaultValue: 0)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
HackleFeatureFlagDecision *decision = [[Hackle app] featureFlagDetailWithFeatureKey:@42];

int intValue = [decision getIntForKey:@"parameter_key_number_type" defaultValue:@0];
```

{% endtab %}
{% endtabs %}

### getDouble

* Number 유형으로 설정된 parameter 값을 Double 타입으로 반환합니다.
* 상태 결정에 따라 False 또는 True에 설정된 값을 반환합니다.

{% tabs %}
{% tab title="Swift" %}

```swift
let decision: FeatureFlagDecision = hackleApp.featureFlagDetail(featureKey: 42)
let config: ParameterConfig = decision.config

let doubleValue: Double = decision.getDouble(forKey: "parameter_key_number_type", defaultValue: 0.0)
let doubleValueInConfig: Double = config.getDouble(forKey: "parameter_key_number_type", defaultValue: 0.0)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
HackleFeatureFlagDecision *decision = [[Hackle app] featureFlagDetailWithFeatureKey:@42];

double doubleValue = [decision getDoubleForKey:@"parameter_key_number_type" defaultValue:0.0];
```

{% endtab %}
{% endtabs %}

### getBool

* Bool 유형으로 설정된 parameter값을 반환합니다.
* 상태 결정에 따라 False 또는 True에 설정된 값을 반환합니다.

{% tabs %}
{% tab title="Swift" %}

```swift
let decision: FeatureFlagDecision = hackleApp.featureFlagDetail(featureKey: 42)
let config: ParameterConfig = decision.config

let boolValue: Bool = decision.getBool(forKey: "parameter_key_boolean_type", defaultValue: false)
let boolValueInConfig: Bool = config.getBool(forKey: "parameter_key_boolean_type", defaultValue: false)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
HackleFeatureFlagDecision *decision = [[Hackle app] featureFlagDetailWithFeatureKey:@42];

bool boolValue = [decision getBoolForKey:@"parameter_key_boolean_type" defaultValue:false];
```

{% endtab %}
{% endtabs %}


# 원격 구성 적용

{% hint style="info" %}
iOS SDK 2.11.0 이상 버전에서 지원하는 기능입니다.
{% endhint %}

원격 구성은 애플리케이션에서 관리되고 있는 값, 또는 속성들을 핵클 대시보드에서 정의한 파라미터 값들로 대체하여 실시간으로 애플리케이션의 동작 및 설정 값들을 제어할 수 있는 기능입니다.

핵클의 대시보드의 원격 구성 화면으로 이동하여 파라미터 정보들을 설정하고, 사용자 식별 규칙에 따른 값들을 설정할 수 있습니다.

## remoteConfig

`remoteConfig()` 메소드를 호출하면 사용자에 대한 원격 구성 정보(설정한 파라미터 및 규칙 정보)를 담고 있는 `HackleRemoteConfig` 인스턴스를 얻을 수 있습니다.

{% tabs %}
{% tab title="Swift" %}

```swift
// 원격 구성 정보 담은 인스턴스를 반환합니다.
let remoteConfig = hackleApp.remoteConfig()
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
// 원격 구성 정보 담은 인스턴스를 반환합니다.
HackleRemoteConfig *remoteConfig = [hackleApp remoteConfig];
```

{% endtab %}
{% endtabs %}

## 원격 구성 파라미터 조회

* `remoteConfig()` 메소드를 통해 전달받은 `HackleRemoteConfig` 인스턴스에는 원격 구성에서 설정한 전체 파라미터 설정 정보가 존재합니다.
* 핵클의 원격 구성 화면에서 설정한 파라미터 값이 key, value 형태로 존재하기 때문에, 설정한 파라미터 유형에 따라 아래 메소드를 사용하여 설정한 파라미터 값을 받아 활용할 수 있습니다.
* 원격 구성 파라미터 설정화면에서 규칙 및 값을 유동적으로 변경할 수 있습니다.

{% hint style="warning" %}
보관 후 원격 구성과 관련된 코드를 제거하세요.

원격 구성 파라미터를 보관한 경우 더이상 파라미터 정보에 접근 할 수 없습니다. 원격 구성 파라미터 보관 후에는 반드시 관련된 코드를 정리해주시기 바랍니다.
{% endhint %}

### getString

* STRING, JSON 유형으로 설정된 파라미터 값을 반환합니다.
* 상태 결정에 따라 기본 값 혹은 규칙에 설정된 값을 반환합니다.

{% tabs %}
{% tab title="Swift" %}

```swift
let remoteConfig: HackleRemoteConfig = hackleClient.remoteConfig()

let strValue: String = remoteConfig.getString(forKey: "parameter_key_string_type", defaultValue: "defaultValue")
let jsonValue: String = remoteConfig.getString(forKey: "parameter_key_json_type", defaultValue: "defaultValue")
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
HackleRemoteConfig *remoteConfig = [[Hackle app] remoteConfig];

NSString *stringValue = [remoteConfig getStringForKey:@"string" defaultValue:@"defaultValue"];

NSString *jsonValue = [remoteConfig getStringForKey:@"parameter_key_json_type" defaultValue:@"defaultValue"];
```

{% endtab %}
{% endtabs %}

### getInt

* Number 유형으로 설정된 parameter 값을 Int 타입으로 반환합니다.
* 상태 결정에 따라 기본 값 혹은 규칙에 설정된 값을 반환합니다.

{% tabs %}
{% tab title="Swift" %}

```swift
let remoteConfig: HackleRemoteConfig = hackleClient.remoteConfig()

let intValue: Int = remoteConfig.getInt(forKey: "parameter_key_number_type", defaultValue: 0)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
HackleRemoteConfig *remoteConfig = [[Hackle app] remoteConfig];

int intValue = [remoteConfig getIntForKey:@"parameter_key_number_type" defaultValue:@0];
```

{% endtab %}
{% endtabs %}

### getDouble

* Number 유형으로 설정된 parameter 값을 Double 타입으로 반환합니다.
* 상태 결정에 따라 기본 값 혹은 규칙에 설정된 값을 반환합니다.

{% tabs %}
{% tab title="Swift" %}

```swift
let remoteConfig: HackleRemoteConfig = hackleClient.remoteConfig()

let doubleValue: Double = remoteConfig.getDouble(forKey: "parameter_key_number_type", defaultValue: 0.0)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
HackleRemoteConfig *remoteConfig = [[Hackle app] remoteConfig];

double doubleValue = [remoteConfig getDoubleForKey:@"parameter_key_number_type" defaultValue:0.0];
```

{% endtab %}
{% endtabs %}

### getBool

* Boolean 유형으로 설정된 parameter값을 반환합니다.
* 상태 결정에 따라 기본 값 혹은 규칙에 설정된 값을 반환합니다.

{% tabs %}
{% tab title="Swift" %}

```swift
let remoteConfig: HackleRemoteConfig = hackleClient.remoteConfig()

let boolValue: Bool = remoteConfig.getBool(forKey: "parameter_key_boolean_type", defaultValue: false)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
HackleRemoteConfig *remoteConfig = [[Hackle app] remoteConfig];

bool boolValue = [remoteConfig getBoolForKey:@"parameter_key_boolean_type" defaultValue:false];
```

{% endtab %}
{% endtabs %}


# 이벤트 전송

핵클 SDK는 사용자 이벤트를 핵클로 전송하는 기능을 제공합니다. 사용자 행동의 변화가 일어나는 지점마다 이 기능을 활용하면 사용자 행동에 대한 유의미한 데이터를 얻을 수 있으며, 그렇게 모인 데이터를 통해 사용자 행동 분석을 할 수 있습니다.

## track

`track()` 메소드에 **이벤트 키**를 전달하여 사용자 이벤트를 전송할 수 있습니다.

<table><thead><tr><th width="150">파라미터</th><th width="120">타입</th><th>설명</th></tr></thead><tbody><tr><td>이벤트 명(key)</td><td><code>string</code></td><td>필수. 글자수 제한은 128자입니다. (128 characters)</td></tr></tbody></table>

#### 예시

사용자가 구매하기 버튼을 눌렀을 때 이벤트를 수집하기 위해 `purchase` 라는 이벤트 키를 정의했다고 가정합니다.

{% tabs %}
{% tab title="Swift" %}

```swift
hackleApp.track(eventKey: "purchase")
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
[hackleApp trackWithEventKey:@"purchase"];
```

{% endtab %}
{% endtabs %}

### 속성(Property)

핵클 SDK는 이벤트(Event) 객체에 속성을 추가할 수 있도록 지원합니다.

* 속성은 속성명(key)과 속성값(value)을 한 쌍으로 보내야 합니다.
* 이벤트 객체에 추가 가능한 속성 개수는 최대 64개입니다.

<table><thead><tr><th width="133.04296875">구분</th><th width="127.60546875">타입</th><th>제약사항</th></tr></thead><tbody><tr><td>속성 명(key)</td><td><code>string</code></td><td><ul><li>글자수 제한은 128자입니다. (128 characters)</li><li>대소문자를 구분하지 않습니다.</li><li>예를 들어 AGE와 age는 동일한 속성명으로 인식합니다.</li></ul></td></tr><tr><td>속성 값(value)</td><td><code>boolean</code>, <code>string</code>, <code>number</code>, <code>array</code></td><td><ul><li>string 타입인 경우 글자수 제한은 1024자입니다.<br>(1024 characters)</li><li>string 타입은 대소문자를 구분합니다.</li><li>예를 들어 APPLE과 apple은 서로 다른 속성값으로 인식합니다.</li><li>number 타입인 경우 정수 최대 15자리, 소수점 최대 6자리를<br>지원합니다.</li></ul></td></tr></tbody></table>

#### 예시

아래 예시에서는 세 가지 속성(`pay_method`, `discount_amount`, `is_discount`)을 추가한 것을 확인할 수 있습니다.

{% tabs %}
{% tab title="Swift" %}

```swift
let event = Hackle.event(
                    key: "purchase",
                    properties: [
                        "pay_method": "CARD",
                        "discount_amount": 800,
                        "is_discount": true
                    ])

// 빌더 패턴
let event2 = Event.builder("purchase")
    .property("pay_method": "credit")
    .build()

hackleApp.track(event: event)
hackleApp.track(event: event2)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
HackleEvent *event = [Hackle eventWithKey:@"purchase"
                       value:3200.0
                       properties:@{
                           @"pay_method": @"CARD",
                           @"discount_amount": @800,
                           @"is_discount": @true
                       }];

[hackleApp trackWithEvent:event];
```

{% endtab %}
{% endtabs %}


# 사용자 화면 추적

Hackle SDK는 ViewController 단위로 화면 정보를 수집합니다. ViewController를 1개만 사용하는 구조의 앱이거나 [SwiftUI](https://developer.apple.com/swiftui/) 를 사용하는 경우 자동으로 화면 정보를 수집하기 어렵습니다.

`$page_view` 및 `$engagement` 이벤트를 정상적으로 수집하려면, 화면이 변경될 때마다 setCurrentScreen 메소드를 직접 호출해야 합니다.

## setCurrentScreen

{% hint style="info" %}
iOS SDK 2.59.0 이상 버전에서 정식으로 지원하는 기능입니다.
{% endhint %}

`setCurrentScreen(screen)` 메소드에 인자로 화면 명과 화면의 클래스 명을 인자로 받아 화면을 추적합니다.

* 화면 추적의 최소 단위 시간은 1초 입니다.
* 1초 이내에 변경된 페이지의 `$engagement` 는 측정되지 않습니다.

<table><thead><tr><th width="150">파라미터</th><th width="120">타입</th><th>설명</th></tr></thead><tbody><tr><td>name</td><td><code>string</code></td><td>필수. 현재 화면의 명입니다.</td></tr><tr><td>className</td><td><code>string</code></td><td>필수. 별도의 클래스 명을 남기지 않을 경우 name과 동일한 값을 기입하면 됩니다.</td></tr></tbody></table>

{% hint style="warning" %}
동일 화면에 대한 화면 추적은 할 수 없습니다.

이전 화면과 동일한(name과 className이 모두 동일한) Screen으로 `setCurrentScreen`을 호출한 경우 화면 전환이 되지 않은 것으로 인식되어 아무런 동직을 하지 않습니다.

* `$page_view`, `$engagement`가 호출되지 않습니다.
  {% endhint %}

#### 예시

Swift UI를 사용하는 경우 `onAppear` 와 `onChange`가 발생할 때 모두 화면 추적을 하는 것을 추천합니다.

{% tabs %}
{% tab title="Swift" %}

```swift
let screen = Screen.builder(name: "screenName", className: "screenName")
    .build()

Hackle.app()?.setCurrentScreen(screen: screen)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
HackleScreen *screen = [[[HackleScreenBuilder alloc] initWithName:@"screenName"
                                                         className:@"screenName"] build];
[[Hackle app] setCurrentScreenWithScreen:screen];
```

{% endtab %}

{% tab title="SwiftUI" %}

```swift
struct MainView: View {
    @Environment(\.scenePhase) var scenePhase

    let screen = Screen.builder(name: "screenName", className: "screenName")
      .build()

    var body: some View {
        VStack {
            Text("메인 화면")
        }
        .onAppear {
            // 뷰가 나타날 때 화면 추적
            Hackle.app()?.setCurrentScreen(screen: screen)
        }
        .onChange(of: scenePhase) { oldPhase, newPhase in
            // 백그라운드 -> 포그라운드 전환될 때 화면 추적
            // 전환될 때 앱의 화면이 변하는 케이스가 없으면 호출하지 않아도 무방합니다.
            if newPhase == .active {
                Hackle.app()?.setCurrentScreen(screen: screen)
            }
        }
    }
}
```

{% endtab %}
{% endtabs %}

### 속성 (Property)

핵클 SDK는 이벤트(Event) 객체에 속성을 추가할 수 있도록 지원합니다.

* 이벤트 객체에 추가 가능한 속성 개수는 최대 64개입니다.

<table><thead><tr><th width="133.04296875">구분</th><th width="127.60546875">타입</th><th>제약사항</th></tr></thead><tbody><tr><td>속성 명(key)</td><td><code>string</code></td><td><ul><li>글자수 제한은 128자입니다. (128 characters)</li><li>대소문자를 구분하지 않습니다.</li><li>예를 들어 AGE와 age는 동일한 속성명으로 인식합니다.</li></ul></td></tr><tr><td>속성 값(value)</td><td><code>boolean</code>, <code>string</code>, <code>number</code>, <code>array</code></td><td><ul><li>string 타입인 경우 글자수 제한은 1024자입니다.<br>(1024 characters)</li><li>string 타입은 대소문자를 구분합니다.</li><li>예를 들어 APPLE과 apple은 서로 다른 속성값으로 인식합니다.</li><li>number 타입인 경우 정수 최대 15자리, 소수점 최대 6자리를<br>지원합니다.</li></ul></td></tr></tbody></table>

#### 예시

{% tabs %}
{% tab title="Swift" %}

```swift
let screen = Screen.builder(name: "screenName", className: "screenName")
    .property("key", "value")
    .build()

Hackle.app()?.setCurrentScreen(screen: screen)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
HackleScreen *screen = [[[[HackleScreenBuilder alloc] initWithName:@"screenName"
                                                          className:@"screenName"]
                          property:@"key" value:@"value"]
                         build];
[[Hackle app] setCurrentScreenWithScreen:screen];
```

{% endtab %}
{% endtabs %}

## automaticScreenTracking 비활성화

SDK에서는 기본적으로 `automaticScreenTracking`이 활성화되어 있습니다. ViewController 단위의 자동 화면 정보 수집을 원하지 않는 경우 `automaticScreenTracking`를 비활성화 해야 합니다.

SDK를 초기화 할 때 `automaticScreenTracking`을 설정할 수 있습니다.

{% hint style="danger" %}
`automaticScreenTracking`이 활성화 된 상태에서 `setCurrentScreen`를 통해 수동으로 화면 수집을 하는 경우, `$page_view`와 `$engagement`가 과수집 되거나 의도하지 않은 속성으로 수집될 수 있습니다.

**수동으로 화면 정보를 수집하는 경우 `automaticScreenTracking` 비활성화를 추천합니다.**
{% endhint %}

#### Example

{% tabs %}
{% tab title="Swift" %}

```swift
let config = HackleConfigBuilder()
  .automaticScreenTracking(false)
  .build()

Hackle.initialize(sdkKey: YOUR_APP_SDK_KEY, config: config) {
    // SDK ready to use.
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
HackleConfigBuilder *builder = [[HackleConfigBuilder alloc] init];
[builder automaticScreenTracking:false];
HackleConfig *config = [builder build];

[Hackle initializeWithSdkKey:@"YOUR_APP_SDK_KEY" config:config completion:^{
    // SDK ready to use.
}];
```

{% endtab %}
{% endtabs %}


# 사용자 탐색

{% hint style="info" %}
**Debug 빌드에서만 사용하는 것을 권장합니다.**
{% endhint %}

{% hint style="warning" %}
원격 평가 방식에서는 사용자 탐색 기능을 사용할 수 없습니다.
{% endhint %}

사용자 식별자를 확인하고 A/B 테스트, 기능플래그에 강제할당 하는 방법을 설명합니다.

아래 코드를 추가합니다.

```swift
override func viewDidLoad() {
  // ...
  hackleApp.showUserExplorer()
  // ...
}
```

화면 하단에 핵클 로고 버튼이 표시됩니다. 버튼 클릭시 설정 화면으로 진입 할 수 있습니다.

![](/files/BF5cOn5FuNPcVPAf5jsZ)

## 사용자 식별자 확인하기

화면 상단에서 사용자 식별자를 확인 및 복사 할 수 있습니다.

푸시 메시지가 지원되는 버전의 경우 푸시 토큰 정보도 확인할 수 있습니다.

![](/files/rAiha6GiF18VjtW49nlM)

## 사용자 강제할당

* 화면하단에서 A/B 테스트, 기능플래그의 분배 결과를 확인할 수 있습니다.
* SelectBox 클릭 시 특정 그룹으로 강제할당 할 수 있습니다.
* `Reset` 버튼 클릭 시 강제할당이 해제됩니다.
* `Reset all` 버튼 클릭시 모든 강제할당이 해제됩니다.
* 앱에서 강제할당한 경우 앱에서 분배하는 경우에만 적용됩니다. (대시보드 테스트기기에 등록되지 않습니다)
* 강제할당이 적용되지 않는경우 앱을 완전 종료 후 재실행 해주세요.

![](/files/g3J0SqjFO5f7xkB8hZ8T)

![](/files/UYMyCitcB09Hg8v65dZB)


# 웹앱 연동

{% hint style="info" %}
iOS SDK 2.27.0 이상, JavaScript SDK 11.25.1 이상 버전에서 지원하는 기능입니다.

웹앱에 대해서는 [문서](/development-guide/faq/web-app-intergration)를 참고해주세요.
{% endhint %}

`WKWebView` 를 통해 자사 웹사이트를 랜더링하는 경우, 다음 같은 설정을 통해 웹사이트에 포함된 핵클 JavaScirpt SDK를 웹사이트 코드 변경없이 핵클 iOS SDK 기능과 동일하게 사용할 수 있습니다.

브릿지 설정을 하면 웹뷰에서 발생하는 핵클 이벤트는 iOS SDK를 통해 수집됩니다.

{% hint style="info" %}
웹뷰 브릿지는 Hackle JavaScript SDK에서 브릿지로 전달한 데이터만 후킹하여 처리하고 나머지 데이터는 참조한 delegate로 전달합니다.
{% endhint %}

{% tabs %}
{% tab title="Swift" %}

```swift
...
Hackle.app().setWebViewBridge(webView)
...
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
...
self.webView = ...
HackleApp *hackleApp = [Hackle app];
[hackleApp setWebViewBridge:self.webView :NULL];
...
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
해당 기능을 사용하기 위해서는 **JavaScript 웹페이지에서 동일한 App SDK 키를 사용**해야 합니다.

핵클 iOS 웹뷰 설정은 iOS `UIDelegate` 및 `WKUserScript` 등을 이용하여 핵클 JavaScript SDK와 상호작용하게 됩니다. 반드시 `WKWebView::load` 함수 호출 이전에 해당 설정이 완료될 수 있도록 코드를 위치시켜 주세요.
{% endhint %}

이미 사용하고 있는 `UIDelegate`가 있는 경우 다음과 같이 사용하고 있는 `UIDelegate`와 함께 해당 함수로 전달해 주세요.

* `UIDelegate`를 설정하지 않은 경우 웹뷰 객체의 `UIDelegate`가 있으면 해당 delegate를 참조합니다.
* `UIDelegate`를 설정한 경우 해당 delegate를 참조합니다.

{% tabs %}
{% tab title="Swift" %}

```swift
...
Hackle.app().setWebViewBridge(webView, myUiDelegate)
...
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
...
self.webView = ...
self.myUIDelegate = ...
HackleApp *hackleApp = [Hackle app];
[hackleApp setWebViewBridge:self.webView :self.myUIDelegate];
...
```

{% endtab %}
{% endtabs %}

{% hint style="danger" %}
앱에서 이미 delegate에 다양한 기능을 추가해서 사용하는 경우 delegate 설정이 모두 완료된 후 `setWebViewBridge`함수 호출을 권장합니다.
{% endhint %}

### 웹뷰에서 발생하는 자동 수집 이벤트 연동

{% hint style="info" %}
iOS SDK 2.57.0 이상, JavaScript SDK 11.51.0 이상 버전에서 지원하는 기능입니다.
{% endhint %}

웹뷰 내 웹사이트에서 발생하는 `$page_view`와 `$engagement`는 비활성화 상태입니다. 웹뷰 브릿지를 설정할 때 `HackleWebViewConfig`를 설정하여 자동 수집 이벤트를 각각 활성화할 수 있습니다.

```swift
let webViewConfig = HackleWebViewConfigBuilder()
	.automaticScreenTracking(true)
	.automaticEngagementTracking(true)
	.build()

Hackle.app().setWebViewBridge(webView, myUiDelegate, webViewConfig)
```

#### 설정 옵션

<table><thead><tr><th width="258.32421875">Option</th><th width="120">Default</th><th>Description</th></tr></thead><tbody><tr><td><code>automaticScreenTracking</code></td><td>false</td><td>웹사이트에서 발생하는 <code>$page_view</code> 수집 여부</td></tr><tr><td><code>automaticEngagementTracking</code></td><td>false</td><td>웹사이트에서 발생하는 <code>$engagement</code> 수집 여부</td></tr><tr><td><code>automaticRouteTracking</code></td><td>true</td><td>웹사이트에서 발생하는 페이지 정보 자동 수집 여부</td></tr></tbody></table>

{% hint style="info" %}
웹페이지 이동 시 `$page_view`와 `$engagement` 를 자동 수집하려면 `automaticScreenTracking`, `automaticEngagementTracking`, `automaticRouteTracking` 를 모두 `true`로 설정하세요.

웹페이지 이동 시 `$page_view`와 `$engagement` 를 [수동 수집](/development-guide/javascript/event-tracking/js-track-page)하는 경우 `automaticScreenTracking`, `automaticEngagementTracking`는 `true`로 설정하고, `automaticRouteTracking` 를 `false`로 설정하세요.
{% endhint %}


# 푸시 메시지 연동

{% hint style="info" %}
**타 푸시 솔루션과 함께 사용할 수 있습니다**

타 푸시 솔루션과 함께 사용하려면 타 푸시 솔루션의 Swizzling 옵션을 비활성화해야 합니다.

Swizzling 비활성화 후, 해당 솔루션의 가이드를 참고하여 푸시 알림 처리를 수동으로 설정해 주세요.
{% endhint %}

{% hint style="danger" %}
FCM을 통한 iOS 푸시 메시지를 지원하지않습니다

iOS 푸시 메시지 사용을 위해서 APNs를 연동해주세요.
{% endhint %}

{% stepper %}
{% step %}
**APNs 설정하기**

iOS 앱에서 푸시 메시지를 사용하기 위해서는 핵클 워크스페이스와 APNs 연동 설정이 필요합니다.

자세한 내용은 [Apple Push Notification Service 설정](/external-link/crm-channels/apple-push-notification-service-integration)을 참고하세요.
{% endstep %}

{% step %}
**앱에 PushNotification Capability 추가**

Xcode 프로젝트 설정의 `Signing & Capabilities` 탭에서 `+ Capability`를 아래와 같이 클릭해주세요.

![](/files/PNiAsTVQze9StBt5bbb3)

`Push Notifications`과 `Background Modes`를 추가해주세요.

![](/files/BlYVYAqrmLVZwdxMTDzK)

![](/files/ABblKDj7endt9vHJrl39)

그리고 `Background Modes`의 `Remote notifications`를 활성화해 주세요.

![](/files/re3CqjDxqPeuuJYty7h3)
{% endstep %}

{% step %}
**AppDelegate 설정**

{% hint style="info" %}
AppDelegate 설정을 해야 푸시 토큰 수집, 푸시 메시지 표시, 푸시 클릭 처리를 할 수 있습니다.
{% endhint %}

푸시 메시지 연동을 위해서는 AppDelegate가 필요합니다.

핵클에서 iOS 앱이 설치된 기기에 푸시 메시지를 전달할수 있도록 아래의 설정을 완료합니다.

{% tabs %}
{% tab title="Swift" %}

```swift
class AppDelegate: NSObject, UIApplicationDelegate {
  func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil
  ) -> Bool {
    return true
  }
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
#import <UIKit/UIKit.h>

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@end
```

```objectivec
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  return YES;
}
```

{% endtab %}
{% endtabs %}

만약 `SwiftUI`의 경우 아래와 같이 `AppDelegate`를 `SwiftUI`에 등록해 주세요.

```swift
import SwiftUI

@main
struct sampleApp: App {
  ...
  @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
  ...
}
```

{% endstep %}

{% step %}
**푸시 토큰 수집**

{% hint style="warning" %}
푸시토큰은 자동으로 수집되지 않습니다.

반드시 아래 코드를 통해 푸시토큰을 수집해야 푸시 수신을 받을 수 있습니다.
{% endhint %}

`AppDelegate`에 아래과 같이 `setPushToken` 메소드를 추가합니다.

{% tabs %}
{% tab title="Swift" %}

```swift
import Hackle

class AppDelegate: NSObject, UIApplicationDelegate {
  ...
  func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {

    // iOS 앱에서 푸시 권한 요청
    let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]

    UNUserNotificationCenter.current().requestAuthorization(
      options: authOptions,
      completionHandler: { _, _ in }
    )

    UNUserNotificationCenter.current().delegate = self
    application.registerForRemoteNotifications()

    // 핵클 SDK 초기화
    Hackle.initialize(sdkKey: YOUR_APP_SDK_KEY)
    return true
  }

  func application(
    _ application: UIApplication,
    didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
  ) {
    // 핵클 서버로 APNs 푸시 토큰 전달
    Hackle.app()?.setPushToken(deviceToken)
  }
  ...
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
#import <UIKit/UIKit.h>
#import <UserNotifications/UserNotifications.h>
@import Hackle;

@interface AppDelegate : UIResponder <UIApplicationDelegate, UNUserNotificationCenterDelegate>

@end
```

```objectivec
#import "AppDelegate.h"

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // iOS 앱에서 푸시 권한 요청
    UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
    [center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert + UNAuthorizationOptionSound)
                          completionHandler:^(BOOL granted, NSError * _Nullable error) {

    }];
    center.delegate = self;
    [[UIApplication sharedApplication] registerForRemoteNotifications];

    // 핵클 SDK 초기화
    [Hackle initializeWithSdkKey:@"YOUR_APP_SDK_KEY"
                          config:[HackleConfig DEFAULT]];

    return YES;
}

- (void)application:(UIApplication *)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
    // 핵클 서버로 APNs 푸시 토큰 전달
    [[Hackle app] setPushToken:deviceToken];
}

```

{% endtab %}
{% endtabs %}
{% endstep %}

{% step %}
**푸시 메시지 표시**

{% hint style="info" %}
백그라운드에서는 코드 구현 없이 자동으로 푸시가 표시됩니다.
{% endhint %}

{% hint style="warning" %}
포그라운드에서 푸시 표시를 위해서 반드시 아래 코드를 구현해야 합니다.
{% endhint %}

포그라운드 푸시 메시지 표시를 위해 `userNotificationCenter` 메소드를 추가합니다.

{% tabs %}
{% tab title="Swift" %}

```swift
import Hackle

extension AppDelegate: UNUserNotificationCenterDelegate {
  // Foreground push message
  func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    willPresent notification: UNNotification,
    withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions
  ) -> Void) {

    if Hackle.userNotificationCenter(
      center: center, willPresent: notification, withCompletionHandler: completionHandler
    ) {
      // Succefully processed notification
      // Automatically consumed completion handler
      return
    } else {
      // Received not hackle notification or error
      print("Do something")

      if #available(iOS 14.0, *) {
        completionHandler([.list, .banner])
      } else {
        completionHandler([.alert])
      }
    }
  }
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
// Foreground push message
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
       willPresentNotification:(UNNotification *)notification
         withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler
{
    if ([Hackle userNotificationCenterWithCenter:center
                                      willPresent:notification
                             withCompletionHandler:completionHandler]) {
        // Succefully processed notification
        // Automatically consumed completion handler
        return;
    } else {
        // Received not hackle notification or error
        NSLog(@"Do something");
        completionHandler(UNNotificationPresentationOptionList | UNNotificationPresentationOptionBanner);
    }
}

```

{% endtab %}
{% endtabs %}

핵클에서 송신한 푸시가 아닌 경우 false가 리턴됩니다.
{% endstep %}

{% step %}
**푸시 클릭 처리**

{% hint style="danger" %}
핵클에서 제공하는 푸시 클릭 처리 함수를 호출하지 않으면 푸시가 정상적으로 처리되지 않습니다.

또한, 푸시 클릭 이벤트가 수집되지 않고 푸시 클릭률 지표를 이용할 수 없습니다.
{% endhint %}

푸시 클릭 처리를 위해 `handleNotification` 메소드를 추가합니다.

{% tabs %}
{% tab title="Swift" %}

```swift
import Hackle

extension AppDelegate: UNUserNotificationCenterDelegate {
  // push click
  public func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse,
    withCompletionHandler completionHandler: @escaping () -> Void
  ) {

    if let _ = Hackle.handleNotification(response: response) {
      // process hackle notification
    } else {
      // not hackle notification or error
      print("do something")
    }

    // handleNotification 에서 completionHandler를 호출하지 않으니
    // 핵클 푸시 여부에 관계없이 반드시 completionHandler를 호출해야 합니다.
    completionHandler()
  }
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
// push click
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
         withCompletionHandler:(void (^)(void))completionHandler
{
    if ([Hackle handleNotificationWithResponse:response] != nil) {
        // process hackle notification
    } else {
        // not hackle notification or error
        NSLog(@"do something");
    }
    // handleNotification 에서 completionHandler를 호출하지 않으니
    // 핵클 푸시 여부에 관계없이 반드시 completionHandler를 호출해야 합니다.
    completionHandler();
}

```

{% endtab %}
{% endtabs %}

푸시 클릭 함수는 아래 순서로 처리를 합니다.

1. 핵클에서 송신한 푸시인지 확인
2. 푸시 클릭 이벤트를 핵클 서버로 송신
3. *(deep link push인 경우)* deep link 처리

핵클에서 송신한 푸시가 아닌 경우 nil이 리턴됩니다.

**푸시 클릭 커스텀 딥링크 처리**

앱 내에서 핵클에서 전달한 링크의 재가공이 필요한 경우 `handleNotification`의 **`handleAction`파라미터를 false로 선언** 해서 사용하면 됩니다.

handleAction 파라미터가 false인 경우

* Hackle SDK는 푸시 클릭 이벤트를 핵클 서버로 송신합니다.
* deep link 처리를 하지 않습니다.

{% tabs %}
{% tab title="Swift" %}

```swift
import Hackle

extension AppDelegate: UNUserNotificationCenterDelegate {
  // push click
  public func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse,
    withCompletionHandler completionHandler: @escaping () -> Void
  ) {

    if let notification = Hackle.handleNotification(response: response, handleAction: false) {
      // 푸시 메시지 Action Type
      print("\(notifiaction.actionType)")

      // 푸시 메시지에 등록된 link
      print("\(notifiaction.link)")
    } else {
      // not hackle notification or error
      print("do something")
    }

    // handleNotification 에서 completionHandler를 호출하지 않으니
    // 핵클 푸시 여부에 관계없이 반드시 completionHandler를 호출해야 합니다.
    completionHandler()
  }
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
// push click
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
         withCompletionHandler:(void (^)(void))completionHandler
{
    id notification = [Hackle handleNotificationWithResponse:response handleAction:NO];
    if (notification != nil) {
        // process hackle notification
        NSLog(@"%@", [notification valueForKey:@"actionType"]);
        NSLog(@"%@", [notification valueForKey:@"link"]);
    } else {
        // not hackle notification or error
        NSLog(@"do something");
    }
    // handleNotification 에서 completionHandler를 호출하지 않으니
    // 핵클 푸시 여부에 관계없이 반드시 completionHandler를 호출해야 합니다.
    completionHandler();
}

```

{% endtab %}
{% endtabs %}

푸시 메시지의 `actionType`은 아래와 같습니다.

<table><thead><tr><th width="150">actionType</th><th>설명</th></tr></thead><tbody><tr><td>appOpen</td><td>앱 실행</td></tr><tr><td>link</td><td>앱 실행 후 링크로 이동</td></tr></tbody></table>

`actionType`이 `appOpen`인 경우 `link`는 nil 입니다.
{% endstep %}

{% step %}
**푸시 메시지 테스트**

**토큰 확인**

* [사용자 식별자 확인하기 가이드](/development-guide/ios/ios-user-explorer) 를 통해 iOS 기기에 설정된 토큰을 확인합니다.
* [사용자 조회 가이드](/user-view/user-profile) 를 통해 특정 사용자에 할당 된 iOS 푸시 토큰을 확인할 수 있습니다.

**테스트**

* [푸시 메시지 테스트 발송 가이드](/crm-marketing/push-message-guide/create-campaign#id-3-1)를 참고하여 푸시 메시지를 iOS 기기에서 확인합니다.
  {% endstep %}

{% step %}
**푸시 메시지 수신**

iOS는 빌드 환경에 따라 푸시 메시지 수신 여부가 다릅니다.

{% hint style="info" %}
APNs Key의 Environment을 `Sandbox & Production`으로 설정한 경우에도 아래와 같이 핵클 환경 및 앱 빌드 환경별로 푸시 메시지를 수신받을 수 있는 범위가 다릅니다.
{% endhint %}

<table><thead><tr><th width="175.78125">핵클 환경</th><th width="175.40625">APNs Environment</th><th>빌드 환경</th></tr></thead><tbody><tr><td>개발 환경,<br>개발/운영 테스트 푸시</td><td>Sandbox</td><td>Xcode에서 직접 실행,<br>개발용 프로비지닝</td></tr><tr><td>운영 환경</td><td>Production</td><td>TestFlight, Ad Hoc,<br>App Store 배포</td></tr></tbody></table>
{% endstep %}
{% endstepper %}

## 딥링크 이동

핵클 푸시 메시지는 클릭 시 딥링크 이동을 지원합니다.

푸시 메시지를 통해 해당 앱이 열리는 경우 아래의 설정을 통해 열린 딥링크 정보를 확인할수 있습니다.

{% hint style="info" %}
iOS 딥링크에 대한 자세한 사항은 [iOS 딥링크 가이드](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app) 에서 확인 가능합니다.

[푸시 클릭 커스텀 딥링크 처리](#푸시-클릭-커스텀-딥링크-처리) 를 한 경우에는 아래와 같이 딥링크 정보가 전달되지 않습니다.
{% endhint %}

{% tabs %}
{% tab title="SwiftUI" %}

```swift
import SwiftUI

@main
struct sampleApp: App {
  ...
  var body: some Scene {
    WindowGroup {
      ContentView()
        .onOpenURL(perform: { url in
          // Handle opened url
          print("\(url.absoluteString) opened.")
        })
    }
  }
  ...
}
```

{% endtab %}

{% tab title="Storyboard" %}

```swift
class AppDelegate: NSObject, UIApplicationDelegate {
  ...
  func application(_ application: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:] ) -> Bool {
    // Handle opened url
    print("\(url.absoluteString) opened.")
  }
  ...
}
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
`SwiftUI`와 `Storyboard`에서의 딥링크 처리는 각각 독립적이기 때문 해당 iOS 앱 프로젝트에 맞도록 설정해 주세요
{% endhint %}


# 푸시메시지 이미지 첨부

{% hint style="info" %}
iOS SDK 2.53.0 버전 이상에서 지원하는 기능입니다.
{% endhint %}

iOS 앱에서 이미지를 포함한 푸시 메시지를 보여주기 위해서는 [Notification Service Extension](https://developer.apple.com/documentation/usernotifications/unnotificationserviceextension)을 추가하여 아래의 설정을 완료합니다.

iOS Rich Push Notification 에 대한 자세한 사항은 [Rich Push Notification](https://developer.apple.com/documentation/usernotificationsui/customizing-the-appearance-of-notifications) 에서 확인 가능합니다.

{% stepper %}
{% step %}
**앱에 Notification Service Extension 추가**

Xcode 프로젝트 상단 `File > New > Target...` 탭을 선택하여 아래와 같이 `Notification Service Extension`을 선택합니다.

![](/files/Zrae7mBVGIdvbe9aLbCw)

알맞은 이름을 입력 후 `Finish`를 눌러주세요.

![](/files/OkrfEN5gsIdfMvpT6y7I)

{% tabs %}
{% tab title="Swift Package Manager 설정" %}
Swift Package Manager를 이용해 핵클 SDK를 추가한 경우 앞서 추가한 `Extension`에 `Hackle` 프레임워크를 추가합니다.

![](/files/zcIt6Tl3w0K0Xrq2vrfJ)

![](/files/Ph1I5Ez2QXsV4rj5EXxo)

![](/files/5vYIWC7r6LP7OEAOTY3A)
{% endtab %}

{% tab title="CocoaPods 설정" %}
CocoaPods을 이용해 핵클 SDK를 추가한 경우 `Podfile`에 앞서 추가한 `Extension` 에 핵클 종속성을 추가해야 합니다.

아래와 같이 `Hackle` 종속성을 추가합니다.

```ruby
target 'NotificationServiceExtension' do
  pod 'Hackle'
end
```

{% endtab %}
{% endtabs %}
{% endstep %}

{% step %}
**Minimum Deployment 설정**

Notification Service Extension은 앱과 별도로 최소 지원버전을 명시해야 합니다.

**`Minimum Deployment`를 앱과 동일하게 설정하는 것을 추천합니다**.

{% hint style="danger" %}
최소 지원 버전이 앱과 extension이 다를 경우, 앱 버전에 따라 이미지가 표시되지 않을 수 있습니다.

ex) App 최소 지원 버전이 iOS 15, Extension 최소 지원 버전이 iOS 18인 경우

* iOS 15 이상, iOS 18 미만 버전은 이미지가 표시되지 않습니다.
* iOS 18 이상 버전은 이미지가 표시됩니다.
  {% endhint %}

![](/files/CE3XvksUz2o9bvLpYI7u)
{% endstep %}

{% step %}
**핵클 SDK와 연동하기**

**푸시 메시지에 이미지 추가**

푸시 이미지 처리를 위해 `didReceive` 함수에서 `handleRichNotification` 함수를 추가합니다.

{% tabs %}
{% tab title="Swift" %}

```swift
import UserNotifications
import Hackle

class NotificationService: UNNotificationServiceExtension {

    var defaultNotificationContent: UNNotificationContent?
    var contentHandler: ((UNNotificationContent) -> Void)?

    override func didReceive(
      _ request: UNNotificationRequest,
      withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
    ) {
        self.defaultNotificationContent = request.content
        self.contentHandler = contentHandler

        if Hackle.handleRichNotification(request: request, contentHandler: contentHandler) {
            return
        }

        contentHandler(request.content)
    }

    override func serviceExtensionTimeWillExpire() {
      if let contentHandler = contentHandler,
         let defaultNotificationContent = defaultNotificationContent {
            contentHandler(defaultNotificationContent)
        }
    }
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
#import <UserNotifications/UserNotifications.h>
@import Hackle;

@interface NotificationService : UNNotificationServiceExtension

@end
```

```objectivec
#import "NotificationService.h"

@interface NotificationService ()

@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;

@end

@implementation NotificationService

- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request
                   withContentHandler:(void (^)(UNNotificationContent *))contentHandler {
    self.defaultNotificationContent = request.content;
    self.contentHandler = contentHandler;

    if ([Hackle handleRichNotificationWithRequest:request contentHandler:contentHandler]) {
        return;
    }
    contentHandler(request.content);
}

- (void)serviceExtensionTimeWillExpire {
    self.contentHandler(self.bestAttemptContent);
}

@end
```

{% endtab %}
{% endtabs %}
{% endstep %}
{% endstepper %}


# 옵트아웃

옵트아웃이 활성화되면 SDK는 모든 이벤트 전송을 중단합니다.

## 초기화 시 설정

{% tabs %}
{% tab title="Swift" %}

```swift
let config = HackleConfigBuilder()
    .optOutTracking(true)
    .build()

Hackle.initialize(sdkKey: YOUR_APP_SDK_KEY, config: config) {
    // SDK ready to use.
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
HackleConfigBuilder *builder = [[HackleConfigBuilder alloc] init];
[builder optOutTracking:YES];
HackleConfig *config = [builder build];

[Hackle initializeWithSdkKey:@"YOUR_APP_SDK_KEY" config:config completion:^{
    // SDK ready to use.
}];
```

{% endtab %}
{% endtabs %}

## 런타임 옵트아웃 제어

{% tabs %}
{% tab title="Swift" %}

```swift
hackleApp.setOptOutTracking(optOut: true)   // activate
hackleApp.setOptOutTracking(optOut: false)  // deactivate
let isOptOut = hackleApp.isOptOutTracking   // query (property, not method)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
[hackleApp setOptOutTrackingWithOptOut:YES];
[hackleApp setOptOutTrackingWithOptOut:NO];
BOOL isOptOut = hackleApp.isOptOutTracking;
```

{% endtab %}
{% endtabs %}

## 영속성 관리

{% hint style="warning" %}
런타임에 변경된 옵트아웃 상태는 앱 재시작 시 초기화 Config에 설정된 값으로 리셋됩니다.

앱 재시작 후에도 상태를 유지하려면 직접 저장 및 복원 로직을 구현해야 합니다.
{% endhint %}

{% tabs %}
{% tab title="Swift" %}

```swift
func saveOptOutState(_ optOut: Bool) {
    UserDefaults.standard.set(optOut, forKey: "hackle_opt_out")
    hackleApp.setOptOutTracking(optOut: optOut)
}

func getOptOutConfig() -> HackleConfig {
    let optOut = UserDefaults.standard.bool(forKey: "hackle_opt_out")
    return HackleConfigBuilder()
        .optOutTracking(optOut)
        .build()
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
- (void)saveOptOutState:(BOOL)optOut {
    [[NSUserDefaults standardUserDefaults] setBool:optOut forKey:@"hackle_opt_out"];
    [hackleApp setOptOutTrackingWithOptOut:optOut];
}

- (HackleConfig *)getOptOutConfig {
    BOOL optOut = [[NSUserDefaults standardUserDefaults] boolForKey:@"hackle_opt_out"];
    HackleConfigBuilder *builder = [[HackleConfigBuilder alloc] init];
    [builder optOutTracking:optOut];
    return [builder build];
}
```

{% endtab %}
{% endtabs %}


# JavaScript

{% hint style="info" %}
JavaScript SDK는 IE11+ 및 모든 주요 브라우저를 지원합니다.

JavaScript SDK를 사용하기 위해서는 `Promise`, `Map` API가 필수로 지원되어야 합니다. 폴리필이 필요하다면 `core-js`를 이용하는 것을 추천합니다.
{% endhint %}

{% hint style="success" %}
프레임워크 지원

* Angular, Vue 또는 기타 프레임워크에서 JavaScript SDK를 사용할 수 있습니다.
* React를 사용중이라면 [React](/development-guide/react) 가이드를 참고해주세요.
* Next.JS를 사용중이라면 [Next.js](/development-guide/nextjs-index) 가이드를 참고해주세요.
* Google Tag Manager를 통한 연동 방법은 [GTM 문서](/development-guide/google-tag-manager)를 확인해주세요.

특정 환경 및 프레임워크에 대한 지원이 필요하시면 [핵클 슬랙 커뮤니티 ](https://join.slack.com/t/hackle-community/shared_invite/zt-1awrnygsh-U8VCHwN06ZDTF9yAzik5SA)또는 <support@hackle.io>로 문의해주세요.
{% endhint %}

## 의존성 추가

[![](https://img.shields.io/npm/v/%40hackler%2Fjavascript-sdk)](https://www.npmjs.com/package/@hackler/javascript-sdk)

{% tabs %}
{% tab title="NPM" %}

```shell
npm install --save @hackler/javascript-sdk
```

{% endtab %}

{% tab title="YARN" %}

```shell
yarn add @hackler/javascript-sdk
```

{% endtab %}

{% tab title="HTML" %}

```html
<!-- HTML의 경우 의존성 추가 작업이 필요하지 않습니다 -->
```

{% endtab %}
{% endtabs %}

## SDK 초기화

SDK를 사용하기 위해서 반드시 HackleClient 를 초기화 해야 합니다.

* `HackleClient`는 SDK의 기능을 사용하기 위한 메소드들을 제공하는 클래스입니다.
* SDK 키는 핵클 서비스의 대시보드 안에 위치한 [SDK 연동 정보](https://dashboard.hackle.io/config/sdk-setting)에서 확인하실 수 있습니다.

{% tabs %}
{% tab title="JavaScript" %}

```javascript
import * as Hackle from "@hackler/javascript-sdk";

// YOUR_BROWSER_SDK_KEY로 초기화
const hackleClient = Hackle.createInstance("YOUR_BROWSER_SDK_KEY");
```

{% endtab %}

{% tab title="HTML" %}

```html
<!-- 기존 코드 head 안에 추가 -->
<script src="https://cdn2.hackle.io/npm/@hackler/javascript-sdk@11.52.0/lib/index.browser.umd.min.js"></script>
<script>
  // YOUR_BROWSER_SDK_KEY로 초기화
  window.hackleClient = Hackle.createInstance("YOUR_BROWSER_SDK_KEY");
</script>
```

{% endtab %}
{% endtabs %}

### 초기화 완료

초기화는 **비동기로 실행**되며, 핵클 서버로부터 필요한 정보들을 가져와서 SDK에 저장합니다.\
await으로 초기화 완료 전까지 대기할 수 있습니다.

{% hint style="warning" %}
초기화가 완료되기 전에 A/B 테스트, 기능 플래그를 호출하면 기본 그룹(A), 꺼짐(false)을 리턴하며, **노출 이벤트가 기록되지 않아 실험 데이터가 누락될 수 있습니다.**
{% endhint %}

`onInitialized()`를 await하면 초기화 완료 후 SDK를 사용할 수 있습니다. 네트워크 오류 등으로 초기화에 실패해도 이후 SDK 호출은 기본값을 안전하게 리턴합니다.

```javascript
async function initHackle(){
  try {
    await hackleClient.onInitialized();
    // SDK 사용 가능
  } catch (e) {
    // 초기화 실패 시에도 이후 SDK 호출은 기본값(A, false)을 리턴합니다.
    console.warn("Hackle 초기화 실패:", e);
  }
}

initHackle();
```

```javascript
// deprecated: onReady는 Promise 기반 async/await 패턴을 지원하지 않아 deprecated 되었습니다.
// onInitialized()를 사용해 주세요.
hackleClient.onReady(() => {
    // SDK 사용 가능
});
```

### 초기화 설정정보

설정정보를 포함하여 SDK를 초기화 할 수 있습니다.

```javascript
const config = {
  debug: true
};

const hackleClient = Hackle.createInstance("YOUR_BROWSER_SDK_KEY", config);
```

#### 설정 옵션

<table data-full-width="false"><thead><tr><th width="213.57421875">설정</th><th width="295.48828125">기능</th><th width="125.8125">기본값</th><th width="108.09765625">지원 버전</th></tr></thead><tbody><tr><td><code>debug</code></td><td>모든 기능에 대한 로그를 콘솔에 출력합니다.</td><td><code>false</code></td><td>1.0.0+</td></tr><tr><td><code>pollingIntervalMillis</code></td><td><p>대시보드에서 설정한 정보를 주기적으로 업데이트 할 수 있습니다.</p><ul><li>최솟값 : 60000 (60초)</li></ul></td><td>-1<br>(주기적으로 업데이트하지 않음)</td><td>11.1.0+</td></tr><tr><td><code>exposureEventDedupIntervalMillis</code></td><td><p>동일한 사용자가 연속으로 발생시킨 동일한 A/B 테스트, 기능플래그 분배결과에 대한 노출 이벤트를 제거합니다.</p><ul><li>최솟값: 1000 (1초)</li><li>최댓값: 3600000 (1시간)</li></ul></td><td>60000 (1분 / 11.23.0 이상)<br>-1 (중복제거 하지 않음 / 11.23.0 미만)</td><td>11.1.0+</td></tr><tr><td><code>devTool</code></td><td><a href="/pages/bBJsfc1CSj4vQ3PW6XXg">사용자 탐색</a>을 사용할 수 있도록 합니다.</td><td><code>undefined</code></td><td>11.13.0+</td></tr><tr><td><code>autoOpenDevTool</code></td><td>사용자 탐색 버튼이 자동으로 나타나도록 하는 옵션입니다.</td><td><code>false</code></td><td>11.13.0+</td></tr><tr><td><code>sameSiteCookie</code></td><td>핵클 쿠키에 sameSite 플래그를 설정하고 쿠키 개인정보 보호 정책을 결정합니다.</td><td><code>Lax</code></td><td>11.20.0+</td></tr><tr><td><code>secureCookie</code></td><td><code>true</code>로 설정하시면 핵클 쿠키에 Secure 플래그를 설정합니다.</td><td><code>false</code></td><td>11.20.0+</td></tr><tr><td><code>user</code></td><td>초기화 시점에 사용자를 주입합니다.</td><td><code>undefined</code></td><td>11.22.3+</td></tr><tr><td><code>sessionPolicy</code></td><td>세션 유지 조건과 만료 조건을 설정합니다.</td><td><p><code>ALWAYS_NEW_SESSION</code> ,</p><p><code>1800000</code></p></td><td>11.54.0+</td></tr><tr><td><code>optOutTracking</code></td><td>옵트아웃 활성화 여부.</td><td><code>false</code></td><td>11.54.0+</td></tr><tr><td><code>evaluationMode</code></td><td>평가 방식을 설정합니다.</td><td><code>local</code></td><td>12.0.0+</td></tr><tr><td><code>sessionTimeoutMillis</code><br>(deprecated)</td><td>세션만료 시간을 설정합니다. <code>sessionPolicy</code>를 사용해 주세요.</td><td>1800000 (30분)</td><td>11.8.0+</td></tr></tbody></table>

#### 평가 방식 설정

평가를 SDK에서 직접 수행할지, 핵클 서버가 미리 수행한 결과를 조회할지 선택할 수 있습니다.\
설정하지 않으면 기본값인 `"local"`(로컬 평가)로 동작합니다.

```javascript
const config = {
    evaluationMode: "remote"
};

const hackleClient = Hackle.createInstance("YOUR_BROWSER_SDK_KEY", config);
```

{% hint style="warning" %}
원격 평가를 사용하면 브라우저에 사용자 정보를 저장하지 않습니다. 기존에 로컬 평가를 사용하며 저장된 사용자 정보가 있는 경우 삭제 처리합니다.

두 방식의 차이와 선택 기준은 [평가 방식](/development-guide/sdk/evaluation-mode) 문서를 참고 바랍니다.
{% endhint %}

#### 세션 정책 설정

세션 정책을 설정하여 세션의 유지 조건과 만료 조건을 제어할 수 있습니다.

```javascript
const config = {
    sessionPolicy: {
        timeoutMillis: 3600000,
        persistCondition: HackleSessionPersistConditions.NULL_TO_USER_ID
    }
};

const hackleClient = Hackle.createInstance("YOUR_BROWSER_SDK_KEY", config);
```

#### 초기화 시 사용자 주입

config에 user를 설정한 경우 유저 정보를 포함하여 SDK를 초기화 할 수 있습니다.

* 유저 정보를 포함하지 않으면 쿠키에 저장된 유저 정보를 사용합니다.
* 유저 정보를 포함하는 경우 쿠키에 저장된 유저 정보는 사용하지 않습니다.
* 쿠키에 저장된 유저 정보가 없는 경우 Hackle Device ID를 device id로 가지고 유저를 사용합니다.

{% hint style="info" %}
유저 정보는 SDK 초기화 이후에도 유저 정보 설정 함수를 통해 자유롭게 수정 할 수 있습니다.
{% endhint %}

{% hint style="warning" %}
초기화 시 주입한 유저 정보와 쿠키에 저장된 유저 정보는 병합하지 않습니다.

ex) 쿠키에 `userId: A` 가 저장된 상태에서 초기화 시 `deviceId: B` 를 주입하는 경우, `userId: null, deviceId: B`인 유저로 설정됩니다.
{% endhint %}

```javascript
const user = {
  userId: "LOGIN_ID",
  deviceId: "CUSTOM_DEVICE_ID"
};

const config = {
  user: user
};

const hackleClient = Hackle.createInstance("YOUR_BROWSER_SDK_KEY", config);
```

### 대시보드 설정 정보 갱신

대시보드 설정 정보를 명시적으로 갱신 할 수 있습니다.

{% hint style="warning" %}
해당 함수는 60초에 한번 제한적으로 호출할 수 있습니다.
{% endhint %}

```javascript
await hackleClient.fetch();
```


# 사용자 식별자와 속성

{% hint style="info" %}
사용자 식별자 관리

사용자 식별자는 사용자를 고유하게 식별하는 목적으로 사용합니다. 사용자 식별자의 의미와 중요성, 선택하는 기준 등에 대해서는 [사용자 식별자 관리하기](/getting-started/user-identifier) 문서를 참고하시기 바랍니다.
{% endhint %}

{% hint style="warning" %}
사용자 정보를 변경하는 함수(`setUser`, `setUserId`, `setDeviceId`, `updateUserProperties`, `resetUser` 등)는 `Promise`를 반환합니다.

사용자 정보 변경 직후  `await`로 반영 완료까지 기다리는 것을 권장합니다.
{% endhint %}

## 사용자 식별자

### 핵클에서 제공하는 기본 식별자

JavaScript SDK는 디바이스의 식별자를 관리하는 기능을 포함하고 있습니다. 따라서 사용자 식별자를 별도로 전달하지 않아도 사용자를 자동으로 식별할 수 있습니다.

SDK에서 관리하는 식별자를 조회하는 방법은 다음과 같습니다.

```javascript
// 사용자 정보 가지고 오기
const user = hackleClient.getUser();

// 디바이스 식별자 가지고 오기
const deviceId = user.deviceId;

// 내부적으로 관리되는 세션 식별자 가져오기
const sessionId = hackleClient.getSessionId();
```

#### 디바이스 ID 수정

핵클에서 제공하는 디바이스 ID를 사용하지 않고 직접 디바이스 ID를 주입할 수 있습니다.

```javascript
// 디바이스 식별자 변경
await hackleClient.setDeviceId("CUSTOM_DEVICE_ID");
```

#### 사용자 식별자(User ID) 설정

로그인 한 사용자의 식별자를 설정하실 수 있습니다.

```javascript
// 로그인 한 사용자 식별자 추가
await hackleClient.setUserId("LOGIN_ID");
```

### 추가 식별자

기본 식별자(deviceid, userid) 외의 식별자 타입을 추가할 경우 아래와 같이 설정할 수 있습니다.

{% hint style="info" %}
추가 식별자는 [핵클 통합 식별자](/getting-started/user-identifier/hackle-id)로 통합되지 않습니다.
{% endhint %}

{% hint style="danger" %}
`setUser` 를 하는 경우 현재 디바이스의 유저 정보를 덮어씁니다.

* 현재 A userId를 사용중인데 setUser 시 A userId 를 전달하지 않으면 userId가 A -> null로 변경됩니다.
* 현재 custom deviceId를 사용중인데 setUser 시 사용중인 deviceId를 전달하지 않으면 custom deviceId -> hackle deviceId로 변경됩니다.
* 추가 식별자를 사용하는 경우에도 setUser 시 전달하지 않으면 추가 식별자가 초기화가 됩니다.
* 프로퍼티의 경우 아래 케이스로 디바이스 내 캐싱된 프로퍼티가 유지 or 초기화 될 수 있습니다.
  * setUser 전 / 후 userId와 deviceId가 동일하다면 캐시된 프로퍼티 유지됩니다.
  * setUser 전 / 후 userId 혹은 deviceId가 변경된다면 캐시된 프로퍼티가 삭제됩니다.
    {% endhint %}

```javascript
const user = {
  userId: "LOGIN_ID",           // 사용자 ID (핵클 통합 식별자 사용가능)
  deviceId: "CUSTOM_DEVICE_ID", // 디바이스 ID (핵클 통합 식별자 사용가능)
  identifiers: {
    myCustomId: "CUSTOM_IDENTIFIER" // Custom ID
  }
};

await hackleClient.setUser(user);
```

## 사용자 속성(Property)

핵클 SDK는 사용자 속성을 추가할 수 있도록 지원합니다.

* 속성은 속성명(key)과 속성값(value)을 한 쌍으로 보내야 합니다.
* 추가 가능한 속성 개수는 최대 128개입니다.

<table><thead><tr><th width="133.04296875">구분</th><th width="127.60546875">타입</th><th>제약사항</th></tr></thead><tbody><tr><td>속성 명(key)</td><td><code>string</code></td><td><ul><li>글자수 제한은 128자입니다. (128 characters)</li><li>대소문자를 구분하지 않습니다.</li><li>예를 들어 AGE와 age는 동일한 속성명으로 인식합니다.</li></ul></td></tr><tr><td>속성 값(value)</td><td><code>boolean</code>, <code>string</code>, <code>number</code>, <code>array</code></td><td><ul><li>string 타입인 경우 글자수 제한은 1024자입니다.<br>(1024 characters)</li><li>string 타입은 대소문자를 구분합니다.</li><li>예를 들어 APPLE과 apple은 서로 다른 속성값으로 인식합니다.</li><li>number 타입인 경우 정수 최대 15자리, 소수점 최대 6자리를<br>지원합니다.</li></ul></td></tr></tbody></table>

### 사용자 속성 추가

`PropertyOperationsBuilder` 객체에 `set` 을 이용하여 속성을 추가한 뒤 `updateUserProperties` 를 호출하면 사용자 속성을 간단하게 추가할 수 있습니다.

{% hint style="warning" %}
`setUserProperty`, `setUserProperties` 함수는 JavaScript SDK 12.0.0 부터 deprecated 되었습니다. `updateUserProperties` 를 사용해 주세요.
{% endhint %}

{% tabs %}
{% tab title="JavaScript" %}

```javascript
import { PropertyOperationsBuilder } from "@hackler/javascript-sdk"

// 속성 설정
const operations = new PropertyOperationsBuilder()
    .set("gender", "female")
    .build();

await hackleClient.updateUserProperties(operations);
```

{% endtab %}

{% tab title="HTML" %}

```html
<script>
  // 속성 설정
  const operations = new Hackle.PropertyOperationsBuilder()
    .set("gender", "female")
    .build();

  hackleClient.updateUserProperties(operations);
</script>
```

{% endtab %}
{% endtabs %}

### 사용자 속성 설정

사용자 속성을 추가, 제거 할 수 있습니다.

<table><thead><tr><th width="150">지원하는 함수</th><th>설명</th></tr></thead><tbody><tr><td><code>set</code></td><td>사용자 속성을 설정합니다. 속성 키에 이미 설정한 속성값이 있는 경우 덮어씁니다</td></tr><tr><td><code>setOnce</code></td><td><p>사용자 속성 값을 한번만 설정합니다. 속성키에 대한 속성이 이미 있는 경우 무시됩니다.</p><p>예를 들어 사용자에 대한 가입일, 초기 가입 위치 등을 설정할 수 있습니다.</p></td></tr><tr><td><code>unset</code></td><td>사용자 속성을 제거합니다.</td></tr><tr><td><code>clearAll</code></td><td>사용자의 모든 속성을 제거합니다.</td></tr></tbody></table>

설정하고 싶은 사용자 속성으로 `PropertyOperationsBuilder` 객체를 인스턴스화 합니다. 다음 `updateUserProperties` 를 호출하여 사용자 속성을 업데이트 합니다. 한 번에 여러개의 속성을 설정할 수도 있습니다.

{% tabs %}
{% tab title="JavaScript" %}

```javascript
import { PropertyOperationsBuilder } from "@hackler/javascript-sdk"

const operations = new PropertyOperationsBuilder()
    .set("age", 42)
    .set("grade", "GOLD")
    .setOnce("sign_up_date", "2020-07-03")
    .build();

await hackleClient.updateUserProperties(operations);
```

{% endtab %}

{% tab title="HTML" %}

```html
<script>
  const operations = new Hackle.PropertyOperationsBuilder()
    .set("age", 42)
    .set("grade", "GOLD")
    .setOnce("sign_up_date", "2020-07-03")
    .build();

  hackleClient.updateUserProperties(operations);
</script>
```

{% endtab %}
{% endtabs %}

## 사용자 초기화

기존에 설정한 정보를 초기화해야 합니다. 초기화를 하는 경우 기존에 설정했던 식별자, 속성이 모두 초기화됩니다.

{% hint style="danger" %}
사용자 초기화를 하는 경우 서버에 저장된 사용자 속성까지 모두 초기화가 됩니다. 로그아웃 처리를 원하는 경우 `hackleClient.setUserId(undefined);`을 사용해주세요.

userId에 undefined을 대입하면 클라이언트 상에서 로그아웃 처리가 됩니다.
{% endhint %}

```javascript
await hackleClient.resetUser();
```

`resetUser()` 를 호출하는 경우 기존에 설정했던 식별자, 속성이 모두 초기화됩니다.


# CRM 속성

CRM 속성은 핵클 서버에만 안전하게 저장되며 SDK를 통해 값을 직접 조회할 수는 없습니다.

{% hint style="danger" %}
CRM 속성은 별도로 관리되며, `resetUser()`를 호출하거나 `updateUserProperties`에서 `clearAll`을 호출해도 삭제되거나 초기화되지 않습니다.

사용자가 회원 탈퇴 등을 한 경우, 반드시 별도로 제공되는 함수를 호출하여 정보를 삭제해야 합니다.
{% endhint %}

## 전화번호 수집

{% hint style="info" %}
JavaScript SDK 11.42.0 버전 이상에서 지원하는 기능입니다.
{% endhint %}

{% hint style="success" %}
카카오 / 문자 메시지 권장 사항

이 기능을 이용하여 사용자 식별자와 전화번호를 매핑하면 핵클을 통한 카카오 / 문자 메시지를 더욱 원활히 이용할 수 있습니다.
{% endhint %}

#### setPhoneNumber

사용자의 전화번호를 등록합니다.

전화번호는 올바른 E.164 포맷일 경우에만 저장됩니다. 국가코드를 지정하지 않았다면 대한민국(+82)이 기본값으로 사용됩니다.

이미 저장된 전화번호가 있는 사용자의 경우, 이 함수를 호출하면 기존 전화번호가 새로운 값으로 교체됩니다.

{% tabs %}
{% tab title="JavaScript" %}

```javascript
const myPhone = '+821012341234';
hackleClient.setPhoneNumber(myPhone);
```

{% endtab %}

{% tab title="HTML" %}

```html
<script>
  const myPhone = '+821012341234';
  hackleClient.setPhoneNumber(myPhone);
</script>
```

{% endtab %}
{% endtabs %}

#### unsetPhoneNumber

사용자에게 등록되어 있는 전화번호를 삭제합니다.

{% tabs %}
{% tab title="JavaScript" %}

```javascript
hackleClient.unsetPhoneNumber();
```

{% endtab %}

{% tab title="HTML" %}

```html
<script>
  hackleClient.unsetPhoneNumber();
</script>
```

{% endtab %}
{% endtabs %}

## CRM 마케팅 메시지 수신 동의

{% hint style="info" %}
JavaScript SDK 11.45.0 버전 이상에서 지원하는 기능입니다.

수신 동의 상태에 대한 자세한 내용은 [CRM 메시지 수신 동의 관리](/development-guide/sdk/user-identifier/crm-subscription) 문서를 참고해주세요.
{% endhint %}

### 수신 동의 속성

메시지 목적 별로 수신 동의/거부를 할 수 있습니다.

`HackleSubscriptionOperationsBuilder`를 사용해 원하는 속성의 동의 상태를 설정한 후, `updatePushSubscriptions()` 같은 메서드로 최종 업데이트를 진행합니다.

메시지 채널 별로 동의 상태를 업데이트할 수 있습니다.

{% tabs %}
{% tab title="JavaScript" %}

```javascript
import { HackleSubscriptionOperationsBuilder, HackleSubscriptionStatus } from "@hackler/javascript-sdk"

const operations = new HackleSubscriptionOperationsBuilder()
  .marketing(HackleSubscriptionStatus.SUBSCRIBED)
  .information(HackleSubscriptionStatus.SUBSCRIBED)
  .build()

hackleClient.updatePushSubscriptions(operations);
```

{% endtab %}

{% tab title="HTML" %}

```html
<script>
  const operations = new Hackle.HackleSubscriptionOperationsBuilder()
    .marketing(HackleSubscriptionStatus.SUBSCRIBED)
    .information(HackleSubscriptionStatus.SUBSCRIBED)
    .build();

  hackleClient.updatePushSubscriptions(operations);
</script>
```

{% endtab %}
{% endtabs %}

#### 광고성 메시지

광고성 메시지 수신 동의 속성을 설정합니다.

{% tabs %}
{% tab title="JavaScript" %}

```javascript
import { HackleSubscriptionOperationsBuilder, HackleSubscriptionStatus } from "@hackler/javascript-sdk"

const operations = new HackleSubscriptionOperationsBuilder()
  .marketing(HackleSubscriptionStatus.SUBSCRIBED)
  .build()

hackleClient.updatePushSubscriptions(operations);
```

{% endtab %}

{% tab title="HTML" %}

```html
<script>
  const operations = new Hackle.HackleSubscriptionOperationsBuilder()
    .marketing(HackleSubscriptionStatus.SUBSCRIBED)
    .build();

  hackleClient.updatePushSubscriptions(operations);
</script>
```

{% endtab %}
{% endtabs %}

#### 정보성 메시지

정보성 메시지 수신 동의 속성을 설정합니다.

{% tabs %}
{% tab title="JavaScript" %}

```javascript
import { HackleSubscriptionOperationsBuilder, HackleSubscriptionStatus } from "@hackler/javascript-sdk"

const operations = new HackleSubscriptionOperationsBuilder()
  .information(HackleSubscriptionStatus.SUBSCRIBED)
  .build()

hackleClient.updatePushSubscriptions(operations);
```

{% endtab %}

{% tab title="HTML" %}

```html
<script>
  const operations = new Hackle.HackleSubscriptionOperationsBuilder()
    .information(HackleSubscriptionStatus.SUBSCRIBED)
    .build();

  hackleClient.updatePushSubscriptions(operations);
</script>
```

{% endtab %}
{% endtabs %}

#### HackleSubscriptionStatus

<table><thead><tr><th width="224.09765625">HackleSubscriptionStatus</th><th>설명</th></tr></thead><tbody><tr><td><code>UNKNOWN</code></td><td>수신 동의/거부를 하지 않음 (<code>default</code>)</td></tr><tr><td><code>SUBSCRIPTION</code></td><td>명시적으로 수신 동의</td></tr><tr><td><code>UNSUBSCRIPTION</code></td><td>명시적으로 수신 거부</td></tr></tbody></table>

### 푸시 수신 동의 상태 업데이트

사용자의 푸시 메시지 수신 동의 상태를 업데이트 합니다.

{% tabs %}
{% tab title="JavaScript" %}

```javascript
import { HackleSubscriptionOperationsBuilder, HackleSubscriptionStatus } from "@hackler/javascript-sdk"

const operations = new HackleSubscriptionOperationsBuilder()
  .marketing(HackleSubscriptionStatus.SUBSCRIBED)
  .information(HackleSubscriptionStatus.SUBSCRIBED)
  .build()

hackleClient.updatePushSubscriptions(operations);
```

{% endtab %}

{% tab title="HTML" %}

```html
<script>
  const operations = new Hackle.HackleSubscriptionOperationsBuilder()
    .marketing(HackleSubscriptionStatus.SUBSCRIBED)
    .information(HackleSubscriptionStatus.SUBSCRIBED)
    .build();

  hackleClient.updatePushSubscriptions(operations);
</script>
```

{% endtab %}
{% endtabs %}

### 카카오 메시지 수신 동의 상태 업데이트

사용자의 카카오 메시지 수신 동의 상태를 업데이트 합니다.

{% tabs %}
{% tab title="JavaScript" %}

```javascript
import { HackleSubscriptionOperationsBuilder, HackleSubscriptionStatus } from "@hackler/javascript-sdk"

const operations = new HackleSubscriptionOperationsBuilder()
  .marketing(HackleSubscriptionStatus.SUBSCRIBED)
  .information(HackleSubscriptionStatus.SUBSCRIBED)
  .build()

hackleClient.updateKakaoSubscriptions(operations);
```

{% endtab %}

{% tab title="HTML" %}

```html
<script>
  const operations = new Hackle.HackleSubscriptionOperationsBuilder()
    .marketing(HackleSubscriptionStatus.SUBSCRIBED)
    .information(HackleSubscriptionStatus.SUBSCRIBED)
    .build();

  hackleClient.updateKakaoSubscriptions(operations);
</script>
```

{% endtab %}
{% endtabs %}

### 문자 메시지 수신 동의 상태 업데이트

사용자의 문자 메시지 수신 동의 상태를 업데이트 합니다.

{% tabs %}
{% tab title="JavaScript" %}

```javascript
import { HackleSubscriptionOperationsBuilder, HackleSubscriptionStatus } from "@hackler/javascript-sdk"

const operations = new HackleSubscriptionOperationsBuilder()
  .marketing(HackleSubscriptionStatus.SUBSCRIBED)
  .information(HackleSubscriptionStatus.SUBSCRIBED)
  .build()

hackleClient.updateSmsSubscriptions(operations);
```

{% endtab %}

{% tab title="HTML" %}

```html
<script>
  const operations = new Hackle.HackleSubscriptionOperationsBuilder()
    .marketing(HackleSubscriptionStatus.SUBSCRIBED)
    .information(HackleSubscriptionStatus.SUBSCRIBED)
    .build();

  hackleClient.updateSmsSubscriptions(operations);
</script>
```

{% endtab %}
{% endtabs %}


# 테스트 그룹 분배

A/B 테스트를 진행할 때, 테스트 그룹을 대상으로 사용자를 분배하고 각 테스트 그룹에 해당하는 로직을 작성해야 합니다. 이 때 사용자 분배를 핵클 SDK를 통해 진행할 수 있습니다.

## variation

`variation()` 메소드에 **실험 키** 를 전달하면 사용자를 분배하고 결과를 전달받을 수 있습니다.

<table><thead><tr><th width="150">파라미터</th><th width="120">타입</th><th width="110">필수</th><th>제약사항</th></tr></thead><tbody><tr><td>실험 키 (key)</td><td><code>int</code></td><td>필수</td><td>-</td></tr></tbody></table>

### 예제

아래 예제 코드에서는 실험 키 42를 전달하고 있으며, 테스트 그룹은 A와 B 두 개가 존재합니다.

```javascript
// 실험 키가 42(Number)인 A/B 테스트에서 사용자에게 노출할 테스트 그룹을 결정합니다.
// 결정하지 못하는 상황인 경우 테스트 그룹 A를 반환합니다.
const variation = hackleClient.variation(42);

// 할당받은 그룹에 대한 로직
if (variation === "A") {
  // 그룹 A 로직
} else if (variation === "B") {
  // 그룹 B 로직
}
```

## variationDetail

`variationDetail()` 메소드는 `variation()` 메소드와 동일하게 동작하고 분배된 사유를 같이 제공합니다. 이 메소드는 분배가 잘 되고 있는지 살펴볼 때 유용하게 사용할 수 있습니다.

<table><thead><tr><th width="150">파라미터</th><th width="120">타입</th><th width="110">필수</th><th>제약사항</th></tr></thead><tbody><tr><td>실험 키 (key)</td><td><code>int</code></td><td>필수</td><td>-</td></tr></tbody></table>

### 예제

파라미터로 실험 키를 전달해야 합니다. 아래 예제 코드의 경우 실험 키 42를 전달하고 있습니다.

```javascript
// 분배 결정 상세
const decision = hackleClient.variationDetail(42);
// 분배 그룹
const variation = decision.variation;
// 분배 결정 사유
const reason = decision.reason;
```

### 분배 사유

분배 결정 사유는 **`SDK_NOT_READY`** 와 같은 형태로 받게 됩니다. 자세한 내용은 아래 표를 참고해주세요.

<table><thead><tr><th width="319.12109375">분배 사유</th><th width="305.35546875">설명</th><th>분배 결과</th></tr></thead><tbody><tr><td><code>SDK_NOT_READY</code></td><td><p>SDK 사용 준비가 되지 않았습니다.</p><p>(예: 잘못된 SDK 키로 초기화 시도)</p></td><td>A (기본 그룹)</td></tr><tr><td><code>EXPERIMENT_NOT_FOUND</code></td><td>전달한 실험 키에 대한 A/B 테스트를 찾을 수 없습니다. 실험 키가 잘못되었거나 해당 실험이 보관 상태일 수 있습니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>NOT_IN_MUTUAL_EXCLUSION_EXPERIMENT</code></td><td>실험이 상호 배타적 설정에 포함되어 있지만<br>해당 상호 배타적 그룹에 할당되지 않은 경우</td><td>A (기본 그룹)</td></tr><tr><td><code>EXPERIMENT_DRAFT</code></td><td>A/B 테스트가 준비 상태입니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>EXPERIMENT_PAUSED</code></td><td>A/B 테스트가 일시 정지 상태입니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>EXPERIMENT_COMPLETED</code></td><td>A/B 테스트가 종료되었습니다.</td><td>종료 시 선택한승리 그룹</td></tr><tr><td><code>OVERRIDDEN</code></td><td>사용자가 수동할당에 의해<br>특정 그룹으로 결정되었습니다.</td><td>수동 할당한<br>그룹</td></tr><tr><td><code>NOT_IN_EXPERIMENT_TARGET</code></td><td>사용자가 A/B 테스트 타겟이 아닙니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>TRAFFIC_NOT_ALLOCATED</code></td><td>A/B 테스트가 실행 중이지만<br>사용자가 테스트에 할당되지 않았습니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>TRAFFIC_ALLOCATED</code></td><td>사용자가 A/B 테스트에 할당되었습니다.</td><td>할당된 그룹</td></tr><tr><td><code>VARIATION_DROPPED</code></td><td>원래 할당된 그룹이 테스트에서 제외되었습니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>INVALID_INPUT</code></td><td>입력값이 유효하지 않습니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>EXCEPTION</code></td><td>알 수 없는 오류가 발생했습니다.</td><td>A (기본 그룹)</td></tr></tbody></table>

### 파라미터

{% hint style="info" %}
Javascript SDK 11.7.3 이상 버전에서 지원하는 기능입니다.
{% endhint %}

* `variationDetail()` 메소드를 통해 분배된 그룹의 파라미터 값도 같이 제공받을 수 있습니다.
* config 객체와 `get()` 메소드를 통해 A/B 테스트 화면에서 설정한 파라미터 설정 값을 받아 활용할 수 있으며 A/B 테스트의 파라미터 설정 화면에서 값을 변경할 경우, 변경된 값이 코드에 적용됩니다.
* `get()` 메소드의 parameterKey는 A/B 테스트의 파라미터 설정에서 설정한 키 정보이며 defaultValue는 분배 결정 실패 시, 또는 잘못된 파라미터 유형의 값을 넣었을 때 Return되는 값입니다.
* 설정한 정보를 제대로 받기 위해서는 defaultValue에 설정하신 파라미터 유형에 맞는 type의 값을 입력해야 합나다.

<table><thead><tr><th width="150">구분</th><th width="120">value type</th><th>설명</th></tr></thead><tbody><tr><td><code>get</code></td><td><code>string</code>, <code>number</code>, <code>boolean</code></td><td>설정된 parameter값을 반환합니다.<br>JSON 타입은 문자열(String)형태로 받을 수 있습니다.<br>JSON 타입의 default 값은 문자열 타입으로 입력해야 합니다.</td></tr></tbody></table>

#### 예제

```javascript
// 분배 결정 상세
const decision = hackleClient.variationDetail(42);

//분배 결정 상세에서 get() 메소드를 통해 parameter 값 가져오기
const parameterValue = decision.get("parameterKey", "defaultValue")

// string 유형의 parameter값 예제
const strValue = decision.get("parmeterKey", "defaultValue")
```


# 기능 플래그 결정

기능 플래그는 켜짐(on) 상태와 꺼짐(off) 상태가 있습니다. 각 상태에 따라 다른 기능을 설정하게 됩니다. 기능 플래그를 적용한 기능에 어떤 사용자가 접근할 경우 켜짐 혹은 꺼짐 상태를 받을 수 있어야 합니다. 이 상태 결정을 핵클 SDK를 통해 진행할 수 있습니다.

## isFeatureOn

`isFeatureOn()` 메소드에 **기능 키**를 전달하면 사용자에 대한 상태 결과를 전달받을 수 있습니다. 이후 상태에 따른 로직을 구현합니다.

아래 예제 코드에서는 기능 키 42를 전달하고 있습니다.

```javascript
// 기능 키가 42인 기능 플래그에서 사용자의 상태를 결정합니다.
// 결정하지 못하는 상황인 경우 false(꺼짐 상태)를 반환합니다.
const isOn = hackleClient.isFeatureOn(42);

// 상태 별 로직
if (isOn) {
  // ON 기능
} else {
  // OFF 기능
}
```

## featureDetail

`featureDetail()` 메소드는 `isFeatureOn()` 메소드와 동일하게 동작하고 추가로 상태 결정에 대한 사유를 같이 제공합니다. 수동할당이 잘 되고 있는지 알아보거나 설정한 트래픽 할당 대비 결과 비중이 이상하다고 여길 때 유용하게 활용할 수 있습니다.

파라미터로 기능 키를 전달해야 합니다. 아래 예제 코드의 경우 기능 키 42를 전달하고 있습니다.

```javascript
// 상태 결정 상세
const decision = hackleClient.featureFlagDetail(42);
// 기능 on/off 여부
const featureOn = decision.isOn;
// 상태 결정 사유
const reason = decision.reason;
```

상태 결정 사유는 **`SDK_NOT_READY`** 와 같은 형태로 받게 됩니다. 자세한 내용은 아래 표를 참고해주세요.

<table><thead><tr><th width="233.71875">결정 사유</th><th width="370.8046875">설명</th><th>분배 결과</th></tr></thead><tbody><tr><td><code>SDK_NOT_READY</code></td><td>SDK 사용 준비가 되지 않았습니다.<br>(예: 잘못된 SDK 키로 초기화 시도)</td><td>기본 상태<br>(꺼짐/off)</td></tr><tr><td><code>FEATURE_FLAG_NOT_FOUND</code></td><td>전달한 기능 키에 대한 기능 플래그를 찾을 수 없습니다.<br>기능 키가 잘못되었거나 해당 기능 플래그가 보관 상태일 수 있습니다.</td><td>기본 상태<br>(꺼짐/off)</td></tr><tr><td><code>FEATURE_FLAG_INACTIVE</code></td><td>기능 플래그가 꺼짐 상태입니다</td><td>기본 상태<br>(꺼짐/off)</td></tr><tr><td><code>INDIVIDUAL_TARGET_MATCH</code></td><td>개별 타겟팅에 매치 되었습니다.</td><td>개별 타겟팅으로<br>설정한 상태</td></tr><tr><td><code>TARGET_RULE_MATCH</code></td><td>사용자 타겟팅에 매치 되었습니다.</td><td>사용자 타겟팅으로 설정한 상태</td></tr><tr><td><code>DEFAULT_RULE</code></td><td>개별 타겟팅, 사용자 타겟팅 중 어디에도 매치 되지 않았습니다.</td><td>기본 룰로<br>설정한 상태</td></tr><tr><td><code>EXCEPTION</code></td><td>알 수 없는 오류가 발생했습니다.</td><td>기본 상태<br>(꺼짐/off)</td></tr></tbody></table>

## 기능 플래그 파라미터

{% hint style="info" %}
Javascript SDK 11.7.3 이상 버전에서 지원하는 기능입니다.
{% endhint %}

* `featureFlagDetail()` 메소드를 통해 상태 결정에 대한 파라미터 값도 같이 제공받을 수 있습니다.
* config 객체와 `get()` 메소드를 통해 기능 플래그 화면에서 설정한 파라미터 설정 값을 받아 활용할 수 있으며 기능 플래그의 파라미터 설정 화면에서 값을 변경할 경우, 변경된 값이 코드에 적용됩니다.

```javascript
const decision = hackleClient.featureFlagDetail(42);

// 상태 결정 상세에서 get() 메소드를 통해 parameter 값 가져오기
const parameterValue = decision.get(parameterKey, defaultValue)

// string 유형의 parameter값 예제
const strValue = decision.get("parmeterKey", "defaultValue")
```

* `get()` 메소드의 parameterKey는 기능 플래그의 파라미터 설정에서 설정한 키 정보이며 defaultValue는 상태 결정 실패 시, 또는 잘못된 파라미터 유형의 값을 넣었을 때 Return되는 값입니다.
* 설정한 정보를 제대로 받기 위해서는 defaultValue에 설정하신 파라미터 유형에 맞는 type의 값을 입력해야 합나다.
* JSON 타입은 문자열(String)형태로 받을 수 있으므로, JSON 타입의 경우 defaultValue를 문자열 타입으로 입력해야 합니다.
* SDK에서 제공되는 파라미터 유형은 string, number, boolean 이며 기능 플래그 화면에서 설정한 JSON 타입은 문자열(String)형태로 받을 수 있습니다. JSON 타입의 default 값은 문자열 타입으로 입력해야 합니다.


# 원격 구성 적용

{% hint style="info" %}
Javascript SDK 11.7.3 이상 버전에서 지원하는 기능입니다.
{% endhint %}

원격 구성은 애플리케이션에서 관리되고 있는 값, 또는 속성들을 핵클 대시보드에서 정의한 파라미터 값들로 대체하여 실시간으로 애플리케이션의 동작 및 설정 값들을 제어할 수 있는 기능입니다.

핵클의 대시보드의 원격 구성 화면으로 이동하여 파라미터 정보들을 설정하고, 사용자 식별 규칙에 따른 값들을 설정할 수 있습니다.

## remoteConfig

`remoteConfig()` 메소드를 호출하면 사용자에 대한 원격 구성 정보(설정한 파라미터 및 규칙 정보)를 담고 있는 `HackleRemoteConfig` 인스턴스를 얻을 수 있습니다. `remoteConfig()` 는 사용자 속성을 원격 구성의 규칙 정보와 매칭 시키기 위해 사용자 식별자 정보를 전달 받을 수 있습니다. `HackleRemoteConfig` 에서 제공하는 메소드들을 통해 원하는 파라미터에 접근하여 값을 제공받을 수 있습니다.

```javascript
hackleClient.onReady(function() {

	// 원격 구성 정보를 담은 인스턴스를 반환합니다.
  const remoteConfig = hackleClient.remoteConfig()
});
```

## 원격 구성 파라미터 조회

* `remoteConfig()` 메소드를 사용하여 반환받은 `HackleRemoteConfig`에는 파라미터 값 조회를 위한 `get()`메소드를 제공합니다.
* 핵클의 원격 구성 화면에서 설정한 파라미터 값이 key, value 형태로 존재하기 때문에, 설정한 파라미터 유형에 따라 아래 메소드를 사용하여 설정한 파라미터 값을 반환받을 수 있습니다.

{% hint style="warning" %}
보관 후 원격 구성과 관련된 코드를 제거하세요.

원격 구성 파라미터를 보관한 경우 더이상 파라미터 정보에 접근 할 수 없습니다. 따라서 원격 구성 파라미터 보관 후에는 반드시 관련된 코드를 정리해주시기 바랍니다.
{% endhint %}

```javascript
hackleClient.onReady(function() {

  // 원격 구성 정보를 담은 인스턴스를 반환합니다.
  const remoteConfig = hackleClient.remoteConfig()

  //remoteConfig 에서 get() 메소드를 통해 parameter 값 가져오기
  const parameterValue = remoteConfig.get(parameterKey, defaultValue)

  // string 유형의 parameter값 예제
  const strValue = remoteConfig.get("parmeterKey", "defaultValue")
});
```

* get() 메소드의 parameterKey는 원격 구성의 파라미터 설정에서 설정한 키 정보입니다.
* defaultValue는 원격 구성 값을 결정할 수 없을 때 반환되는 값입니다. 입력한 defaultValue는 다음과 같은 상황에서 반환될 수 있습니다. A. 원격 구성화면에서 설정한 파라미터 유형과 다른 유형의 defaultValue 값을 입력 B. 설정되지 않은 parameter key 호출 C. Hackle SDK 초기화 실패 D. 잘못된 식별자 정보가 입력되거나 존재하지 않을 때 E. ETC
* 설정한 정보를 제대로 받기 위해서는 defaultValue에 설정하신 파라미터 유형에 맞는 type의 값을 입력해야 합나다.
* SDK에서 제공되는 원격 구성 파라미터 유형은 string, number, boolean 이며 원격 구성 파라미터 화면에서 설정한 JSON 타입은 문자열(String)형태로 받을 수 있습니다. JSON 타입의 default 값은 문자열 타입으로 입력해야 합니다.


# 이벤트 전송

핵클 SDK는 사용자 이벤트를 핵클로 전송하는 기능을 제공합니다. 사용자 행동의 변화가 일어나는 지점마다 이 기능을 활용하면 사용자 행동에 대한 유의미한 데이터를 얻을 수 있으며, 그렇게 모인 데이터를 통해 사용자 행동 분석을 할 수 있습니다.

## track

`track()` 메소드에 **이벤트 키**를 전달하여 사용자 이벤트를 전송할 수 있습니다.

<table><thead><tr><th width="150">파라미터</th><th width="120">타입</th><th width="110">필수</th><th>제약사항</th></tr></thead><tbody><tr><td>이벤트 명(key)</td><td><code>string</code></td><td>필수</td><td>글자수 제한은 128자입니다. (128 characters)</td></tr></tbody></table>

#### 예시

사용자가 구매하기 버튼을 눌렀을 때 이벤트를 수집하기 위해 `purchase` 라는 이벤트 키를 정의했다고 가정합니다.

```javascript
/* 예시 1: 이벤트 키만 전송 */
hackleClient.track({key: "purchase"});
```

### 속성(Property)

핵클 SDK는 이벤트(Event) 객체에 속성을 추가할 수 있도록 지원합니다.

* 속성은 속성명(key)과 속성값(value)을 한 쌍으로 보내야 합니다.
* 이벤트 객체에 추가 가능한 속성 개수는 최대 64개입니다.

<table><thead><tr><th width="133.04296875">구분</th><th width="127.60546875">타입</th><th>제약사항</th></tr></thead><tbody><tr><td>속성 명(key)</td><td><code>string</code></td><td><ul><li>글자수 제한은 128자입니다. (128 characters)</li><li>대소문자를 구분하지 않습니다.</li><li>예를 들어 AGE와 age는 동일한 속성명으로 인식합니다.</li></ul></td></tr><tr><td>속성 값(value)</td><td><code>boolean</code>, <code>string</code>, <code>number</code>, <code>array</code></td><td><ul><li>string 타입인 경우 글자수 제한은 1024자입니다.<br>(1024 characters)</li><li>string 타입은 대소문자를 구분합니다.</li><li>예를 들어 APPLE과 apple은 서로 다른 속성값으로 인식합니다.</li><li>number 타입인 경우 정수 최대 15자리, 소수점 최대 6자리를<br>지원합니다.</li></ul></td></tr></tbody></table>

#### 예시

아래 예시에서는 세 가지 속성(`pay_method`, `discount_amount`, `is_discount`)을 추가한 것을 확인할 수 있습니다.

```javascript
const event = {
  key: "purchase",
  properties: {
    pay_method: "CARD",
    discount_amount: 800,
    is_discount: true
  }
}
hackleClient.track(event);
```


# 사용자 화면 추적

Hackle SDK는 기본적으로 브라우저의 URL 변경을 감지하여 자동으로 페이지뷰 정보를 수집합니다. 하지만 Single Page Application(SPA)이나 동적 페이지 구성 환경에서는 자동 수집이 어려울 수 있습니다.

이러한 경우 `setCurrentPage` 메소드를 직접 호출하여 페이지뷰를 수동으로 추적할 수 있습니다.

## setCurrentPage

{% hint style="info" %}
JavaScript SDK 11.53.0 이상 버전에서 지원하는 기능입니다.
{% endhint %}

`setCurrentPage` 메소드를 호출하여 현재 페이지 정보를 수동으로 설정할 수 있습니다.

* 화면 추적의 최소 단위 시간은 1초 입니다.
* 1초 이내에 변경된 페이지의 `$engagement` 는 측정되지 않습니다.

<table><thead><tr><th width="150">파라미터</th><th width="120">타입</th><th width="110">필수</th><th>설명</th></tr></thead><tbody><tr><td><code>pageName</code></td><td><code>string</code></td><td>필수</td><td>페이지명</td></tr><tr><td><code>properties</code></td><td><code>object</code></td><td>선택</td><td>페이지에 추가할 커스텀 속성</td></tr></tbody></table>

{% hint style="warning" %}
`setCurrentPage` 는 호출할 때마다 페이지 정보를 수집합니다.

동일한 pageName이 들어와도 `$page_view`와 `$engagement` 를 수집합니다.
{% endhint %}

#### 예시

이벤트 리스너를 활용해 이벤트가 발생할 때 화면 추적을 하는 것을 추천합니다.

{% tabs %}
{% tab title="JavaScript" %}

```javascript
// 1. 페이지 로드 + 뒤로가기/앞으로가기 (bfcache 복원)
window.addEventListener('pageshow', (event) => {
  hackleClient.setCurrentPage({pageName: "page"})
)

// 2. 탭 복귀 / 최소화 복귀
document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'visible') {
        hackleClient.setCurrentPage({pageName: "page"})
    }
})
```

{% endtab %}

{% tab title="Vue.js" %}

```javascript
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    // ... routes definitions
  ]
})

// 네비게이션이 완료된 후 호출되는 전역 가드
router.afterEach((to) => {
  hackleClient.setCurrentPage({
    pageName: "pageName"
  })
})

export default router
```

{% endtab %}
{% endtabs %}

### 속성 (Property)

핵클 SDK는 이벤트(Event) 객체에 속성을 추가할 수 있도록 지원합니다.

* 이벤트 객체에 추가 가능한 속성 개수는 최대 64개입니다.

<table><thead><tr><th width="150">구분</th><th width="120">타입</th><th>제약사항</th></tr></thead><tbody><tr><td>속성 명(key)</td><td><code>string</code></td><td>글자수 제한은 128자입니다. (128 characters)<br>대소문자를 구분하지 않습니다. 예를 들어 amount와 AMOUNT는 같은 키로 인식합니다.</td></tr><tr><td>속성 값(value)</td><td><code>boolean</code>, <code>string</code>, <code>number</code>, <code>array</code></td><td>string 타입인 경우 글자수 제한은 1024자입니다. (1024 characters)<br>number 타입인 경우 정수 최대 15자리, 소수점 최대 6자리를 지원합니다.</td></tr></tbody></table>

#### 예시

```javascript
hackleClient.setCurrentPage({
  pageName: "Product Detail",
  properties: {
    productId: "12345",
    category: "Electronics",
    price: 99000,
    inStock: true
  }
});
```

## automaticRouteTracking 비활성화

SDK에서는 기본적으로 `automaticRouteTracking`이 활성화되어 있습니다. 페이지 이동을 감지하여 자동 화면 정보 수집을 원하지 않는 경우 `automaticRouteTracking`를 비활성화 해야 합니다.

SDK를 초기화 할 때 `automaticRouteTracking`을 설정할 수 있습니다.

{% hint style="danger" %}
`automaticRouteTracking`이 활성화 된 상태에서 `setCurrentPage`를 통해 수동으로 화면 수집을 하는 경우, `$page_view`와 `$engagement`가 과수집 되거나 의도하지 않은 속성으로 수집될 수 있습니다.

**수동으로 화면 정보를 수집하는 경우 `automaticRouteTracking` 비활성화를 추천합니다.**
{% endhint %}

#### 예시

```javascript
const hackleClient = HackleClient.create("YOUR_SDK_KEY", {
  automaticRouteTracking: false
});
```


# 사용자 탐색

{% hint style="info" %}
**디버깅 용도로만 사용하는 것을 권장합니다.**
{% endhint %}

{% hint style="warning" %}
원격 평가 방식에서는 사용자 탐색 기능을 사용할 수 없습니다.
{% endhint %}

사용자 식별자를 확인하고 A/B 테스트, 기능플래그에 강제할당 하는 방법을 설명합니다.

아래의 의존성을 추가합니다.

{% tabs %}
{% tab title="npm" %}

```shell
// javascript-sdk@12.0.0 이상일 경우
npm install @hackler/javascript-devtools@2.0.0

// javascript-sdk@11.21.0 이상 12.0.0 미만일 경우
npm install @hackler/javascript-devtools@1.0.2

// javascript-sdk@11.21.0 미만일 경우
npm install @hackler/javascript-devtools@1.0.1
```

{% endtab %}

{% tab title="yarn" %}

```shell
// javascript-sdk@12.0.0 이상일 경우
yarn add @hackler/javascript-devtools@2.0.0

// javascript-sdk@11.21.0 이상 12.0.0 미만일 경우
yarn add @hackler/javascript-devtools@1.0.2

// javascript-sdk@11.21.0 미만일 경우
yarn add @hackler/javascript-devtools@1.0.1
```

{% endtab %}
{% endtabs %}

<table><thead><tr><th width="278.48046875">javascript-sdk 버전</th><th>javascript-devtools 버전</th></tr></thead><tbody><tr><td><code>11.13.0 &#x3C;= version &#x3C; 11.21.0</code></td><td>1.0.1</td></tr><tr><td><code>11.21.0 &#x3C;= version &#x3C; 12.0.0</code></td><td>1.0.2</td></tr><tr><td><code>12.0.0 &#x3C;= version</code></td><td>2.0.0</td></tr></tbody></table>

## 사용자 탐색 설정

<table><thead><tr><th width="175.1875">키</th><th width="389.8984375">기능</th><th width="102.5">기본값</th><th width="120">지원</th></tr></thead><tbody><tr><td>devTool</td><td>사용자 탐색을 사용할 수 있도록 합니다.</td><td>undefined</td><td>11.13.0+</td></tr><tr><td>autoOpenDevTool</td><td>사용자 탐색 버튼이 자동으로 나타나도록 하는 옵션입니다.</td><td>false</td><td>11.13.0+</td></tr></tbody></table>

* `devTool` 에 `"@hackler/javascript-devtools"` 에서 import한 `HackleDevTools`를 넣어주세요.
* `autoOpenDevTool`을 `true`로 설정할 경우 `showUserExplorer`을 호출하지 않아도 자동으로 사용자 탐색 창이 나타납니다.

기존 Hackle Client를 초기화하는 코드에 아래와 같이 옵션을 추가합니다.

{% tabs %}
{% tab title="JavaScript" %}

```javascript
import * as Hackle from "@hackler/javascript-sdk";
import HackleDevTools from "@hackler/javascript-devtools"

const config = {
  devTool: HackleDevTools,
  autoOpenDevTool: false,
};

// YOUR_BROWSER_SDK_KEY로 초기화
const hackleClient = Hackle.createInstance(YOUR_BROWSER_SDK_KEY, config);

// 사용자 탐색을 나타내고 싶은 시점에 Trigger 되도록 해주세요.
hackleClient.showUserExplorer()

// 사용자 탐색 창을 닫고 싶을 경우에 Trigger 되도록 해주세요.
hackleClient.hideUserExplorer()
```

{% endtab %}

{% tab title="HTML" %}

```html
<script src="https://cdn2.hackle.io/npm/@hackler/javascript-devtools@[호환되는 버전]/lib/index.browser.umd.min.js"></script>
<script>
  const config = {
    devTool: HackleDevTools.default,
  };

  // YOUR_BROWSER_SDK_KEY로 초기화
  const hackleClient = Hackle.createInstance(YOUR_BROWSER_SDK_KEY, config);

  // 사용자 탐색을 나타내고 싶은 시점에 Trigger 되도록 해주세요.
  hackleClient.showUserExplorer()

  // 사용자 탐색 창을 닫고 싶을 경우에 Trigger 되도록 해주세요.
  hackleClient.hideUserExplorer()

</script>
```

{% endtab %}
{% endtabs %}

{% hint style="danger" %}
Production에서는 `autoOpenDevTool` 옵션을 `false` 로 설정하세요.

autoOpenDevTool을 true로 설정할 경우 자동으로 사용자 탐색 창이 나타납니다.

사용자 탐색 창을 나타내고 싶은 시점에 `hackleClient.showUserExplorer`을 호출하는 식으로 사용하시는 것을 권장합니다.
{% endhint %}

## 사용자 탐색 창

화면 하단에 핵클 로고 버튼이 표시됩니다. 버튼 클릭시 설정 화면으로 진입 할 수 있습니다.

![](/files/DhV2Cp2xi3lomA6y6Slj)

## 사용자 식별자 확인하기

화면 상단에서 사용자 식별자를 확인 및 복사 할 수 있습니다.

![](/files/TrHtboRjUyl5Xn3EKDy2)

## 사용자 강제할당

* 화면하단에서 A/B 테스트, 기능플래그의 분배 결과를 확인할 수 있습니다.
* SelectBox 클릭 시 특정 그룹으로 강제할당 할 수 있습니다.
* `Reset` 버튼 클릭 시 강제할당이 해제됩니다.
* `Reset all` 버튼 클릭시 모든 강제할당이 해제됩니다.
* 브라우저에서 강제할당한 경우 해당 브라우저에서 분배하는 경우에만 적용됩니다. (대시보드 테스트기기에 등록되지 않습니다)
* 강제할당이 적용되지 않는경우 브라우저를 새로고침 해주세요.

![](/files/gC5JcHuH9ZCsbg6onF58)


# 인앱 메시지

{% hint style="info" %}
JavaScript SDK 11.15.0 버전 이상에서 지원하는 기능입니다.
{% endhint %}

## 인앱메시지 개발자 가이드

#### HackleInAppMessage

```typescript
interface HackleInAppMessage {
  key: number
}
```

* 인앱메시지의 키를 반환합니다.

#### HackleInAppMessageView

```typescript
interface HackleInAppMessageView {
  close(): void
  inAppMessage: HackleInAppMessage // 11.48.0+
}
```

* `close()` 를 호출하여 인앱메시지를 리스너 함수 내에서 직접 닫을 수 있습니다.

### Methods

#### getDisplayedInAppMessageView

```javascript
const hackleClient = createInstance("YOUR_SDK_KEY");

const view = hackleClient.getDisplayedInAppMessageView();
if (view !== null) {
  view.close(); // 현재 표시 중인 인앱메시지를 닫습니다.
}
```

* 현재 브라우저에 표시된 인앱메시지를 반환합니다.
* 반환 타입은 `HackleInAppMessageView | null` 입니다.
  * 현재 표시 중인 인앱메시지가 존재하지 않는 경우 `null` 을 반환합니다.


# 인앱메시지 이벤트 리스너

{% hint style="info" %}
JavaScript SDK 11.37.0 이상 버전에서 지원하는 기능입니다.
{% endhint %}

## Interfaces

### HackleInAppMessageListener

```typescript
interface HackleInAppMessageListener {
  beforeInAppMessageOpen?(inAppMessage: HackleInAppMessage): void
  afterInAppMessageOpen?(inAppMessage: HackleInAppMessage): void
  beforeInAppMessageClose?(inAppMessage: HackleInAppMessage): void
  afterInAppMessageClose?(inAppMessage: HackleInAppMessage): void
  onInAppMessageClick?(
    inAppMessage: HackleInAppMessage,
    view: HackleInAppMessageView,
    action: HackleInAppMessageAction
  ): boolean
}
```

#### onInAppMessageClick

`onInAppMessageClick` 메서드가 반환하는 boolean 값을 통해 인앱메시지의 동작을 제어할 수 있습니다.

* `true` 를 반환하는 경우 기존 인앱메시지의 액션을 덮어씁니다.
  * 가령, 기존 인앱메시지의 클릭 시 액션이 `하루 동안 보지 않기` 였다면 해당 액션은 동작하지 않습니다.
* `false`를 반환하는 경우 기존 인앱메시지의 액션이 그대로 동작합니다.

{% hint style="warning" %}
`view.close()` 를 `onInAppMessageClick` 내부에서 호출하는 경우

`view.close()` 를 리스너 내부에서 호출한 경우 `true` 를 반환한 것과 동일하게 처리됩니다.

```typescript
onInAppMessageClick: (message, view, action) => {
  view.close();

  // return false를 하더라도 true한 것과 동일하게 처리됨.
  return false;
},
```

{% endhint %}

### HackleInAppMessage

```typescript
interface HackleInAppMessage {
  key: number
}
```

* 인앱메시지의 키를 반환합니다.

### HackleInAppMessageView

```typescript
interface HackleInAppMessageView {
  close(): void
  inAppMessage: HackleInAppMessage // 11.48.0+
}
```

* `close()` 를 호출하여 인앱메시지를 리스너 함수 내에서 직접 닫을 수 있습니다.

### HackleInAppMessageAction

```typescript
type HackleInAppMessageActionType = "CLOSE" | "LINK"
type HackleInAppMessageActionLinkTarget = "CURRENT" | "NEW_TAB" | "NEW_WINDOW"

interface HackleInAppMessageAction {
  type: HackleInAppMessageActionType
  close?: {
    hideDurationMillis: number | null
  }
  link?: {
    url: string
    target: HackleInAppMessageActionLinkTarget
    shouldCloseAfterLink: boolean
  }
}
```

<table><thead><tr><th width="255.3828125">property</th><th>description</th></tr></thead><tbody><tr><td><code>close?.hideDurationMillis</code></td><td>메시지를 특정 기간동안 숨김 처리하는 액션인 경우, 해당 기간을 밀리초로 반환합니다.</td></tr><tr><td><code>link?.url</code></td><td>링크가 포함된 경우 링크를 반환합니다. e.g) <a href="https://hackle.io">https://hackle.io</a></td></tr><tr><td><code>link?.target</code></td><td>새 탭으로 이동인 경우 <code>NEW_TAB</code> 새 창으로 이동인 경우 <code>NEW_WINDOW</code> 현재 탭에서 이동인 경우 <code>CURRENT</code></td></tr><tr><td><code>link?.shouldCloseAfterLink</code></td><td>링크 이동 후 닫기 옵션이 <code>ON</code>인 경우 true를 반환합니다.</td></tr></tbody></table>

## 사용 예제

### 리스너 등록

```javascript
hackleClient.setInAppMessageListener({
  onInAppMessageClick: (message, view, action) => {
    if (message.key === 99) {
      // 99번 키의 인앱메시지에 대하여 커스텀 리스너를 적용합니다.

      // e.g) 쿠폰 발급 API 호출, history API 등 내부 라우팅 로직 작성

      // 인앱메시지를 리스너를 통해서 닫고 싶을 때 사용합니다.
      view.close();

      // false를 반환하는 경우 기존 인앱메시지의 동작을 그대로 유지합니다.
      return false;
    }
  },
  afterInAppMessageClose(message) {
    console.log("afterInAppMessageClose", message);
  },
  afterInAppMessageOpen(message) {
    console.log("afterInAppMessageOpen", message);
  },
  beforeInAppMessageClose(message) {
    console.log("before view close", message);
  },
  beforeInAppMessageOpen(message) {
    console.log("before view open", message);
  },
});
```

### 리스너 해제

```javascript
hackleClient.setInAppMessageListener(null);
```


# 옵트아웃

옵트아웃이 활성화되면 SDK는 모든 이벤트 전송을 중단합니다.

## 초기화 시 설정

```javascript
const config = {
    optOutTracking: true
};
const hackleClient = Hackle.createInstance("YOUR_BROWSER_SDK_KEY", config);
```

## 런타임 옵트아웃 제어

```javascript
hackleClient.setOptOutTracking(true);
hackleClient.setOptOutTracking(false);
const isOptOut = hackleClient.isOptOutTracking();
```

## 영속성 관리

{% hint style="warning" %}
페이지 새로고침 또는 재방문 시 Config에 설정된 값으로 리셋됩니다.

페이지 새로고침 시 상태를 유지하려면 직접 저장 및 복원 로직을 구현해야 합니다.
{% endhint %}

```javascript
function saveOptOutState(optOut) {
    localStorage.setItem("hackle_opt_out", JSON.stringify(optOut));
    hackleClient.setOptOutTracking(optOut);
}

function getOptOutConfig() {
    const optOut = JSON.parse(localStorage.getItem("hackle_opt_out") || "false");
    return {
        optOutTracking: optOut
    };
}

const config = getOptOutConfig();
const hackleClient = Hackle.createInstance("YOUR_BROWSER_SDK_KEY", config);
```


# React

{% hint style="info" %}
Hackle React SDK는 React 16.8 이상 버전을 지원합니다.

React SDK는 [JavaScript SDK](/development-guide/javascript) 를 기반으로 동작합니다.
{% endhint %}

## 의존성 추가

[![](https://img.shields.io/npm/v/%40hackler%2Freact-sdk)](https://www.npmjs.com/package/@hackler/react-sdk)

{% tabs %}
{% tab title="NPM" %}

```shell
npm install --save @hackler/react-sdk
```

{% endtab %}

{% tab title="YARN" %}

```shell
yarn add @hackler/react-sdk
```

{% endtab %}
{% endtabs %}

## SDK 초기화

SDK를 사용하기 위해서 반드시 HackleReactSDKClient 를 초기화 해야 합니다.

* `HackleReactSDKClient`는 SDK의 기능을 사용하기 위한 메소드들을 제공하는 클래스입니다.
* SDK 키는 핵클 서비스의 대시보드 안에 위치한 [SDK 연동 정보](https://dashboard.hackle.io/config/sdk-setting) 에서 확인하실 수 있습니다.

어플리케이션 초기화 단계에 핵클 서버와 데이터 동기화를 위한 통신을 진행합니다. 일반적으로 이 시간은 수 밀리 초에 불과합니다. 동기화가 완료되면 즉시 렌더링이 진행됩니다.

{% hint style="warning" %}
Google Tag Manager (GTM) 사용시 주의 사항

GTM을 사용하여 연동을 진행할 경우, Hackle SDK 초기화를 하고, SDK를 GTM 에서 접근할 수 있도록 window에 추가 선언이 필요합니다.
{% endhint %}

{% tabs %}
{% tab title="React" %}

```javascript
import { createInstance, HackleProvider } from "@hackler/react-sdk";

// YOUR_BROWSER_SDK_KEY 자리에 SDK 키를 넣습니다.
const hackleClient = createInstance("YOUR_BROWSER_SDK_KEY")

ReactDOM.render(
  <HackleProvider hackleClient={hackleClient}>
    <YourApp />
  </HackleProvider>,
  document.getElementById('root')
);

// GTM 연동 시 추가 script
window.hackleClient = hackleClient
```

{% endtab %}

{% tab title="Next.js" %}

```javascript
import {createInstance, HackleProvider} from "@hackler/react-sdk";

// YOUR_BROWSER_SDK_KEY 자리에 SDK 키를 넣습니다.
const hackleClient = createInstance("YOUR_BROWSER_SDK_KEY")

function MyApp({Component, pageProps}) {
  return (
    <HackleProvider hackleClient={hackleClient} supportSSR>
      <Component {...pageProps} />
    </HackleProvider>
  )
}

export default MyApp
```

{% endtab %}

{% tab title="Gatsby" %}

```javascript
import {createInstance, HackleProvider} from "@hackler/react-sdk";

// YOUR_BROWSER_SDK_KEY 자리에 SDK 키를 넣습니다.
const hackleClient = createInstance("YOUR_BROWSER_SDK_KEY")

function App() {
  return (
    <HackleProvider hackleClient={hackleClient} supportSSR>
      <YourApp />
    </HackleProvider>
  )
}

export default App
```

{% endtab %}
{% endtabs %}

### 초기화 설정정보

설정정보를 포함하여 SDK를 초기화 할 수 있습니다.

{% tabs %}
{% tab title="React" %}

```javascript
import { createInstance, HackleProvider } from "@hackler/react-sdk";

// 설정정보를 포함하여 초기화
const config = {
  debug: true
};

const hackleClient = createInstance("YOUR_BROWSER_SDK_KEY", config);

ReactDOM.render(
  <HackleProvider hackleClient={hackleClient}>
    <YourApp />
  </HackleProvider>,
  document.getElementById('root')
);
```

{% endtab %}

{% tab title="Next.js" %}

```javascript
import {createInstance, HackleProvider} from "@hackler/react-sdk";

// 설정정보를 포함하여 초기화
const config = {
  debug: true
};

const hackleClient = createInstance("YOUR_BROWSER_SDK_KEY", config);

function MyApp({Component, pageProps}) {
  return (
    <HackleProvider hackleClient={hackleClient} supportSSR>
      <Component {...pageProps} />
    </HackleProvider>
  )
}

export default MyApp
```

{% endtab %}
{% endtabs %}

#### 모든 설정옵션

<table data-full-width="false"><thead><tr><th width="213.57">설정</th><th width="295.49">기능</th><th width="125.81">기본값</th><th width="108.1">지원 버전</th></tr></thead><tbody><tr><td><code>debug</code></td><td>모든 기능에 대한 로그를 콘솔에 출력합니다.</td><td><code>false</code></td><td>1.0.0+</td></tr><tr><td><code>pollingIntervalMillis</code></td><td>대시보드에서 설정한 정보를 주기적으로 업데이트 할 수 있습니다.<br>최솟값 : 60000 (60초)</td><td><code>-1</code></td><td>11.1.0+</td></tr><tr><td><code>exposureEventDedupIntervalMillis</code></td><td>동일한 사용자가 연속으로 발생시킨 동일한 A/B 테스트, 기능플래그 분배결과에 대한 노출 이벤트를 제거합니다.<br>최솟값: 1000 (1초)<br>최댓값: 3600000 (1시간)</td><td><p><code>60000</code>(1분 / 11.23.0 버전 이상)</p><p><code>-1</code>(중복제거 하지 않음 / 11.23.0 버전 미만)</p></td><td>11.1.0+</td></tr><tr><td><code>sessionTimeoutMillis</code> (deprecated)</td><td>세션만료 시간을 설정합니다. <code>sessionPolicy</code>를 사용해 주세요.</td><td><code>1800000</code> (30분)</td><td>11.8.0+</td></tr><tr><td><code>devTool</code></td><td><a href="/pages/sFgrgnXtOO2Fl04wIpeY">사용자 탐색</a>을 사용할 수 있도록 합니다.</td><td><code>undefined</code></td><td>11.13.0+</td></tr><tr><td><code>autoOpenDevTool</code></td><td>사용자 탐색 버튼이 자동으로 나타나도록 하는 옵션입니다.</td><td><code>false</code></td><td>11.13.0+</td></tr><tr><td><code>user</code></td><td>초기화 시점에 사용자를 주입합니다.</td><td><code>undefined</code></td><td>11.22.3+</td></tr><tr><td><code>sessionPolicy</code></td><td>세션 유지 조건과 만료 조건을 설정합니다.</td><td><p><code>ALWAYS_NEW_SESSION</code> ,</p><p><code>1800000</code></p></td><td>11.54.0+</td></tr><tr><td><code>optOutTracking</code></td><td>옵트아웃 활성화 여부.</td><td><code>false</code></td><td>11.54.0+</td></tr><tr><td><code>evaluationMode</code></td><td>평가 방식을 설정합니다.</td><td><code>local</code></td><td>12.0.0+</td></tr></tbody></table>

#### 평가 방식 설정

평가를 SDK에서 직접 수행할지, 핵클 서버가 미리 수행한 결과를 조회할지 선택할 수 있습니다.\
설정하지 않으면 기본값인 `"local"`(로컬 평가)로 동작합니다.

```javascript
import { createInstance, HackleProvider } from "@hackler/react-sdk";

const config = {
    evaluationMode: "remote"
};

const hackleClient = createInstance("YOUR_BROWSER_SDK_KEY", config);

ReactDOM.render(
  <HackleProvider hackleClient={hackleClient}>
    <YourApp />
  </HackleProvider>,
  document.getElementById('root')
);
```

{% hint style="warning" %}
원격 평가를 사용하면 브라우저에 사용자 정보를 저장하지 않습니다. 기존에 로컬 평가를 사용하며 저장된 사용자 정보가 있는 경우 삭제 처리합니다.

두 방식의 차이와 선택 기준은 [평가 방식](/development-guide/sdk/evaluation-mode) 문서를 참고 바랍니다.
{% endhint %}

#### 세션 정책 설정

세션 정책을 설정하여 세션의 유지 조건과 만료 조건을 제어할 수 있습니다.

```javascript
import { createInstance, HackleProvider, HackleSessionPersistConditions } from "@hackler/react-sdk";

const config = {
    sessionPolicy: {
        timeoutMillis: 3600000,
        persistCondition: HackleSessionPersistConditions.NULL_TO_USER_ID
    }
};

const hackleClient = createInstance("YOUR_BROWSER_SDK_KEY", config);

ReactDOM.render(
  <HackleProvider hackleClient={hackleClient}>
    <YourApp />
  </HackleProvider>,
  document.getElementById('root')
);
```

#### 초기화 시 사용자 주입

config에 user를 설정한 경우 유저 정보를 포함하여 SDK를 초기화 할 수 있습니다.

* 유저 정보를 포함하지 않으면 쿠키에 저장된 유저 정보를 사용합니다.
* 유저 정보를 포함하는 경우 쿠키에 저장된 유저 정보는 사용하지 않습니다.
* 쿠키에 저장된 유저 정보가 없는 경우 Hackle Device ID를 device id로 가지고 유저를 사용합니다.

{% hint style="info" %}
유저 정보는 SDK 초기화 이후에도 유저 정보 설정 함수를 통해 자유롭게 수정 할 수 있습니다.
{% endhint %}

{% hint style="warning" %}
초기화 시 주입한 유저 정보와 쿠키에 저장된 유저 정보는 병합하지 않습니다.

ex) 쿠키에 `userId: A` 가 저장된 상태에서 초기화 시 `deviceId: B` 를 주입하는 경우, `userId: null, deviceId: B`인 유저로 설정됩니다.
{% endhint %}

{% hint style="warning" %}
React SDK 12.0.0 부터 `HackleProvider`의 `user` prop은 값이 변경된 경우에만 적용되며, 적용 시 기존 사용자 정보를 병합하지 않고 대체합니다.

`user` prop 방식과 사용자 정보 변경 함수(`setUser`, `setUserId`, `setDeviceId` 등) 방식을 함께 사용하면 사용자 정보가 유실될 수 있습니다. 두 방식 중 하나만 사용해주세요. 초기 사용자 정보만 필요한 경우 `user` prop 대신 config의 `user` 사용을 권장합니다.
{% endhint %}

{% tabs %}
{% tab title="React" %}

```javascript
import { createInstance, HackleProvider } from "@hackler/react-sdk";

const user = {
  userId: "LOGIN_ID",
  deviceId: "CUSTOM_DEVICE_ID"
};

const config = {
  debug: true,
  user: user
};

const hackleClient = createInstance("YOUR_BROWSER_SDK_KEY", config);

ReactDOM.render(
  <HackleProvider hackleClient={hackleClient}>
    <YourApp />
  </HackleProvider>,
  document.getElementById('root')
);
```

{% endtab %}

{% tab title="Next.js" %}

```javascript
import {createInstance, HackleProvider} from "@hackler/react-sdk";

const user = {
  userId: "LOGIN_ID",
  deviceId: "CUSTOM_DEVICE_ID"
};

const config = {
  debug: true,
  user: user
};

const hackleClient = createInstance("YOUR_BROWSER_SDK_KEY", config);

function MyApp({Component, pageProps}) {
  return (
    <HackleProvider hackleClient={hackleClient} supportSSR>
      <Component {...pageProps} />
    </HackleProvider>
  )
}

export default MyApp
```

{% endtab %}
{% endtabs %}

### 대시보드 설정 정보 갱신

대시보드 설정 정보를 명시적으로 갱신 할 수 있습니다.

{% hint style="warning" %}
해당 함수는 60초에 한번 제한적으로 호출할 수 있습니다.
{% endhint %}

```javascript
await hackleClient.fetch();
```

React SDK 12.0.0 부터 `fetch()` 완료 후 훅(`useVariation` 등)이 최신 설정 정보 기준으로 다시 평가됩니다.


# 사용자 식별자와 속성

{% hint style="info" %}
사용자 식별자 관리

사용자 식별자는 사용자를 고유하게 식별하는 목적으로 사용합니다. 사용자 식별자의 의미와 중요성, 선택하는 기준 등에 대해서는 [사용자 식별자 관리하기](/getting-started/user-identifier) 문서를 참고하시기 바랍니다.
{% endhint %}

{% hint style="warning" %}
사용자 정보를 변경하는 함수(`setUser`, `setUserId`, `setDeviceId`, `updateUserProperties`, `resetUser` 등)는 `Promise`를 반환합니다.

사용자 정보 변경 직후  `await`로 반영 완료까지 기다리는 것을 권장합니다.
{% endhint %}

{% hint style="danger" %}
`HackleProvider`의 `user` prop 방식과 사용자 정보 변경 함수 방식을 함께 사용하면 사용자 정보가 유실될 수 있습니다. 두 방식 중 하나만 사용해주세요.
{% endhint %}

## 사용자 식별자

### 핵클에서 제공하는 기본 식별자

JavaScript SDK는 디바이스의 식별자를 관리하는 기능을 포함하고 있습니다. 따라서 사용자 식별자를 별도로 전달하지 않아도 사용자를 자동으로 식별할 수 있습니다.

SDK에서 관리하는 식별자를 조회하는 방법은 다음과 같습니다.

```javascript
// 사용자 정보 가지고 오기
const user = hackleClient.getUser();

// 디바이스 식별자 가지고 오기
const deviceId = user.deviceId;

// 내부적으로 관리되는 세션 식별자 가져오기
const sessionId = hackleClient.getSessionId();
```

#### 디바이스 ID 수정

핵클에서 제공하는 디바이스 ID를 사용하지 않고 직접 디바이스 ID를 주입할 수 있습니다.

```javascript
// 디바이스 식별자 변경
await hackleClient.setDeviceId("CUSTOM_DEVICE_ID");
```

#### 사용자 식별자(User ID) 설정

로그인 한 사용자의 식별자를 설정하실 수 있습니다.

```javascript
// 로그인 한 사용자 식별자 추가
await hackleClient.setUserId("LOGIN_ID");
```

### 추가 식별자

기본 식별자(deviceid, userid) 외의 식별자 타입을 추가할 경우 아래와 같이 설정할 수 있습니다.

{% hint style="info" %}
추가 식별자는 [핵클 통합 식별자](/getting-started/user-identifier/hackle-id)로 통합되지 않습니다.
{% endhint %}

{% hint style="danger" %}
`setUser` 를 하는 경우 현재 디바이스의 유저 정보를 덮어씁니다.

* 현재 A userId를 사용중인데 setUser 시 A userId 를 전달하지 않으면 userId가 A -> null로 변경됩니다.
* 현재 custom deviceId를 사용중인데 setUser 시 사용중인 deviceId를 전달하지 않으면 custom deviceId -> hackle deviceId로 변경됩니다.
* 추가 식별자를 사용하는 경우에도 setUser 시 전달하지 않으면 추가 식별자가 초기화가 됩니다.
* 프로퍼티의 경우 아래 케이스로 디바이스 내 캐싱된 프로퍼티가 유지 or 초기화 될 수 있습니다.
  * setUser 전 / 후 userId와 deviceId가 동일하다면 캐시된 프로퍼티 유지됩니다.
  * setUser 전 / 후 userId 혹은 deviceId가 변경된다면 캐시된 프로퍼티가 삭제됩니다.
    {% endhint %}

```javascript
const user = {
  userId: "LOGIN_ID",           // 사용자 ID (핵클 통합 식별자 사용가능)
  deviceId: "CUSTOM_DEVICE_ID", // 디바이스 ID (핵클 통합 식별자 사용가능)
  identifiers: {
    myCustomId: "CUSTOM_IDENTIFIER" // Custom ID
  }
};

await hackleClient.setUser(user);
```

## 사용자 속성(Property)

핵클 SDK는 사용자 속성을 추가할 수 있도록 지원합니다.

* 속성은 속성명(key)과 속성값(value)을 한 쌍으로 보내야 합니다.
* 추가 가능한 속성 개수는 최대 128개입니다.

<table><thead><tr><th width="133.04296875">구분</th><th width="127.60546875">타입</th><th>제약사항</th></tr></thead><tbody><tr><td>속성 명(key)</td><td><code>string</code></td><td><ul><li>글자수 제한은 128자입니다. (128 characters)</li><li>대소문자를 구분하지 않습니다.</li><li>예를 들어 AGE와 age는 동일한 속성명으로 인식합니다.</li></ul></td></tr><tr><td>속성 값(value)</td><td><code>boolean</code>, <code>string</code>, <code>number</code>, <code>array</code></td><td><ul><li>string 타입인 경우 글자수 제한은 1024자입니다.<br>(1024 characters)</li><li>string 타입은 대소문자를 구분합니다.</li><li>예를 들어 APPLE과 apple은 서로 다른 속성값으로 인식합니다.</li><li>number 타입인 경우 정수 최대 15자리, 소수점 최대 6자리를<br>지원합니다.</li></ul></td></tr></tbody></table>

### 사용자 속성 추가

`PropertyOperationsBuilder` 객체에 `set` 을 이용하여 속성을 추가한 뒤 `updateUserProperties` 를 호출하면 사용자 속성을 간단하게 추가할 수 있습니다.

{% hint style="warning" %}
`setUserProperty`, `setUserProperties` 함수는 React SDK 12.0.0 부터 deprecated 되었습니다. `updateUserProperties` 를 사용해 주세요.
{% endhint %}

```javascript
import { PropertyOperationsBuilder } from "@hackler/react-sdk"

// 속성 설정
const operations = new PropertyOperationsBuilder()
    .set("gender", "female")
    .build();

await hackleClient.updateUserProperties(operations);
```

### 사용자 속성 설정

사용자 속성을 추가, 제거 할 수 있습니다.

<table><thead><tr><th width="150">지원하는 함수</th><th>설명</th></tr></thead><tbody><tr><td><code>set</code></td><td>사용자 속성을 설정합니다. 속성 키에 이미 설정한 속성값이 있는 경우 덮어씁니다</td></tr><tr><td><code>setOnce</code></td><td><p>사용자 속성 값을 한번만 설정합니다. 속성키에 대한 속성이 이미 있는 경우 무시됩니다.</p><p>예를 들어 사용자에 대한 가입일, 초기 가입 위치 등을 설정할 수 있습니다.</p></td></tr><tr><td><code>unset</code></td><td>사용자 속성을 제거합니다.</td></tr><tr><td><code>clearAll</code></td><td>사용자의 모든 속성을 제거합니다.</td></tr></tbody></table>

설정하고 싶은 사용자 속성으로 `PropertyOperationsBuilder` 객체를 인스턴스화 합니다. 다음 `updateUserProperties` 를 호출하여 사용자 속성을 업데이트 합니다. 한 번에 여러개의 속성을 설정할 수도 있습니다.

```javascript
import { PropertyOperationsBuilder } from "@hackler/react-sdk"

const operations = new PropertyOperationsBuilder()
    .set("age", 42)
    .set("grade", "GOLD")
    .setOnce("sign_up_date", "2020-07-03")
    .build();

await hackleClient.updateUserProperties(operations);
```

## 사용자 초기화

기존에 설정한 정보를 초기화해야 합니다. 초기화를 하는 경우 기존에 설정했던 식별자, 속성이 모두 초기화됩니다.

{% hint style="danger" %}
사용자 초기화를 하는 경우 서버에 저장된 사용자 속성까지 모두 초기화가 됩니다. 로그아웃 처리를 원하는 경우 `hackleClient.setUserId(undefined);`을 사용해주세요.

userId에 undefined을 대입하면 클라이언트 상에서 로그아웃 처리가 됩니다.
{% endhint %}

```javascript
await hackleClient.resetUser();
```

`resetUser()` 를 호출하는 경우 기존에 설정했던 식별자, 속성이 모두 초기화됩니다.


# CRM 속성

CRM 속성은 핵클 서버에만 안전하게 저장되며 SDK를 통해 값을 직접 조회할 수는 없습니다.

{% hint style="danger" %}
CRM 속성은 별도로 관리되며, `resetUser()`를 호출하거나 `updateUserProperties`에서 `clearAll`을 호출해도 삭제되거나 초기화되지 않습니다.

사용자가 회원 탈퇴 등을 한 경우, 반드시 별도로 제공되는 함수를 호출하여 정보를 삭제해야 합니다.
{% endhint %}

## 전화번호 수집

{% hint style="info" %}
React SDK 11.42.0 버전 이상에서 지원하는 기능입니다.
{% endhint %}

{% hint style="success" %}
카카오 / 문자 메시지 권장 사항

이 기능을 이용하여 사용자 식별자와 전화번호를 매핑하면 핵클을 통한 카카오 / 문자 메시지를 더욱 원활히 이용할 수 있습니다.
{% endhint %}

#### setPhoneNumber

사용자의 전화번호를 등록합니다.

전화번호는 올바른 E.164 포맷일 경우에만 저장됩니다. 국가코드를 지정하지 않았다면 대한민국(+82)이 기본값으로 사용됩니다.

이미 저장된 전화번호가 있는 사용자의 경우, 이 함수를 호출하면 기존 전화번호가 새로운 값으로 교체됩니다.

```javascript
const myPhone = '+821012341234';
hackleClient.setPhoneNumber(myPhone);
```

#### unsetPhoneNumber

사용자에게 등록되어 있는 전화번호를 삭제합니다.

```javascript
hackleClient.unsetPhoneNumber();
```

## CRM 마케팅 메시지 수신 동의

{% hint style="info" %}
React SDK 11.45.0 버전 이상에서 지원하는 기능입니다.

수신 동의 상태에 대한 자세한 내용은 [CRM 메시지 수신 동의 관리 문서](/development-guide/sdk/user-identifier/crm-subscription)를 참고해주세요.
{% endhint %}

### 수신 동의 속성

메시지 목적 별로 수신 동의/거부를 할 수 있습니다.

`HackleSubscriptionOperationsBuilder`를 사용해 원하는 속성의 동의 상태를 설정한 후, `updatePushSubscriptions()` 같은 메서드로 최종 업데이트를 진행합니다.

메시지 채널 별로 동의 상태를 업데이트할 수 있습니다.

```javascript
import { HackleSubscriptionOperationsBuilder, HackleSubscriptionStatus } from "@hackler/react-sdk";

const operations = new HackleSubscriptionOperationsBuilder()
  .marketing(HackleSubscriptionStatus.SUBSCRIBED)
  .information(HackleSubscriptionStatus.SUBSCRIBED)
  .build();

hackleClient.updatePushSubscriptions(operations);
```

#### 광고성 메시지

광고성 메시지 수신 동의 속성을 설정합니다.

```javascript
import { HackleSubscriptionOperationsBuilder, HackleSubscriptionStatus } from "@hackler/react-sdk";

const operations = new HackleSubscriptionOperationsBuilder()
  .marketing(HackleSubscriptionStatus.SUBSCRIBED)
  .build();

hackleClient.updatePushSubscriptions(operations);
```

#### 정보성 메시지

정보성 메시지 수신 동의 속성을 설정합니다.

```javascript
import { HackleSubscriptionOperationsBuilder, HackleSubscriptionStatus } from "@hackler/react-sdk";

const operations = new HackleSubscriptionOperationsBuilder()
  .information(HackleSubscriptionStatus.SUBSCRIBED)
  .build();

hackleClient.updatePushSubscriptions(operations);
```

<table><thead><tr><th width="224.09765625">HackleSubscriptionStatus</th><th>설명</th></tr></thead><tbody><tr><td><code>UNKNOWN</code></td><td>수신 동의/거부를 하지 않음 (<code>default</code>)</td></tr><tr><td><code>SUBSCRIPTION</code></td><td>명시적으로 수신 동의</td></tr><tr><td><code>UNSUBSCRIPTION</code></td><td>명시적으로 수신 거부</td></tr></tbody></table>

### 푸시 수신 동의 상태 업데이트

사용자의 푸시 메시지 수신 동의 상태를 업데이트 합니다.

```javascript
import { HackleSubscriptionOperationsBuilder, HackleSubscriptionStatus } from "@hackler/react-sdk";

const operations = new HackleSubscriptionOperationsBuilder()
  .marketing(HackleSubscriptionStatus.SUBSCRIBED)
  .information(HackleSubscriptionStatus.SUBSCRIBED)
  .build();

hackleClient.updatePushSubscriptions(operations);
```

### 카카오 메시지 수신 동의 상태 업데이트

사용자의 카카오 메시지 수신 동의 상태를 업데이트 합니다.

```javascript
import { HackleSubscriptionOperationsBuilder, HackleSubscriptionStatus } from "@hackler/react-sdk";

const operations = new HackleSubscriptionOperationsBuilder()
  .marketing(HackleSubscriptionStatus.SUBSCRIBED)
  .information(HackleSubscriptionStatus.SUBSCRIBED)
  .build();

hackleClient.updateKakaoSubscriptions(operations);
```

### 문자 메시지 수신 동의 상태 업데이트

사용자의 문자 메시지 수신 동의 상태를 업데이트 합니다.

```javascript
import { HackleSubscriptionOperationsBuilder, HackleSubscriptionStatus } from "@hackler/react-sdk";

const operations = new HackleSubscriptionOperationsBuilder()
  .marketing(HackleSubscriptionStatus.SUBSCRIBED)
  .information(HackleSubscriptionStatus.SUBSCRIBED)
  .build();

hackleClient.updateSmsSubscriptions(operations);
```


# 테스트 그룹 분배

A/B 테스트를 진행할 때, 테스트 그룹을 대상으로 사용자를 분배하고 각 테스트 그룹에 해당하는 로직을 작성해야 합니다. 이 때 사용자 분배를 핵클 SDK를 통해 진행할 수 있습니다.

## useVariation or useLoadableVariation

핵클이 제공하는 컴포넌트를 사용하거나 Hooks API를 사용하여 사용자를 특정 그룹으로 분배하고 분배 결과를 전달받을 수 있습니다. 분배 시에는 **실험 키**를 전달해야 합니다.

<table><thead><tr><th width="150">파라미터</th><th width="120">타입</th><th width="110">필수</th><th>제약사항</th></tr></thead><tbody><tr><td>실험 키 (key)</td><td><code>int</code></td><td>필수</td><td>-</td></tr></tbody></table>

#### 예제

아래 예제 코드의 경우 실험 키 42를 전달하고 있으며, 테스트 그룹은 A와 B 두 개가 존재합니다.

{% tabs %}
{% tab title="Component 사용" %}

```javascript
import {HackleExperiment, HackleVariation} from "@hackler/react-sdk";

function App() {
  return (
    // 실험 키가 42인 A/B 테스트에서 사용자에게 노출할 테스트 그룹을 결정합니다.
    // 결정하지 못하는 상황인 경우 테스트 그룹 A를 반환합니다.
    <HackleExperiment experimentKey={42}>
      <HackleVariation variation={"A"}>
        <AwesomeFeature />
      </HackleVariation>
      <HackleVariation variation={"B"}>
        <SuperAwesomeFeature />
      </HackleVariation>
    </HackleExperiment>
  )
}
```

{% endtab %}

{% tab title="Hooks API 사용" %}

```javascript
// 일반적인 경우
function App() {
  // 실험 키가 42인 A/B 테스트에서 사용자에게 노출할 테스트 그룹을 결정합니다.
  // 결정하지 못하는 상황인 경우 테스트 그룹 A를 반환합니다.
  const variation = useVariation(42)

  // 할당받은 그룹에 대한 로직
  if (variation === "A") return <AwesomeFeature />
  if (variation === "B") return <SuperAwesomeFeature />
  return <AwesomeFeature />
}

// SSR 사용 시
function App() {
  // 실험 키가 42인 A/B 테스트에서 사용자에게 노출할 테스트 그룹을 결정합니다.
  // 결정하지 못하는 상황인 경우 테스트 그룹 A를 반환합니다.
  const { isLoading, variation } = useLoadableVariation(42)

  // SDK Loading 전 컴포넌트 비노출
  if (isLoading) return null

  // 할당받은 그룹에 대한 로직
  if (variation === "A") return <AwesomeFeature />
  if (variation === "B") return <SuperAwesomeFeature />
  return <AwesomeFeature />
}
```

{% endtab %}
{% endtabs %}

## useVariationDetail or useLoadableVariationDetail

`useVariationDetail()` Hooks API는 `useVariation()`와 동일하게 동작하고 분배된 사유를 같이 제공합니다. 이 메소드는 분배가 잘 되고 있는지 살펴볼 때 유용하게 사용할 수 있습니다.

<table><thead><tr><th width="150">파라미터</th><th width="120">타입</th><th width="110">필수</th><th>제약사항</th></tr></thead><tbody><tr><td>실험 키 (key)</td><td><code>int</code></td><td>필수</td><td>-</td></tr></tbody></table>

#### 예제

파라미터로 실험 키를 전달해야 합니다. 아래 예제 코드의 경우 실험 키 42를 전달하고 있습니다.

```javascript
// 분배 결정 상세
const decision = useVariationDetail(42)

// 분배 그룹
const variation = decision.variation

// 분배 결정 사유
const reason = decision.reason

// SSR 사용 시
// 분배 결정 상세
const { isLoading, decision } = useLoadableVariationDetail(42)

// 분배 그룹
const variation = decision.variation

// 분배 결정 사유
const reason = decision.reason
```

### 분배 사유

분배 결정 사유는 **`SDK_NOT_READY`** 와 같은 형태로 받게 됩니다. 자세한 내용은 아래 표를 참고해주세요.

<table><thead><tr><th width="319.12109375">분배 사유</th><th width="305.35546875">설명</th><th>분배 결과</th></tr></thead><tbody><tr><td><code>SDK_NOT_READY</code></td><td><p>SDK 사용 준비가 되지 않았습니다.</p><p>(예: 잘못된 SDK 키로 초기화 시도)</p></td><td>A (기본 그룹)</td></tr><tr><td><code>EXPERIMENT_NOT_FOUND</code></td><td>전달한 실험 키에 대한 A/B 테스트를 찾을 수 없습니다. 실험 키가 잘못되었거나 해당 실험이 보관 상태일 수 있습니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>NOT_IN_MUTUAL_EXCLUSION_EXPERIMENT</code></td><td>실험이 상호 배타적 설정에 포함되어 있지만<br>해당 상호 배타적 그룹에 할당되지 않은 경우</td><td>A (기본 그룹)</td></tr><tr><td><code>EXPERIMENT_DRAFT</code></td><td>A/B 테스트가 준비 상태입니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>EXPERIMENT_PAUSED</code></td><td>A/B 테스트가 일시 정지 상태입니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>EXPERIMENT_COMPLETED</code></td><td>A/B 테스트가 종료되었습니다.</td><td>종료 시 선택한승리 그룹</td></tr><tr><td><code>OVERRIDDEN</code></td><td>사용자가 수동할당에 의해<br>특정 그룹으로 결정되었습니다.</td><td>수동 할당한<br>그룹</td></tr><tr><td><code>NOT_IN_EXPERIMENT_TARGET</code></td><td>사용자가 A/B 테스트 타겟이 아닙니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>TRAFFIC_NOT_ALLOCATED</code></td><td>A/B 테스트가 실행 중이지만<br>사용자가 테스트에 할당되지 않았습니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>TRAFFIC_ALLOCATED</code></td><td>사용자가 A/B 테스트에 할당되었습니다.</td><td>할당된 그룹</td></tr><tr><td><code>VARIATION_DROPPED</code></td><td>원래 할당된 그룹이 테스트에서 제외되었습니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>INVALID_INPUT</code></td><td>입력값이 유효하지 않습니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>EXCEPTION</code></td><td>알 수 없는 오류가 발생했습니다.</td><td>A (기본 그룹)</td></tr></tbody></table>

### 파라미터

{% hint style="info" %}
React SDK 11.7.3 이상 버전에서 지원하는 기능입니다.
{% endhint %}

* `useVariationDetail()` Hooks API를 통해 분배된 그룹의 파라미터 값도 같이 제공받을 수 있습니다.
* config 객체와 `get()` 메소드를 통해 A/B 테스트 화면에서 설정한 파라미터 설정 값을 받아 활용할 수 있으며 A/B 테스트의 파라미터 설정 화면에서 값을 변경할 경우, 변경된 값이 코드에 적용됩니다.
* `get()` 메소드의 parameterKey는 A/B 테스트의 파라미터 설정에서 설정한 키 정보이며 defaultValue는 분배 결정 실패 시, 또는 잘못된 파라미터 유형의 값을 넣었을 때 Return되는 값입니다.
* 설정한 정보를 제대로 받기 위해서는 defaultValue에 설정하신 파라미터 유형에 맞는 type의 값을 입력해야 합나다.

<table><thead><tr><th width="150">구분</th><th width="200">value type</th><th>설명</th></tr></thead><tbody><tr><td><code>get</code></td><td><code>string</code>, <code>number</code>, <code>boolean</code></td><td><ul><li>설정된 parameter값을 반환합니다.</li><li>JSON 타입은 문자열(String)형태로 받을 수 있습니다.</li><li>JSON 타입의 default 값은 문자열 타입으로 입력해야 합니다.</li></ul></td></tr></tbody></table>

#### 예제

```javascript
// 분배 결정 상세
const decision = useVariationDetail(42)

//분배 결정 상세에서 get() 메소드를 통해 parameter 값 가져오기
const parameterValue = decision.get("parameterKey", "defaultValue")

// string 유형의 parameter값 예제
const strValue = decision.get("parmeterKey", "defaultValue")


// SSR 사용 시
// 분배 결정 상세
const { isLoading, decision } = useLoadableVariationDetail(42)

//분배 결정 상세에서 get() 메소드를 통해 parameter 값 가져오기
const parameterValue = decision.get("parameterKey", "defaultValue")

// string 유형의 parameter값 예제
const strValue = decision.get("parmeterKey", "defaultValue")
```


# 기능 플래그 결정

기능 플래그는 켜짐(on) 상태와 꺼짐(off) 상태가 있습니다. 각 상태에 따라 다른 기능을 설정하게 됩니다. 기능 플래그를 적용한 기능에 어떤 사용자가 접근할 경우 켜짐 혹은 꺼짐 상태를 받을 수 있어야 합니다. 이 상태 결정을 핵클 SDK를 통해 진행할 수 있습니다.

## useFeature or useLoadableFeature

핵클이 제공하는 Hooks API를 사용하여 사용자에 대한 상태 결과를 전달받을 수 있습니다. 이 때 **기능 키**를 전달해야 하며, 이후 상태에 따른 로직을 구현합니다.

아래 예제 코드에서는 기능 키 42를 전달하고 있습니다.

{% tabs %}
{% tab title="Component 사용" %}

```javascript
function App() {
  return (
    // 기능 키가 42인 기능 플래그에서 사용자의 상태를 결정합니다.
    // 결정하지 못하는 상황인 경우 false(꺼짐 상태)를 반환합니다.
    <Feature featureKey={42}>
      {(featureOn) =>
        featureOn ? (
          <SuperAwesomeFeature /> // 켜짐 상태일 때의 기능
        ) : (
          <AwesomeFeature /> // 꺼짐 상태일 때의 기능
        )
      }
    </Feature>
  )
}
```

{% endtab %}

{% tab title="Hooks API 사용" %}

```javascript
function App() {
  // 기능 키가 42인 기능 플래그에서 사용자의 상태를 결정합니다.
  // 결정하지 못하는 상황인 경우 false(꺼짐 상태)를 반환합니다.
  const featureOn = useFeature(42)
  return (
    <>
    {
      featureOn ? (
        <SuperAwesomeFeature /> // 켜짐 상태일 때의 기능
      ) : (
        <AwesomeFeature /> // 꺼짐 상태일 때의 기능
      )
    }
    </>
  )
}

// SSR 사용 시
function App() {
  // 기능 키가 42인 기능 플래그에서 사용자의 상태를 결정합니다.
  // 결정하지 못하는 상황인 경우 false(꺼짐 상태)를 반환합니다.
  const { isLoading, isOn } = useLoadableFeature(42)
  return (
    <>
    {
      isOn ? (
        <SuperAwesomeFeature /> // 켜짐 상태일 때의 기능
      ) : (
        <AwesomeFeature /> // 꺼짐 상태일 때의 기능
      )
    }
    </>
  )
}
```

{% endtab %}
{% endtabs %}

## useFeatureFlagDetail or useLoadableFeatureDetail

`useFeatureFlagDetail()` Hooks API는 `useFeature()`와 동일하게 동작하고 추가로 상태 결정에 대한 사유를 같이 제공합니다. 수동할당이 잘 되고 있는지 알아보거나 설정한 트래픽 할당 대비 결과 비중이 이상하다고 여길 때 유용하게 활용할 수 있습니다.

파라미터로 기능 키를 전달해야 합니다. 아래 예제 코드의 경우 기능 키 42를 전달하고 있습니다.

```javascript
// 분배 결정 상세
const decision = useFeatureFlagDetail(42)

// 분배 그룹
const isOn = decision.isOn

// 분배 결정 사유
const reason = decision.reason


// SSR 사용 시
// 분배 결정 상세
const { isLoading, decision } = useLoadableFeatureDetail(42)

// 분배 그룹
const isOn = decision.isOn

// 분배 결정 사유
const reason = decision.reason
```

상태 결정 사유는 **`SDK_NOT_READY`** 와 같은 형태로 받게 됩니다. 자세한 내용은 아래 표를 참고해주세요.

<table><thead><tr><th width="233.71875">결정 사유</th><th width="370.8046875">설명</th><th>분배 결과</th></tr></thead><tbody><tr><td><code>SDK_NOT_READY</code></td><td>SDK 사용 준비가 되지 않았습니다.<br>(예: 잘못된 SDK 키로 초기화 시도)</td><td>기본 상태<br>(꺼짐/off)</td></tr><tr><td><code>FEATURE_FLAG_NOT_FOUND</code></td><td>전달한 기능 키에 대한 기능 플래그를 찾을 수 없습니다.<br>기능 키가 잘못되었거나 해당 기능 플래그가 보관 상태일 수 있습니다.</td><td>기본 상태<br>(꺼짐/off)</td></tr><tr><td><code>FEATURE_FLAG_INACTIVE</code></td><td>기능 플래그가 꺼짐 상태입니다</td><td>기본 상태<br>(꺼짐/off)</td></tr><tr><td><code>INDIVIDUAL_TARGET_MATCH</code></td><td>개별 타겟팅에 매치 되었습니다.</td><td>개별 타겟팅으로<br>설정한 상태</td></tr><tr><td><code>TARGET_RULE_MATCH</code></td><td>사용자 타겟팅에 매치 되었습니다.</td><td>사용자 타겟팅으로 설정한 상태</td></tr><tr><td><code>DEFAULT_RULE</code></td><td>개별 타겟팅, 사용자 타겟팅 중 어디에도 매치 되지 않았습니다.</td><td>기본 룰로<br>설정한 상태</td></tr><tr><td><code>EXCEPTION</code></td><td>알 수 없는 오류가 발생했습니다.</td><td>기본 상태<br>(꺼짐/off)</td></tr></tbody></table>

## 기능 플래그 파라미터

{% hint style="info" %}
React SDK 11.7.3 이상 버전에서 지원하는 기능입니다.
{% endhint %}

* `useFeatureFlagDetail()` Hooks API를 통해 상태 결정에 대한 파라미터 값도 같이 제공받을 수 있습니다.
* config 객체와 `get()` 메소드를 통해 기능 플래그 화면에서 설정한 파라미터 설정 값을 받아 활용할 수 있으며 기능 플래그의 파라미터 설정 화면에서 값을 변경할 경우, 변경된 값이 코드에 적용됩니다.

```javascript
// 분배 결정 상세
const decision = useFeatureFlagDetail(42)

//상태 결정 상세에서 get() 메소드를 통해 parameter 값 가져오기
const parameterValue = decision.get("parameterKey", "defaultValue")

// string 유형의 parameter값 예제
const strValue = decision.get("parmeterKey", "defaultValue")


// SSR 사용 시
// 분배 결정 상세
const { isLoading, decision } = useLoadableFeatureDetail(42)

//상태 결정 상세에서 get() 메소드를 통해 parameter 값 가져오기
const parameterValue = decision.get("parameterKey", "defaultValue")

// string 유형의 parameter값 예제
const strValue = decision.get("parmeterKey", "defaultValue")
```

* `get()` 메소드의 parameterKey는 기능 플래그의 파라미터 설정에서 설정한 키 정보이며 defaultValue는 상태 결정 실패 시, 또는 잘못된 파라미터 유형의 값을 넣었을 때 Return되는 값입니다.
* 설정한 정보를 제대로 받기 위해서는 defaultValue에 설정하신 파라미터 유형에 맞는 type의 값을 입력해야 합나다.
* JSON 타입은 문자열(String)형태로 받을 수 있으므로, JSON 타입의 경우 defaultValue를 문자열 타입으로 입력해야 합니다.
* SDK에서 제공되는 파라미터 유형은 string, number, boolean 이며 기능 플래그 화면에서 설정한 JSON 타입은 문자열(String)형태로 받을 수 있습니다. JSON 타입의 default 값은 문자열 타입으로 입력해야 합니다.


# 원격 구성 적용

{% hint style="info" %}
React SDK 11.7.3 이상 버전에서 지원하는 기능입니다.
{% endhint %}

원격 구성은 애플리케이션에서 관리되고 있는 값, 또는 속성들을 핵클 대시보드에서 정의한 파라미터 값들로 대체하여 실시간으로 애플리케이션의 동작 및 설정 값들을 제어할 수 있는 기능입니다.

핵클의 대시보드의 원격 구성 화면으로 이동하여 파라미터 정보들을 설정하고, 사용자 식별 규칙에 따른 값들을 설정할 수 있습니다.

## useRemoteConfig() or useLoadableRemoteConfig()

`useRemoteConfig()` 또는 `useLoadableRemoteConfig()` Hooks API를 사용하면 사용자에 대한 원격 구성 정보(설정한 파라미터 및 규칙 정보)를 담고 있는 `HackleRemoteConfig` 인스턴스를 얻을 수 있습니다. `HackleRemoteConfig` 에서 제공하는 메소드들을 통해 원하는 파라미터에 접근하여 값을 제공받을 수 있습니다.

```javascript
// 원격 구성 정보를 담은 인스턴스를 반환합니다. 
const remoteConfig = useRemoteConfig()

// SSR 사용 시
// 원격 구성 정보를 담은 인스턴스를 반환합니다. - Loadable 사용 시
const { isLoading, remoteConfig } = useLoadableRemoteConfig()
```

## 원격 구성 파라미터 조회

* `useRemoteConfig()` 또는 `useLoadableRemoteConfig()` Hooks API를 사용하여 반환받은 `HackleRemoteConfig`에는 파라미터 값 조회를 위한 `get()`메소드를 제공합니다.
* 핵클의 원격 구성 화면에서 설정한 파라미터 값이 key, value 형태로 존재하기 때문에, 설정한 파라미터 유형에 따라 아래 메소드를 사용하여 설정한 파라미터 값을 반환받을 수 있습니다.

{% hint style="warning" %}
보관 후 원격 구성과 관련된 코드를 제거하세요.

원격 구성 파라미터를 보관한 경우 더이상 파라미터 정보에 접근 할 수 없습니다. 따라서 원격 구성 파라미터 보관 후에는 반드시 관련된 코드를 정리해주시기 바랍니다.
{% endhint %}

```javascript
// 원격 구성 정보를 담은 인스턴스를 반환합니다. 
const remoteConfig = useRemoteConfig()

//remoteConfig 에서 get() 메소드를 통해 parameter 값 가져오기
const parameterValue = remoteConfig.get(parameterKey, defaultValue)

// string 유형의 parameter값 예제
const strValue = remoteConfig.get("parameterKey", "defaultValue")


// SSR 사용 시
// 원격 구성 정보를 담은 인스턴스를 반환합니다. - Loadable 사용 시
const { isLoading, remoteConfig } = useLoadableRemoteConfig()

//remoteConfig 에서 get() 메소드를 통해 parameter 값 가져오기
const parameterValue = remoteConfig.get(parameterKey, defaultValue)

// string 유형의 parameter값 예제
const strValue = remoteConfig.get("parameterKey", "defaultValue")
```

* get() 메소드의 parameterKey는 원격 구성의 파라미터 설정에서 설정한 키 정보입니다.
* defaultValue는 원격 구성 값을 결정할 수 없을 때 반환되는 값입니다. 입력한 defaultValue는 다음과 같은 상황에서 반환될 수 있습니다.\
  A. 원격 구성화면에서 설정한 타입 유형과 다른 유형의 값을 입력\
  B. 설정되지 않은 parameter key 호출\
  C. Hackle SDK 초기화 실패\
  D. 잘못된 식별자 정보가 입력되거나 존재하지 않을 때\
  E. ETC
* 설정한 정보를 제대로 받기 위해서는 defaultValue에 설정하신 파라미터 유형에 맞는 type의 값을 입력해야 합나다.
* SDK에서 제공되는 원격 구성 파라미터 유형은 string, number, boolean 이며 원격 구성 파라미터 화면에서 설정한 JSON 타입은 문자열(String)형태로 받을 수 있습니다. JSON 타입의 default 값은 문자열 타입으로 입력해야 합니다.


# 이벤트 전송

핵클 SDK는 사용자 이벤트를 핵클로 전송하는 기능을 제공합니다. 사용자 행동의 변화가 일어나는 지점마다 이 기능을 활용하면 사용자 행동에 대한 유의미한 데이터를 얻을 수 있으며, 그렇게 모인 데이터를 통해 사용자 행동 분석을 할 수 있습니다.

## track

`track()` 메소드에 **이벤트 키**를 전달하여 사용자 이벤트를 전송할 수 있습니다.

<table><thead><tr><th width="150">파라미터</th><th width="120">타입</th><th width="110">필수</th><th>제약사항</th></tr></thead><tbody><tr><td>이벤트 명(key)</td><td><code>string</code></td><td>필수</td><td><ul><li>글자수 제한은 128자입니다. (128 characters)</li></ul></td></tr></tbody></table>

#### 예시

사용자가 구매하기 버튼을 눌렀을 때 이벤트를 수집하기 위해 `purchase` 라는 이벤트 키를 정의했다고 가정합니다.

```jsx
import { useTrack } from '@hackle/react-sdk';

function PurchaseButton() {
  const track = useTrack();

  const handleClick = () => {
    track({ key: "purchase" });
  };

  return <button onClick={handleClick}>구매하기</button>;
}
```

### 속성(Property)

핵클 SDK는 이벤트(Event) 객체에 속성을 추가할 수 있도록 지원합니다.

* 속성은 속성명(key)과 속성값(value)을 한 쌍으로 보내야 합니다.
* 이벤트 객체에 추가 가능한 속성 개수는 최대 64개입니다.

<table><thead><tr><th width="133.04296875">구분</th><th width="127.60546875">타입</th><th>제약사항</th></tr></thead><tbody><tr><td>속성 명(key)</td><td><code>string</code></td><td><ul><li>글자수 제한은 128자입니다. (128 characters)</li><li>대소문자를 구분하지 않습니다.</li><li>예를 들어 AGE와 age는 동일한 속성명으로 인식합니다.</li></ul></td></tr><tr><td>속성 값(value)</td><td><code>boolean</code>, <code>string</code>, <code>number</code>, <code>array</code></td><td><ul><li>string 타입인 경우 글자수 제한은 1024자입니다.<br>(1024 characters)</li><li>string 타입은 대소문자를 구분합니다.</li><li>예를 들어 APPLE과 apple은 서로 다른 속성값으로 인식합니다.</li><li>number 타입인 경우 정수 최대 15자리, 소수점 최대 6자리를<br>지원합니다.</li></ul></td></tr></tbody></table>

#### 예시

아래 예시에서는 세 가지 속성(`pay_method`, `discount_amount`, `is_discount`)을 추가한 것을 확인할 수 있습니다.

```jsx
import { useTrack } from '@hackle/react-sdk';

function PurchaseButton() {
  const track = useTrack();

  const handleClick = () => {
    track({
      key: "purchase",
      properties: {
        pay_method: "CARD",
        discount_amount: 800,
        is_discount: true
      }
    });
  };

  return <button onClick={handleClick}>구매하기</button>;
}
```


# 사용자 화면 추적

Hackle SDK는 기본적으로 브라우저의 URL 변경을 감지하여 자동으로 페이지뷰 정보를 수집합니다. 하지만 페이지별로 URL을 구분하지 않는 경우 자동 수집이 어려울 수 있습니다.

이러한 경우 `setCurrentPage` 메소드를 직접 호출하여 페이지뷰를 수동으로 추적할 수 있습니다.

## setCurrentPage

{% hint style="info" %}
React SDK 11.53.0 이상 버전에서 지원하는 기능입니다.
{% endhint %}

`setCurrentPage` 메소드를 호출하여 현재 페이지 정보를 수동으로 설정할 수 있습니다.

* 화면 추적의 최소 단위 시간은 1초 입니다.
* 1초 이내에 변경된 페이지의 `$engagement` 는 측정되지 않습니다.

<table><thead><tr><th width="150">파라미터</th><th width="120">타입</th><th width="110">필수</th><th>설명</th></tr></thead><tbody><tr><td><code>pageName</code></td><td><code>string</code></td><td>필수</td><td>페이지명</td></tr><tr><td><code>properties</code></td><td><code>object</code></td><td>선택</td><td>페이지에 추가할 커스텀 속성</td></tr></tbody></table>

{% hint style="warning" %}
`setCurrentPage` 는 호출할 때마다 페이지 정보를 수집합니다.

동일한 pageName이 들어와도 `$page_view`와 `$engagement` 를 수집합니다.
{% endhint %}

#### 예시

React에서는 라우트 변경이 완료된 시점에 화면 추적을 하는 것을 추천합니다.

{% tabs %}
{% tab title="React" %}

```javascript
import { useEffect } from 'react'
import { useLocation } from 'react-router-dom'
import { useHackleClient } from '@hackler/react-sdk'

function usePageTracking() {
  const location = useLocation()
  const hackleClient = useHackleClient()
  useEffect(() => {
    hackleClient.setCurrentPage({
      pageName: "pageName"
    })
  }, [location, hackleClient])
}
```

{% endtab %}

{% tab title="Next.js (App Router)" %}

```javascript
'use client'

import { useEffect } from 'react'
import { usePathname, useSearchParams } from 'next/navigation'

export function PageTracker() {
  const pathname = usePathname()
  const searchParams = useSearchParams()

  useEffect(() => {
    // 페이지 경로(pathname) 또는 쿼리 파라미터(searchParams)가 변경될 때마다 실행
    hackleClient.setCurrentPage({
      pageName: "pageName"
    })
  }, [pathname, searchParams])

  return null
}
```

{% endtab %}

{% tab title="Next.js (Pages Router)" %}

```javascript
import { useEffect } from 'react'
import { useRouter } from 'next/router'

export default function App({ Component, pageProps }) {
  const router = useRouter()

  useEffect(() => {
    // 페이지 변경 시 호출될 핸들러 함수
    const handleRouteChange = (url: string) => {
      hackleClient.setCurrentPage({
        pageName: "pageName"
      })
    }

    handleRouteChange(router.asPath)
    router.events.on('routeChangeComplete', handleRouteChange)

    return () => {
      router.events.off('routeChangeComplete', handleRouteChange)
    }
  }, [router])

  return <Component {...pageProps} />
```

{% endtab %}
{% endtabs %}

### 속성 (Property)

핵클 SDK는 이벤트(Event) 객체에 속성을 추가할 수 있도록 지원합니다.

* 이벤트 객체에 추가 가능한 속성 개수는 최대 64개입니다.

<table><thead><tr><th width="150">구분</th><th width="200">타입</th><th>제약사항</th></tr></thead><tbody><tr><td>속성 명(key)</td><td><code>string</code></td><td><ul><li>글자수 제한은 128자입니다. (128 characters)</li><li>대소문자를 구분하지 않습니다. 예를 들어 amount와 AMOUNT는 같은 키로 인식합니다.</li></ul></td></tr><tr><td>속성 값(value)</td><td><code>boolean</code>, <code>string</code>, <code>number</code>, <code>array</code></td><td><ul><li>string 타입인 경우 글자수 제한은 1024자입니다. (1024 characters)</li><li>number 타입인 경우 정수 최대 15자리, 소수점 최대 6자리를 지원합니다.</li></ul></td></tr></tbody></table>

#### 예시

```javascript
hackleClient.setCurrentPage({
  pageName: "Product Detail",
  properties: {
    productId: "12345",
    category: "Electronics",
    price: 99000,
    inStock: true
  }
});
```

## automaticRouteTracking 비활성화

SDK에서는 기본적으로 `automaticRouteTracking`이 활성화되어 있습니다. 페이지 이동을 감지하여 자동 화면 정보 수집을 원하지 않는 경우 `automaticRouteTracking`를 비활성화 해야 합니다.

SDK를 초기화 할 때 `automaticRouteTracking`을 설정할 수 있습니다.

{% hint style="danger" %}
`automaticRouteTracking`이 활성화 된 상태에서 `setCurrentPage`를 통해 수동으로 화면 수집을 하는 경우, `$page_view`와 `$engagement`가 과수집 되거나 의도하지 않은 속성으로 수집될 수 있습니다.

**수동으로 화면 정보를 수집하는 경우 `automaticRouteTracking` 비활성화를 추천합니다.**
{% endhint %}

#### 예시

```jsx
const config = {
  automaticRouteTracking: false
};

const hackleClient = createInstance("YOUR_BROWSER_SDK_KEY", config);

ReactDOM.render(
  <HackleProvider hackleClient={hackleClient}>
    <YourApp />
  </HackleProvider>,
  document.getElementById('root')
);
```


# 사용자 탐색

{% hint style="info" %}
**디버깅 용도로만 사용하는 것을 권장합니다.**
{% endhint %}

{% hint style="warning" %}
원격 평가 방식에서는 사용자 탐색 기능을 사용할 수 없습니다.
{% endhint %}

사용자 식별자를 확인하고 A/B 테스트, 기능플래그에 강제할당 하는 방법을 설명합니다.

아래의 의존성을 추가합니다.

{% tabs %}
{% tab title="npm" %}

```shell
// react-sdk@12.0.0 이상일 경우
npm install @hackler/javascript-devtools@2.0.0

// react-sdk@11.21.0 이상 12.0.0 미만일 경우
npm install @hackler/javascript-devtools@1.0.2

// react-sdk@11.21.0 미만일 경우
npm install @hackler/javascript-devtools@1.0.1
```

{% endtab %}

{% tab title="yarn" %}

```shell
// react-sdk@12.0.0 이상일 경우
yarn add @hackler/javascript-devtools@2.0.0

// react-sdk@11.21.0 이상 12.0.0 미만일 경우
yarn add @hackler/javascript-devtools@1.0.2

// react-sdk@11.21.0 미만일 경우
yarn add @hackler/javascript-devtools@1.0.1
```

{% endtab %}
{% endtabs %}

<table><thead><tr><th width="250">react-sdk 버전</th><th>javascript-devtools 버전</th></tr></thead><tbody><tr><td>11.13.0 &#x3C;= version &#x3C; 11.21.0</td><td>1.0.1</td></tr><tr><td>11.21.0 &#x3C;= version &#x3C; 12.0.0</td><td>1.0.2</td></tr><tr><td>12.0.0 &#x3C;= version</td><td>2.0.0</td></tr></tbody></table>

### 사용자 탐색 설정

<table><thead><tr><th width="150">키</th><th>기능</th><th width="120">기본값</th><th width="110">지원</th></tr></thead><tbody><tr><td><code>devTool</code></td><td>사용자 탐색을 사용할 수 있도록 합니다.</td><td><code>undefined</code></td><td>11.13.0+</td></tr><tr><td><code>autoOpenDevTool</code></td><td>사용자 탐색 버튼이 자동으로 나타나도록 하는 옵션입니다.</td><td><code>false</code></td><td>11.13.0+</td></tr></tbody></table>

* `devTool` 에 `"@hackler/javascript-devtools"` 에서 import한 `HackleDevTools`를 넣어주세요. **(필수\*)**
* `autoOpenDevTool`을 `true`로 설정할 경우 `showUserExplorer`을 호출하지 않아도 자동으로 사용자 탐색 창이 나타납니다. (기본 값은 `false` 입니다.)

기존 Hackle Client를 초기화하는 코드에 아래와 같이 옵션을 추가합니다.

```javascript
import { createInstance } from "@hackler/react-sdk";
import HackleDevTools from "@hackler/javascript-devtools"

const config = {
  devTool: HackleDevTools,
  autoOpenDevTool: false,
};

// YOUR_BROWSER_SDK_KEY로 초기화
const hackleClient = createInstance(YOUR_BROWSER_SDK_KEY, config);

// 사용자 탐색을 나타내고 싶은 시점에 Trigger 되도록 해주세요.
hackleClient.showUserExplorer()

// 사용자 탐색 창을 닫고 싶을 경우에 Trigger 되도록 해주세요.
hackleClient.hideUserExplorer()
```

{% hint style="danger" %}
Production에서는 `autoOpenDevTool` 옵션을 `false` 로 설정하세요.

autoOpenDevTool을 true로 설정할 경우 자동으로 사용자 탐색 창이 나타납니다.

사용자 탐색 창을 나타내고 싶은 시점에 `hackleClient.showUserExplorer`을 호출하는 식으로 사용하시는 것을 권장합니다.
{% endhint %}

### 사용자 탐색 창

화면 하단에 핵클 로고 버튼이 표시됩니다. 버튼 클릭시 설정 화면으로 진입 할 수 있습니다.

![](/files/DhV2Cp2xi3lomA6y6Slj)

## 사용자 식별자 확인하기

화면 상단에서 사용자 식별자를 확인 및 복사 할 수 있습니다.

![](/files/TrHtboRjUyl5Xn3EKDy2)

## 사용자 강제할당

* 화면하단에서 A/B 테스트, 기능플래그의 분배 결과를 확인할 수 있습니다.
* SelectBox 클릭 시 특정 그룹으로 강제할당 할 수 있습니다.
* `Reset` 버튼 클릭 시 강제할당이 해제됩니다.
* `Reset all` 버튼 클릭시 모든 강제할당이 해제됩니다.
* 브라우저에서 강제할당한 경우 해당 브라우저에서 분배하는 경우에만 적용됩니다. (대시보드 테스트기기에 등록되지 않습니다)
* 강제할당이 적용되지 않는경우 브라우저를 새로고침 해주세요.

![](/files/gC5JcHuH9ZCsbg6onF58)


# 인앱 메시지

{% hint style="info" %}
React SDK 11.15.0 버전 이상에서 지원하는 기능입니다.
{% endhint %}

{% hint style="info" %}
인앱메시지 캠페인에 관한 자세한 사항은 [인앱메시지](/crm-marketing/in-app-message-guide) 가이드를 확인해주세요.
{% endhint %}

## 인앱메시지 개발자 가이드

### Interfaces

#### HackleInAppMessage

```typescript
interface HackleInAppMessage {
  key: number
}
```

* 인앱메시지의 키를 반환합니다.

#### HackleInAppMessageView

```typescript
interface HackleInAppMessageView {
  close(): void
  inAppMessage: HackleInAppMessage // 11.48.0+
}

```

* `close()` 를 호출하여 인앱메시지를 리스너 함수 내에서 직접 닫을 수 있습니다.

### Methods

#### getDisplayedInAppMessageView

```javascript
const hackleClient = createInstance("YOUR_SDK_KEY");

const view = hackleClient.getDisplayedInAppMessageView();
if (view !== null) {
  view.close(); // 현재 표시 중인 인앱메시지를 닫습니다.
}
```

* 현재 브라우저에 표시된 인앱메시지를 반환합니다.
* 반환 타입은 `HackleInAppMessageView | null` 입니다.
  * 현재 표시 중인 인앱메시지가 존재하지 않는 경우 `null` 을 반환합니다.


# 인앱메시지 이벤트 리스너

{% hint style="info" %}
React SDK 11.37.0 이상 버전에서 지원하는 기능입니다.
{% endhint %}

## Interfaces

### HackleInAppMessageListener

```typescript
interface HackleInAppMessageListener {
  beforeInAppMessageOpen?(inAppMessage: HackleInAppMessage): void
  afterInAppMessageOpen?(inAppMessage: HackleInAppMessage): void
  beforeInAppMessageClose?(inAppMessage: HackleInAppMessage): void
  afterInAppMessageClose?(inAppMessage: HackleInAppMessage): void
  onInAppMessageClick?(
    inAppMessage: HackleInAppMessage,
    view: HackleInAppMessageView,
    action: HackleInAppMessageAction
  ): boolean
}
```

#### onInAppMessageClick

`onInAppMessageClick` 메서드가 반환하는 boolean 값을 통해 인앱메시지의 동작을 제어할 수 있습니다.

* `true` 를 반환하는 경우 기존 인앱메시지의 액션을 덮어씁니다.
  * 가령, 기존 인앱메시지의 클릭 시 액션이 `하루 동안 보지 않기` 였다면 해당 액션은 동작하지 않습니다.
* `false`를 반환하는 경우 기존 인앱메시지의 액션이 그대로 동작합니다.

{% hint style="warning" %}
`view.close()` 를 `onInAppMessageClick` 내부에서 호출하는 경우

`view.close()` 를 리스너 내부에서 호출한 경우 `true` 를 반환한 것과 동일하게 처리됩니다.

```typescript
onInAppMessageClick: (message, view, action) => {
  view.close();

  // return false를 하더라도 true한 것과 동일하게 처리됨.
  return false;

},
```

{% endhint %}

### HackleInAppMessage

```typescript
interface HackleInAppMessage {
  key: number
}
```

* 인앱메시지의 키를 반환합니다.

### HackleInAppMessageView

```typescript
interface HackleInAppMessageView {
  close(): void
  inAppMessage: HackleInAppMessage // 11.48.0+
}

```

* `close()` 를 호출하여 인앱메시지를 리스너 함수 내에서 직접 닫을 수 있습니다.

### HackleInAppMessageAction

```typescript
type HackleInAppMessageActionType = "CLOSE" | "LINK"
type HackleInAppMessageActionLinkTarget = "CURRENT" | "NEW_TAB" | "NEW_WINDOW"

interface HackleInAppMessageAction {
  type: HackleInAppMessageActionType
  close?: {
    hideDurationMillis: number | null
  }
  link?: {
    url: string
    target: HackleInAppMessageActionLinkTarget
    shouldCloseAfterLink: boolean
  }
}
```

<table><thead><tr><th width="250">property</th><th>description</th></tr></thead><tbody><tr><td><code>close?.hideDurationMillis</code></td><td>메시지를 특정 기간동안 숨김 처리하는 액션인 경우, 해당 기간을 밀리초로 반환합니다.</td></tr><tr><td><code>link?.url</code></td><td>링크가 포함된 경우 링크를 반환합니다. e.g) https://hackle.io</td></tr><tr><td><code>link?.target</code></td><td>새 탭으로 이동인 경우 <code>NEW_TAB</code><br>새 창으로 이동인 경우 <code>NEW_WINDOW</code><br>현재 탭에서 이동인 경우 <code>CURRENT</code></td></tr><tr><td><code>link?.shouldCloseAfterLink</code></td><td>링크 이동 후 닫기 옵션이 <code>ON</code>인 경우 true를 반환합니다.</td></tr></tbody></table>

## 사용 예제

### 리스너 등록

예제는 `useEffect`로 내부에 작성되어 있으나, 사용 목적에 맞는 위치에 자유롭게 작성할 수 있습니다.

```typescript
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
// import hackleClient
import { hackleClient } from "./hackleClient";

export default function Example() {
  const navigate = useNavigate();

  useEffect(() => {
    hackleClient.setInAppMessageListener({
      onInAppMessageClick: (message, view, action) => {
        // 100번 키의 인앱메시지에 대하여 대시보드에서 설정한 link URL을 React의 Route 로직으로 대체합니다.
        if (message.key === 100) {
          if (action.link?.url) {
            view.close()
	          navigate(action.link.url);
            return true;
          }
        }

        return false;
      },
      afterInAppMessageClose(message) {
        console.log("afterInAppMessageClose", message);
      },
      afterInAppMessageOpen(message) {
        console.log("afterInAppMessageOpen", message);
      },
      beforeInAppMessageClose(message) {
        console.log("before view close", message);
      },
      beforeInAppMessageOpen(message) {
        console.log("before view open", message);
      },
    });
  }, [navigate]);

  return;
}

```

### 리스너 해제

```javascript
hackleClient.setInAppMessageListener(null);
```


# 옵트아웃

옵트아웃이 활성화되면 SDK는 모든 이벤트 전송을 중단합니다.

{% hint style="info" %}
React SDK는 JavaScript SDK를 기반으로 동작합니다. 옵트아웃 API는 JavaScript SDK와 동일합니다.
{% endhint %}

## 초기화 시 설정

{% tabs %}
{% tab title="React" %}

```javascript
import { createInstance, HackleProvider } from "@hackler/react-sdk";

const config = {
    optOutTracking: true
};

const hackleClient = createInstance("YOUR_BROWSER_SDK_KEY", config);

ReactDOM.render(
  <HackleProvider hackleClient={hackleClient}>
    <YourApp />
  </HackleProvider>,
  document.getElementById('root')
);
```

{% endtab %}

{% tab title="Next.js" %}

```javascript
import { createInstance, HackleProvider } from "@hackler/react-sdk";

const config = {
    optOutTracking: true
};

const hackleClient = createInstance("YOUR_BROWSER_SDK_KEY", config);

function MyApp({ Component, pageProps }) {
  return (
    <HackleProvider hackleClient={hackleClient} supportSSR>
      <Component {...pageProps} />
    </HackleProvider>
  )
}

export default MyApp
```

{% endtab %}
{% endtabs %}

## 런타임 옵트아웃 제어

```javascript
hackleClient.setOptOutTracking(true);
hackleClient.setOptOutTracking(false);
const isOptOut = hackleClient.isOptOutTracking();
```

## 영속성 관리

{% hint style="warning" %}
페이지 새로고침 또는 재방문 시 Config에 설정된 값으로 리셋됩니다.
{% endhint %}

```javascript
function saveOptOutState(optOut) {
    localStorage.setItem("hackle_opt_out", JSON.stringify(optOut));
    hackleClient.setOptOutTracking(optOut);
}

function getOptOutConfig() {
    const optOut = JSON.parse(localStorage.getItem("hackle_opt_out") || "false");
    return {
        optOutTracking: optOut
    };
}

const config = getOptOutConfig();
const hackleClient = Hackle.createInstance("YOUR_BROWSER_SDK_KEY", config);
```


# Next.js

{% hint style="info" %}
Next.js는 `@hackler/react-sdk` 패키지를 이용하여 핵클을 연동할 수 있습니다.

핵클 SDK의 자세한 연동 및 사용 가이드는 [React](/development-guide/react) 가이드를 참고해주세요.
{% endhint %}

## 1. 설치

Next.js 에서 Hackle 을 사용하시려면 `@hackler/react-sdk`를 설치해야 합니다.

{% tabs %}
{% tab title="npm" %}

```shell
npm install @hackler/react-sdk
```

{% endtab %}

{% tab title="yarn" %}

```shell
yarn add @hackler/react-sdk
```

{% endtab %}

{% tab title="pnpm" %}

```shell
pnpm add @hackler/react-sdk
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
.env.development, .env.production 파일에 Hackle SDK 키를 개발환경, 운영환경에 맞게 각각 추가하세요.

* SDK 키는 핵클 서비스의 대시보드 안에 위치한 [SDK 연동 정보](https://dashboard.hackle.io/config/sdk-setting)에서 확인하실 수 있습니다.
  {% endhint %}

```
NEXT_PUBLIC_HACKLE_SDK_KEY=your-hackle-sdk-key
```

## 2. 연동 방법

Hackle은 Next.js 의 [Page Router](https://nextjs.org/docs/pages) 와 [App Router](https://nextjs.org/docs/app)를 모두 지원합니다. 각각 Hackle을 연동하는 방법에는 몇 가지 차이점이 있습니다.

#### 인스턴스 생성

{% tabs %}
{% tab title="App Router" %}

```typescript
// app/hackleClient.client.ts

"use client";

import { createInstance } from "@hackler/react-sdk";

export const hackleClient = createInstance(
  process.env.NEXT_PUBLIC_HACKLE_SDK_KEY!
);
```

{% endtab %}

{% tab title="Page Router" %}

```typescript
// app/hackleClient.client.ts

import { createInstance } from "@hackler/react-sdk";

export const hackleClient = createInstance(
  process.env.NEXT_PUBLIC_HACKLE_SDK_KEY!
);
```

{% endtab %}
{% endtabs %}

#### App Router - 프로바이더 생성

```typescript
// app/HackleClientProvider.tsx

"use client";

import { HackleProvider } from "@hackler/react-sdk";
import { hackleClient } from "@/app/hackleClient.client";

export function HackleClientProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <HackleProvider hackleClient={hackleClient} user={{userId: "a-user-id"}} supportSSR>
      {children}
    </HackleProvider>
  );
}
```

#### App Router - 루트 레이아웃 설정

```typescript
import { HackleClientProvider } from "@/app/HackleClientProvider";
import "./globals.css";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ko">
      <body>
        <HackleClientProvider>
          {children}
        </HackleClientProvider>
      </body>
    </html>
  );
}
```

#### Page Router - 프로바이더 사용

```typescript
// pages/_app.tsx

import type { AppProps } from "next/app"
import { HackleProvider } from "@hackler/react-sdk"
import { hackleClient } from "@/app/hackleClient.client"

export default function App({ Component, pageProps }: AppProps) {
  return (
    <HackleProvider hackleClient={hackleClient} user={{ userId: "a-user-id" }} supportSSR>
      <Component {...pageProps} />
    </HackleProvider>
  )
}
```

{% hint style="warning" %}
React SDK 12.0.0 부터 `HackleProvider`의 `user` prop은 값이 변경된 경우에만 적용되며, 적용 시 기존 사용자 정보를 병합하지 않고 대체합니다.

`user` prop 방식과 사용자 정보 변경 함수(`setUser`, `setUserId`, `setDeviceId` 등) 방식을 함께 사용하면 사용자 정보가 유실될 수 있습니다. 두 방식 중 하나만 사용해주세요.
{% endhint %}

## 3. 주요 기능

#### A/B 테스트 그룹 분배

{% tabs %}
{% tab title="App Router" %}

```typescript
"use client"

import { useLoadableVariationDetail } from "@hackler/react-sdk"
import DecisionComponent from "./DecisionComponent"

interface ClientComponentProps {
  experimentKey: number
}

export default function ClientComponent({ experimentKey }: ClientComponentProps) {
  const { decision, isLoading } = useLoadableVariationDetail(experimentKey)
  if (isLoading) return <div>Loading...</div>
  const { variation, reason, experiment } = decision

  return (
    <div>
      <h3>Client Component</h3>
      <DecisionComponent
        variation={variation}
        reason={String(reason)}
        experimentKey={experiment?.key.toString() ?? "-"}
        experimentVersion={experiment?.version.toString() ?? "-"}
      />
    </div>
  )
}
```

{% endtab %}

{% tab title="Page Router" %}

```typescript
import { useLoadableVariationDetail } from "@hackler/react-sdk"
import DecisionComponent from "./DecisionComponent"

interface ClientComponentProps {
  experimentKey: number
}

export default function ClientComponent({ experimentKey }: ClientComponentProps) {
  const { decision, isLoading } = useLoadableVariationDetail(experimentKey)
  if (isLoading) return <div>Loading...</div>
  const { variation, reason, experiment } = decision

  return (
    <div>
      <h3>Client Component</h3>
      <DecisionComponent
        variation={variation}
        reason={String(reason)}
        experimentKey={experiment?.key.toString() ?? "-"}
        experimentVersion={experiment?.version.toString() ?? "-"}
      />
    </div>
  )
}
```

{% endtab %}
{% endtabs %}

#### 기능플래그 결정

{% tabs %}
{% tab title="App Router" %}

```typescript
"use client"

import { useLoadableFeatureDetail } from "@hackler/react-sdk"
import { FEATURE_FLAG_KEY } from "@/app/constants"
import DecisionComponent from "./DecisionComponent"

export default function ClientComponent() {
  const { decision, isLoading } = useLoadableFeatureDetail(FEATURE_FLAG_KEY)
  if (isLoading) return <div>Loading...</div>
  const { isOn, reason, experiment } = decision

  return (
    <div>
      <h3>Client Component</h3>
      <DecisionComponent
        isOn={isOn}
        reason={String(reason)}
        experimentKey={experiment?.key.toString() ?? "-"}
        experimentVersion={experiment?.version.toString() ?? "-"}
      />
    </div>
  )
}
```

{% endtab %}

{% tab title="Page Router" %}

```typescript
import { useLoadableFeatureDetail } from "@hackler/react-sdk"
import { FEATURE_FLAG_KEY } from "@/app/constants"
import DecisionComponent from "./DecisionComponent"

export default function ClientComponent() {
  const { decision, isLoading } = useLoadableFeatureDetail(FEATURE_FLAG_KEY)
  if (isLoading) return <div>Loading...</div>
  const { isOn, reason, experiment } = decision

  return (
    <div>
      <h3>Client Component</h3>
      <DecisionComponent
        isOn={isOn}
        reason={String(reason)}
        experimentKey={experiment?.key.toString() ?? "-"}
        experimentVersion={experiment?.version.toString() ?? "-"}
      />
    </div>
  )
}
```

{% endtab %}
{% endtabs %}

#### 원격구성 적용

{% tabs %}
{% tab title="App Router" %}

```typescript
"use client";

import useRemoteConfigWithParsing from "@/app/hooks/useRemoteConfigWithParsing";
import { REMOTE_CONFIG_KEY } from "@/app/constants";

const defaultConfig = {
  isDemo: false,
};

export default function ClientComponent() {
  const config = useRemoteConfigWithParsing(REMOTE_CONFIG_KEY, defaultConfig);
  if (config.isLoading) return <div>Loading...</div>

  return (
    <div>
      <h3>Client Component</h3>
      <dl>
        <dt>defaultConfig</dt>
        <dd>{JSON.stringify(defaultConfig, null, 2)}</dd>
        <dt>config</dt>
        <dd>{JSON.stringify(config.value, null, 2)}</dd>
      </dl>
    </div>
  );
}
```

{% endtab %}

{% tab title="Page Router" %}

```typescript
import useRemoteConfigWithParsing from "@/app/hooks/useRemoteConfigWithParsing";
import { REMOTE_CONFIG_KEY } from "@/app/constants";

const defaultConfig = {
  isDemo: false,
};

export default function ClientComponent() {
  const config = useRemoteConfigWithParsing(REMOTE_CONFIG_KEY, defaultConfig);
  if (config.isLoading) return <div>Loading...</div>

  return (
    <div>
      <h3>Client Component</h3>
      <dl>
        <dt>defaultConfig</dt>
        <dd>{JSON.stringify(defaultConfig, null, 2)}</dd>
        <dt>config</dt>
        <dd>{JSON.stringify(config.value, null, 2)}</dd>
      </dl>
    </div>
  );
}
```

{% endtab %}
{% endtabs %}

```typescript
// app/hooks/useRemoteConfigWithParsing.ts

import { useLoadableRemoteConfig } from "@hackler/react-sdk"

export default function useRemoteConfigWithParsing<T>(key: string, defaultValue: T): { value: T; isLoading: boolean } {
  const { remoteConfig, isLoading } = useLoadableRemoteConfig()
  if (isLoading) return { value: defaultValue, isLoading }

  try {
    const configValue = remoteConfig.get(key, JSON.stringify(defaultValue))
    return { value: JSON.parse(configValue), isLoading }
  } catch (error) {
    console.warn("Failed to parse remote config:", error)
    return { value: defaultValue, isLoading }
  }
}
```

#### 이벤트 전송

```typescript
// app/example.ts

"use client"

import { useTrack } from "@hackler/react-sdk";

export default function Example() {
  const track = useTrack();

  return <button onClick={() => track({ key: "test" })}>test</button>;
```

### 고급설정

Next.js 에서 서버사이드 분배를 위해서는 추가 설정이 필요합니다. 서버사이드에서 분배를 하는 경우 분배 식별자와 서버 사이드와 클라이언트 사이드에서 실행되는 코드를 주의 깊게 관리해야 합니다.

#### 설치

서버사이드(Node.js 환경)에서 사용될 sdk 를 별도로 설치해야 합니다.

{% tabs %}
{% tab title="npm" %}

```shell
npm install @hackler/javascript-sdk
```

{% endtab %}

{% tab title="yarn" %}

```shell
yarn add @hackler/javascript-sdk
```

{% endtab %}

{% tab title="pnpm" %}

```shell
pnpm add @hackler/javascript-sdk
```

{% endtab %}
{% endtabs %}

#### SDK 키 추가

.env.development, .env.production 파일에 Hackle SDK 키를 개발환경, 운영환경에 맞게 각각 추가하세요.

```
HACKLE_SDK_KEY_SERVER=your-hackle-sdk-key
```

SDK 키는 <https://dashboard.hackle.io/config/sdk-setting> 에서 Server 키를 찾을 수 있습니다.

#### 연동 방법

```typescript
// app/hackleClient.server.ts

import { createInstance } from "@hackler/javascript-sdk";

export const hackleClient = createInstance(
  process.env.NEXT_PUBLIC_HACKLE_SDK_KEY!
);
```

#### 서버사이드 사용 방법

**A/B 테스트 그룹 분배**

{% tabs %}
{% tab title="App Router" %}

```typescript
import { hackleClient } from "@/app/HackleClient.server";
import DecisionComponent from "./DecisionComponent";

interface ServerComponentProps {
  experimentKey: number;
}

export default async function ServerComponent({
  experimentKey,
}: ServerComponentProps) {
  const userId = "a-user-id";
  await hackleClient.onInitialized();
  const { variation, reason, experiment } = hackleClient.variationDetail(
    experimentKey,
    { userId }
  );

  return (
    <div>
      <h3>Server Component</h3>
      <DecisionComponent
        variation={variation}
        reason={String(reason)}
        experimentKey={experiment?.key.toString() ?? "-"}
        experimentVersion={experiment?.version.toString() ?? "-"}
      />
    </div>
  );
}
```

{% endtab %}

{% tab title="Page Router" %}

```typescript
import { Decision } from "@hackler/javascript-sdk";
import { hackleClient } from "@/app/HackleClient.server";
import DecisionComponent from "./DecisionComponent";

interface ServerComponentProps {
  decison: Decision;
}

export default async function ServerComponent({
  decison
}: ServerComponentProps) {
  return (
    <div>
      <h3>Server Component</h3>
      <DecisionComponent
        variation={decison.variation}
        reason={String(decison.reason)}
        experimentKey={decison.experiment?.key.toString() ?? "-"}
        experimentVersion={decison.experiment?.version.toString() ?? "-"}
      />
    </div>
  );
}

export const getServerSideProps = async () => {
  const userId = "a-user-id";
  await hackleClient.onInitialized();
  const decision = hackleClient.variationDetail(experimentKey, { userId });
  return {
    props: {
      decision,
    },
  };
};
```

{% endtab %}
{% endtabs %}

**기능플래그 결정**

{% tabs %}
{% tab title="App Router" %}

```typescript
import { hackleClient } from "@/app/HackleClient.server";
import DecisionComponent from "./DecisionComponent";

interface ServerComponentProps {
  featureFlagKey: number;
}

export default async function ServerComponent({
  featureFlagKey,
}: ServerComponentProps) {
  const userId = "a-user-id";
  await hackleClient.onInitialized()
  const featureFlagDetail = hackleClient.featureFlagDetail(featureFlagKey, {
    userId
  });

  return (
    <div>
      <h3>Server Component</h3>
      <DecisionComponent
        isOn={featureFlagDetail.isOn}
        reason={String(featureFlagDetail.reason)}
        experimentKey={featureFlagDetail.experiment?.key.toString() ?? "-"}
        experimentVersion={
          featureFlagDetail.experiment?.version.toString() ?? "-"
        }
      />
    </div>
  );
}
```

{% endtab %}

{% tab title="Page Router" %}

```typescript
import { FeatureFlagDecision } from "@hackler/javascript-sdk";
import { hackleClient } from "@/app/HackleClient.server";
import DecisionComponent from "./DecisionComponent";

interface ServerComponentProps {
  featureFlagDecision: FeatureFlagDecision;
}

export default async function ServerComponent({
  featureFlagDecision,
}: ServerComponentProps) {
  return (
    <div>
      <h3>Server Component</h3>
      <DecisionComponent
        isOn={featureFlagDecision.isOn}
        reason={String(featureFlagDecision.reason)}
        experimentKey={featureFlagDecision.experiment?.key.toString() ?? "-"}
        experimentVersion={
          featureFlagDecision.experiment?.version.toString() ?? "-"
        }
      />
    </div>
  );
}

export const getServerSideProps = async () => {
  const userId = "a-user-id";

  await hackleClient.onInitialized();
  const featureFlagDecision = hackleClient.featureFlagDetail(featureFlagKey, {
    userId,
  });

  return {
    props: {
      featureFlagDecision,
    },
  };
};
```

{% endtab %}
{% endtabs %}

**원격구성 적용**

{% tabs %}
{% tab title="App Router" %}

```typescript
import { hackleClient } from "@/app/HackleClient.server";

const defaultConfig = {
  isDemo: false,
};

interface ServerComponentProps {
  remoteConfigKey: string;
}

export default async function ServerComponent({
  remoteConfigKey,
}: ServerComponentProps) {
  const userId = "a-user-id";
  await hackleClient.onInitialized()
  const remoteConfig = hackleClient.remoteConfig({
    userId
  });

  const config = JSON.parse(
    remoteConfig.get(remoteConfigKey, JSON.stringify(defaultConfig))
  );

  return (
    <div>
      <h3>Server Component</h3>
      <dl>
        <dt>defaultConfig</dt>
        <dd>{JSON.stringify(defaultConfig, null, 2)}</dd>
        <dt>config</dt>
        <dd>{JSON.stringify(config, null, 2)}</dd>
      </dl>
    </div>
  );
}
```

{% endtab %}

{% tab title="Page Router" %}

```typescript
import { hackleClient } from "@/app/HackleClient.server";

const defaultConfig = {
  isDemo: false,
};

interface ServerComponentProps {
  config: typeof defaultConfig;
}

export default async function ServerComponent({
  config,
}: ServerComponentProps) {
  return (
    <div>
      <h3>Server Component</h3>
      <dl>
        <dt>defaultConfig</dt>
        <dd>{JSON.stringify(defaultConfig, null, 2)}</dd>
        <dt>config</dt>
        <dd>{JSON.stringify(config, null, 2)}</dd>
      </dl>
    </div>
  );
}

export const getServerSideProps = async () => {
  const userId = "a-user-id";
  await hackleClient.onInitialized();
  const remoteConfig = hackleClient.remoteConfig({
    userId,
  });

  const config = JSON.parse(
    remoteConfig.get(remoteConfigKey, JSON.stringify(defaultConfig))
  );

  return {
    props: {
      config: config,
    },
  };
};
```

{% endtab %}
{% endtabs %}

**이벤트 전송**

{% tabs %}
{% tab title="App Router" %}

```typescript
import { hackleClient } from "@/app/HackleClient.server";

export default async function Example() {
	hackleClient.track({key: "test"}, {userId: "a-user-id"});
}
```

{% endtab %}

{% tab title="Page Router" %}

```typescript
import { hackleClient } from "@/app/HackleClient.server";

export default async function Example() {
	hackleClient.track({key: "test"}, {userId: "a-user-id"});
}
```

{% endtab %}
{% endtabs %}

#### instrumentation

HackleClient 는 초기화시 Hackle Server 로 부터 설정 정보를 받아오고 이후 주기적으로 동기화를 합니다. 따라서 instrumentation.ts 를 활용하면 await hackleClient.onInitialized() 와 같은 코드를 매 페이지에서 사용하지 않고 코드를 더 간단하게 유지할 수 있습니다. 단 instrumentationHook 이 기본설정이 되는 Next.js v15 이상에서 권장 합니다.

{% tabs %}
{% tab title="hackleClient.server.ts" %}

```typescript
// app/hackleClient.server.ts

import { createInstance } from "@hackler/javascript-sdk";

declare global {
  var __hackleClient: ReturnType<typeof createInstance> | undefined;
  var __hackleInitialized: boolean | undefined;
}

if (!global.__hackleClient) {
  global.__hackleClient = createInstance(
    process.env.NEXT_PUBLIC_HACKLE_SDK_KEY!
  );
}

export async function initializeHackle() {
  if (global.__hackleInitialized) {
    return;
  }

  try {
    await global.__hackleClient!.onInitialized({
      timeout: 10000,
    });

    global.__hackleInitialized = true;
  } catch (error) {
    console.error("❌ Hackle SDK initialization failed:", error);
    throw error;
  }
}

export const hackleClient = global.__hackleClient;
```

{% endtab %}

{% tab title="instrumentation.ts" %}

```typescript
// instrumentation.ts

import { initializeHackle } from "@/app/hackleClient.server";

export async function register() {
  await initializeHackle();
}
```

{% endtab %}
{% endtabs %}


# React Native

{% hint style="info" %}
Hackle React Native SDK는 React 17.0.1 이상 및 React Native 0.64.1 이상을 지원합니다.

React Native SDK는 [Android SDK](/development-guide/android), [iOS SDK](/development-guide/ios) 를 기반으로 작동하며 아래 OS를 지원합니다.

* Android API 21 (5.0 LOLLIPOP) 이상
* iOS 13 이상
  {% endhint %}

<details>

<summary>React Native SDK 버전 별 OS 지원 정보</summary>

<table><thead><tr><th width="244.765625">Hackle React Native SDK 버전</th><th>지원 OS</th></tr></thead><tbody><tr><td>3.31.0 미만</td><td><ul><li>Android API 16 이상</li><li>iOS 10 이상</li></ul></td></tr><tr><td>3.31.0 이상, 3.36.0 미만</td><td><ul><li>Android API 16 이상</li><li>iOS 13 이상</li></ul></td></tr><tr><td>3.36.0 이상</td><td><ul><li>Android API 21 이상</li><li>iOS 13 이상</li></ul></td></tr></tbody></table>

</details>

## 의존성 추가

{% hint style="warning" %}
SDK 설치/업데이트 후 앱을 새롭게 빌드해야 연동이 변경사항이 적용됩니다.
{% endhint %}

[![](https://img.shields.io/npm/v/%40hackler%2Freact-native-sdk)](https://www.npmjs.com/package/@hackler/react-native-sdk)

{% tabs %}
{% tab title="npm" %}

```shell
npm install --save @hackler/react-native-sdk
```

{% endtab %}

{% tab title="yarn" %}

```shell
yarn add @hackler/react-native-sdk
```

{% endtab %}
{% endtabs %}

#### iOS

```shell
cd ios
pod install
```

### Expo 사용 시 의존성 추가

{% hint style="info" %}
React Native SDK 3.17.0 이상 버전에서 Expo를 지원합니다.
{% endhint %}

{% hint style="danger" %}
React Native SDK는 `Expo Go` 환경을 지원하지 않습니다. `expo prebuild` 를 사용해주세요.
{% endhint %}

{% tabs %}
{% tab title="npm" %}

```shell
npm install --save @hackler/react-native-sdk
```

{% endtab %}

{% tab title="yarn" %}

```shell
yarn add @hackler/react-native-sdk
```

{% endtab %}
{% endtabs %}

expo 사용 시에는 link나 pod install은 실행하지 않아도 무방합니다.

### 안드로이드 설정

{% hint style="warning" %}
React Native SDK 3.25.0 이하 버전에서만 아래 코드를 직접 입력해주세요.
{% endhint %}

{% hint style="warning" %}
**SDK 3.26.0 이상 버전부터는 아래 설정이 자동으로 적용**됩니다. 이 단계를 건너뛰고 다음으로 진행해 주세요.

아래 코드를 참고해서 `registerActivityLifecycleCallbacks`를 추가해주세요.

생성된 [Application](https://developer.android.com/reference/android/app/Application) 클래스가 없다면 새로 생성하여 `AndroidManifest.xml`에 등록해 주세요.
{% endhint %}

`Application` 클래스에 다음과 같은 코드를 `onCreate` 함수 아래 추가해 주세요.

{% tabs %}
{% tab title="Java" %}

```java
import android.app.Application;
import io.hackle.android.HackleApp;

public class MyApplication extends Application {
  @Override
  public void onCreate() {
    super.onCreate();
    ...
    HackleApp.registerActivityLifecycleCallbacks(this);
    ...
  }
}
```

{% endtab %}

{% tab title="Kotlin" %}

```kotlin
import android.app.Application
import io.hackle.android.Hackle
import io.hackle.android.registerActivityLifecycleCallbacks

class MyApplication : Application() {
  override fun onCreate() {
    super.onCreate()
    ...
    Hackle.registerActivityLifecycleCallbacks(this)
    ...
  }
}
```

{% endtab %}
{% endtabs %}

## SDK 초기화

SDK를 사용하기 위해서 반드시 `createInstance()`에 SDK 키를 전달하여 `HackleReactNativeSDKClient`를 생성하고 React 어플리케이션을 감싸는 `HackleProvider`에 전달해야 합니다.

* SDK 키는 핵클 서비스의 대시보드 안에 위치한 [SDK 연동 정보](https://dashboard.hackle.io/config/sdk-setting)에서 확인하실 수 있습니다.

초기화 시 핵클 서버로부터 필요한 정보들을 가져와 SDK에 저장합니다. 일반적으로 이 시간은 수 밀리 초에 불과합니다. 동기화가 완료되면 즉시 렌더링이 진행됩니다.

```javascript
import { createInstance, HackleProvider } from "@hackler/react-native-sdk";

// YOUR_APP_SDK_KEY 자리에 SDK 키를 넣습니다.
const hackleClient = createInstance("YOUR_APP_SDK_KEY");

const App = () => {
  return (
    <HackleProvider hackleClient={hackleClient}>
      <YourApp />
    </HackleProvider>
  );
};
```

### 초기화 시 사용자 주입

{% hint style="info" %}
React Native SDK 3.31.0 버전 이상에서 지원하는 기능입니다.
{% endhint %}

유저 정보를 포함하여 SDK를 초기화 할 수 있습니다.

* 유저 정보를 포함하지 않으면 로컬 스토리지에 저장된 유저 정보를 사용합니다.
* 유저 정보를 포함하는 경우 로컬 스토리지에 저장된 유저 정보는 사용하지 않습니다.
* 사용자 주입을 하지 않고, 로컬 스토리지에 저장된 유저 정보도 없는 경우 Hackle Device ID를 device id로 가지고 유저를 사용합니다.

{% hint style="info" %}
유저 정보는 SDK 초기화 이후에도 유저 정보 설정 함수를 통해 자유롭게 수정 할 수 있습니다.
{% endhint %}

{% hint style="warning" %}
초기화 시 주입한 유저 정보와 로컬 스토리지에 저장된 유저 정보는 병합하지 않습니다.

ex) 스토리지에 userId: A 가 저장된 상태에서 초기화 시 deviceId: B 를 주입하는 경우, userId: null, deviceId: B인 유저로 설정됩니다.
{% endhint %}

```javascript
import { createInstance, HackleProvider } from "@hackler/react-native-sdk";

const config = {
  debug: true
};

const user = {
  userId: "YOUR_USER_ID"
}

const hackleClient = createInstance("YOUR_APP_SDK_KEY", config, user);

const App = () => {
  return (
    <HackleProvider hackleClient={hackleClient}>
      <YourApp />
    </HackleProvider>
  );
};
```

### 초기화 설정정보

설정정보를 포함하여 SDK를 초기화 할 수 있습니다

```javascript
import { createInstance, HackleProvider } from "@hackler/react-native-sdk";

const config = {
  debug: true
};

const hackleClient = createInstance("YOUR_APP_SDK_KEY", config);

const App = () => {
  return (
    <HackleProvider hackleClient={hackleClient}>
      <YourApp />
    </HackleProvider>
  );
};
```

<table data-full-width="false"><thead><tr><th width="224.4684375">설정</th><th width="295.49">기능</th><th width="125.81">기본값</th><th width="99.209375">지원 버전</th></tr></thead><tbody><tr><td><code>exposureEventDedupIntervalMillis</code></td><td>동일한 사용자가 연속으로 발생시킨 동일한 A/B 테스트, 기능플래그 분배결과에 대한 노출 이벤트를 제거합니다.<br>최솟값: 1000 (1초)<br>최댓값: 3600000 (1시간)</td><td><code>-1</code> (중복제거 하지 않음)</td><td>3.3.1+</td></tr><tr><td><code>debug</code></td><td>모든 기능에 대한 로그를 콘솔에 출력하고, 이벤트를 즉시 전송합니다.</td><td><code>false</code></td><td>3.4.1+</td></tr><tr><td><code>pollingIntervalMillis</code></td><td>대시보드에서 설정한 정보를 주기적으로 업데이트 할 수 있습니다.<br>최솟값 : 60000 (60초)</td><td><code>-1</code> (주기적으로 업데이트하지 않음)</td><td>3.6.0+</td></tr><tr><td><code>automaticAppLifecycleTracking</code></td><td>앱 시작 / 종료 자동 추적 활성화 여부</td><td><code>true</code></td><td>3.30.0+</td></tr><tr><td><code>automaticScreenTracking</code></td><td>화면 자동 추적 활성화 여부</td><td><code>true</code></td><td>3.30.0+</td></tr><tr><td><code>sessionPolicy</code></td><td>세션 유지 조건과 만료 조건을 설정합니다.</td><td><p><code>persistCondition: alwaysNewSession</code>,</p><p><code>timeoutMillis: 1800000</code> (30분)</p></td><td>3.32.0+</td></tr><tr><td><code>optOutTracking</code></td><td>옵트아웃 활성화 여부. 활성화 시 모든 이벤트 전송이 중단됩니다.</td><td><code>false</code></td><td>3.32.0+</td></tr><tr><td><code>sessionTimeoutMillis</code> (deprecated)</td><td>세션만료 시간을 설정합니다. <code>sessionPolicy</code>를 사용해 주세요.</td><td><code>1800000</code> (30분)</td><td>3.11.0+</td></tr></tbody></table>

#### 세션 정책 설정

세션 정책을 설정하여 세션의 유지 조건과 만료 조건을 제어할 수 있습니다.

```javascript
import { createInstance, HackleProvider } from "@hackler/react-native-sdk";

const config = {
    sessionPolicy: {
        timeoutMillis: 3600000,
        persistCondition: 'nullToUserId'
    }
};

const hackleClient = createInstance("YOUR_APP_SDK_KEY", config);

const App = () => {
  return (
    <HackleProvider hackleClient={hackleClient}>
      <YourApp />
    </HackleProvider>
  );
};
```

### 대시보드 설정 정보 갱신

대시보드 설정 정보를 명시적으로 갱신 할 수 있습니다.

{% hint style="warning" %}
해당 함수는 60초에 한번 제한적으로 호출할 수 있습니다.
{% endhint %}

```javascript
hackleClient.fetch();
```


# 사용자 식별자와 속성

{% hint style="info" %}
사용자 식별자 관리

사용자 식별자는 사용자를 고유하게 식별하는 목적으로 사용합니다. 사용자 식별자의 의미와 중요성, 선택하는 기준 등에 대해서는 [사용자 식별자 관리하기](/getting-started/user-identifier) 문서를 참고하시기 바랍니다.
{% endhint %}

## 사용자 식별자

### 핵클에서 제공하는 기본 식별자

JavaScript SDK는 디바이스의 식별자를 관리하는 기능을 포함하고 있습니다. 따라서 사용자 식별자를 별도로 전달하지 않아도 사용자를 자동으로 식별할 수 있습니다.

SDK에서 관리하는 식별자를 조회하는 방법은 다음과 같습니다.

```javascript
// 사용자 정보 가지고 오기
const user = await hackleClient.getUser();

// 디바이스 식별자 가져오기
const deviceId = getDeviceId();
```

#### 디바이스 ID 수정

핵클에서 제공하는 디바이스 ID를 사용하지 않고 직접 디바이스 ID를 주입할 수 있습니다.

```javascript
// 디바이스 식별자 변경
await hackleClient.setDeviceId("CUSTOM_DEVICE_ID");
```

#### 사용자 식별자(User ID) 설정

로그인 한 사용자의 식별자를 설정하실 수 있습니다.

```javascript
// 로그인 한 사용자 식별자 추가
await hackleClient.setUserId("LOGIN_ID");
```

### 추가 식별자

기본 식별자(deviceid, userid) 외의 식별자 타입을 추가할 경우 아래와 같이 설정할 수 있습니다.

{% hint style="info" %}
추가 식별자는 [핵클 통합 식별자](/getting-started/user-identifier/hackle-id)로 통합되지 않습니다.
{% endhint %}

{% hint style="danger" %}
`setUser` 를 하는 경우 현재 디바이스의 유저 정보를 덮어씁니다.

* 현재 A userId를 사용중인데 setUser 시 A userId 를 전달하지 않으면 userId가 A -> null로 변경됩니다.
* 현재 custom deviceId를 사용중인데 setUser 시 사용중인 deviceId를 전달하지 않으면 custom deviceId -> hackle deviceId로 변경됩니다.
* 추가 식별자를 사용하는 경우에도 setUser 시 전달하지 않으면 추가 식별자가 초기화가 됩니다.
* 프로퍼티의 경우 아래 케이스로 디바이스 내 캐싱된 프로퍼티가 유지 or 초기화 될 수 있습니다.
  * setUser 전 / 후 userId와 deviceId가 동일하다면 캐시된 프로퍼티 유지됩니다.
  * setUser 전 / 후 userId 혹은 deviceId가 변경된다면 캐시된 프로퍼티가 삭제됩니다.
    {% endhint %}

```javascript
const prevUser = await hackleClient.getUser();

const user = {
  ...prevUser, // 이전 유저 정보
  userId: "143", // 사용자 ID (핵클 통합 식별자 사용가능)
  deviceId: "ae2182e0", // 디바이스 ID (핵클 통합 식별자 사용가능)
  identifiers: {
    myCustomId: "42" // Custom ID
  }
}

await hackleClient.setUser(user);
```

## 사용자 속성(Property)

핵클 SDK는 사용자 속성을 추가할 수 있도록 지원합니다.

* 속성은 속성명(key)과 속성값(value)을 한 쌍으로 보내야 합니다.
* 추가 가능한 속성 개수는 최대 128개입니다.

<table><thead><tr><th width="133.04296875">구분</th><th width="127.60546875">타입</th><th>제약사항</th></tr></thead><tbody><tr><td>속성 명(key)</td><td><code>string</code></td><td><ul><li>글자수 제한은 128자입니다. (128 characters)</li><li>대소문자를 구분하지 않습니다.</li><li>예를 들어 AGE와 age는 동일한 속성명으로 인식합니다.</li></ul></td></tr><tr><td>속성 값(value)</td><td><code>boolean</code>, <code>string</code>, <code>number</code>, <code>array</code></td><td><ul><li>string 타입인 경우 글자수 제한은 1024자입니다.<br>(1024 characters)</li><li>string 타입은 대소문자를 구분합니다.</li><li>예를 들어 APPLE과 apple은 서로 다른 속성값으로 인식합니다.</li><li>number 타입인 경우 정수 최대 15자리, 소수점 최대 6자리를<br>지원합니다.</li></ul></td></tr></tbody></table>

### 사용자 속성 추가

사용자 속성을 간단하게 추가할 수 있습니다. 아래 함수를 호출 시 PropertyOperations 객체에 `set` 을 이용하여 속성을 추가하는 것과 동일하게 동작합니다.

```javascript
await hackleClient.setUserProperty("gender", "female");
```

### 사용자 속성 설정

사용자 속성을 추가, 제거 할 수 있습니다.

<table><thead><tr><th width="150">지원하는 함수</th><th>설명</th></tr></thead><tbody><tr><td><code>set</code></td><td>사용자 속성을 설정합니다. 속성 키에 이미 설정한 속성값이 있는 경우 덮어씁니다</td></tr><tr><td><code>setOnce</code></td><td><p>사용자 속성 값을 한번만 설정합니다. 속성키에 대한 속성이 이미 있는 경우 무시됩니다.</p><p>예를 들어 사용자에 대한 가입일, 초기 가입 위치 등을 설정할 수 있습니다.</p></td></tr><tr><td><code>unset</code></td><td>사용자 속성을 제거합니다.</td></tr><tr><td><code>clearAll</code></td><td>사용자의 모든 속성을 제거합니다.</td></tr></tbody></table>

설정하고 싶은 사용자 속성으로 `PropertyOperationsBuilder` 객체를 인스턴스화 합니다. 다음 `updateUserProperties` 를 호출하여 사용자 속성을 업데이트 합니다. 한 번에 여러개의 속성을 설정할 수도 있습니다.

```javascript
import { PropertyOperationsBuilder } from "@hackler/react-native-sdk"

const operations = new PropertyOperationsBuilder()
  .set("age", 42)
  .set("grade", "GOLD")
  .setOnce("sign_up_date", "2020-07-03")
  .build();

await hackleClient.updateUserProperties(operations);
```

## 사용자 초기화

기존에 설정한 정보를 초기화해야 합니다. 초기화를 하는 경우 기존에 설정했던 식별자, 속성이 모두 초기화됩니다.

{% hint style="danger" %}
사용자 초기화를 하는 경우 서버에 저장된 사용자 속성까지 모두 초기화가 됩니다. 로그아웃 처리를 원하는 경우 `hackleClient.setUserId(undefined);`을 사용해주세요.

userId에 undefined을 대입하면 클라이언트 상에서 로그아웃 처리가 됩니다.
{% endhint %}

```javascript
await hackleClient.resetUser();
```


# CRM 속성

CRM 속성은 핵클 서버에만 안전하게 저장되며 SDK를 통해 값을 직접 조회할 수는 없습니다.

{% hint style="danger" %}
CRM 속성은 별도로 관리되며, `resetUser()`를 호출하거나 `updateUserProperties`에서 `clearAll`을 호출해도 삭제되거나 초기화되지 않습니다.

사용자가 회원 탈퇴 등을 한 경우, 반드시 별도로 제공되는 함수를 호출하여 정보를 삭제해야 합니다.
{% endhint %}

## 전화번호 수집

{% hint style="info" %}
React Native SDK 3.20.0 버전 이상에서 지원하는 기능입니다.
{% endhint %}

{% hint style="success" %}
카카오 / 문자 메시지 권장 사항

이 기능을 이용하여 사용자 식별자와 전화번호를 매핑하면 핵클을 통한 카카오 / 문자 메시지를 더욱 원활히 이용할 수 있습니다.
{% endhint %}

#### setPhoneNumber

사용자의 전화번호를 등록합니다.

전화번호는 올바른 E.164 포맷일 경우에만 저장됩니다. 국가코드를 지정하지 않았다면 대한민국(+82)이 기본값으로 사용됩니다.

이미 저장된 전화번호가 있는 사용자의 경우, 이 함수를 호출하면 기존 전화번호가 새로운 값으로 교체됩니다.

```javascript
const myPhone = '+821012341234';
hackleClient.setPhoneNumber(myPhone);
```

#### unsetPhoneNumber

사용자에게 등록되어 있는 전화번호를 삭제합니다.

```javascript
hackleClient.unsetPhoneNumber();
```

## CRM 마케팅 메시지 수신 동의

{% hint style="info" %}
React Native SDK 3.24.0 버전 이상에서 지원하는 기능입니다.

수신 동의 상태에 대한 자세한 내용은 [CRM 메시지 수신 동의 관리 문서](/development-guide/sdk/user-identifier/crm-subscription)를 참고해주세요.
{% endhint %}

### 수신 동의 속성

메시지 목적 별로 수신 동의/거부를 할 수 있습니다.

`HackleSubscriptionOperationsBuilder`를 사용해 원하는 속성의 동의 상태를 설정한 후, `updatePushSubscriptions()` 같은 메서드로 최종 업데이트를 진행합니다.

메시지 채널 별로 동의 상태를 업데이트할 수 있습니다.

```javascript
import { HackleSubscriptionOperationsBuilder, HackleSubscriptionStatus } from "@hackler/react-native-sdk"

const operations = new HackleSubscriptionOperationsBuilder()
  .marketing(HackleSubscriptionStatus.SUBSCRIBED)
  .information(HackleSubscriptionStatus.SUBSCRIBED)
  .build()

hackleClient.updatePushSubscriptions(operations);
```

#### 광고성 메시지

광고성 메시지 수신 동의 속성을 설정합니다.

```javascript
import { HackleSubscriptionOperationsBuilder, HackleSubscriptionStatus } from "@hackler/react-native-sdk"

const operations = new HackleSubscriptionOperationsBuilder()
  .marketing(HackleSubscriptionStatus.SUBSCRIBED)
  .build()

hackleClient.updatePushSubscriptions(operations);
```

#### 정보성 메시지

정보성 메시지 수신 동의 속성을 설정합니다.

```javascript
import { HackleSubscriptionOperationsBuilder, HackleSubscriptionStatus } from "@hackler/react-native-sdk"

const operations = new HackleSubscriptionOperationsBuilder()
  .information(HackleSubscriptionStatus.SUBSCRIBED)
  .build()

hackleClient.updatePushSubscriptions(operations);
```

<table><thead><tr><th width="224.09765625">HackleSubscriptionStatus</th><th>설명</th></tr></thead><tbody><tr><td><code>UNKNOWN</code></td><td>수신 동의/거부를 하지 않음 (<code>default</code>)</td></tr><tr><td><code>SUBSCRIPTION</code></td><td>명시적으로 수신 동의</td></tr><tr><td><code>UNSUBSCRIPTION</code></td><td>명시적으로 수신 거부</td></tr></tbody></table>

### 푸시 수신 동의 상태 업데이트

사용자의 푸시 메시지 수신 동의 상태를 업데이트 합니다.

```javascript
import { HackleSubscriptionOperationsBuilder, HackleSubscriptionStatus } from "@hackler/react-native-sdk"

const operations = new HackleSubscriptionOperationsBuilder()
  .marketing(HackleSubscriptionStatus.SUBSCRIBED)
  .information(HackleSubscriptionStatus.SUBSCRIBED)
  .build()

hackleClient.updatePushSubscriptions(operations);
```

### 카카오 메시지 수신 동의 상태 업데이트

사용자의 카카오 메시지 수신 동의 상태를 업데이트 합니다.

```javascript
import { HackleSubscriptionOperationsBuilder, HackleSubscriptionStatus } from "@hackler/react-native-sdk"

const operations = new HackleSubscriptionOperationsBuilder()
  .marketing(HackleSubscriptionStatus.SUBSCRIBED)
  .information(HackleSubscriptionStatus.SUBSCRIBED)
  .build()

hackleClient.updateKakaoSubscriptions(operations);
```

### 문자 메시지 수신 동의 상태 업데이트

사용자의 문자 메시지 수신 동의 상태를 업데이트 합니다.

```javascript
import { HackleSubscriptionOperationsBuilder, HackleSubscriptionStatus } from "@hackler/react-native-sdk"

const operations = new HackleSubscriptionOperationsBuilder()
  .marketing(HackleSubscriptionStatus.SUBSCRIBED)
  .information(HackleSubscriptionStatus.SUBSCRIBED)
  .build()

hackleClient.updateSmsSubscriptions(operations);
```


# 테스트 그룹 분배

A/B 테스트를 진행할 때, 테스트 그룹을 대상으로 사용자를 분배하고 각 테스트 그룹에 해당하는 로직을 작성해야 합니다. 이 때 사용자 분배를 핵클 SDK를 통해 진행할 수 있습니다.

## useVariation or useLoadableVariation

핵클이 제공하는 컴포넌트를 사용하거나 Hooks API를 사용하여 사용자를 특정 그룹으로 분배하고 분배 결과를 전달받을 수 있습니다. 분배 시에는 **실험 키**를 전달해야 합니다.

<table><thead><tr><th width="150">파라미터</th><th width="120">타입</th><th width="110">필수</th><th>제약사항</th></tr></thead><tbody><tr><td>실험 키 (key)</td><td><code>int</code></td><td>필수</td><td><code>-</code></td></tr></tbody></table>

#### 예제

아래 예제 코드의 경우 실험 키 42를 전달하고 있으며, 테스트 그룹은 A와 B 두 개가 존재합니다.

{% tabs %}
{% tab title="Component 사용" %}

```javascript
import {HackleExperiment, HackleVariation} from '@hackler/react-native-sdk';

function App() {
  return (
    // 실험 키가 42인 A/B 테스트에서 사용자에게 노출할 테스트 그룹을 결정합니다.
    // 결정하지 못하는 상황인 경우 테스트 그룹 A를 반환합니다.
    <HackleExperiment experimentKey={42}>
      <HackleVariation variation={"A"}> // 할당받은 그룹에 대한 로직
        <AwesomeFeature />
      </HackleVariation>
      <HackleVariation variation={"B"}>
        <SuperAwesomeFeature />
      </HackleVariation>
    </HackleExperiment>
  )
}
```

{% endtab %}

{% tab title="Hooks API 사용" %}

```javascript
function App() {
  // 실험 키가 42인 A/B 테스트에서 사용자에게 노출할 테스트 그룹을 결정합니다.
  // 결정하지 못하는 상황인 경우 테스트 그룹 A를 반환합니다.
  const variation = useVariation(42)

  // 할당받은 그룹에 대한 로직
  if (variation === "A") return <AwesomeFeature />
  if (variation === "B") return <SuperAwesomeFeature />
  return <AwesomeFeature />
}


// Loadable 사용 시
function App() {
  // 실험 키가 42인 A/B 테스트에서 사용자에게 노출할 테스트 그룹을 결정합니다.
  // 결정하지 못하는 상황인 경우 테스트 그룹 A를 반환합니다.
  const { loaded, result } = useLoadableVariation(42)

  // SDK Loading 전 컴포넌트 비노출
  if (!loaded) return null

  // 할당받은 그룹에 대한 로직
  if (result === "A") return <AwesomeFeature />
  if (result === "B") return <SuperAwesomeFeature />
  return <AwesomeFeature />
}
```

{% endtab %}
{% endtabs %}

## useVariationDetail or useLoadableVariationDetail

`useVariationDetail()` Hooks API는 `useVariation()`와 동일하게 동작하고 분배된 사유를 같이 제공합니다. 이 메소드는 분배가 잘 되고 있는지 살펴볼 때 유용하게 사용할 수 있습니다.

<table><thead><tr><th width="150">파라미터</th><th width="120">타입</th><th width="110">필수</th><th>제약사항</th></tr></thead><tbody><tr><td>실험 키 (key)</td><td><code>int</code></td><td>필수</td><td><code>-</code></td></tr></tbody></table>

#### 예제

파라미터로 실험 키를 전달해야 합니다. 아래 예제 코드의 경우 실험 키 42를 전달하고 있습니다.

```javascript
// 분배 결정 상세
const decision = useVariationDetail(42)

// 분배 그룹
const variation = decision.variation

// 분배 결정 사유
const reason = decision.reason

// 분배 결정 상세 - Loadable 사용 시
const { loaded, result } = useLoadableVariationDetail(42)

// 분배 그룹
const variation = result.variation

// 분배 결정 사유
const reason = result.reason
```

### 분배 사유

분배 결정 사유는 **`SDK_NOT_READY`** 와 같은 형태로 받게 됩니다. 자세한 내용은 아래 표를 참고해주세요.

<table><thead><tr><th width="319.12109375">분배 사유</th><th width="305.35546875">설명</th><th>분배 결과</th></tr></thead><tbody><tr><td><code>SDK_NOT_READY</code></td><td><p>SDK 사용 준비가 되지 않았습니다.</p><p>(예: 잘못된 SDK 키로 초기화 시도)</p></td><td>A (기본 그룹)</td></tr><tr><td><code>EXPERIMENT_NOT_FOUND</code></td><td>전달한 실험 키에 대한 A/B 테스트를 찾을 수 없습니다. 실험 키가 잘못되었거나 해당 실험이 보관 상태일 수 있습니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>NOT_IN_MUTUAL_EXCLUSION_EXPERIMENT</code></td><td>실험이 상호 배타적 설정에 포함되어 있지만<br>해당 상호 배타적 그룹에 할당되지 않은 경우</td><td>A (기본 그룹)</td></tr><tr><td><code>EXPERIMENT_DRAFT</code></td><td>A/B 테스트가 준비 상태입니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>EXPERIMENT_PAUSED</code></td><td>A/B 테스트가 일시 정지 상태입니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>EXPERIMENT_COMPLETED</code></td><td>A/B 테스트가 종료되었습니다.</td><td>종료 시 선택한승리 그룹</td></tr><tr><td><code>OVERRIDDEN</code></td><td>사용자가 수동할당에 의해<br>특정 그룹으로 결정되었습니다.</td><td>수동 할당한<br>그룹</td></tr><tr><td><code>NOT_IN_EXPERIMENT_TARGET</code></td><td>사용자가 A/B 테스트 타겟이 아닙니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>TRAFFIC_NOT_ALLOCATED</code></td><td>A/B 테스트가 실행 중이지만<br>사용자가 테스트에 할당되지 않았습니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>TRAFFIC_ALLOCATED</code></td><td>사용자가 A/B 테스트에 할당되었습니다.</td><td>할당된 그룹</td></tr><tr><td><code>VARIATION_DROPPED</code></td><td>원래 할당된 그룹이 테스트에서 제외되었습니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>INVALID_INPUT</code></td><td>입력값이 유효하지 않습니다.</td><td>A (기본 그룹)</td></tr><tr><td><code>EXCEPTION</code></td><td>알 수 없는 오류가 발생했습니다.</td><td>A (기본 그룹)</td></tr></tbody></table>

### 파라미터

{% hint style="info" %}
React Native SDK 3.3.0 이상 버전에서 지원하는 기능입니다.
{% endhint %}

* `useVariationDetail()` Hooks API를 통해 분배된 그룹의 파라미터 값도 같이 제공받을 수 있습니다.
* config 객체와 `getSync()` 메소드를 통해 A/B 테스트 화면에서 설정한 파라미터 설정 값을 받아 활용할 수 있으며 A/B 테스트의 파라미터 설정 화면에서 값을 변경할 경우, 변경된 값이 코드에 적용됩니다.
* `getSync()` 메소드의 parameterKey는 A/B 테스트의 파라미터 설정에서 설정한 키 정보이며 defaultValue는 분배 결정 실패 시, 또는 잘못된 파라미터 유형의 값을 넣었을 때 Return되는 값입니다.
* 설정한 정보를 제대로 받기 위해서는 defaultValue에 설정하신 파라미터 유형과 동일한 type의 값을 입력해야 합나다.

<table><thead><tr><th width="120">구분</th><th width="200">value type</th><th>설명</th></tr></thead><tbody><tr><td><code>get</code></td><td><code>string</code>, <code>number</code>, <code>boolean</code></td><td><ul><li>설정된 parameter값을 반환합니다.</li><li>JSON 타입은 문자열(String)형태로 받을 수 있습니다.</li><li>JSON 타입의 default 값은 문자열 타입으로 입력해야 합니다.</li></ul></td></tr></tbody></table>

#### 예제

```javascript
// 분배 결정 상세
const decision = useVariationDetail(42)

//분배 결정 상세에서 getSync() 메소드를 통해 parameter 값 가져오기
const parameterValue = decision.getSync("parameterKey", "defaultValue")

// string 유형의 parameter값 예제
const strValue = decision.getSync("parmeterKey", "defaultValue")

// 분배 결정 상세 - Loadable 사용 시
const { loaded, result } = useLoadableVariationDetail(42)

//분배 결정 상세에서 getSync() 메소드를 통해 parameter 값 가져오기
const parameterValue = result.getSync("parameterKey", "defaultValue")

// string 유형의 parameter값 예제
const strValue = result.getSync("parmeterKey", "defaultValue")
```


# 기능 플래그 결정

기능 플래그는 켜짐(on) 상태와 꺼짐(off) 상태가 있습니다. 각 상태에 따라 다른 기능을 설정하게 됩니다. 기능 플래그를 적용한 기능에 어떤 사용자가 접근할 경우 켜짐 혹은 꺼짐 상태를 받을 수 있어야 합니다. 이 상태 결정을 핵클 SDK를 통해 진행할 수 있습니다.

## useFeature or useLoadableFeature

핵클이 제공하는 Hooks API를 사용하여 사용자에 대한 상태 결과를 전달받을 수 있습니다. 이 때 **기능 키**를 전달해야 하며, 이후 상태에 따른 로직을 구현합니다.

아래 예제 코드에서는 기능 키 42를 전달하고 있습니다.

{% tabs %}
{% tab title="Hooks API 사용 (1)" %}

```javascript
function App() {
  return (
    // 기능 키가 42인 기능 플래그에서 사용자의 상태를 결정합니다.
    // 결정하지 못하는 상황인 경우 false(꺼짐 상태)를 반환합니다.
    <Feature featureKey={42}>
      {(featureOn) =>
        featureOn ? (
          <SuperAwesomeFeature /> // 켜짐 상태일 때의 기능
        ) : (
          <AwesomeFeature /> // 꺼짐 상태일 때의 기능
        )
      }
    </Feature>
  )
}
```

{% endtab %}

{% tab title="Hooks API 사용 (2)" %}

```javascript
function App() {
  // 기능 키가 42인 기능 플래그에서 사용자의 상태를 결정합니다.
  // 결정하지 못하는 상황인 경우 false(꺼짐 상태)를 반환합니다.
  const featureOn = useFeature(42)
  return (
    <>
    {
      featureOn ? (
        <SuperAwesomeFeature /> // 켜짐 상태일 때의 기능
      ) : (
        <AwesomeFeature /> // 꺼짐 상태일 때의 기능
      )
    }
    </>
  )
}

// Loadable 사용 시
function App() {
  // 기능 키가 42인 기능 플래그에서 사용자의 상태를 결정합니다.
  // 결정하지 못하는 상황인 경우 false(꺼짐 상태)를 반환합니다.
  const { loaded, result } = useLoadableFeature(42)
  return (
    <>
    {
      result ? (
        <SuperAwesomeFeature /> // 켜짐 상태일 때의 기능
      ) : (
        <AwesomeFeature /> // 꺼짐 상태일 때의 기능
      )
    }
    </>
  )
}
```

{% endtab %}
{% endtabs %}

## useFeatureFlagDetail or useLoadableFeatureDetail

`useFeatureFlagDetail()` Hooks API는 `useFeature()`와 동일하게 동작하고 추가로 상태 결정에 대한 사유를 같이 제공합니다. 수동할당이 잘 되고 있는지 알아보거나 설정한 트래픽 할당 대비 결과 비중이 이상하다고 여길 때 유용하게 활용할 수 있습니다.

파라미터로 기능 키를 전달해야 합니다. 아래 예제 코드의 경우 기능 키 42를 전달하고 있습니다.

```javascript
// 분배 결정 상세
const decision = useFeatureFlagDetail(42)

// 분배 그룹
const isOn = decision.isOn

// 분배 결정 사유 
const reason = decision.reason


// 분배 결정 상세 - Loadable 사용 시
const { loaded, result } = useLoadableFeatureDetail(42)

// 분배 그룹
const isOn = result.isOn

// 분배 결정 사유
const reason = result.reason
```

상태 결정 사유는 **`SDK_NOT_READY`** 와 같은 형태로 받게 됩니다. 자세한 내용은 아래 표를 참고해주세요.

<table><thead><tr><th width="233.71875">결정 사유</th><th width="370.8046875">설명</th><th>분배 결과</th></tr></thead><tbody><tr><td><code>SDK_NOT_READY</code></td><td>SDK 사용 준비가 되지 않았습니다.<br>(예: 잘못된 SDK 키로 초기화 시도)</td><td>기본 상태<br>(꺼짐/off)</td></tr><tr><td><code>FEATURE_FLAG_NOT_FOUND</code></td><td>전달한 기능 키에 대한 기능 플래그를 찾을 수 없습니다.<br>기능 키가 잘못되었거나 해당 기능 플래그가 보관 상태일 수 있습니다.</td><td>기본 상태<br>(꺼짐/off)</td></tr><tr><td><code>FEATURE_FLAG_INACTIVE</code></td><td>기능 플래그가 꺼짐 상태입니다</td><td>기본 상태<br>(꺼짐/off)</td></tr><tr><td><code>INDIVIDUAL_TARGET_MATCH</code></td><td>개별 타겟팅에 매치 되었습니다.</td><td>개별 타겟팅으로<br>설정한 상태</td></tr><tr><td><code>TARGET_RULE_MATCH</code></td><td>사용자 타겟팅에 매치 되었습니다.</td><td>사용자 타겟팅으로 설정한 상태</td></tr><tr><td><code>DEFAULT_RULE</code></td><td>개별 타겟팅, 사용자 타겟팅 중 어디에도 매치 되지 않았습니다.</td><td>기본 룰로<br>설정한 상태</td></tr><tr><td><code>EXCEPTION</code></td><td>알 수 없는 오류가 발생했습니다.</td><td>기본 상태<br>(꺼짐/off)</td></tr></tbody></table>

## 기능 플래그 파라미터

{% hint style="info" %}
React Native SDK 3.3.0 이상 버전에서 지원하는 기능입니다.
{% endhint %}

* `useFeatureFlagDetail()` Hooks API를 통해 상태 결정에 대한 파라미터 값도 같이 제공받을 수 있습니다.
* config 객체와 `getSync()` 메소드를 통해 기능 플래그 화면에서 설정한 파라미터 설정 값을 받아 활용할 수 있으며 기능 플래그의 파라미터 설정 화면에서 값을 변경할 경우, 변경된 값이 코드에 적용됩니다.

```javascript
// 분배 결정 상세
const decision = useFeatureFlagDetail(42)

//분배 결정 상세에서 getSync() 메소드를 통해 parameter 값 가져오기
const parameterValue = decision.getSync("parameterKey", "defaultValue")

// string 유형의 parameter값 예제
const strValue = decision.getSync("parmeterKey", "defaultValue")


// 분배 결정 상세 - Loadable 사용 시
const { loaded, result } = useLoadableFeatureFlagDetail(42)

//분배 결정 상세에서 getSync() 메소드를 통해 parameter 값 가져오기
const parameterValue = result.getSync("parameterKey", "defaultValue")

// string 유형의 parameter값 예제
const strValue = result.getSync("parmeterKey", "defaultValue")
```

* `getSync()` 메소드의 parameterKey는 기능 플래그의 파라미터 설정에서 설정한 키 정보이며 defaultValue는 상태 결정 실패 시, 또는 잘못된 파라미터 유형의 값을 넣었을 때 Return되는 값입니다.
* 설정한 정보를 제대로 받기 위해서는 defaultValue에 설정하신 파라미터 유형과 동일한 type의 값을 입력해야 합나다.
* JSON 타입은 문자열(String)형태로 받을 수 있으므로, JSON 타입의 경우 defaultValue를 문자열 타입으로 입력해야 합니다.
* SDK에서 제공되는 파라미터 유형은 string, number, boolean 이며 기능 플래그 화면에서 설정한 JSON 타입은 문자열(String)형태로 받을 수 있습니다. JSON 타입의 default 값은 문자열 타입으로 입력해야 합니다.


# 원격 구성 적용

{% hint style="info" %}
React Native SDK 3.3.0 이상 버전에서 지원하는 기능입니다.
{% endhint %}

원격 구성은 애플리케이션에서 관리되고 있는 값, 또는 속성들을 핵클 대시보드에서 정의한 파라미터 값들로 대체하여 실시간으로 애플리케이션의 동작 및 설정 값들을 제어할 수 있는 기능입니다.

핵클의 대시보드의 원격 구성 화면으로 이동하여 파라미터 정보들을 설정하고, 사용자 식별 규칙에 따른 값들을 설정할 수 있습니다.

## useRemoteConfig() or useLoadableRemoteConfig()

`useRemoteConfig()` 또는 `useLoadableRemoteConfig()` Hooks API를 사용하면 사용자에 대한 원격 구성 정보(설정한 파라미터 및 규칙 정보)를 담고 있는 `HackleRemoteConfig` 인스턴스를 얻을 수 있습니다. `HackleRemoteConfig` 에서 제공하는 메소드들을 통해 원하는 파라미터에 접근하여 값을 제공받을 수 있습니다.

```javascript
// 원격 구성 정보를 담은 인스턴스를 반환합니다. 
const remoteConfig = useRemoteConfig()

// 원격 구성 정보를 담은 인스턴스를 반환합니다. - Lodable 사용 시
const {loaded, result} = useLoadableRemoteConfig()
```

## 원격 구성 파라미터 조회

* `useRemoteConfig()` 또는 `useLoadableRemoteConfig()` Hooks API를 사용하여 반환받은 `HackleRemoteConfig`에는 파라미터 값 조회를 위한 `getSync()`메소드를 제공합니다.
* 핵클의 원격 구성 화면에서 설정한 파라미터 값이 key, value 형태로 존재하기 때문에, 설정한 파라미터 유형에 따라 아래 메소드를 사용하여 설정한 파라미터 값을 반환받을 수 있습니다.

{% hint style="warning" %}
보관 후 원격 구성과 관련된 코드를 제거하세요.

원격 구성 파라미터를 보관한 경우 더이상 파라미터 정보에 접근 할 수 없습니다. 따라서 원격 구성 파라미터 보관 후에는 반드시 관련된 코드를 정리해주시기 바랍니다.
{% endhint %}

```javascript
// 원격 구성 정보를 담은 인스턴스를 반환합니다. 
const remoteConfig = useRemoteConfig()

//remoteConfig 에서 get() 메소드를 통해 parameter 값 가져오기
const parameterValue = remoteConfig.getSync(parameterKey, defaultValue)

// string 유형의 parameter값 예제
const strValue = remoteConfig.getSync("parmeterKey", "defaultValue")


// 원격 구성 정보를 담은 인스턴스를 반환합니다. - Lodable 사용 시
const { loaded, result } = useLoadableRemoteConfig()

if(loaded) {
  const parameterValue = result.getSync(parameterKey, defaultValue) 
  const parameterValuePromise = result.get(parameterKey, defaultValue)
}
```

* getSync() 메소드의 parameterKey는 원격 구성의 파라미터 설정에서 설정한 키 정보입니다.
* defaultValue는 원격 구성 값을 결정할 수 없을 때 반환되는 값입니다. 입력한 defaultValue는 다음과 같은 상황에서 반환될 수 있습니다.\
  A. 원격 구성화면에서 설정한 타입 유형과 다른 유형의 값을 입력\
  B. 설정되지 않은 parameter key 호출\
  C. Hackle SDK 초기화 실패\
  D. 잘못된 식별자 정보가 입력되거나 존재하지 않을 때\
  E. ETC
* 설정한 정보를 올바르게 받기 위해서는 입력하신 defaultValue와 핵클 원격 구성 파라미터 화면에서 설정한 유형(type)이 동일해야 합니다.
* SDK에서 제공되는 원격 구성 파라미터 유형은 string, number, boolean 이며 원격 구성 파라미터 화면에서 설정한 JSON 타입은 문자열(String)형태로 받을 수 있습니다. JSON 타입의 default 값은 문자열 타입으로 입력해야 합니다.




---

[Next Page](/llms-full.txt/1)

