# 웹사이트에 측정 넣기

웹사이트나 웹앱에 allob 웹 SDK 를 넣는 방법이에요. 마치면 방문자의 행동이 allob 에 쌓이고, 원하면 Meta 픽셀·GA4·TikTok 에도 같은 이벤트가 전송돼요.

시작하기 전에 [첫 제품 연결하기](/docs/first-product)의 1~2단계(제품 만들기, 코드 폴더 연결하기)를 마쳐 주세요.

## 에이전트에게 맡기기

:::prompt
https://allob.app/docs/web.md 를 읽고 이 웹사이트에 allob 웹 SDK 를 넣어 줘.
1. allob.json 의 "sdk" 에서 web 경로가 이 프로젝트의 웹 코드 폴더 안을 가리키는지 확인하고, 아니면 고쳐.
2. allob sdk pull 로 SDK 를 받아.
3. 앱이 시작될 때 build() 를 한 번 부르고, 스키마의 이벤트가 일어나는 곳에서 track 함수를 불러.
4. allob 제품에 사이트 주소가 없으면 넣어 달라고 알려 줘(사이트 밖 주소·로컬에서는 이벤트가 쌓이지 않게). 미리보기 주소나 다른 도메인에서도 측정해야 하면 스키마 tracking.allowedHosts 에 넣어.
끝나면 바꾼 파일과 각 이벤트를 보내는 위치를 알려 줘.
:::

에이전트가 하는 일을 아래에 단계별로 적었어요. 에이전트가 이 페이지를 읽고 따라 하니까, 결과를 확인할 때 참고하세요.

## SDK 받기

```sh
allob sdk pull
```

SDK 는 npm 패키지가 아니라, 스키마로 만든 TypeScript 파일이에요. `allob.json` 의 `"sdk"` 에 적은 `web` 폴더에 들어가요. 처음에는 `allob/` 이고, 웹 코드가 하위 폴더에 있으면 그 안으로 바꿔요.

```json
{
  "version": 2,
  "account": "my-studio",
  "product": "diary",
  "sdk": { "web": "web/src/allob" }
}
```

경로는 `allob.json` 이 있는 폴더를 기준으로 적고, 그 폴더 밖은 가리킬 수 없어요. 아래 표의 `allob/` 은 이 폴더를 말해요.

| 파일 | 내용 |
|---|---|
| `allob/index.ts` | `build()` 함수. 앱이 시작될 때 한 번 불러요 |
| `allob/analytics.ts` | 이벤트별 함수와 타입, 광고·분석 도구 ID |
| `allob/sdk/` | SDK 본체(기기 ID, 유입 경로, 전송) |

결제 알림을 받는 서버 코드(`server.ts`)는 `"sdk"` 에 `server` 경로를 추가하면 함께 받아요. [결제 기록하기](/docs/payments)를 참고하세요.

:::warning
SDK 폴더 안의 파일은 직접 고치지 마세요. 스키마를 바꾼 뒤 `allob sdk pull` 을 다시 실행하면 새로 만들어져요. SDK 가 스키마보다 오래되면 `allob folder status` 가 알려 줘요.
:::

TypeScript 가 `.ts` 확장자 import 에서 에러를 내면 `tsconfig.json` 에 `"allowImportingTsExtensions": true` 와 `"noEmit": true` 를 넣어요. Vite, esbuild, Bun 은 설정 없이 동작해요.

## 이벤트 보내기

```ts
import { build } from "./allob/index.ts";

export const analytics = build({ allowedHosts: ["diary.example.com"] });

analytics.track.entryWrite({ mood: "good" });
analytics.track.checkoutStart({ plan: "yearly", price: 39 });
analytics.trackOnce.paywallView({ from: "limit" });   // 같은 페이지에서 같은 값이면 한 번만
```

- 이벤트 함수 이름은 스키마의 이벤트 이름을 카멜 표기로 바꾼 거예요(`checkout_start` → `checkoutStart`).
- 측정 코드에서 에러가 나도 화면은 멈추지 않아요. 서버 렌더링처럼 브라우저가 아닌 곳에서는 아무 일도 하지 않아요.
- 이벤트는 allob 수집 주소로 바로 전송돼요. 사이트에 따로 설정할 것은 없어요.

