
Next.js나 React에서 채널톡 서비스를 연동하려고 구글링을 하다보면 dangerouslySetInnerHTML를 사용하는 등 마음에 안드는 방법으로 연동하는 케이스가 많았습니다.
이 글에서 가장 깔끔하게 Next.js에서 채널톡을 연결하는 저만의 방법을 설명하겠습니다.
일단 채널톡은 SSR(서버-사이드 랜더링) 환경에서는 동작하지 않습니다.
기본 설정이 SSR인 Next.js 13버전에서 useEffect 훅을 사용하여야 하기 때문에 "use client"를 소스코드 맨 상단에 추가합니다.
"use client"
export default function BlahBlah(){
return(<div>...</div>)
}
다음 채널톡 공식 문서에 나와있는 SPA용 Class를 복붙하여 따로 ChannelTalk 파일을 생성합니다. 저는 타입스크립트를 사용하기 때문에 타입스크립트 파일을 가져왔습니다.
App 디렉토리 구조
── src
├── app
├── SomeTingDirectory
├── page.tsx (또는 jsx)
├── third-party
├── ChannelTalk.tsx (또는 jsx)
아래는 채널톡 공식문서에 있는 코드
ChannelTalk.tsx
declare global {
interface Window {
ChannelIO?: IChannelIO;
ChannelIOInitialized?: boolean;
}
}
interface IChannelIO {
c?: (...args: any) => void;
q?: [methodName: string, ...args: any[]][];
(...args: any): void;
}
interface BootOption {
appearance?: string;
customLauncherSelector?: string;
hideChannelButtonOnBoot?: boolean;
hidePopup?: boolean;
language?: string;
memberHash?: string;
memberId?: string;
pluginKey: string;
profile?: Profile;
trackDefaultEvent?: boolean;
trackUtmSource?: boolean;
unsubscribe?: boolean;
unsubscribeEmail?: boolean;
unsubscribeTexting?: boolean;
zIndex?: number;
}
interface Callback {
(error: Error | null, user: CallbackUser | null): void;
}
interface CallbackUser {
alert: number;
avatarUrl: string;
id: string;
language: string;
memberId: string;
name?: string;
profile?: Profile | null;
tags?: string[] | null;
unsubscribeEmail: boolean;
unsubscribeTexting: boolean;
}
interface UpdateUserInfo {
language?: string;
profile?: Profile | null;
profileOnce?: Profile;
tags?: string[] | null;
unsubscribeEmail?: boolean;
unsubscribeTexting?: boolean;
}
interface Profile {
[key: string]: string | number | boolean | null | undefined;
}
interface FollowUpProfile {
name?: string | null;
mobileNumber?: string | null;
email?: string | null;
}
interface EventProperty {
[key: string]: string | number | boolean | null | undefined;
}
type Appearance = "light" | "dark" | "system" | null;
class ChannelService {
constructor() {
this.loadScript();
}
loadScript() {
(function () {
var w = window;
if (w.ChannelIO) {
return w.console.error("ChannelIO script included twice.");
}
var ch: IChannelIO = function () {
ch.c?.(arguments);
};
ch.q = [];
ch.c = function (args) {
ch.q?.push(args);
};
w.ChannelIO = ch;
function l() {
if (w.ChannelIOInitialized) {
return;
}
w.ChannelIOInitialized = true;
var s = document.createElement("script");
s.type = "text/javascript";
s.async = true;
s.src = "https://cdn.channel.io/plugin/ch-plugin-web.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(option: BootOption, callback?: Callback) {
window.ChannelIO?.("boot", option, callback);
}
shutdown() {
window.ChannelIO?.("shutdown");
}
showMessenger() {
window.ChannelIO?.("showMessenger");
}
hideMessenger() {
window.ChannelIO?.("hideMessenger");
}
openChat(chatId?: string | number, message?: string) {
window.ChannelIO?.("openChat", chatId, message);
}
track(eventName: string, eventProperty?: EventProperty) {
window.ChannelIO?.("track", eventName, eventProperty);
}
onShowMessenger(callback: () => void) {
window.ChannelIO?.("onShowMessenger", callback);
}
onHideMessenger(callback: () => void) {
window.ChannelIO?.("onHideMessenger", callback);
}
onBadgeChanged(callback: (unread: number, alert: number) => void) {
window.ChannelIO?.("onBadgeChanged", callback);
}
onChatCreated(callback: () => void) {
window.ChannelIO?.("onChatCreated", callback);
}
onFollowUpChanged(callback: (profile: FollowUpProfile) => void) {
window.ChannelIO?.("onFollowUpChanged", callback);
}
onUrlClicked(callback: (url: string) => void) {
window.ChannelIO?.("onUrlClicked", callback);
}
clearCallbacks() {
window.ChannelIO?.("clearCallbacks");
}
updateUser(userInfo: UpdateUserInfo, callback?: Callback) {
window.ChannelIO?.("updateUser", userInfo, callback);
}
addTags(tags: string[], callback?: Callback) {
window.ChannelIO?.("addTags", tags, callback);
}
removeTags(tags: string[], callback?: Callback) {
window.ChannelIO?.("removeTags", tags, callback);
}
setPage(page: string) {
window.ChannelIO?.("setPage", page);
}
resetPage() {
window.ChannelIO?.("resetPage");
}
showChannelButton() {
window.ChannelIO?.("showChannelButton");
}
hideChannelButton() {
window.ChannelIO?.("hideChannelButton");
}
setAppearance(appearance: Appearance) {
window.ChannelIO?.("setAppearance", appearance);
}
}
export default ChannelService;
해당 채널톡 파일에 가장 아랫부분인 export 부분에 기본적으로 new 생성자를 통해 객체 인스턴스를 생성하기 때문에 이부분을 아래와 같이 수정합니다.
export default new ChannelService();
아래와 같이 수정
export default ChannelService;
이렇게 수정하는 이유는 채널톡 인스턴스가 중복 생성되는 것을 방지하기 위해 window가 마운트 되었을 때 useEffect 훅을 통해 채널톡 인스턴스를 생성해주기 위함 입니다.
이처럼 하지 않았을 경우 아래와 같은 오류를 맞이하게 될 것 입니다.
ChannelIO script included twice.
이렇게 export 후 채널톡 연동을 하고 싶은 페이지 소스로 이동하여 useEffect 훅 안에 아래와 같이 선언합니다.
useEffect(() => {
const CT = new ChannelTalk();
//주의! 여기서 CT.loadScript()를 선언하면 ChannelIO script included twice. 오류 발생합니다!
CT.boot({ pluginKey: "플러그인 Key" });
//for unmount
return () => {
CT.shutdown();
};
}, []);
이렇게 하게 되면 귀여운 채널톡 로고를 볼 수 있습니다!

GOURL
제가 개발한 커스텀 링크 단축 URL 서비스 입니다. 많이 사용해주세요!