> ## Documentation Index
> Fetch the complete documentation index at: https://documentation.onesignal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 버튼 기반 온보딩 캐러셀 구축하기

> 버튼 내비게이션과 자동 닫기 기능이 있는 HTML 인앱 메시지를 사용하여 다단계 온보딩 플로우를 만듭니다.

## 개요

이 튜토리얼에서는 단일 HTML 인앱 메시지를 사용하여 **다단계 온보딩 캐러셀**을 만드는 방법을 보여줍니다. 스와이프 제스처에 의존하는 기존 캐러셀과 달리, 이 접근 방식은 **버튼 기반 내비게이션**을 사용하고 모든 단계를 하나의 메시지 내에 유지합니다.

**구축할 내용:**

* 이미지, 텍스트, 버튼이 포함된 2단계 온보딩 플로우
* 버튼 내비게이션 ("다음"을 탭하여 진행, "시작하기"를 탭하여 닫기)
* 진행 표시 점
* 단계 간 부드러운 페이드 전환

<Frame caption="버튼 기반 내비게이션을 사용한 온보딩 캐러셀 예시">
  <img src="https://mintcdn.com/onesignal/t_Vr4vLAhranGx-O/images/docs/onboarding-carousel-demo.gif?s=39418d8af9896323694f46f9cb6eebec" alt="이미지, 텍스트, 다음 버튼이 있는 환영 화면을 보여주는 온보딩 캐러셀" style={{width: '200px', maxWidth: '100%'}} width="768" height="1660" data-path="images/docs/onboarding-carousel-demo.gif" />
</Frame>

**이 접근 방식을 사용해야 할 때:**

* 사용자를 **짧은 온보딩 또는 교육 플로우**(2-5단계)를 통해 안내하려는 경우
* 사용자가 계속하려면 **명시적으로 버튼을 탭**해야 하는 경우 (스와이프 제스처 없음)
* 단순화를 위해 모든 것을 **하나의 HTML 인앱 메시지** 내에 유지하려는 경우
* 플로우가 완료되면 메시지를 자동으로 닫으려는 경우

