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 파라미터 레퍼런스를 참고하세요.
show / hide — 페이지별 위젯 노출 제어
특정 페이지에서만 위젯을 표시하고 싶을 때 사용합니다. 캠페인 API 재호출 없이 즉시 반영됩니다.
Incento.show(); // 런처 버튼 표시
Incento.hide(); // 런처 버튼 숨김 + 위젯이 열려 있으면 닫음라우터의 이동 이벤트에 연결해서 사용합니다.
// 마이페이지에서만 위젯 표시
router.afterEach((to) => {
if (to.path === '/mypage') {
Incento.show();
} else {
Incento.hide();
}
});shutdown()은 런처 노출 상태를 초기값(표시)으로 리셋합니다. 인증 상태 변경으로
shutdown → boot를 재실행할 때 현재 페이지가 위젯 미표시 페이지라면, boot의 visible
파라미터를 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에서 로그인 · 로그아웃은 페이지 새로고침 없이 발생합니다. 인증 상태가 바뀔 때는
shutdown → boot 패턴을 사용합니다.
// 로그인 완료 콜백
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_popup은 boot() 시점에 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();
}
});