incentoincento 개발자센터
JS SDK

SPA 연동

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

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

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

일반 멀티 페이지 웹사이트는 MPA 연동 가이드를 참고하세요. 연동 전 필요한 조건은 사전 준비에서 확인하세요.

연동 절차

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');
  }

  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;
  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');
  }

  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' });
  • 라우트 변경 시: boot를 재호출하지 않습니다. show / hide로 노출만 제어하세요.
  • 인증 상태 변경 시(로그인 · 로그아웃): shutdown 후 재호출합니다.

전체 파라미터는 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 });

이벤트 훅

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

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 등 다른 프레임워크도 동일한 패턴을 적용합니다.

커스텀 버튼

id가 show-incento-widget인 요소를 클릭하면 위젯이 열립니다. SDK가 자동으로 클릭 이벤트를 연결합니다.

<button id="show-incento-widget">혜택 받기</button>

여러 요소에 같은 id를 사용해도 모두 동작합니다. SDK는 DOM에 즉시 존재하지 않는 경우에도 최대 10초간 DOM 추가를 감지하므로, 동적으로 삽입되는 버튼에도 적용됩니다.

프레임워크별 예시

// 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