3분만에 Next.js 13에서 React스럽게 채널톡 연동하기

HongRok Kim·2023년 9월 9일
post-thumbnail

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 서비스 입니다. 많이 사용해주세요!

profile
high school developer

0개의 댓글