incentoincento 개발자센터
SDKJS SDK

SPA 연동

React·Vue·Next.js 등 SPA(Single-Page Application) 환경에서 인센토 위젯을 연동하는 가이드입니다.

이 가이드는 페이지 전환이 HTML 리로드 없이 일어나는 SPA(React · Vue · Next.js 등) 기준입니다.

JS SDK - SPA는 클라이언트 사이드에서만 실행되어야 합니다. 서버 사이드(SSR)에서 미리 실행될 수 없습니다.

멀티 페이지 웹사이트는 SDK > MPA 연동을 참고하세요. 연동 전 필요한 조건은 SDK > 사전 준비에서 확인하세요.

실제 동작 예제는 github.com/OpenFormatKorea/incento-sdk-sample-apps를 참고해주세요.

연동 절차

Service 추가하기

프로젝트에 incento.service.js (또는 .ts) 파일을 추가합니다. SDK 로더와 커맨드 호출을 한곳에 모아 앱 전역에서 재사용합니다.

class IncentoService {
  loadScript() {
    (function(){var w=window;if(w.Incento&&!w.Incento.q){return;}var i=function(){i.c(arguments);};i.q=[];i.c=function(a){i.q.push(a);};w.Incento=i;function l(){if(w.IncentoInitialized){return;}w.IncentoInitialized=true;var s=document.createElement('script');s.type='text/javascript';s.async=true;s.src='https://s3.incento.kr/scripts/sdk/incento.min.js';var x=document.getElementsByTagName('script')[0];if(x.parentNode){x.parentNode.insertBefore(s,x);}}if(document.readyState==='complete'){l();}else{w.addEventListener('DOMContentLoaded',l);w.addEventListener('load',l);}})();
  }

  boot(config) {
    window.Incento('boot', config);
  }

  show() {
    window.Incento('show');
  }

  hide() {
    window.Incento('hide');
  }

  shutdown() {
    window.Incento('shutdown');
  }

  setPath(path) {
    window.Incento('setPath', path);
  }

  open() {
    window.Incento('open');
  }

  close() {
    window.Incento('close');
  }

  on(eventName, handler) {
    window.Incento('on', eventName, handler);
  }
}

export default new IncentoService();
declare global {
  interface Window {
    Incento?: IIncento;
    IncentoInitialized?: boolean;
    INCENTO_SCRIPT_ALREADY_LOADED?: boolean;
  }
}

interface IIncento {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  c?: (args: any) => void;
  q?: unknown[][];
  (...args: unknown[]): void;
}

interface BootConfig {
  apiKey: string;
  userId?: string | null;
  userCreatedAt?: string | null;
  visible?: boolean;
  pagePath?: string | null;
  debug?: boolean;
}

type EventName = 'widgetOpen' | 'widgetClose' | 'loginRequired';

class IncentoService {
  loadScript() {
    (function(){var w=window;if(w.Incento&&!w.Incento.q){return;}var i:IIncento=function(...args: unknown[]){i.c?.(args);} as IIncento;i.q=[];i.c=function(a){i.q?.push(a);};w.Incento=i;function l(){if(w.IncentoInitialized){return;}w.IncentoInitialized=true;var s=document.createElement('script');s.type='text/javascript';s.async=true;s.src='https://s3.incento.kr/scripts/sdk/incento.min.js';var x=document.getElementsByTagName('script')[0];if(x.parentNode){x.parentNode.insertBefore(s,x);}}if(document.readyState==='complete'){l();}else{w.addEventListener('DOMContentLoaded',l);w.addEventListener('load',l);}})();
  }

  boot(config: BootConfig) {
    window.Incento?.('boot', config);
  }

  show() {
    window.Incento?.('show');
  }

  hide() {
    window.Incento?.('hide');
  }

  shutdown() {
    window.Incento?.('shutdown');
  }

  setPath(path: string) {
    window.Incento?.('setPath', path);
  }

  open() {
    window.Incento?.('open');
  }

  close() {
    window.Incento?.('close');
  }

  on(eventName: EventName, handler: () => void) {
    window.Incento?.('on', eventName, handler);
  }
}

export default new IncentoService();

설치하기

앱의 진입점에서 loadScript()를 호출합니다. 전체 앱 수명 동안 한 번만 실행됩니다.

import Incento from './incento.service';

Incento.loadScript();

boot — SDK 초기화

앱 최초 마운트 시 로그인 여부와 관계없이 그 시점의 인증 상태로 호출합니다.

// 비로그인 상태로 앱 진입
Incento.boot({ apiKey: 'inc_pk_YOUR_KEY' });

// 로그인 상태로 앱 진입
Incento.boot({ apiKey: 'inc_pk_YOUR_KEY', userId: '회원_고유_ID' });

