메인 콘텐츠로 건너뛰기

개요

이 튜토리얼에서는 단일 HTML 인앱 메시지를 사용하여 다단계 온보딩 캐러셀을 만드는 방법을 보여줍니다. 스와이프 제스처에 의존하는 기존 캐러셀과 달리, 이 접근 방식은 버튼 기반 내비게이션을 사용하고 모든 단계를 하나의 메시지 내에 유지합니다. 구축할 내용:
  • 이미지, 텍스트, 버튼이 포함된 2단계 온보딩 플로우
  • 버튼 내비게이션 (“다음”을 탭하여 진행, “시작하기”를 탭하여 닫기)
  • 진행 표시 점
  • 단계 간 부드러운 페이드 전환
이미지, 텍스트, 다음 버튼이 있는 환영 화면을 보여주는 온보딩 캐러셀
이 접근 방식을 사용해야 할 때:
  • 사용자를 짧은 온보딩 또는 교육 플로우(2-5단계)를 통해 안내하려는 경우
  • 사용자가 계속하려면 명시적으로 버튼을 탭해야 하는 경우 (스와이프 제스처 없음)
  • 단순화를 위해 모든 것을 하나의 HTML 인앱 메시지 내에 유지하려는 경우
  • 플로우가 완료되면 메시지를 자동으로 닫으려는 경우
이 가이드는 완전한 제어를 위해 HTML 인앱 메시지를 사용합니다. 드래그 앤 드롭 편집기로 카드 기반 온보딩 플로우를 구축할 수도 있습니다—해당 카드는 스와이프 가능하지만 커스터마이징 옵션이 적습니다.

사전 요구 사항

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

다단계 플로우 작동 방식

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

각 단계의 카드 컨테이너

각 단계는 card 클래스와 고유 ID를 가진 <div>로 래핑됩니다:
<div id="card-0" class="card active">...</div>
<div id="card-1" class="card">...</div>
  • 모든 카드가 동시에 DOM에 존재합니다
  • 한 번에 하나의 카드만 표시됩니다 (active 클래스로 제어)
2

CSS 가시성 제어

CSS는 불투명도와 포인터 이벤트를 사용하여 표시/숨기기 로직을 처리합니다:
.card {
  opacity: 0;
  pointer-events: none;  /* 숨겨진 카드와의 상호작용 방지 */
  transition: opacity .25s ease;
}

.card.active {
  opacity: 1;
  pointer-events: auto;  /* 보이는 카드와의 상호작용 허용 */
}
이것이 중요한 이유:
  • opacity: 0은 카드를 시각적으로 숨기지만 레이아웃에는 유지됩니다
  • pointer-events: none은 숨겨진 카드에 대한 실수로 인한 클릭을 방지합니다
  • transition은 부드러운 페이드 효과를 생성합니다
3

JavaScript 상태 관리

setActive(i) 함수가 어떤 카드가 보이는지 제어합니다:
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를 추가합니다
  • 진행 표시 점을 업데이트합니다
4

버튼 이벤트 리스너

버튼이 내비게이션 또는 닫기를 트리거합니다:
// 다음 단계로 진행
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 메서드입니다.
핵심 포인트: 이것은 인앱 메시지 내의 단일 페이지 애플리케이션(SPA) 패턴입니다. 모든 콘텐츠는 한 번 로드되고, JavaScript가 리로드 없이 상태 변경을 관리합니다.

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

  1. OneSignal 대시보드에서 Messages → In-App Messages로 이동합니다
  2. New In-App Message를 클릭합니다
  3. 메시지 유형으로 HTML을 선택합니다
  4. Full Screen 또는 Large 레이아웃을 선택합니다 (시각적 임팩트를 극대화하기 위해 온보딩에 권장)
  5. HTML 편집기로 계속 진행합니다
HTML 편집기 미리보기가 런타임 동작을 완전히 반영하지 않을 수 있습니다. 애니메이션, 버튼 동작, 닫기 동작을 확인하려면 항상 실제 기기 또는 테스트 사용자에서 테스트하세요.

2단계: HTML 템플릿 추가

편집기 내용을 아래 템플릿으로 교체합니다. 이 템플릿에는 다음이 포함됩니다:
  • 자체 포함 코드: 모든 HTML, CSS, JavaScript가 하나의 파일에
  • 버튼 기반 내비게이션: 스와이프 제스처 없음 (다양한 기기에서 더 안정적)
  • 페이드 전환: 단계 간 부드러운 불투명도 변화
  • OneSignal SDK 통합: 메시지를 닫기 위해 OneSignalIamApi.close(e) 사용
  • 모바일 최적화: viewport 메타 태그가 있는 반응형 레이아웃
<!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>

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

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

기능을 손상시키지 않고 다음 요소를 수정할 수 있습니다: 콘텐츠:
  • <h1> 태그의 헤드라인 텍스트
  • <p> 태그의 본문 카피
  • 버튼 라벨 (다음, 시작하기)
  • background-image: url('...') 스타일의 이미지 URL
시각적 스타일:
  • 색상: .btn 배경, 텍스트 색상 또는 점 색상 변경
  • 간격: 패딩과 마진 조정
  • 타이포그래피: font-family, font-size, font-weight 수정
  • 테두리 반경: 버튼과 이미지의 border-radius 값 업데이트

단계 추가

세 번째 단계를 추가하려면 다음 패턴을 따르세요:
  1. HTML 카드 추가:
<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>
  1. 진행 점 추가:
<div class="dots" id="dots">
  <div class="dot active"></div>
  <div class="dot"></div>
  <div class="dot"></div> <!-- 새 점 -->
</div>
  1. setActive() 함수 업데이트:
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); // 새 점
}
  1. 이전 단계의 버튼 ID 업데이트: 카드 1의 버튼에서 id="done"id="next-1"로 변경하고 클릭 리스너를 추가:
document.getElementById("next-1").addEventListener("click", function () {
  setActive(2);
});
  1. 새 마지막 카드(card-2)에 닫기 버튼 추가:
document.getElementById("done").addEventListener("click", function (e) {
  if (window.OneSignalIamApi && OneSignalIamApi.close) {
    OneSignalIamApi.close(e);
  }
});
온보딩 플로우는 짧게 유지하세요 (최대 2-4단계). 사용자는 긴 플로우에서 빠르게 이탈합니다. 클릭 추적으로 완료율을 테스트하세요.

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

테스트 체크리스트

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

일반적인 문제 해결

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

다음 단계

사용자 참여 추적:
  • 단계 간 이탈을 측정하기 위해 data-onesignal-unique-label 속성을 사용하여 클릭 추적 추가 (템플릿에 이미 포함됨)
  • Messages → In-App Messages → [메시지] → Analytics에서 클릭 분석 보기
경험 개인화: 고급 커스터마이징:
  • 닫은 후 사용자를 특정 화면으로 딥링크
  • Liquid 구문을 사용하여 사용자 이름이나 속성으로 헤드라인 개인화
  • 완료율을 최적화하기 위해 다양한 온보딩 플로우로 A/B 테스트 구현