회원 인증
memberId 와 memberHash 를 함께 전달하면 로그인된 방문자를 식별해 이전 상담 내역을 이어서 보여주고, 상담원에게도 어떤 회원인지 표시할 수 있습니다.
memberHash 는 위변조 방지를 위해 자사 백엔드에서 HMAC-SHA256 으로 계산해야 합니다.
Secret Key 가 노출되면 누구든 임의의 사용자로 위장해 채팅을 시작할 수 있습니다. 환경 변수나 시크릿 매니저에만 보관하고, 계산된 memberHash 결과값만 클라이언트로 전달하세요.
인증 흐름
1. 백엔드에서 HMAC 계산
memberHash = HMAC_SHA256(memberId, SECRET_KEY) — 결과는 hex 문자열입니다.
- Node.js
- Python
- Go
- Ruby
- PHP
import crypto from "node:crypto";
const SECRET_KEY = process.env.ZEROTALK_SECRET_KEY!;
export function getMemberHash(memberId: string): string {
return crypto
.createHmac("sha256", SECRET_KEY)
.update(memberId)
.digest("hex");
}
import hmac
import hashlib
import os
SECRET_KEY = os.environ["ZEROTALK_SECRET_KEY"]
def get_member_hash(member_id: str) -> str:
return hmac.new(
SECRET_KEY.encode(),
member_id.encode(),
hashlib.sha256,
).hexdigest()
package zerotalk
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"os"
)
func GetMemberHash(memberID string) string {
h := hmac.New(sha256.New, []byte(os.Getenv("ZEROTALK_SECRET_KEY")))
h.Write([]byte(memberID))
return hex.EncodeToString(h.Sum(nil))
}
require "openssl"
SECRET_KEY = ENV.fetch("ZEROTALK_SECRET_KEY")
def get_member_hash(member_id)
OpenSSL::HMAC.hexdigest("SHA256", SECRET_KEY, member_id)
end
<?php
function getMemberHash(string $memberId): string {
$secret = getenv("ZEROTALK_SECRET_KEY");
return hash_hmac("sha256", $memberId, $secret);
}
2. 클라이언트에 주입
서버 템플릿에서 계산된 값을 init 옵션으로 전달합니다.
- HTML (Server template)
- Next.js (서버 세션)
- Next.js (클라이언트 세션)
<script>
ZeroTalk.init({
pluginKey: "YOUR_PLUGIN_KEY",
apiBaseUrl: "https://api.talk.zeroworks.ai/api/v1",
wsUrl: "wss://api.talk.zeroworks.ai/ws/sdk",
memberId: "{{ user.id }}",
memberHash: "{{ member_hash }}",
profile: {
name: "{{ user.name }}",
email: "{{ user.email }}",
},
});
</script>
import { ZeroTalk } from "@zerotalk/react-sdk";
import { getCurrentUser } from "@/lib/auth";
import { getMemberHash } from "@/lib/zerotalk";
export default async function RootLayout({ children }) {
const user = await getCurrentUser();
const memberHash = user ? getMemberHash(user.id) : undefined;
return (
<html lang="ko">
<body>
{children}
<ZeroTalk
pluginKey={process.env.NEXT_PUBLIC_ZEROTALK_PLUGIN_KEY!}
apiBaseUrl="https://api.talk.zeroworks.ai/api/v1"
wsUrl="wss://api.talk.zeroworks.ai/ws/sdk"
memberId={user?.id}
memberHash={memberHash}
profile={user ? { name: user.name, email: user.email } : undefined}
/>
</body>
</html>
);
}
localStorage 등 클라이언트 저장소에서 사용자 정보를 읽는 경우, 'use client' Provider 로 감싸 마운트 후 한 번만 hydrate 합니다.
"use client";
import { useEffect, useState } from "react";
import { ZeroTalk } from "@zerotalk/react-sdk";
export function ZeroTalkProvider() {
const [user, setUser] = useState(null);
const [hydrated, setHydrated] = useState(false);
useEffect(() => {
const raw = localStorage.getItem("user");
if (raw) setUser(JSON.parse(raw));
setHydrated(true);
}, []);
if (!hydrated) return null;
return (
<ZeroTalk
pluginKey={process.env.NEXT_PUBLIC_ZEROTALK_PLUGIN_KEY!}
apiBaseUrl="https://api.talk.zeroworks.ai/api/v1"
wsUrl="wss://api.talk.zeroworks.ai/ws/sdk"
memberId={user?.memberId}
memberHash={user?.memberHash}
profile={user ? { name: user.name, email: user.email } : undefined}
/>
);
}
import { ZeroTalkProvider } from "./ZeroTalkProvider";
export default function RootLayout({ children }) {
return (
<html lang="ko">
<body>
{children}
<ZeroTalkProvider />
</body>
</html>
);
}
주의사항
회원 인증을 적용하기 전에 두 가지를 반드시 지켜주세요.
1. memberId 와 memberHash 는 항상 함께
한 쪽만 전달하면 boot 가 422 Unprocessable Content 로 실패합니다. 익명 상태에선 둘 다 생략하고, 로그인 상태에선 둘 다 전달하세요.
// ❌ memberId 만, hash 누락
<ZeroTalk memberId={user.id} ... />
// ✅ 둘 다 함께 (또는 둘 다 없이)
<ZeroTalk memberId={user?.id} memberHash={user?.hash} ... />
2. 로그인 상태에 따라 위젯을 조건부로 마운트하지 마세요 (React 환경)
<ZeroTalk> 컴포넌트 자체를 분기로 렌더하면, 로그인 시점에 unmount → mount 가 발생해 SDK 가 destroy → init 을 빠르게 반복하며 race condition 으로 boot 가 5xx 를 반환할 수 있습니다.
// ❌ 컴포넌트 자체를 분기 — destroy/init race 발생
{user
? <ZeroTalk memberId={user.id} memberHash={user.hash} ... />
: <ZeroTalk ... />}
// ✅ 단일 컴포넌트, props 만 변경
<ZeroTalk
memberId={user?.id}
memberHash={user?.hash}
profile={user ? { name: user.name } : undefined}
...
/>
vanilla 환경에서도 동일 — 로그인 시 ZeroTalk.destroy() 후 곧바로 ZeroTalk.init({ ... }) 를 호출하되, 둘 사이에 동기적인 다른 init 호출이 끼지 않도록 주의하세요.
자주 묻는 질문
익명으로 시작했다가 로그인 후 식별로 전환할 수 있나요?
네. 위 패턴을 따르면 자동으로 처리됩니다. vanilla 에선 로그인 시점에 ZeroTalk.destroy() 후 ZeroTalk.init({ memberId, memberHash, ... }) 로 재초기화하세요.
Plugin Key 를 secret 으로 써도 되나요?
안 됩니다. Plugin Key 는 공개 식별자이고, HMAC 계산에는 별도 발급된 Secret Key 를 사용해야 합니다.
memberHash 가 매 요청마다 바뀌어야 하나요?
아니요. 같은 memberId 와 같은 Secret Key 면 항상 같은 해시가 나옵니다. 캐싱해도 무방합니다.
Secret Key 가 유출됐다면?
대시보드 → 설정 → 연동에서 Secret Key 를 재발급한 뒤 백엔드 환경 변수를 교체하세요. 구 키로 계산된 모든 memberHash 는 즉시 무효화됩니다.