표준 이벤트는 따로 정의하지 않아도 함수가 있어요.

| 함수 | 이벤트 | 언제 |
|---|---|---|
| `lead()` | `lead` | 연락처나 대기 명단을 남겼을 때 |
| `ctaClick(category?)` | `cta_click` | 주요 버튼을 눌렀을 때 |
| `signUpStart()`·`signUpComplete()` | `signup_start`·`signup_complete` | 가입을 시작했을 때, 마쳤을 때 |
| `login()` | `login` | 로그인했을 때 |
| `purchase(value, transactionId)` | `purchase` | 결제했을 때. 광고·분석 도구로만 보내고, allob 기록은 서버가 남겨요 |
| `watchScrollDepth()` | `scroll_depth` | 페이지를 25·50·75·90% 내렸을 때 |

## build() 옵션

| 옵션 | 설명 |
|---|---|
| `allowedHosts` | 측정할 도메인 목록. 목록 밖(localhost, 미리보기 주소)에서는 allob·광고 도구 모두 아무것도 보내지 않아요. `"*.example.com"` 은 하위 도메인 전체. 기본값은 스키마의 `tracking.allowedHosts` |
| `siteHosts` | 광고·분석 도구를 켤 도메인. 기본값은 allob 에 넣은 제품 사이트 주소(하위 도메인 포함)예요. 이 밖에서는 도구를 켜지 않지만 allob 으로는 보내요(allob 이 개발 주소·다른 사이트를 세고 걸러요). 앱 안 웹뷰에서는 쓰지 않아요 |
| `toolsAllowed` | `false` 면 광고·분석 도구를 켜지 않아요. 동의를 받은 뒤 `allowTools()` 를 불러요 |
| `channelIds` | 도구 ID 를 바꿔 쓸 때. 예: 개발 서버에서 테스트 픽셀 |
| `privacy` | URL 속 개인정보를 도구에 보내지 않게 하는 규칙. [개인정보 보호](#개인정보-보호) 참고 |
| `clean` | 이벤트가 브라우저를 떠나기 전에 한 건씩 고치거나 버리는 함수. [보내기 전에 개인정보 걷기](#보내기-전에-개인정보-걷기) 참고 |
| `appShell` | 웹 화면을 앱에 담았을 때(Capacitor), 광고·분석 도구를 앱의 측정 모듈로 보내요. [앱 안 웹 화면에서 도구까지](#앱-안-웹-화면capacitor에서-도구까지) 참고 |
| `batch` | `false` 면 이벤트를 모으지 않고 바로바로 보내요. 화면이 보이는 동안은 일반 요청으로, 페이지가 가려졌거나 닫힐 때는 `sendBeacon` 으로 보내고, 한쪽이 막히면 다른 쪽으로 다시 보내요 |
| `currency` | 결제 금액 통화(기본 USD) |
| `gaCookieDomain`·`gaAdSignals` | GA 쿠키 도메인, Google 신호·광고 개인화 끄기(`false`) |
| `importFrom` | 다른 측정 도구에서 옮겨 올 때, 이전 도구가 저장한 기기 ID 앞부분 이름. 옮긴 날 모든 방문자가 새 사람으로 세지지 않게 해요 |

## 광고·분석 도구로 보내기

스키마 `tracking` 에 도구 ID 를 적으면 SDK 가 그 도구를 켜요. 적지 않은 도구의 코드는 아예 포함되지 않아요.

```json
"tracking": {
  "metaPixel": { "id": "1234567890" },
  "tiktokPixel": { "id": "C0XXXXXXXXXX" },
  "ga4": { "measurementId": "G-XXXXXXXXXX" }
}
```

이벤트마다 `pixel`·`ga`·`tiktok` 에 그 도구의 이벤트 이름을 적어야 그 도구로 전송돼요([스키마 레퍼런스](/docs/schema#이벤트-필드)). 결제는 브라우저와 서버가 같은 거래 ID 를 써서 도구에서 한 건으로 합쳐져요.

## 자동으로 걸러지는 것

allob 이 이벤트를 받을 때 아래 것은 지표에 넣지 않아요. 제품 설정의 **데이터 거르기**에서 지난 24시간 동안 무엇을 몇 건 걸렀는지 볼 수 있어요.

| 걸러지는 것 | 어떻게 아나요 |
|---|---|
| 봇·자동화 | 검색 엔진 같은 확인된 봇, 봇 브라우저 표시(`curl`·`python`·headless 등), 브라우저 정보가 빈 요청, 아주 오래된 Chrome, 데이터센터·애플 망에서 온 요청. 자동화 브라우저(`navigator.webdriver`)는 SDK 가 처음부터 보내지 않아요 |
| 개발 주소 | `localhost`·`127.0.0.1`·`*.local` 같은 내 컴퓨터 주소에서 온 이벤트 |
| 다른 사이트 | 제품 사이트 주소가 있으면, 그 사이트와 하위 도메인이 아닌 곳(미리보기 배포 주소 등)에서 온 이벤트. 스키마 `tracking.allowedHosts` 에 넣은 곳은 받아요. 사이트 주소가 없으면 개발 주소만 걸러요 |
| 뺀 기기·브라우저 | 아래 [내 방문 빼기](#내-방문-빼기)로 뺀 기기 |

어느 사이트에서 왔는지는 브라우저가 보내는 출처(Origin)로 알아요. 출처가 없거나 `null` 이면(프록시, `Referrer-Policy: no-referrer` 등) SDK 가 같이 보내는 페이지 주소로 봐요.

- **처음 7일은 세기만 해요.** 사이트 주소를 정하거나 바꾸면(또는 `tracking.allowedHosts` 를 바꾸면) 7일 동안은 다른 사이트에서 온 이벤트를 버리지 않고 데이터 거르기에 "다른 사이트(지금은 세기만)"로 보여 줘요. 실제 서비스 주소가 여기 보이면 7일 안에 사이트 주소나 `allowedHosts` 를 고쳐 주세요. 그 뒤로는 걸러요.
- **앱 안 웹뷰는 사이트를 보지 않아요.** Capacitor·Cordova·React Native WebView 처럼 앱 안에서 웹 SDK 를 쓰면 페이지 주소가 `localhost` 라서 개발 주소와 같아 보여요. SDK 가 웹뷰인 걸 알려 주고, `capacitor://localhost` 같은 출처도 앱으로 봐서 개발 주소·다른 사이트 거르기를 하지 않아요. 봇 거르기는 그대로 해요.
- 사이트 주소를 바꿨다면 `allob sdk pull` 을 다시 실행해 배포하세요. SDK 가 새 주소에서 광고·분석 도구를 켜요.

## 내 방문 빼기

내가 사이트를 열어 본 기록이 지표에 섞이지 않게 하는 방법은 두 가지예요.

- **allob 에서 빼기(추천)**: 제품 설정 › 데이터 거르기에서 **이 브라우저 빼기**를 눌러요. 제품 사이트가 열리면서 이 브라우저를 allob 의 뺀 기기 목록에 넣고, 이후로 아무것도 보내지 않아요. 이미 쌓인 이 브라우저의 지난 이벤트도 지표에서 빠져요. 링크는 30분 안에 한 번만, 브라우저 하나에만 써요. 뺀 기기는 50개까지 둘 수 있어서, 꽉 차면 쓰지 않는 기기를 먼저 지워요.
- **주소로 빼기**: 사이트 주소 끝에 `?notrack=1` 을 붙여 한 번 열어요. 그 브라우저에서는 이후로 아무것도 보내지 않아요. 다시 측정하려면 `?notrack=0` 을 붙여 열어요. 코드에서는 `analytics.excludeBrowser()` 로 같은 일을 해요.

브라우저 데이터를 지우면 이 표시도 지워져요. 그때는 한 번 더 빼 주세요.

뺀 브라우저에서는 `analytics.context()` 가 `null` 이에요. 결제 정보(metadata)에 `off: analytics.isOff()` 를 같이 넣어 주세요. allob 은 `off` 가 실린 결제를 매출에는 넣고, 결제 수·전환·광고 도구에서는 빼요. 진짜 돈은 매출이지만 내 결제가 광고 성과를 부풀리면 안 되니까요.

```ts
const metadata = { ...(analytics.context() ?? {}), ...(analytics.isOff() ? { off: "1" } : {}) };
```

## 동의 받기

유럽처럼 광고·분석 도구를 켜기 전에 동의를 받아야 하는 지역이 있어요. 스키마 `tracking.consent` 에 지역별 방식을 적어요.

```json
"tracking": { "consent": { "EEA+UK+CH": "opt-in", "default": "opt-out+gpc" } }
```

| 방식 | 뜻 |
|---|---|
| `opt-in` | 동의를 받기 전에는 광고·분석 도구를 켜지 않아요 |
| `opt-out` | 처음부터 켜고, 거부하면 꺼요 |
| `opt-out+gpc` | `opt-out` 과 같고, 브라우저가 추적 거부(GPC) 신호를 보내면 꺼요 |
| `notice` | 안내만 해요 |

지역은 `EU`·`EEA`·`EEA+UK+CH`·`LATAM` 묶음, 두 글자 국가 코드, `default` 를 쓸 수 있어요. `opt-in` 지역이 있으면 SDK 에 `consentOptInCountries` 가 생겨요. 방식에 맞게 켜고 끄는 코드는 사이트에 직접 넣어야 해요.

```ts
import { build, consentOptInCountries, forgetStoredIds } from "./allob/index.ts";

const needsConsent = consentOptInCountries.has(visitorCountry);
const analytics = build({ toolsAllowed: !needsConsent });

// 동의 창에서 허용을 누르면
analytics.allowTools();
// 측정 자체를 거부하면 이 브라우저에 저장한 값을 지워요
forgetStoredIds();
```

방문자의 국가는 SDK 가 알 수 없어요. 서버가 페이지에 넣어 주거나(예: Cloudflare 의 `cf.country`) 따로 알아내야 해요.

## 결제를 방문 기록과 연결하기

결제는 결제 서비스의 알림(웹훅)을 받는 서버가 기록해요. 서버가 누가 결제했는지 알 수 있도록, 결제창을 열 때 `context()` 값을 결제 정보(metadata)에 담아 보내요.

```ts
const ctx = analytics.context();   // { sid, us?, uc?, un?, ttclid? }, 측정하지 않는 브라우저면 null
await openCheckout({ plan: "yearly", metadata: { sid: ctx?.sid ?? "", us: ctx?.us ?? "", uc: ctx?.uc ?? "" } });
```

서버에서 이 값을 그대로 서버 이벤트에 넣으면, 결제가 그 방문자의 퍼널과 유입 경로에 이어져요. [결제 기록하기](/docs/payments)를 참고하세요.

## 개인정보 보호

이름, 이메일 같은 개인정보는 이벤트에 넣지 않아요. 이벤트 값은 누구나 주소를 바꿔서 조작할 수 있으니, 화면에 표시할 때는 항상 이스케이프해요.

사용자가 직접 보내는 의견은 이벤트가 아니라 `feedback.send()` 로 받아요. 사용자 정보도 같이 보낼 수 있고, allob 의 **리뷰·피드백** 탭에만 보여요([앱 안 피드백 받기](/docs/feedback)).

URL 에 다시 찾아갈 수 있는 ID 가 들어가는 사이트(예: `/r/8f3a…` 같은 결과 링크)는 `privacyGuard` 로 그 부분을 도구에서 가려요. 광고·분석 도구는 페이지 주소를 직접 읽어 가기 때문에, 도구로 나가는 요청에서 그 글자를 `:id` 로 바꿔요.

```ts
import { build, privacyGuard } from "./allob/index.ts";

const rules = {
  toolUrl: (href) => href.replace(/\/r\/[^/?#]+/, "/r/:id"),
  secrets: (href) => [...href.matchAll(/\/r\/([^/?#]+)/g)].map((m) => m[1]),
  ownPath: (path) => path.replace(/^\/r\/[^/]+/, "/r/:id"),
};
const analytics = build({ privacy: privacyGuard(rules) });
```

### 보내기 전에 개인정보 걷기

`privacy` 는 광고·분석 도구로 가는 요청을 지켜요. allob 으로 가는 이벤트에도 주소나 버튼 이름에 개인정보가 섞일 수 있다면 `clean` 으로 보내기 전에 걷어요. 이벤트는 내 서버를 거치지 않고 브라우저에서 allob 으로 바로 가기 때문에, 걷는 일은 브라우저에서 해요.

```ts
const analytics = build({
  privacy: privacyGuard(rules),
  clean: (item) => ({
    ...item,
    path: String(item.path).replace(/\/r\/[^/]+/, "/r/:id"),
    category: String(item.category ?? "").replace(/\d{4}-\d{2}-\d{2}/g, ":date"),
  }),
});
```

- `item` 은 보낼 이벤트 한 건이에요. 경로(`path`)·들어온 곳(`ref`)·언어(`lang`)·utm·속성(`category` 와 필드 속성)이 들어 있어요.
- 고친 이벤트를 돌려주면 그걸 보내요. `null` 을 돌려주면 그 이벤트를 보내지 않아요.
- 함수가 오류를 내면 그 이벤트는 보내지 않아요. 걷지 못한 개인정보가 나가지 않게요.
- 이벤트 이름(`e`)과 이벤트 ID(`i`)는 바꿀 수 없어요. 글자·숫자·참거짓이 아닌 값은 빠져요.

## 앱 안 웹 화면(Capacitor)에서 도구까지

Capacitor 처럼 웹 화면을 앱에 담으면 이벤트는 웹 SDK 가 그대로 보내고, 광고·분석 도구는 브라우저 픽셀 대신 앱의 Google 애널리틱스(Firebase)·Meta 앱 이벤트로 보내야 앱 설치 광고를 잴 수 있어요. 앱 측정 모듈을 받아([광고·분석 도구 붙이기](/docs/app#광고분석-도구google-애널리틱스meta-붙이기)) `appShell` 로 넘기면 돼요.

```ts
import { registerPlugin } from "@capacitor/core";
import { capacitorTools } from "allob-measurement/capacitor";
import { build } from "./allob/index.ts";

const tools = capacitorTools(registerPlugin, { metaClientToken: import.meta.env.VITE_META_CLIENT_TOKEN });
const analytics = build({ appShell: { tools, excluded: await tools.isTestLabDevice() } });
```

- 시작할 때 나라를 한 번 물어요. 동의를 먼저 받아야 하는 나라(스키마 `tracking.consent`)에서는 `appShell.prompt: true` 로 두고 동의를 받은 뒤 `allowTools()` 를 불러요. 동의 창이 없으면 그 나라에서는 도구를 켜지 않아요.
- 이벤트마다 스키마의 `appTools` 줄대로 도구로 보내요. 앱 이벤트가 아니어도(웹 쪽 이벤트) 줄을 적으면 돼요.
- 스토어 결제는 `analytics.storePurchase({ productId, transactionId, price, currency })` 로 도구에 넘겨요.
- 앱 껍데기에서는 브라우저 픽셀·GA 스크립트를 불러오지 않아요.
- `excluded: true` 면 이번 실행은 아무 데도 보내지 않아요. 구글 플레이 출시 전 보고서 같은 자동 테스트 기기를 빼는 데 써요.

## 전송 방식

이벤트는 최대 20개(16KB)씩 모아서 보내고, 페이지를 떠날 때는 `sendBeacon` 으로 남은 것을 보내요. 전송에 실패하면 최대 세 번까지 보내요. 스키마에서 `send: "now"` 로 정한 이벤트는 모으지 않고 바로 보내요. 같은 이벤트를 다시 보내도 이벤트 ID 가 같아서 한 번으로 세요.

이벤트는 내 사이트를 거치지 않고 allob 으로 바로 가요. 국가·봇·요청 한도(IP·기기마다)는 allob 이 요청을 받을 때 직접 확인해요([자동으로 걸러지는 것](#자동으로-걸러지는-것)). 따로 넣을 코드는 없어요.

SDK 에는 제품의 수집 키가 들어 있어요. 수집 키는 공개 값이라 코드에 들어가도 괜찮아요. 키를 바꾸려면 `allob ingest key --rotate --yes` 를 실행해요. 이전 키는 1분 안에 멈추기 때문에, `allob sdk pull` 로 SDK 를 다시 받아 배포하기 전까지는 이벤트가 쌓이지 않아요.

배포하기 전에 `allob sdk pull --check` 를 실행하면 받은 SDK 가 `allob.schema.json` 과 같은 버전인지, 손으로 고친 파일이 없는지 확인해요. 어긋나면 끝 코드 1 로 멈춰서 배포 스크립트나 CI 에 넣기 좋아요. 로그인 없이 돌아요.