<Info>
  이 가이드는 완전한 제어를 위해 HTML 인앱 메시지를 사용합니다. [드래그 앤 드롭 편집기로 카드 기반 온보딩 플로우를 구축](./design-your-in-app-message#carousels)할 수도 있습니다—해당 카드는 스와이프 가능하지만 커스터마이징 옵션이 적습니다.
</Info>

***

## 사전 요구 사항

시작하기 전에 다음을 확인하세요:

* 인앱 메시지가 활성화된 활성 OneSignal 앱
* [HTML 인앱 메시지를 생성하거나 편집할 권한](./manage-team-members#team-members)
* 모바일 앱에 [Mobile SDK](./mobile-sdk-setup) 설치됨
* HTML, CSS, JavaScript에 대한 기본적인 이해

***

## 다단계 플로우 작동 방식

코드를 살펴보기 전에 기술적 접근 방식을 이해하는 것이 중요합니다. 이 구현은 **하나의 HTML 인앱 메시지**를 사용하며, 여러 개의 별도 메시지를 로드하는 것이 아니라 **콘텐츠를 표시하고 숨김**으로써 단계 간을 전환합니다.

아키텍처는 **네 가지 핵심 구성 요소**에 의존합니다:

<Steps>
  <Step title="각 단계의 카드 컨테이너">
    각 단계는 `card` 클래스와 고유 ID를 가진 `<div>`로 래핑됩니다:

    ```html theme={null}
    <div id="card-0" class="card active">...</div>
    <div id="card-1" class="card">...</div>
    ```

    * 모든 카드가 동시에 DOM에 존재합니다
    * 한 번에 하나의 카드만 표시됩니다 (`active` 클래스로 제어)
  </Step>

  <Step title="CSS 가시성 제어">
    CSS는 불투명도와 포인터 이벤트를 사용하여 표시/숨기기 로직을 처리합니다:

    ```css theme={null}
    .card {
      opacity: 0;
      pointer-events: none;  /* 숨겨진 카드와의 상호작용 방지 */
      transition: opacity .25s ease;
    }

    .card.active {
      opacity: 1;
      pointer-events: auto;  /* 보이는 카드와의 상호작용 허용 */
    }
    ```

    **이것이 중요한 이유:**

    * `opacity: 0`은 카드를 시각적으로 숨기지만 레이아웃에는 유지됩니다
    * `pointer-events: none`은 숨겨진 카드에 대한 실수로 인한 클릭을 방지합니다
    * `transition`은 부드러운 페이드 효과를 생성합니다
  </Step>

  <Step title="JavaScript 상태 관리">
    `setActive(i)` 함수가 어떤 카드가 보이는지 제어합니다:

    ```javascript theme={null}
    function setActive(i) {
      // 카드 가시성 업데이트
      document.getElementById("card-0").className = i === 0 ? "card active" : "card";
      document.getElementById("card-1").className = i === 1 ? "card active" : "card";

      // 진행 점 업데이트
      var dots = document.getElementById("dots").children;
      dots[0].classList.toggle("active", i === 0);
      dots[1].classList.toggle("active", i === 1);
    }
    ```

    이 함수는:

    * 모든 카드에서 `active`를 제거합니다
    * 대상 카드에 `active`를 추가합니다
    * 진행 표시 점을 업데이트합니다
  </Step>

  <Step title="버튼 이벤트 리스너">
    버튼이 내비게이션 또는 닫기를 트리거합니다:

    ```javascript theme={null}
    // 다음 단계로 진행
    document.getElementById("next-0").addEventListener("click", function () {
      setActive(1);
    });

    // 인앱 메시지 닫기
    document.getElementById("done").addEventListener("click", function (e) {
      if (window.OneSignalIamApi && OneSignalIamApi.close) {
        OneSignalIamApi.close(e);
      }
    });
    ```

    **중요:** `OneSignalIamApi.close(e)`는 HTML 내에서 인앱 메시지를 닫는 OneSignal SDK 메서드입니다.
  </Step>
</Steps>

<Note>
  **핵심 포인트:** 이것은 인앱 메시지 내의 **단일 페이지 애플리케이션(SPA)** 패턴입니다. 모든 콘텐츠는 한 번 로드되고, JavaScript가 리로드 없이 상태 변경을 관리합니다.
</Note>

***

## 1단계: 새 HTML 인앱 메시지 생성

1. OneSignal 대시보드에서 **Messages → In-App Messages**로 이동합니다
2. **New In-App Message**를 클릭합니다
3. 메시지 유형으로 **HTML**을 선택합니다
4. **Full Screen** 또는 **Large** 레이아웃을 선택합니다 (시각적 임팩트를 극대화하기 위해 온보딩에 권장)
5. HTML 편집기로 계속 진행합니다

<Note>
  HTML 편집기 미리보기가 런타임 동작을 완전히 반영하지 않을 수 있습니다. 애니메이션, 버튼 동작, 닫기 동작을 확인하려면 항상 실제 기기 또는 테스트 사용자에서 테스트하세요.
</Note>

***

## 2단계: HTML 템플릿 추가

편집기 내용을 아래 템플릿으로 교체합니다. 이 템플릿에는 다음이 포함됩니다:

* **자체 포함 코드:** 모든 HTML, CSS, JavaScript가 하나의 파일에
* **버튼 기반 내비게이션:** 스와이프 제스처 없음 (다양한 기기에서 더 안정적)
* **페이드 전환:** 단계 간 부드러운 불투명도 변화
* **OneSignal SDK 통합:** 메시지를 닫기 위해 `OneSignalIamApi.close(e)` 사용
* **모바일 최적화:** viewport 메타 태그가 있는 반응형 레이아웃

<Accordion title="전체 HTML 템플릿 보기">
  ```html theme={null}
  <!doctype html>
  <html>
  <head>
    <meta charset="UTF-8" />
    <!-- viewport-fit=cover는 노치가 있는 기기에서 안전 영역 커버리지를 보장합니다 -->
    <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
    <style>
      /* 기본 스타일 - 리셋 및 시스템 폰트 */
      html, body {
        margin: 0;
        padding: 0;
        background: #ffffff;
        font-family: -apple-system, system-ui;
      }

      /* 패딩이 있는 메인 컨테이너 */
      .wrap {
        padding: 28px 22px 24px;
      }

      /* 스테이지 컨테이너 - 모든 카드를 같은 위치에 유지 */
      .stage {
        position: relative;
        min-height: 74vh;  /* 충분한 수직 공간 확보 */
      }

      /* 카드 - 온보딩 플로우의 각 단계 */
      .card {
        position: absolute;  /* 모든 카드가 같은 위치에 겹침 */
        inset: 0;            /* 스테이지 전체 커버 */
        display: flex;
        flex-direction: column;
        align-items: center;
        opacity: 0;               /* 기본적으로 숨김 */
        pointer-events: none;     /* 숨겨졌을 때 클릭 방지 */
        transition: opacity .25s ease;  /* 부드러운 페이드 효과 */
      }

      /* 활성 카드는 보이고 상호작용 가능 */
      .card.active {
        opacity: 1;
        pointer-events: auto;
      }

      /* 타이포그래피 */
      h1 {
        margin: 44px 0 12px;
        font-size: 26px;
        text-align: center;
      }

      p {
        margin: 0;
        color: #6b7280;
        text-align: center;
        max-width: 260px;
        line-height: 1.35;
      }

      /* 이미지 컨테이너 - 둥근 모서리의 정사각형 */
      .image {
        width: 240px;
        height: 240px;
        border-radius: 16px;
        margin: 24px 0 12px;
        background-size: cover;
        background-position: center;
      }

      /* 기본 버튼 */
      .btn {
        margin-top: auto;  /* 버튼을 카드 하단으로 밀기 */
        width: 100%;
        max-width: 260px;
        height: 52px;
        border: 0;
        border-radius: 12px;
        background: #3b82f6;  /* 파란색 - 브랜드에 맞게 커스터마이징 */
        color: #fff;
        font-size: 18px;
        font-weight: 600;
      }

      /* 진행 표시 점 */
      .dots {
        display: flex;
        justify-content: center;
        gap: 8px;
        padding: 12px 0 8px;
      }

      .dot {
        width: 8px;
        height: 8px;
        border-radius: 999px;
        background: #d1d5db;  /* 비활성 점 색상 */
      }

      .dot.active {
        background: #6b7280;  /* 활성 점 색상 */
        transform: scale(1.15);  /* 활성 시 약간 더 크게 */
      }
    </style>
  </head>

  <body>
    <div class="wrap">
      <div class="stage">

        <!-- 1단계: 환영 카드 ("active" 클래스로 표시 시작) -->
        <div id="card-0" class="card active">
          <h1>환영합니다</h1>
          <div
            class="image"
            style="background-image: url('https://images.pexels.com/photos/6153129/pexels-photo-6153129.jpeg');">
          </div>
          <p>몇 분 만에 차분한 일상 습관을 만들어 보세요.</p>
          <button
            id="next-0"
            class="btn"
            data-onesignal-unique-label="onboarding_next_0">
            다음
          </button>
        </div>

        <!-- 2단계: 호흡 카드 (숨겨진 상태로 시작, 사용자가 "다음"을 탭하면 표시) -->
        <div id="card-1" class="card">
          <h1>호흡</h1>
          <div
            class="image"
            style="background-image: url('https://images.pexels.com/photos/417173/pexels-photo-417173.jpeg');">
          </div>
          <p>리셋이 필요할 때마다 가이드 호흡을.</p>
          <button
            id="done"
            class="btn"
            data-onesignal-unique-label="onboarding_done">
            시작하기
          </button>
        </div>

      </div>

      <!-- 진행 표시기: 2개의 점, 첫 번째가 활성 상태로 시작 -->
      <div class="dots" id="dots">
        <div class="dot active"></div>
        <div class="dot"></div>
      </div>
    </div>

    <script>
      (function () {
        /**
         * "active" 클래스를 토글하여 카드 간 전환
         * @param {number} i - 표시할 카드의 인덱스 (0 또는 1)
         */
        function setActive(i) {
          // 카드 가시성 업데이트
          document.getElementById("card-0").className = i === 0 ? "card active" : "card";
          document.getElementById("card-1").className = i === 1 ? "card active" : "card";

          // 진행 점 업데이트
          var dots = document.getElementById("dots").children;
          dots[0].classList.toggle("active", i === 0);
          dots[1].classList.toggle("active", i === 1);
        }

        // 버튼: 다음 (카드 0 → 카드 1)
        document.getElementById("next-0").addEventListener("click", function () {
          setActive(1);
        });

        // 버튼: 시작하기 (인앱 메시지 닫기)
        document.getElementById("done").addEventListener("click", function (e) {
          // OneSignal IAM API 사용 가능 여부 확인
          if (window.OneSignalIamApi && OneSignalIamApi.close) {
            OneSignalIamApi.close(e);  // 메시지 닫기
          }
        });
      })();
    </script>
  </body>
  </html>
  ```
</Accordion>

***

## 3단계: 콘텐츠 커스터마이징

### 안전하게 커스터마이징할 수 있는 항목

기능을 손상시키지 않고 다음 요소를 수정할 수 있습니다:

**콘텐츠:**

* `<h1>` 태그의 헤드라인 텍스트
* `<p>` 태그의 본문 카피
* 버튼 라벨 (`다음`, `시작하기`)
* `background-image: url('...')` 스타일의 이미지 URL

**시각적 스타일:**

* 색상: `.btn` 배경, 텍스트 색상 또는 점 색상 변경
* 간격: 패딩과 마진 조정
* 타이포그래피: font-family, font-size, font-weight 수정
* 테두리 반경: 버튼과 이미지의 `border-radius` 값 업데이트

### 단계 추가

세 번째 단계를 추가하려면 다음 패턴을 따르세요:

1. **HTML 카드 추가:**

```html theme={null}
<div id="card-2" class="card">
  <h1>제목</h1>
  <div class="image" style="background-image: url('your-image-url');"></div>
  <p>설명</p>
  <button id="next-2" class="btn">다음</button>
</div>
```

2. **진행 점 추가:**

```html theme={null}
<div class="dots" id="dots">
  <div class="dot active"></div>
  <div class="dot"></div>
  <div class="dot"></div> <!-- 새 점 -->
</div>
```

3. **`setActive()` 함수 업데이트:**

```javascript theme={null}
function setActive(i) {
  document.getElementById("card-0").className = i === 0 ? "card active" : "card";
  document.getElementById("card-1").className = i === 1 ? "card active" : "card";
  document.getElementById("card-2").className = i === 2 ? "card active" : "card"; // 새 카드

  var dots = document.getElementById("dots").children;
  dots[0].classList.toggle("active", i === 0);
  dots[1].classList.toggle("active", i === 1);
  dots[2].classList.toggle("active", i === 2); // 새 점
}
```

4. **이전 단계의 버튼 ID 업데이트:**
   카드 1의 버튼에서 `id="done"`을 `id="next-1"`로 변경하고 클릭 리스너를 추가:

```javascript theme={null}
document.getElementById("next-1").addEventListener("click", function () {
  setActive(2);
});
```

5. **새 마지막 카드(card-2)에 닫기 버튼 추가:**

```javascript theme={null}
document.getElementById("done").addEventListener("click", function (e) {
  if (window.OneSignalIamApi && OneSignalIamApi.close) {
    OneSignalIamApi.close(e);
  }
});
```

<Warning>
  온보딩 플로우는 짧게 유지하세요 (최대 2-4단계). 사용자는 긴 플로우에서 빠르게 이탈합니다. [클릭 추적](./in-app-message-api#click-name)으로 완료율을 테스트하세요.
</Warning>

***

## 4단계: 인앱 메시지 테스트

### 테스트 체크리스트

1. OneSignal 대시보드에서 메시지를 **저장**합니다
2. **전송 설정 구성:**
   * 트리거 조건 설정 (예: 세션 시작, 특정 페이지 조회)
   * 대상 오디언스 선택 또는 테스트 사용자 선택
3. **테스트 기기로 전송:**
   * [테스트 사용자](./test-users)를 사용하여 프로덕션 사용자에게 영향을 주지 않고 미리보기
   * 물리적 기기에 앱 설치 (정확한 동작을 위해 시뮬레이터보다 권장)
4. **기능 확인:**
   * ✓ 첫 번째 카드가 올바른 콘텐츠로 나타남
   * ✓ "다음" 버튼이 카드 2로 진행
   * ✓ 진행 점이 올바르게 업데이트됨
   * ✓ 페이드 전환이 부드러움
   * ✓ "시작하기" 버튼이 메시지를 닫음
   * ✓ 메시지가 즉시 다시 나타나지 않음 (빈도 제한 설정 확인)

<Note>
  시뮬레이터/에뮬레이터는 특히 터치 상호작용과 SDK 통합에서 실제 기기 동작을 정확하게 반영하지 않을 수 있습니다. 프로덕션에 출시하기 전에 항상 물리적 기기에서 테스트하세요.
</Note>

### 일반적인 문제 해결

| 문제                | 가능한 원인                  | 해결책                                                      |
| ----------------- | ----------------------- | -------------------------------------------------------- |
| 메시지가 나타나지 않음      | 트리거 조건이 충족되지 않음         | [인앱 메시지 트리거](./iam-triggers)를 확인하고 테스트 사용자가 기준을 충족하는지 확인 |
| 버튼이 작동하지 않음       | JavaScript 오류 또는 ID 불일치 | 브라우저 콘솔에서 오류 확인; 버튼 ID가 이벤트 리스너 ID와 일치하는지 확인             |
| 이미지가 로드되지 않음      | CORS 문제 또는 잘못된 URL      | HTTPS URL 사용; 먼저 브라우저에서 이미지 URL 테스트                      |
| 메시지는 나타나지만 닫히지 않음 | OneSignal SDK가 로드되지 않음  | [Mobile SDK 설정](./mobile-sdk-setup)이 완료되었는지 확인           |

***

## 다음 단계

**사용자 참여 추적:**

* 단계 간 이탈을 측정하기 위해 [`data-onesignal-unique-label`](./in-app-message-api#click-name) 속성을 사용하여 클릭 추적 추가 (템플릿에 이미 포함됨)
* **Messages → In-App Messages → \[메시지] → Analytics**에서 클릭 분석 보기

**경험 개인화:**

* 온보딩을 완료한 [사용자에게 태그 지정](./in-app-message-api#tag-user) (예: `onboarding_completed: true`)
* 태그를 사용하여 [사용자 세그먼트](./segmentation)하고 온보딩 플로우 재표시 방지
* [사용자 데이터 추가](./add-user-data-tags)하여 향후 메시지에서 콘텐츠 개인화

**고급 커스터마이징:**

* 닫은 후 사용자를 특정 화면으로 [딥링크](./deep-linking#send-in-app-messages-with-deep-links)
* [Liquid 구문](./using-liquid-syntax)을 사용하여 사용자 이름이나 속성으로 헤드라인 개인화
* 완료율을 최적화하기 위해 다양한 온보딩 플로우로 A/B 테스트 구현