// 기존 회원 — 회원 DB의 원래 가입 일시를 userCreatedAt로 함께 전달 (ISO 8601)
Incento.boot({
  apiKey: 'inc_pk_YOUR_KEY',
  userId: '회원_고유_ID',
  userCreatedAt: '2023-03-15T09:30:00+09:00',
});
  • 라우트 변경 시: boot를 재호출하지 않습니다. show / hide로 노출만 제어하세요.
  • 인증 상태 변경 시(로그인 · 로그아웃): shutdown 후 재호출합니다.

userCreatedAt는 SDK 설치 이전부터 존재하던 기존 회원을 신규 회원과 구분하기 위한 값으로, 고객사 회원 DB의 가입 일시를 그대로 넘기면 됩니다. 자세한 내용은 boot 파라미터 레퍼런스 — userCreatedAt을 참고하세요.

전체 파라미터는 boot 파라미터 레퍼런스를 참고하세요.

이벤트 훅 등록

앱 초기화 시 loginRequired한 번만 등록합니다. 자세한 내용은 이벤트 훅 절을 참고하세요.

show / hide — 페이지별 위젯 노출 제어

특정 페이지에서만 위젯을 표시하고 싶을 때 사용합니다. 캠페인 API 재호출 없이 즉시 반영됩니다.

Incento.show(); // 런처 표시
Incento.hide(); // 런처 숨김 + 위젯이 열려 있으면 닫음

라우터의 이동 이벤트에 연결해서 사용합니다.

// 마이페이지에서만 위젯 표시
router.afterEach((to) => {
  if (to.path === '/mypage') {
    Incento.show();
  } else {
    Incento.hide();
  }
});

shutdown()은 런처 노출 상태를 초기값(표시)으로 리셋합니다. 인증 상태 변경으로 shutdownboot를 재실행할 때 현재 페이지가 위젯 미표시 페이지라면, bootvisible 파라미터를 false로 설정해 런처가 잘못 표시되는 것을 방지하세요.

Incento.boot({ apiKey: 'inc_pk_YOUR_KEY', userId, visible: false });

경로 추적 — setPath

SPA는 페이지 전환이 HTML 리로드 없이 일어나므로, 라우트가 바뀌어도 SDK는 이를 자동으로 알지 못합니다. 라우터 이동 이벤트에 setPath를 연결하면 대시보드의 '경로별 리퍼럴 시도 횟수' 차트에서 어느 경로에서 공유가 일어났는지 집계됩니다. 초기 경로는 bootpagePath로 지정하며, 생략 시 location.pathname이 사용됩니다.

// boot 시 초기 경로 지정 (생략하면 location.pathname)
Incento.boot({ apiKey: 'inc_pk_YOUR_KEY', userId, pagePath: '/mypage' });
// 라우트 변경 시 현재 경로 통지
router.afterEach((to) => {
  Incento.setPath(to.path);
});
// React Router 예시
useEffect(() => {
  Incento.setPath(pathname);
}, [pathname]);

웹 스타일(/screen-name)의 정규화된 저카디널리티(low cardinality) 경로를 넘기세요. 쿼리스트링·동적 id(/products/123)를 그대로 넣으면 경로가 폭발하므로 /products처럼 정규화합니다. 자세한 내용은 경로별 리퍼럴 시도 추적을 참고하세요.

이벤트 훅

이벤트 목록과 공통 규칙은 이벤트 훅 레퍼런스에 정리되어 있습니다.

SPA에서는 이벤트를 앱 초기화 시 한 번만 등록하세요. shutdown / boot 사이클에서 재등록하면 핸들러가 중복 실행됩니다.

// loginRequired — 미등록 시 위젯 내 로그인 버튼이 동작하지 않습니다
Incento.on('loginRequired', () => {
  router.push('/login?show_incento_popup=true');
});

Incento.on('widgetOpen', () => {
  console.log('위젯 열림');
});

Incento.on('widgetClose', () => {
  console.log('위젯 닫힘');
});

shutdown — SDK 종료 및 인증 상태 변경

SPA에서 로그인 · 로그아웃은 페이지 새로고침 없이 발생합니다. 인증 상태가 바뀔 때는 shutdownboot 패턴을 사용합니다.

// 로그인 완료 콜백
function onLoginSuccess(userId) {
  Incento.shutdown();
  Incento.boot({ apiKey: 'inc_pk_YOUR_KEY', userId });
}

// 로그아웃 완료 콜백
function onLogout() {
  Incento.shutdown();
  Incento.boot({ apiKey: 'inc_pk_YOUR_KEY' });
}

URL 파라미터 — SPA에서 loginRequired 후 위젯 자동 오픈

URL 파라미터 전체 목록은 URL 파라미터 레퍼런스를 참고하세요. SPA에서는 페이지 전환이 HTML 리로드 없이 발생하므로, MPA의 show_incento_popup 쿠키 방식이 동작하지 않습니다.

문제 원인: loginRequired 핸들러에서 /login?show_incento_popup=true로 이동해도 URL만 바뀔 뿐 boot()가 재실행되지 않아 쿠키가 저장되지 않습니다. 이후 로그인 완료로 boot()가 재실행될 때는 이미 URL이 바뀌고 쿠키도 없어 위젯이 열리지 않습니다.

