프로젝트를 코드로 관리하기
프로젝트 설정, 에러 트래킹 룰, 알림을 저장소의 파일로 선언하고 reopt-data pull / diff / push / verify로 서버와 맞춥니다.
콘솔에서 바꾼 설정은 "어느 커밋이 이 설정을 만들었나"에 답할 수 없고, 스테이징과 프로덕션이 조용히 갈라져요. Project as Code(PaC)는 프로젝트 설정·에러 트래킹 룰·알림을 저장소의 파일로 선언하고, 배포와 같은 커밋에서 서버에 맞추는 방법입니다. 이벤트 카탈로그를 코드로 관리하기와 같은 규칙을 나머지 설정에 넓힌 것이에요.
먼저 확인할 것
@reopt-ai/data-cli가 설치되어 있어야 해요. 설치와 자격증명은 CLI 사용하기를 따르세요.- 조직 키(
REOPT_DATA_ORG_KEY)가 필요합니다. 개인 로그인 세션이나 클라이언트 자격증명으로는 동작하지 않아요. - 저장소를 프로젝트에 연결해 두면
--project-id를 매번 줄 필요가 없어요.
reopt-data link --project-id prj_123 # reopt-data.config.mjs를 만들거나 갱신합니다- 서버가 프로젝트 응답에
updatedAt을 주지 않으면 PaC 이전 배포입니다.pull·verify·push는 "변경 없음"으로 넘어가지 않고 종료 코드3으로 멈춰요.
어떤 파일을 다루나요?
기본 위치는 저장소 루트의 reopt-data/ 디렉터리이고, reopt-data.config.mjs의 definition.dir로 바꿀 수 있어요.
| 파일 | 담는 것 | 비고 |
|---|---|---|
reopt-data/project.json | 타임존, 보존 기간, 파워 유저·라이프사이클 임계값, 저장소 링크, 심볼 호스트 앱 | 프로젝트당 하나. key가 없어요 |
reopt-data/error-rules.json | 에러 트래킹 룰. 배열 순서가 평가 순서입니다 | 각 룰에 key |
reopt-data/alerts.json | 알림 조건, 채널, 목적지 | 각 알림에 key. 목적지는 ${env:NAME} 가능 |
reopt-data.lock.json | 마지막 동기화 시점에 서버가 들고 있던 updatedAt과 해석된 값의 해시 | 디렉터리 옆에 하나. 커밋하세요 |
reopt-data/.schemas/*.json | 위 세 파일의 JSON Schema. pull이 매번 다시 씁니다 | 에디터 검증용. 커밋하세요 |
// reopt-data/project.json
{
"$schema": "./.schemas/project-settings.v1.json",
"version": 1,
"projectId": "prj_123",
"settings": {
"timezone": "Asia/Seoul",
"retentionDays": 90,
"lifecycleActiveDays": 7,
"lifecycleAtRiskDays": 21,
"repositoryUrl": "https://github.com/acme/shop",
"repositoryBranch": "main",
},
}// reopt-data/error-rules.json — 위에 있는 룰이 먼저 평가됩니다
{
"$schema": "./.schemas/error-rules.v1.json",
"version": 1,
"projectId": "prj_123",
"rules": [
{
"key": "ignore-vendor",
"name": "벤더 번들 무시",
"action": { "kind": "suppression" },
"filters": [{ "field": "$exception_source", "operator": "contains", "value": "node_modules" }],
},
{
"key": "checkout-critical",
"name": "결제 경로는 critical",
"action": { "kind": "severity", "severity": "critical" },
"filters": [{ "field": "path", "operator": "startsWith", "value": "/checkout" }],
},
],
}// reopt-data/alerts.json
{
"$schema": "./.schemas/alerts.v1.json",
"version": 1,
"projectId": "prj_123",
"alerts": [
{
"key": "purchase-drop",
"name": "구매 급감",
"eventName": "purchase",
"condition": { "kind": "event_count", "operator": "lte", "threshold": 10, "timeWindow": "1h" },
"channel": "slack",
"destination": "${env:SLACK_ALERTS_URL}",
},
{
"key": "new-critical-issues",
"name": "새 critical 이슈",
"condition": { "kind": "error_issue", "trigger": "created", "severityMin": "critical" },
"channel": "email",
"destination": "oncall@example.com",
},
],
}pull은 파일과 함께 reopt-data/.schemas/에 JSON Schema 세 개를 써요. $schema가 그 사본을 가리키므로 VS Code 같은 에디터가 네트워크 없이도 오타 키, 잘못된 값, 빠진 필수값을 표시하고 자동완성해 줍니다. 이 디렉터리도 커밋하세요.
같은 스키마가 https://data.reopt.ai/schemas/<이름>.v1.json으로도 서빙되니, 직접 만든 파일에는 그 주소를 써도 됩니다. CLI는 어느 쪽이든 읽어요. 룰의 field·operator·action과 알림의 condition이 받는 값은 그 스키마가 알려줍니다.
타입 검사는 두 층이에요. 에디터는 스키마로 형태를, CLI는 diff·verify·push에서 같은 스키마에 더해 교차 규칙(event_count에는 eventName, in 연산자는 배열, lifecycle 대소, 채널별 목적지 형식, 중복 key)까지 검사하고 종료 코드 3에 경로를 붙여 거절합니다.
꼭 알아야 할 네 가지 규칙
- 파일이 진실이에요. 파일에서 생략한 필드는 "기존 값 유지"가 아니라 기본값입니다.
"settings": {}는 "타임존 UTC, 보존 기간은 플랜 기본값"이라는 선언이에요. 그래서verify가 파일과 서버가 같은지 판정할 수 있습니다. - lock으로 세 방향을 봅니다. lock이 있으면 "파일이 바뀌었다"와 "콘솔이 바뀌었다"를 구분해요. lock이 없으면 둘이 다르다는 것만 알 수 있고, 기존 항목을 바꾸는 push는
--force없이 거절됩니다. - 쓰기는 전부 아니면 전무예요. push는 마지막 동기화의
updatedAt을 함께 보내고, 한 항목이라도 그 사이 콘솔에서 바뀌었으면 서버가 요청 전체를409로 거절합니다. 반만 적용된 상태는 생기지 않아요. - 소유권은
key입니다.key가 있는 룰과 알림은 저장소가 다스려요. 파일이 만들고, 고치고, 파일에서 빠지면 삭제합니다.key가 없는 항목은 콘솔이 만든 것이라 어떤 push도 건드리지 않고, 개수만 보고해요.
사용 흐름
1. 서버 상태를 파일로 받기
reopt-data pull
reopt-data pull --project-id prj_123 # 파일이 아직 없을 때파일 세 개와 lock을 씁니다. 기본값은 생략하고 키 순서를 고정하므로, 바꾼 것이 없으면 두 번째 pull은 바이트까지 같은 파일을 써요.
key가 없는 룰·알림은 파일에 들어가지 않고 "console-owned"로 개수만 보입니다. 파일로 끌어오려면 아래 import를 쓰세요.
마지막 동기화 뒤 파일 내용이나 룰 순서를 고쳤다면 pull은 덮어쓰지 않고 멈춰요. push하거나, 버릴 거면 --force를 주세요. 룰 순서 기준이 없는 구형 lock과 여러 룰이 있으면 먼저 로컬 순서를 확인한 뒤 pull --force로 기준을 갱신해야 해요.
2. 파일을 고치고 차이를 보기
reopt-data diff --format textkind key operation grade
project timezone update metric-changing
error-rule ignore-vendor create metric-changing
alert purchase-drop create safe
timezone: "UTC" → "Asia/Seoul"
console-owned rules: 1 (import them with `reopt-data import error-rule <id> --key <key>`)
console-owned alerts: 0
last push: 2026-09-08T14:57:40.001Z by org-key:a6a0b13019f4!로 시작하는 줄은 마지막 동기화 뒤 콘솔에서 바뀐 것이에요. 파일과 콘솔이 같은 항목을 함께 고쳤으면 CONFLICT로 표시됩니다.
diff는 항상 종료 코드 0이에요. 판정은 verify가 합니다.
3. 계획을 확인하고 반영하기
reopt-data push # 계획만 출력, 아무것도 쓰지 않아요
reopt-data push --apply # safe 변경만 있을 때 반영
reopt-data push --apply --yes # metric-changing · expensive 변경 승인
reopt-data push --apply --yes --allow-destructive # 보존 기간 변경 승인--yes가 필요한 변경이 하나라도 있으면 첫 쓰기 요청을 보내기 전에 종료 코드 7로 멈춰요. 프로젝트 설정은 반영됐는데 룰은 확인을 기다리는 상태는 생기지 않습니다.
설정·룰·알림은 한 요청의 한 트랜잭션으로 적용돼요. lock은 해당 트랜잭션이 반환한 스냅샷으로 갱신합니다.
4. CI에서 어긋남을 잡기
reopt-data verify # 어느 쪽이든 움직였으면 exit 8verify는 diff와 같은 보고를 찍고, 파일과 서버가 다르면 8로 끝나요. 서버에 연결하지 못하거나 서버가 PaC 이전 배포면 "드리프트 없음"이 아니라 실패입니다.
PR마다 verify를, 배포 직전에 push --apply --yes를 두는 것이 기본 배치예요.
# PR 검사
reopt-data verify
# 배포 파이프라인, 앱을 배포하기 직전
reopt-data push --apply --yeslock 파일은 커밋해야 해요. CI가 lock 없이 push하면 기존 항목을 바꾸는 쓰기에 비교 기준이 없어 거절됩니다.
변경 등급은 어떻게 읽나요?
| 등급 | 뜻 | 해당하는 변경 | 필요한 것 |
|---|---|---|---|
safe | 표시나 메타만 바뀜 | 저장소 링크, 심볼 호스트 앱, 네이밍 패턴, assignment·severity 룰, 알림 전부, 보존 기간 연장 | 없음 |
metric-changing | 대시보드 숫자나 이슈 목록이 바뀜 | 타임존, 라이프사이클·파워 유저 임계값, suppression·grouping 룰의 생성·수정·삭제·순서 변경 | --yes |
expensive | 서버가 재계산을 큐에 넣음 | (이벤트 카탈로그의 rollupProperties. 이 파일들에는 아직 없어요) | --yes |
destructive | 데이터가 지워질 수 있음 | retentionDays 감소·신규 지정·해제 | --yes --allow-destructive |
destructive는 --yes --allow-destructive 없이 쓰기 요청을 보내기 전에 막혀요. 서버도 allowDestructive 없이 보존 기간을 줄일 수 있는 REST 요청을 HTTP 409 destructive_blocked로 거절합니다. 기존 값을 해제하면 플랜 한도를 다시 적용하므로 승인 대상이에요. 보존 기간을 줄이면 기준일 이전 데이터가 정리 대상이 되므로, 비용과 보존 정책의 preview를 먼저 확인하세요.
콘솔과 함께 쓰기
콘솔 편집은 계속 가능해요. 세 가지 상황만 알면 됩니다.
콘솔에서 만든 룰을 파일로 가져오기. key를 붙이면 그 순간부터 저장소 소유가 됩니다. 콘솔이 보여주는 id를 쓰세요.
reopt-data import error-rule 054af108-… --key ignore-vendor
reopt-data import alert 0bb6e8b3-… --key purchase-dropimport는 서버에 key를 쓰고, 파일의 알맞은 자리(서버의 평가 순서)에 넣고, lock에도 기록해요. 바로 verify가 0으로 끝납니다.
알림을 가져오면 목적지가 평문으로 파일에 들어가요. 웹훅 URL이면 ${env:NAME}으로 바꾸고 커밋하세요.
파일에서 손을 떼기. unlink는 key만 떼고 항목은 서버에 남겨요. 삭제가 아니라서 --yes가 필요 없습니다.
reopt-data unlink error-rule ignore-vendor항목을 지우려면 파일에서 빼고 push하세요.
콘솔과 파일이 동시에 바뀌었을 때. verify가 8, push가 종료 코드 3으로 멈춰요. 쓰기 도중 경합은 서버의 HTTP 409가 같은 종료 코드로 변환됩니다. 두 가지 중 고릅니다.
- 콘솔 쪽을 받아들이려면
pull. 파일에 로컬 편집이 있으면pull --force로 버려야 해요. - 파일 쪽으로 덮으려면
push --apply --yes --force. 비교를 건너뛰고 서버 값을 덮어씁니다.
다른 저장소나 동료가 그 사이 import한 key 항목이 있으면, push는 그것을 몰라서 지우지 않고 종료 코드 3으로 멈춰요. pull로 파일에 들여오거나, 정말 지울 거면 --prune-imported를 씁니다. --force는 삭제를 승인하지 않아요. 삭제도 등급 검사를 거치므로 suppression·grouping 룰에는 --yes가 필요해요. diff 조회 이후 새로 import된 항목은 --prune-imported로도 지우지 않고 충돌로 거절합니다.
웹훅 URL 같은 값은 어떻게 넣나요?
알림의 destination은 커밋되는 파일에 그대로 넣을 수 없어요. 환경 변수 이름을 적으세요.
"destination": "${env:SLACK_ALERTS_URL}"- 이름은 CI secret store가 쓰는 대문자 형식(
[A-Z_][A-Z0-9_]*)이에요. 잘못된 구문과 중첩 참조도 종료 코드3으로 거절해요. diff·verify·push는 파일을 읽은 직후 치환하고, 빠진 이름이 있으면 전부 나열해 종료 코드3으로 멈춰요. 아무것도 보내지 않습니다.- 서버는 치환된 값만 봅니다. 출력에서는 서버 값을
<redacted>로 가리고 템플릿만 보여줘요. pull은 파일의 기존 템플릿이 서버 값으로 정확히 풀릴 때만 템플릿을 보존해요. 아니면 평문을 쓰고secretsWritten으로 알려줍니다. 그때는 값을 secret store로 옮기고 파일은${env:NAME}으로 되돌리세요.
이메일 목적지는 평문으로 두어도 괜찮아요. 채널별 검증은 서버가 해요(이메일 형식, https URL).
스테이징과 프로덕션을 한 저장소에서
파일은 하나로 두고, 프로젝트만 환경마다 바꿔요.
// reopt-data.config.mjs
export default defineConfig({
environments: {
staging: { projectId: "prj_staging", apiUrl: "https://data.reopt.io" },
production: { projectId: "prj_prod" },
},
});reopt-data --env staging pull
reopt-data --env staging verify
REOPT_DATA_ENV=production reopt-data push --apply --yes- 환경이 활성일 때
pull은 파일에projectId를 쓰지 않아요. 파일은 환경끼리 공유됩니다. - lock은 환경별로
reopt-data.staging.lock.json처럼 따로 둬요. 모두 커밋하세요. - 파일이
projectId를 고정하고 있는데 환경의 프로젝트와 다르면 종료 코드3으로 멈춰요. 파일에서projectId를 빼거나--env를 바로잡으세요. - 모르는 환경 이름은 종료 코드
4로 멈추고, 설정된 이름을 알려줍니다.
프로젝트와 서버 주소가 정해지는 순서는 다음과 같아요.
| 값 | 순서 |
|---|---|
| 프로젝트 | --env의 프로젝트 → 파일의 projectId → --project-id → reopt-data.config의 projectId |
| 서버 주소 | --api-url → REOPT_DATA_API_URL → --env의 apiUrl → reopt-data.config의 apiUrl |
종료 코드
| 코드 | 언제 | 다음 행동 |
|---|---|---|
0 | 성공. verify라면 파일과 서버가 같음 | 진행 |
3 | 파일이 잘못됨, ${env:} 미해결, 충돌, lock 없는 변경, 서버 409 | 메시지의 항목을 고치거나 pull |
4 | 프로젝트를 정할 수 없음, 모르는 --env | link, --env, --project-id를 확인 |
7 | --yes가 필요한 등급 | 영향을 확인한 뒤 --yes |
8 | verify가 어긋남을 발견 | pull(서버를 받아들임) 또는 push --apply(파일) |
주의할 점
- 보존 기간 축소는 데이터 삭제예요.
retentionDays를 줄이는 push는destructive이고--yes --allow-destructive가 필요합니다. 되돌릴 수 없어요. --force와--yes는 다른 플래그예요.--yes는 등급 승인,--force는 비교를 건너뛰고 콘솔 편집을 덮어씁니다. 가져온key항목 삭제에는 별도로--prune-imported가 필요합니다. 둘을 함께 쓰는 순간을 CI에서 자동화하지 마세요.- lock을 커밋하지 않으면 CI push가 거절돼요. 기존 항목을 바꾸는 쓰기에는 비교 기준이 필요합니다.
- 평문 시크릿을 커밋하지 마세요.
pull과import alert가 평문을 쓸 때는 반드시 알려주니, 그 자리에서${env:NAME}으로 바꾸세요. - 콘솔 편집은 다음 push에 덮일 수 있어요.
key가 있는 항목을 콘솔에서 고치면verify가 잡아내지만,--forcepush는 그 편집을 지웁니다. 팀에 "이 항목은 저장소가 관리한다"를 알려 두세요.
자주 막히는 지점
| 증상 | 확인할 것 |
|---|---|
unresolved ${env:…} references … NAME | 나열된 이름을 실행 환경에 설정하세요. 로컬에서는 export NAME=…, CI에서는 secret으로. |
no sync point for N item(s) | lock이 없거나 오래됐어요. pull로 lock을 만들고 커밋하세요. 정말 덮을 거면 --force. |
The project changed since its settings were last read | 콘솔이 먼저 바꿨어요. pull로 받아들이거나 --force로 덮으세요. |
keyed rule(s) were imported on the server since the last pull | 다른 곳에서 import했어요. pull이 파일에 들여옵니다. |
this deployment sent no updatedAt | 서버가 PaC 이전 배포예요. 서버를 올려야 합니다. |
names no project | 파일에 projectId가 없고 환경도 없어요. --env를 주거나 reopt-data link를 하세요. |
import 직후 verify가 reorder를 잡음 | 정상은 아니에요. import는 서버 순서대로 끼워 넣습니다. 파일을 손으로 정렬했는지 확인하세요. |
다음 단계
- 이벤트와 속성의 의미도 코드로 두려면 CLI 사용하기 — 이벤트 카탈로그
- 룰이 이슈를 어떻게 바꾸는지는 실시간·인사이트·알림
- 보존 기간과 정리 preview는 비용과 보존 정책
- 프로젝트 설정 화면은 설정과 DevTools
적용 중 연결이 끊겼을 때
같은 push --apply 명령을 다시 실행하세요. CLI는 쓰기 전에 private pending 파일에 작업 ID와 요청 해시를 기록하고, 재실행 시 서버의 원래 적용 결과를 조회합니다. 새 쓰기 없이 그 결과로 lock을 복구하며, 이후 콘솔 변경은 드리프트로 남아요. 서버에 결과가 없으면 원래 파일·lock·환경 변수·대상·옵션이 같을 때만 같은 작업을 다시 보냅니다. 결과를 모르는 동안 <lock 경로>.pending.json을 삭제하지 마세요. pending과 *.json.*.tmp 파일은 Git에서 제외하고 lock은 커밋하세요.
서버의 적용 결과는 프로젝트를 삭제할 때 함께 삭제됩니다. 조직 키로만 조회할 수 있고 알림 수신처가 포함되므로 로그에 남기지 마세요. 오류 응답의 classification, retryable, resolution.action/message로 복구 방법을 확인할 수 있습니다. 전체 공개 컨트롤 API는 OpenAPI에 있어요.
설정·알림 화면에는 마지막 코드 적용 시각과 주체, keyed 룰·알림에는 코드 관리 배지가 표시됩니다. 콘솔 편집은 계속 가능하며, 적용 전에 저장소에도 변경을 반영하라는 안내가 표시돼요.