추적 설정
프로젝트를 만든 뒤 브라우저, React, 서버리스 환경에 이벤트 수집을 붙이는 방법을 안내합니다.
프로젝트를 만들었다면 다음 단계는 이벤트 수집을 붙이는 것입니다. 처음에는 많은 이벤트를 보내는 것보다, 키를 올바른 환경에 배포하고 로그인 전후 사용자를 안정적으로 연결하는 것이 더 중요해요.
처음 연동한다면 빠른 연동을 먼저 따라 해보세요. 이 문서는 SDK 동작, 직접 HTTP 계약, 검증 기준을 더 자세히 다룹니다.
먼저 지켜야 할 원칙
- 공개 환경에는
writeKey만 배포합니다. - 서버 환경에는
clientId와serverSecret만 배포합니다. - 로그인 직후에는
identify로 익명 행동과 사용자 프로필을 연결합니다. - 서버리스 route handler나 job에서는 이벤트를 큐에 넣은 뒤 같은 요청 안에서 flush까지 끝냅니다.
제품 화면에서는 서버 비밀 키를 serverSecret으로 보여줍니다.
SDK 설정과 환경변수 예시에서는 같은 값을 clientSecret이라는 이름으로 넣습니다.
시작하기 전에 준비할 것
| 준비물 | 설명 |
|---|---|
writeKey | 브라우저와 client component에서 사용하는 공개 수집 키입니다. |
clientId | 서버 SDK가 client를 식별할 때 사용합니다. |
serverSecret | 서버 SDK 인증에 쓰는 비밀 키입니다. |
baseUrl | 리옵트 데이터 배포 origin입니다. 기본값이 없으므로 로컬도 http://localhost:4001처럼 명시합니다. |
| 테스트 이벤트 이름 | 예: signup_clicked, checkout_completed |
키를 확인한 뒤에는 배포 대상에 맞게 환경변수를 나눠 넣으세요.
공개 prefix가 붙은 값은 브라우저에 노출될 수 있다는 점을 전제로 관리해야 합니다.
운영에서는 baseUrl을 생략하지 말고 https://data.reopt.ai 또는 자체 배포 주소를 명시하세요.
핵심 객체
| 객체 | 역할 |
|---|---|
Client | 프로젝트의 추적 주체를 식별하는 API client |
writeKey | 브라우저 또는 공개 환경에서 쓰는 수집 키 |
clientId | 서버 SDK가 사용하는 client ID |
serverSecret | 서버 환경 전용 비밀 키 |
deviceId | 로그인 전 사용자를 묶는 익명 디바이스 ID |
profileId | 로그인 후 식별된 사용자 ID |
eventId | 클라이언트가 생성하는 raw event deduplication 키 |
timestamp | 클라이언트가 이벤트를 만든 millisecond Unix timestamp |
serverSecret은 브라우저 번들, 공개 저장소, client component, HTML snippet에 들어가면 안 됩니다.
노출되었다면 새 client secret을 발급하고 기존 값을 폐기하세요.
수집 진입점
- API route:
POST /api/track - CORS preflight:
OPTIONS /api/track
SDK는 ${baseUrl}/api/track로 이벤트를 보냅니다. apiUrl은 같은 값의 deprecated 별칭입니다.
직접 HTTP 호출을 한다면 브라우저 키와 서버 키의 header를 구분하세요.
| 환경 | 필요한 header |
|---|---|
| 브라우저 또는 공개 환경 | reopt-write-key |
| 서버 환경 | reopt-client-id, reopt-client-secret |
| 익명 사용자 연결 보강 | reopt-device-id |
도입 순서
- 조직 홈에서 프로젝트를 생성합니다.
- 서로 분리된 browser client와 server client를 받습니다.
- 브라우저/React/Next.js client component에는 browser client의
writeKey를 넣습니다. - 서버 route handler, server action, worker, cron job에는 server client의
clientId + serverSecret을 넣습니다. - 프로젝트 홈의 SDK snippet으로 첫 이벤트를 붙입니다.
- DevTools 또는 이벤트 카탈로그에서 이벤트 이름, 속성, 프로필 연결을 확인합니다.
- 카탈로그에서 전환, 태그, 카테고리, 상태를 정리합니다.
브라우저에 붙이기
import { init, identify, pageView, track } from "@reopt-ai/data-sdk";
init({
writeKey: process.env.NEXT_PUBLIC_REOPT_WRITE_KEY!,
baseUrl: process.env.NEXT_PUBLIC_REOPT_DATA_URL!,
});
track("signup_clicked", {
source: "landing",
plan: "starter",
});
identify("user_123", {
email: "user@example.com",
plan: "starter",
});
pageView();브라우저 SDK는 이벤트를 바로 네트워크로 보내지 않고 내부 큐에 넣습니다.
기본값은 1초마다 flush이며, 페이지가 숨겨지거나 언로드될 때는 keepalive 전송을 시도합니다.
이 방식은 페이지 이동 중에도 이벤트 손실을 줄이는 데 도움이 됩니다.
React Provider
"use client";
import { ReoptProvider, useTrack } from "@reopt-ai/data-sdk/react";
export function AnalyticsProvider({ children }: { children: React.ReactNode }) {
return (
<ReoptProvider
config={{
writeKey: process.env.NEXT_PUBLIC_REOPT_WRITE_KEY!,
baseUrl: process.env.NEXT_PUBLIC_REOPT_DATA_URL!,
}}
autoPageView
>
{children}
</ReoptProvider>
);
}
function SignupButton() {
const track = useTrack();
return <button onClick={() => track("signup_clicked", { location: "header" })}>Sign up</button>;
}Provider가 unmount되면 SDK는 남은 이벤트를 flush하고 page lifecycle listener를 정리합니다. 테스트, Storybook, micro frontend처럼 Provider가 반복적으로 mount/unmount되는 환경에서는 이 정리가 중복 전송과 listener 누수를 막습니다.
서버와 서버리스에 붙이기
서버에서는 @reopt-ai/data-sdk/node를 사용합니다.
긴 수명의 서버는 track() 후 주기 flush를 사용할 수 있지만, 서버리스 route handler는 요청이 끝나기 전에 전송까지 완료해야 합니다.
import { Reopt } from "@reopt-ai/data-sdk/node";
const reopt = new Reopt({
clientId: process.env.REOPT_CLIENT_ID!,
clientSecret: process.env.REOPT_CLIENT_SECRET!,
baseUrl: process.env.REOPT_DATA_URL!,
});
export async function POST(request: Request) {
const body = await request.json();
const delivery = await reopt.trackAndFlush("checkout_completed", {
orderId: body.orderId,
amount: body.amount,
});
if (!delivery.queue.queued || delivery.flush?.status !== "success") {
return Response.json({ ok: false, delivery }, { status: 202 });
}
return Response.json({ ok: true, eventId: delivery.queue.eventId });
}서버리스에서는 trackAndFlush, identifyAndFlush, incrementAndFlush, decrementAndFlush를 우선 사용합니다.
이 메서드들은 큐잉 결과와 전송 결과를 한 번에 반환합니다.
type DeliveryResult = {
queue: {
eventId: string;
queued: boolean;
reason?: "tracking_paused" | "consent_denied" | "validation_failed";
};
flush: {
status: "idle" | "success" | "failed" | "skipped";
sent: number;
failed: number;
pending: number;
} | null;
};queue.queued가 false이면 이벤트가 로컬 큐에 들어가지 않은 상태이므로 flush는 null입니다.
flush.status가 failed이면 pending도 함께 확인하세요. 전송·계약 버전·quota 문제면 배치가 큐에
보존되지만, 서버가 특정 행을 영구 거부한 경우에는 그 행이 failed에 집계되고 pending에는 남지 않습니다.
서버 SDK 응답 해석하기
서버 SDK 응답은 두 단계로 나눠 읽습니다.
| 위치 | 의미 |
|---|---|
queue.queued | SDK 로컬 큐에 이벤트가 들어갔는지 나타냅니다. |
queue.reason | 큐에 들어가지 않았다면 이유를 알려줍니다. |
flush.status | 네트워크 전송이 성공했는지 나타냅니다. |
flush.sent | 이번 flush에서 전송된 이벤트 수입니다. |
flush.pending | 아직 SDK 큐에 남아 있는 이벤트 수입니다. |
서버리스에서는 queue.queued === true와 flush.status === "success"를 함께 확인하세요.
둘 중 하나라도 기대와 다르면 API 응답에는 200 대신 202나 내부 상태를 반환해 재시도 정책을 세우는 편이 안전합니다.
로그인 사용자 연결하기
로그인 전에는 deviceId를 기준으로 익명 행동을 묶습니다.
로그인 직후 identify(profileId, properties)를 호출하면 이후 이벤트가 식별된 사용자 프로필과 연결됩니다.
로그아웃할 때는 reset()을 호출하세요.
현재 프로필 ID와 큐를 초기화해 다음 사용자의 이벤트가 섞이지 않게 합니다.
SDK가 자동으로 붙이는 기본 메타데이터
SDK는 각 이벤트에 eventId와 timestamp를 붙이고, 요청 헤더에는 reopt-device-id를 보냅니다.
eventId는 프로젝트 범위의 클라이언트 멱등성 키입니다. 서버는 별도의 전역 고유 raw event ID를 만들며, 같은 프로젝트에 같은eventId가 다시 들어오면 새 raw event로 저장하지 않고 duplicate로 처리합니다.timestamp는 클라이언트 생성 시각입니다. 서버는 별도로 수신 시각도 저장합니다.deviceId는 로그인 전 익명 행동을 세션/프로필 후보와 연결하는 기준입니다.- 브라우저에서는 기본적으로
localStorage에 device ID, 동의 상태, offline queue를 저장합니다. localStorage가 막힌 환경에서는 수집은 계속 시도하지만 device ID와 offline queue의 영속성은 보장되지 않습니다.
직접 HTTP 호출 시 v2 계약
SDK를 통하지 않고 POST /api/track를 호출할 때도 v2 계약을 지켜야 합니다.
track, identify, increment, decrement 이벤트에는 UUID eventId와 millisecond timestamp가 필수입니다.
alias 타입은 현재 수집 API에서 지원하지 않습니다.
[
{
"type": "track",
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": 1716000000000,
"payload": {
"name": "signup_clicked",
"properties": {
"source": "landing"
},
"profileId": "user_123"
}
}
]브라우저 키로 보낼 때는 reopt-write-key를, 서버 키로 보낼 때는 reopt-client-id와 reopt-client-secret을 헤더에 넣습니다.
reopt-device-id는 익명 사용자와 세션 연결 품질을 높이므로 직접 호출에서도 보내는 것이 좋습니다.
직접 호출의 최소 예시는 다음과 같습니다.
curl -X POST "$REOPT_API_URL/api/track" \
-H "content-type: application/json" \
-H "reopt-write-key: $NEXT_PUBLIC_REOPT_WRITE_KEY" \
-H "reopt-device-id: dev-device-1" \
--data '{
"type": "track",
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": 1716000000000,
"payload": {
"name": "signup_clicked",
"properties": { "source": "docs" }
}
}'실제 수집 후 이어지는 처리
- 요청 인증과 quota 검증
- rate limit과 short-window deduplication
- raw event 적재
- raw event id 기반 background task enqueue
- worker가 raw event를 다시 읽어 event, session, profile 갱신
- realtime publish
이 구조에서는 큐 메시지에 전체 payload를 복사하지 않고 raw event id만 싣습니다. 큐 enqueue 이후 실패하거나 재시도가 발생해도 원본 raw event를 기준으로 materialize할 수 있어서 replay와 장애 복구가 단순합니다.
이벤트 이름과 속성 정하기
- 이벤트 이름은 사용자 행동을 과거형 동사로 표현합니다. 예:
signup_clicked,checkout_completed - SDK 예약 이벤트는
$pageview,$screen_view입니다. session_start,session_end,screen_view는 직접 생성하지 않습니다.- 속성 값은 문자열, 숫자, boolean, null, 배열, object처럼 JSON으로 직렬화 가능한 값만 사용합니다.
- 결제 금액, 플랜, 실험군처럼 분석 축이 되는 값은 이벤트 속성에 명시적으로 넣습니다.
- 이메일, 전화번호, 주소, access token 같은 민감 정보는 이벤트 속성에 넣지 않습니다.
주의할 점
serverSecret은 서버 전용입니다. 공개 환경에 포함되면 안 됩니다.- 서버리스에서
track()만 호출하고 요청을 끝내면 이벤트 전송이 완료되지 않을 수 있습니다. - 같은
eventId를 재사용하면 새 raw event로 저장되지 않고 duplicate로 처리됩니다. accepted가 0이어도duplicates가 1 이상이면 요청 자체는 정상일 수 있습니다.- 이벤트 속성에는 개인식별정보와 인증 토큰을 넣지 마세요.
온보딩 검증 체크리스트
첫 배포 전에는 아래 순서로 확인하세요.
- 로컬 또는 preview 환경에서
debug: true로 큐잉 결과를 확인합니다. - 네트워크 탭에서
POST /api/track응답이 200인지 확인합니다. - 응답의
accepted가 증가하고duplicates가 비정상적으로 높지 않은지 봅니다. - 프로젝트 DevTools 또는 이벤트 카탈로그에서 이벤트 이름과 속성이 기대한 형태인지 확인합니다.
- 로그인 직후
identify가 들어오고 이후 이벤트에profileId가 붙는지 확인합니다. - 서버리스 이벤트는
trackAndFlush계열을 사용해flush.status === "success"까지 확인합니다. - 이벤트 카탈로그에서 전환, 태그, 카테고리, 상태를 정리합니다.
원본 보기정상 응답 예시
수집 API가 정상 처리하면 다음과 같은 응답을 반환합니다.
{
"status": "ok",
"requestId": "req_abc123",
"accepted": 1,
"duplicates": 0,
"queued": true
}accepted는 새 raw event로 저장된 수입니다.
duplicates가 1 이상이면 같은 eventId가 이미 들어온 상태일 수 있습니다.
queued가 true이면 분석 테이블 반영을 위한 background task가 만들어졌다는 뜻입니다.
자주 보는 문제
| 증상 | 확인할 것 | 해결 방향 |
|---|---|---|
| 401 응답 | 브라우저에는 writeKey, 서버에는 clientId + serverSecret을 썼는지 확인 | 환경변수 이름과 header를 분리하세요. |
| 400 validation failed | eventId, timestamp, 이벤트 타입별 payload가 v2 계약에 맞는지 확인 | SDK를 쓰거나 직접 HTTP payload를 v2 계약에 맞추세요. |
| 429 응답 | 프로젝트 rate limit 또는 조직 quota 상태 확인 | 테스트 이벤트 빈도를 낮추거나 플랜/쿼터 상태를 확인하세요. |
| 이벤트가 큐에는 들어가지만 안 보임 | flush.status, ingest worker/queue consumer, materialization 상태 확인 | DevTools와 health endpoint에서 queue 상태를 확인하세요. |
| 같은 이벤트가 반복적으로 duplicate | 같은 eventId를 재사용하고 있지 않은지 확인 | 이벤트마다 새 UUID를 생성하세요. |
| 사용자가 연결되지 않음 | 로그인 직후 identify와 이후 이벤트의 profileId 확인 | 로그인 성공 직후 identify(profileId)를 호출하세요. |
다음 단계
- 이벤트 정의 정리는 이벤트 카탈로그
- 수집 파이프라인 상세는 데이터 수집과 거버넌스
- 비용과 보존 정책은 비용과 보존