대신 로그인 페이지에서 show_incento_popup 파라미터를 감지하고, 로그인 완료 시 incento_popup=true로 이동합니다. incento_popupboot() 시점에 URL에서 직접 읽어 즉시 위젯을 여는 파라미터입니다.

loginRequired 발생
→ router.push('/login?show_incento_popup=true')

로그인 완료
→ URL에 show_incento_popup 감지
→ router.push('/?incento_popup=true')
→ 인증 상태 변경으로 shutdown() + boot() 재실행
→ boot()가 URL의 incento_popup=true 감지 → 위젯 즉시 오픈
// 로그인 페이지 (React 예시)
function handleLoginSubmit() {
  await login(userId);
  const params = new URLSearchParams(location.search);
  router.push(params.has('show_incento_popup') ? '/?incento_popup=true' : '/');
}

Vue, Next.js 등 다른 프레임워크도 동일한 패턴을 적용합니다.

커스텀 런처

기본 런처 대신 직접 만든 버튼으로 위젯을 열려면, boot에서 visible: false로 기본 런처를 숨기고 버튼 클릭 핸들러에서 open을 호출합니다.

import Incento from './incento.service';

// boot 시 기본 런처 숨김
Incento.boot({ apiKey: 'inc_pk_YOUR_KEY', userId, visible: false });

// 직접 만든 버튼에서 위젯 열기
<button onClick={() => Incento.open()}>혜택 받기</button>

위젯 닫기(close)나 사용자 클릭 없는 조건·시점 기반 자동 오픈 등 open / close의 상세 동작과 사용 시점은 위젯 열기/닫기 레퍼런스를 참고하세요.

프레임워크별 예시

// App.tsx
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import Incento from './incento.service';

const WIDGET_PAGES = ['/mypage', '/event'];

Incento.loadScript();

Incento.on('loginRequired', () => {
  router.push('/login?show_incento_popup=true');
});

export default function App() {
  const { pathname } = useLocation();
  const { user } = useAuth(); // 고객사 인증 훅

  // 인증 상태 변경 시 재boot
  useEffect(() => {
    Incento.shutdown();
    Incento.boot({
      apiKey: 'inc_pk_YOUR_KEY',
      userId: user?.id ?? null,
      visible: WIDGET_PAGES.includes(pathname),
    });
  }, [user?.id]);

  // 라우트 변경 시 show / hide
  useEffect(() => {
    if (WIDGET_PAGES.includes(pathname)) {
      Incento.show();
    } else {
      Incento.hide();
    }
  }, [pathname]);

  return <Routes>...</Routes>;
}

Next.js App Router는 서버 컴포넌트가 기본이므로 'use client' 지시어가 필요합니다.

// app/IncentoProvider.tsx
'use client';

import { useEffect } from 'react';
import { usePathname, useRouter } from 'next/navigation';
import Incento from '@/incento.service';

const WIDGET_PAGES = ['/mypage', '/event'];

export default function IncentoProvider() {
  const pathname = usePathname();
  const router = useRouter();
  const { user } = useAuth(); // 고객사 인증 훅

  useEffect(() => {
    Incento.loadScript();
    Incento.on('loginRequired', () => {
      router.push('/login?show_incento_popup=true');
    });
  }, []);

  useEffect(() => {
    Incento.shutdown();
    Incento.boot({
      apiKey: 'inc_pk_YOUR_KEY',
      userId: user?.id ?? null,
      visible: WIDGET_PAGES.includes(pathname),
    });
  }, [user?.id]);

  useEffect(() => {
    if (WIDGET_PAGES.includes(pathname)) {
      Incento.show();
    } else {
      Incento.hide();
    }
  }, [pathname]);

  return null;
}
// app/layout.tsx
import IncentoProvider from './IncentoProvider';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <IncentoProvider />
        {children}
      </body>
    </html>
  );
}
// App.vue <script setup>
import { watch } from 'vue';
import { useRouter } from 'vue-router';
import Incento from './incento.service';

const WIDGET_PAGES = ['/mypage', '/event'];
const router = useRouter();
const { user } = useAuth(); // 고객사 인증 composable

Incento.loadScript();

Incento.on('loginRequired', () => {
  router.push('/login?show_incento_popup=true');
});

// 인증 상태 변경 시 재boot
watch(
  () => user.value?.id,
  (userId) => {
    Incento.shutdown();
    Incento.boot({
      apiKey: 'inc_pk_YOUR_KEY',
      userId: userId ?? null,
      visible: WIDGET_PAGES.includes(router.currentRoute.value.path),
    });
  },
  { immediate: true },
);

// 라우트 변경 시 show / hide
router.afterEach((to) => {
  if (WIDGET_PAGES.includes(to.path)) {
    Incento.show();
  } else {
    Incento.hide();
  }
});

더 알아보기

On this page