앱에 측정 넣기
React Native·Expo 앱이나 웹뷰 앱(Capacitor 등)에 allob 앱 SDK 를 넣는 방법이에요. 기기 ID, 동의, 세션, 전송은 SDK 가 처리해요. 앱은 값을 저장할 곳, 앱이 화면에 떠 있는지 알려 주는 신호, 네트워크 전송 방법 세 가지만 SDK 에 넘겨주면 돼요. 앱 버전도 넘기면 릴리스를 버전별로 비교할 수 있어요.
에이전트에게 맡기기
코딩 에이전트에게 붙여 넣기
https://allob.app/docs/app.md 를 읽고 이 앱에 allob 앱 SDK 를 넣어 줘.
1. allob schema pull 로 스키마를 받고, 앱 이벤트에 "surface": "app" 이 있는지 확인해. 없으면 추가해서 allob schema push 해.
2. allob.json 의 "sdk" 에 app 경로가 앱 코드 폴더 안(예: "app/src/allob")을 가리키는지 확인하고, 없거나 다르면 고쳐.
3. allob sdk pull 로 app.ts 를 받아.
4. 앱이 켜질 때 buildApp() 으로 만들고 start() 와 askCountry() 를 불러. buildApp 에 앱 버전(version)을 넘겨 — Expo 면 expo-application(OTA 를 쓰면 expo-updates 도)을 설치하고 version: expoVersion(Application, Updates) 로.
5. 앱 열기·화면 이동·핵심 행동에서 이벤트를 보내.
6. 결제 직전에 buyerSid() 값을 결제 서비스에 넘겨서 서버가 결제를 이 기기에 연결할 수 있게 해. storePurchase 를 부르면 시험 결제에는 sandbox: true 를 넣어.
7. 개발 빌드는 이벤트를 보내지 않아. 개발 빌드에서 확인하려면 sendInDev: true 로 잠깐 켜고, 끝나면 그 기기를 빼(https://allob.app/docs/app.md 의 "내 기기 빼기").
끝나면 바꾼 파일과 각 이벤트를 보내는 위치를 알려 줘.앱 SDK 받기
앱 SDK 는 allob.json 의 "sdk" 에 적은 app 폴더에 들어가요. allob folder init 은 폴더를 보고 대상을 골라요. package.json 에 Expo·React Native 가 있으면 "sdk" 에 "app": "allob" 이 들어가요. 폴더에서 알 수 있는 게 없을 때만 제품 플랫폼으로 정해요(iOS·Android·macOS 가 있으면 app). 스키마에 앱 이벤트가 아직 없으면 앱 SDK 는 이번엔 받지 않고, 이벤트를 추가한 뒤 allob sdk pull 로 받아요. 웹과 앱이 한 저장소에 있으면 폴더마다 적어요.
"sdk": { "web": "web/src/allob", "app": "app/src/allob" }allob sdk pull| 파일 | 내용 |
|---|---|
allob/app.ts | buildApp() 함수와 수집 주소(ALLOB_APP) |
allob/analytics.ts | 이벤트별 함수와 타입 |
allob/sdk/ | 앱 SDK 본체(기기 ID, 동의, 세션, 전송) |
아래 예시의 ./allob/ 은 이 폴더를 말해요. 앱은 내 서버를 거치지 않고 allob 으로 바로 보내요. 이미 내 서버로 보내던 옛 앱 버전이 있다면 이미 설치된 옛 앱 버전을 봐요.
앱 이벤트 정의하기
스키마에 앱 이벤트가 있어야 앱 SDK 를 받을 수 있어요. 앱 이벤트가 없으면 allob sdk pull 이 앱 이벤트를 먼저 추가하라고 알려 줘요. 이벤트마다 "surface": "app" 을 적거나, 제품 전체가 앱이면 tracking.surface 를 "app" 으로 둬요. 제품 플랫폼에 앱(iOS·Android·macOS)을 고르고 allob folder init 으로 시작했다면 첫 스키마에 앱 열기 이벤트 app_open 이 들어 있어요. 앱만 골랐다면 tracking.surface 도 "app" 이고, 웹과 앱을 함께 골랐다면 tracking.surface 는 "web" 이고 app_open 에만 "surface": "app" 이 붙어요.
"events": {
"app_open": { "label": "앱 열기", "surface": "app", "props": { "how": { "enum": ["cold", "resume"] } }, "order": ["how"] },
"screen_view": { "label": "화면 보기", "surface": "app", "props": { "screen": { "enum": ["home", "settings"] } }, "order": ["screen"] },
"entry_write": { "label": "일기 쓰기", "surface": "app" }
}React Native·Expo
import { AppState } from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import * as Application from "expo-application";
import * as Updates from "expo-updates";
import { buildApp, ALLOB_APP, appStateLifecycle, expoVersion, fetchNetwork } from "./allob/app.ts";
const storage = {
get: (k: string) => AsyncStorage.getItem(k),
set: (k: string, v: string) => AsyncStorage.setItem(k, v),
delete: (k: string) => AsyncStorage.removeItem(k),
};
export const allob = buildApp({
network: fetchNetwork(ALLOB_APP.endpoint),
lifecycle: appStateLifecycle(AppState),
storage,
prompt: true, // 앱에 동의 창이 있으면 true
version: expoVersion(Application, Updates), // 앱 버전·빌드 번호·OTA 업데이트 ID
});
await allob.start();
await allob.askCountry(ALLOB_APP.countryEndpoint); // 실패하면 false — 앱이 다시 열릴 때 재시도
allob.track.entryWrite();ALLOB_APP.endpoint 에는 수집 키가 들어 있어요. 수집 키는 공개 값이라 앱에 들어가도 괜찮아요.
앱 버전
version 을 넘기면 SDK 가 모든 이벤트에 앱 버전(1.2.0), 빌드 번호(34), OTA 업데이트 ID 를 실어 보내요. allob 은 이 값으로 릴리스를 그 버전을 쓰는 기기와 이어서, 같은 기간에 새 버전 기기와 옛 버전 기기를 나눠 비교하고 업데이트가 얼마나 퍼졌는지 보여 줘요(전후 비교). 넘기지 않으면 버전별 비교를 할 수 없어요(개발 빌드에서 경고가 떠요).
| 앱 | 넘기는 값 |
|---|---|
| Expo | npx expo install expo-application(OTA 를 쓰면 expo-updates 도) 후 version: expoVersion(Application, Updates). OTA 를 안 쓰면 expoVersion(Application) |
| React Native(Expo 없이) | version: { version: DeviceInfo.getVersion(), build: DeviceInfo.getBuildNumber() }(react-native-device-info) |
| Capacitor | version: () => App.getInfo()(@capacitor/app — 버전과 빌드 번호를 줘요) |
- OTA 업데이트 ID 는 OTA 로 받은 코드로 돌 때만 실어요. 앱에 들어 있던 코드로 돌면 비워요.
- 버전은
start()에서 한 번 읽어요. 함수를 넘기면 비동기여도 돼요. - 웹 이벤트와 서버 이벤트(결제 웹훅 등)에는 버전이 없어요. 서버 이벤트는 같은 기기의 앱 이벤트로 그때 쓰던 버전을 찾아요.
개발 빌드(__DEV__ — 시뮬레이터, Expo 개발 빌드)에서는 이벤트를 보내지 않아요. 설치를 확인하려고 개발 빌드에서 보내 보고 싶으면 buildApp({ ..., sendInDev: true }) 로 켜요. 이때 보낸 이벤트는 지표에 쌓이니, 확인이 끝나면 그 기기를 빼 주세요(내 기기 빼기).
웹뷰 앱
Capacitor 나 네이티브 앱 안의 웹 화면에서는 웹뷰용 연결을 써요. 웹 화면을 그대로 담아 웹 SDK 로 측정하는 앱이라면, 광고·분석 도구만 앱으로 보내는 앱 안 웹 화면에서 도구까지도 있어요. 화면이 가려지면 백그라운드로, 페이지가 닫히면 남은 이벤트를 sendBeacon 으로 보내요.
import { App } from "@capacitor/app";
import { buildApp, ALLOB_APP, webLifecycle, webNetwork, webStorage } from "./allob/app.ts";
export const allob = buildApp({
network: webNetwork(ALLOB_APP.endpoint),
lifecycle: webLifecycle(),
storage: webStorage(),
prompt: false,
version: () => App.getInfo(), // 앱 버전·빌드 번호
});동의
앱 SDK 는 동의 여부가 정해지기 전에는 아무것도 보내지 않아요. 그동안 생긴 이벤트는 메모리에 최대 20개까지 보관했다가 정해지면 보내요.
askCountry()가 allob 에 이 기기의 국가를 물어요. 스키마tracking.consent에서opt-in으로 정한 지역이면 동의가 필요해요.- 동의 창이 있는 앱(
prompt: true)은allob.consent.decision().ask가true일 때 창을 띄우고, 사용자의 선택을allob.consent.choose("1")(허용) 또는allob.consent.choose("0")(거부)으로 넣어요. - 동의가 필요 없는 지역이면 바로 측정을 시작해요.
allob.consent.subscribe() 로 상태가 바뀔 때마다 화면을 다시 그릴 수 있어요.
세션과 화면
const session = allob.session({
onOpen: ({ start, first }) => allob.track.appOpen({ how: start }), // start: cold(새로 실행) · resume(백그라운드에서 복귀), first: 이 기기 첫 실행
screenOf: (route) => (route.startsWith("/settings") ? "settings" : "home"),
onScreen: (screen) => allob.track.screenView({ screen }), // 세션마다 화면 종류별로 한 번
});
session.install();
// 화면이 바뀔 때마다
session.route(currentPath);앱이 백그라운드에 오래 있다가 돌아오면 새 세션으로 쳐요. 기준 시간은 스키마 tracking.session 에서 정해요.
결제
스토어 결제는 결제 확인을 받는 서버(RevenueCat 웹훅 등)가 기록해요. 앱은 결제 직전에 기기 ID 를 결제 서비스에 넘겨 두기만 해요.
const sid = await allob.buyerSid(); // 측정하지 않는 기기면 null
if (sid) await Purchases.setAttributes({ allob_sid: sid });서버에서 이 값을 서버 이벤트의 sid 로 넣으면 결제가 이 기기에 이어져요. 결제 기록하기를 참고하세요. 광고 도구에도 결제를 알리려면 allob.storePurchase({ productId, transactionId, price, currency }) 를 불러요.
시험 결제(TestFlight·샌드박스·Play 테스트 트랙)는 sandbox: true 를 넣어 주세요. 광고 도구로 보내지 않아요. RevenueCat 이면 결제 정보의 환경이 PRODUCTION 이 아닐 때, StoreKit 2 면 transaction.environment 가 .production 이 아닐 때예요.
allob.storePurchase({ productId, transactionId, price, currency, sandbox: environment !== "PRODUCTION" });광고·분석 도구(Google 애널리틱스·Meta) 붙이기
앱 설치 광고의 성과를 재려면 앱 안에 Google 애널리틱스(Firebase)와 Meta 앱 이벤트가 있어야 해요. allob 이 이 둘과 앱 추적 허용(ATT) 창, 스토어 결제 넘기기까지 하는 앱 측정 모듈(allob-measurement)을 줘요. 동의를 받기 전에는 아무것도 켜지 않고, 앱 SDK 가 켜고 끄는 때를 정해요.
- 스키마에
apps.measurement를 적어요(스키마 레퍼런스).
``json "apps": { "measurement": { "google": true, "meta": { "appId": "1234567890", "clientTokenEnv": "EXPO_PUBLIC_META_CLIENT_TOKEN" }, "att": { "text": { "en": "Used to measure which ads brought you here.", "ko": "어떤 광고를 보고 왔는지 측정하는 데 쓰여요." } } } } ``
allob.json의"sdk"에 모듈 폴더를 적어요. 패키지 폴더라서 다른 대상과 같은 폴더에 둘 수 없고, 앱 폴더(allob.json이 있는 폴더) 자체도 안 돼요. 앱의package.json·ios/·android/와 겹치기 때문이에요. 새 폴더를 적어요. 앱의 이 파일들이 이미 있는 폴더를 적으면--force를 붙여도 받지 않아요.
``json "sdk": { "app": "src/allob", "native": "modules/allob-measurement" } ``
allob schema push로 올리고allob sdk pull로 받아요.- 앱에 붙여요.
- Expo:
app.config의plugins에"./modules/allob-measurement/plugin"을 넣고(옵션 없이)npx expo prebuild를 실행해요. Firebase 를 켜면expo-build-properties에ios.extraPods: [{ "name": "GoogleUtilities", "modular_headers": true }]도 필요해요. - Capacitor:
pnpm add ./modules/allob-measurement뒤npx cap sync를 실행해요. - Google 은 앱 폴더에
GoogleService-Info.plist·google-services.json이 있어야 켜져요. Meta 클라이언트 토큰은 스키마가 아니라 빌드 환경 변수(clientTokenEnv에 적은 이름)에 넣어요.
- Expo:
- 앱 SDK 에 도구로 넘겨요.
```ts import { expoTools } from "./modules/allob-measurement";
const tools = expoTools({ metaClientToken: process.env.EXPO_PUBLIC_META_CLIENT_TOKEN }); export const allob = buildApp({ ..., tools, excluded: tools.isTestLabDevice() }); ```
Capacitor 면 import { capacitorTools } from "allob-measurement/capacitor" 를 쓰고 capacitorTools(registerPlugin, { metaClientToken }) 로 만들어요(isTestLabDevice() 는 await 해요).
- 도구를 켜면 iOS 는 앱이 앞에 있을 때 앱 추적 허용 창을 한 번 띄워요. "허용"일 때만 광고 식별자를 Google·Meta 로 보내요.
- 사용자가 거부하면 도구가 기기에 모아 둔 것도 지워요.
- 이벤트마다 어떤 도구 이벤트로 보낼지는 스키마의
appTools줄로 정해요. - 네이티브 코드가 바뀌므로 OTA 업데이트로는 나가지 않아요. 새로 빌드해서 스토어에 올려요.
- 도구 없이 AdMob 광고만 싣는 앱은
"apps": { "measurement": { "admob": {} } }만 적어요. 그러면 config plugin 만 받아서 SKAdNetwork 목록과 미디에이션 어댑터만 붙고, Firebase·Meta 는 앱에 들어가지 않아요.
이미 설치된 옛 앱 버전
새 앱 빌드는 allob 으로 바로 보내요. 그런데 예전 버전이 내 서버의 /api/e·/api/country 로 보내도록 만들어져 이미 설치돼 있다면, 그 버전이 사라질 때까지 내 서버가 allob 으로 넘겨 줘야 해요.
allob.json에"proxy": "/api/e"를 넣고,"sdk"에server폴더가 있게 해요.allob sdk pull로 받으면server.ts에relayEvents·relayCountry가 들어 있어요.allob server-key create --label relay로 서버 키를 만들어 서버의 비밀 값ALLOB_SERVER_KEY에 넣어요. 없으면 allob 이 방문자 대신 내 서버의 IP·국가로 세요.- 서버에 두 줄을 넣어요.
Cloudflare Workers 라면 이런 코드예요.
// Cloudflare Worker — only for app builds already installed that still send to your server. New app builds and web pages send straight to allob.
// ALLOB_SERVER_KEY: a Worker secret from `allob server-key create` — without it allob counts these events with your server's IP and country
import { relayCountry, relayEvents } from "./allob/server.ts";
const url = new URL(req.url);
if (url.pathname === "/api/e") return (await relayEvents(req, env, ctx)).response;
if (url.pathname === "/api/country") return relayCountry(req, env);Next.js 라면 이렇게 넣어요.
// Next.js — the same for app builds already installed: app/api/e/route.ts and app/api/country/route.ts
// ALLOB_SERVER_KEY: an environment secret from `allob server-key create`
import { relayCountry, relayEvents } from "@/allob/server";
export async function POST(req: Request) {
return (await relayEvents(req, { ALLOB_SERVER_KEY: process.env.ALLOB_SERVER_KEY })).response; // app/api/e/route.ts
}
export async function GET(req: Request) {
return relayCountry(req, { ALLOB_SERVER_KEY: process.env.ALLOB_SERVER_KEY }); // app/api/country/route.ts
}- 옛 앱의 다시 보내기가 그대로 동작하도록, allob 의 응답 코드(429·413·400)와 기다릴 시간을 앱에 그대로 돌려줘요.
- 웹 페이지는 넘기지 않아요. 웹 SDK 는 늘 allob 으로 바로 보내요.
그 밖의 함수
| 함수 | 설명 |
|---|---|
setExcluded(true) | 내 기기나 테스트 기기를 측정에서 빼요. 이벤트를 보내지 않고 도구도 멈춰요. 기기에 기억해서 앱을 다시 켜도 빠져 있어요. setExcluded(false) 로 풀어요 |
deviceId() | 이 기기의 allob 기기 ID(없으면 null). 내 기기 빼기에서 써요 |
forgetDevice() | 로그아웃이나 계정 삭제 때 이 기기의 기록(대기 중인 이벤트, 기기 ID, 도구가 모은 정보)을 지워요 |
adoptSid(next) | 로그인으로 다른 기기 ID 와 같은 사람인 걸 알았을 때 기기 ID 를 바꿔요 |
setUserId(id) | 로그인한 사용자 ID 를 알려 줘요. 제품 안 A/B 그룹을 사용자 ID 로 정해서 다른 기기에서도 같은 그룹이에요. 이벤트에는 ID 를 싣지 않고, 피드백을 보낼 때만 보낸 사람으로 같이 가요. 로그아웃이면 null(제품 안 A/B) |
measured() | 지금 측정 중인지 알려 줘요 |
사용자 의견을 앱 안에서 받으려면 feedback.send() 를 써요(앱 안 피드백 받기).
내 기기 빼기
내 폰이나 테스트 기기의 사용 기록이 지표에 섞이지 않게 하려면 두 가지를 해요.
- 앱에서 그 기기를 빼요. 예를 들어 설정 화면의 숨은 메뉴에서
allob.setExcluded(true)를 불러요. 기기에 기억돼서 다음 실행부터도 보내지 않아요. - 이미 쌓인 기록까지 빼려면
await allob.deviceId()로 기기 ID 를 확인해서(빼기 전에), allob 화면의 제품 설정 › 데이터 거르기 › 앱 기기 ID 에 넣어요. allob 이 그 기기의 지난 이벤트를 지표에서 빼고, 그 기기에서 오는 이벤트도 받지 않아요.
자동 테스트 기기(Google Play 출시 전 보고서, Firebase Test Lab)도 같은 방법으로 빼요. 이 기기들은 시스템 설정 firebase.test.lab 이 "true" 예요. 앱 측정 모듈의 isTestLabDevice() 가 이 값을 읽어 줘요. iOS 는 믿을 만한 신호가 없어서 늘 false 예요.
export const allob = buildApp({ ..., tools, excluded: tools.isTestLabDevice() }); // 이번 실행만 빼요(기억하지 않아요)전송 방식
앱은 allob 의 응답 코드(400·413·429)에 맞춰 동작해요. 보내지 못한 이벤트는 기기에 보관했다가 다시 보내고, 서버가 기다리라고 하면 기다려요. 앱이 백그라운드로 가거나 닫힐 때 남은 이벤트를 보내요. allob 이 원격으로 전송을 끄라고 응답하면 allob 으로 보내는 것만 멈춰요.
전송은 allob 화면의 제품 › 설정 › 측정에서 「이벤트 수집」을 끄거나 allob ingest pause 로 원격으로 끌 수 있어요. 끄면 1분 안에 allob 이 이 제품의 이벤트를 받지 않고, 앱은 다음 실행부터 allob 으로 보내는 것을 멈추고 모아 둔 이벤트를 지워요. Meta·Google 같은 다른 측정 도구로 보내는 것과 서버 이벤트는 그대로예요. 다시 켜려면 allob ingest resume 을 실행해요.