프로젝트를 코드로 관리하기
프로젝트 설정, 에러 트래킹 룰, 알림을 저장소의 파일로 선언하고 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이 받는 값은 그 스키마에 정의되어 있습니다.
자주 쓰는 값은 다음과 같습니다.
| 항목 | 받는 값 |
|---|---|
룰 action.kind | suppression, grouping(fingerprint), assignment(assigneeUserId), severity(low·medium·high·critical) |
룰 filters[].field | $exception_type, $exception_message, $exception_source, $exception_handled, $exception_level, $release_id, path, properties.<key> |
룰 filters[].operator | eq, neq, contains, not_contains, startsWith, in, not_in |
알림 condition.kind | event_count, error_issue(created·reopened·spiking), ai_metric(cost_usd·error_rate·latency_p95_ms·unknown_cost_ratio) |
알림 timeWindow | 5m, 15m, 1h, 6h, 24h |
알림 channel | email, slack, webhook |
룰은 "enabled": false로, 알림은 "status": "paused"로 지우지 않고 꺼 둘 수 있습니다. 생략하면 켜진 상태입니다.
한 파일에 룰과 알림은 각각 200개까지, 룰 하나의 필터는 20개까지 둘 수 있습니다.
타입 검사는 에디터와 CLI 두 곳에서 합니다. 에디터는 스키마로 형태를 검사합니다. CLI는 diff·verify·push에서 같은 스키마에 더해 교차 규칙(event_count에는 eventName, in 연산자는 배열, lifecycle 대소, 채널별 목적지 형식, 중복 key)까지 검사하고, 어긋난 경로와 함께 종료 코드 3으로 거절합니다.
기본 규칙
- 파일이 기준입니다. 파일에서 생략한 필드는 기존 값을 유지하지 않고 기본값이 됩니다.
"settings": {}는 타임존 UTC, 보존 기간 무제한이라는 선언입니다. 그래서verify가 파일과 서버가 같은지 판정할 수 있습니다. - lock이 있어야 파일이 바뀌었는지 콘솔이 바뀌었는지 구분합니다. lock이 없으면 둘이 다르다는 것만 알 수 있고, 기존 항목을 바꾸는 push는
--force없이 거절됩니다. - 쓰기는 전부 적용되거나 전부 거절됩니다. push는 마지막 동기화의
updatedAt을 함께 보내고, 그 사이 콘솔에서 한 항목이라도 바뀌었으면 서버가 요청 전체를409로 거절합니다. 일부만 적용된 상태는 생기지 않습니다. 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로 거절합니다. 기존 값을 해제하는 변경도 플랜 한도를 다시 적용하므로 승인 대상입니다. 보존 기간을 줄이면 기준일 이전 데이터가 정리 대상이 되므로, 비용과 보존 정책의 삭제 미리보기를 먼저 확인하세요.
콘솔과 함께 쓰기
콘솔에서도 계속 편집할 수 있습니다. 다음 세 가지 상황만 알아 두세요.
콘솔에서 만든 룰을 파일로 가져오려면 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 같은 값 넣기
웹훅 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
/** @type {import("@reopt-ai/data-cli").Config} */
export default {
environments: {
staging: { projectId: "prj_staging" },
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입니다.
종료 코드
| 코드 | 언제 | 다음 행동 |
|---|---|---|
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가 필요합니다. push 자체는 기간만 바꿉니다. 줄인 기간을 넘긴 AI 호출 기록은 다음 일일 정리에서 지워지고, 나머지 데이터는 프로젝트 설정에서 정리를 실행할 때 지워집니다. 지워진 데이터는 되돌릴 수 없습니다. --force와--yes는 서로 다른 플래그입니다.--yes는 등급을 승인하고,--force는 비교를 건너뛰고 콘솔 편집을 덮어씁니다. 가져온key항목을 삭제하려면 별도로--prune-imported가 필요합니다.--force와--yes를 함께 쓰는 실행은 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 item(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는 서버 순서대로 끼워 넣습니다. 파일을 손으로 정렬했는지 확인하세요. |
적용 중 연결이 끊겼을 때
같은 push --apply 명령을 다시 실행하세요. CLI는 쓰기 전에 private pending 파일에 작업 ID와 요청 해시를 기록해 두고, 다시 실행하면 서버에서 원래 적용 결과를 조회합니다. 결과가 있으면 새로 쓰지 않고 그 결과로 lock을 복구하며, 이후 콘솔 변경은 드리프트로 남습니다. 서버에 결과가 없으면 원래 파일·lock·환경 변수·대상·옵션이 같을 때만 같은 작업을 다시 보냅니다. 결과를 모르는 동안에는 <lock 경로>.pending.json을 삭제하지 마세요. pending 파일과 *.json.*.tmp 파일은 Git에서 제외하고 lock은 커밋하세요.
서버의 적용 결과는 프로젝트를 삭제할 때 함께 삭제됩니다. 조직 키로만 조회할 수 있고 알림 수신처가 포함되므로 로그에 남기지 마세요. 복구 방법은 오류 응답의 classification, retryable, resolution.action/message에서 확인하세요. 전체 공개 컨트롤 API는 OpenAPI에 있습니다.
설정·알림 화면에는 마지막 코드 적용 시각과 주체가, key가 있는 룰·알림에는 코드 관리 배지가 표시됩니다. 콘솔에서도 계속 편집할 수 있으며, 적용 전에 저장소에도 변경을 반영하라는 안내가 표시됩니다.
다음 단계
- 이벤트와 속성의 의미도 코드로 두려면 CLI 사용하기의 이벤트 카탈로그 절
- 룰이 이슈를 어떻게 바꾸는지는 에러 트래킹, 알림의 평가와 통지는 실시간, 인사이트, 알림
- 보존 기간과 삭제 미리보기는 비용과 보존 정책
- 프로젝트 설정 화면은 프로젝트 설정