[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
---
|
||||
id: backend-feature-flags-deep
|
||||
title: Feature Flags 심화 — 점진 / A/B / 격리
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [backend, feature-flags, ab-testing, vibe-coding]
|
||||
tech_stack: { language: "TS / GrowthBook / LaunchDarkly / Statsig", applicable_to: ["Backend", "Frontend"] }
|
||||
applied_in: []
|
||||
aliases: [feature flag, ab test, gradual rollout, GrowthBook, LaunchDarkly, Unleash, kill switch]
|
||||
---
|
||||
|
||||
# Feature Flags Deep
|
||||
|
||||
> 단순 on/off 가 아님. **% rollout / 사용자 segment / A/B 실험 / kill switch** 4종. SDK + dashboard. **GrowthBook (open) / LaunchDarkly / Statsig**.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- Targeting: 사용자 attribute 기반 (plan / country / cohort).
|
||||
- Rollout: 0% → 10% → 100% 점진.
|
||||
- A/B: variant 분배 + metric 측정.
|
||||
- Kill switch: emergency off.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### GrowthBook (open source)
|
||||
```ts
|
||||
import { GrowthBook } from '@growthbook/growthbook';
|
||||
|
||||
const gb = new GrowthBook({
|
||||
apiHost: 'https://cdn.growthbook.io',
|
||||
clientKey: process.env.GB_KEY,
|
||||
attributes: {
|
||||
id: user.id,
|
||||
plan: user.plan,
|
||||
country: user.country,
|
||||
},
|
||||
});
|
||||
|
||||
await gb.loadFeatures();
|
||||
|
||||
if (gb.isOn('new-checkout')) {
|
||||
// 새 flow
|
||||
}
|
||||
|
||||
const variant = gb.getFeatureValue('button-color', 'blue'); // 'red' | 'blue'
|
||||
```
|
||||
|
||||
### LaunchDarkly
|
||||
```ts
|
||||
import { init, type LDClient } from 'launchdarkly-node-server-sdk';
|
||||
|
||||
const ld: LDClient = init(process.env.LD_KEY);
|
||||
await ld.waitForInitialization();
|
||||
|
||||
const flag = await ld.variation('new-checkout', { key: user.id, custom: { plan: user.plan } }, false);
|
||||
```
|
||||
|
||||
### 점진 rollout
|
||||
```yaml
|
||||
# Dashboard (GrowthBook)
|
||||
flag: new-checkout
|
||||
rules:
|
||||
- condition: { plan: 'pro' }
|
||||
rollout: 100%
|
||||
- condition: { country: 'US' }
|
||||
rollout: 10%
|
||||
- default: 0%
|
||||
```
|
||||
|
||||
### A/B 실험
|
||||
```yaml
|
||||
flag: button-color
|
||||
experiment:
|
||||
hypothesis: red converts higher than blue
|
||||
variants:
|
||||
- { id: blue, weight: 50 }
|
||||
- { id: red, weight: 50 }
|
||||
metric: checkout_completed
|
||||
```
|
||||
|
||||
```ts
|
||||
const color = gb.getFeatureValue('button-color', 'blue');
|
||||
analytics.track('button_seen', { variant: color });
|
||||
// CTA 클릭 시
|
||||
analytics.track('checkout_completed', { variant: color });
|
||||
```
|
||||
|
||||
### Kill switch
|
||||
```ts
|
||||
if (gb.isOn('use-new-payment-provider')) {
|
||||
await stripeNew.charge(...);
|
||||
} else {
|
||||
await stripeOld.charge(...);
|
||||
}
|
||||
|
||||
// 새 provider 가 깨지면 → flag off → 즉시 fallback
|
||||
```
|
||||
|
||||
### Stable hash (consistent variant)
|
||||
```ts
|
||||
import { hash } from '@growthbook/growthbook';
|
||||
|
||||
// 같은 user.id = 항상 같은 variant
|
||||
const bucket = hash('exp-key', user.id, 1) * 100;
|
||||
const variant = bucket < 50 ? 'a' : 'b';
|
||||
```
|
||||
|
||||
→ 사용자가 새로고침 해도 같은 variant 받음.
|
||||
|
||||
### Server-side vs client-side
|
||||
```ts
|
||||
// Server: SDK 직접 호출 — 비밀 attribute 사용 가능
|
||||
gb.isOn('admin-feature'); // 서버에서
|
||||
|
||||
// Client: rules 가 client 에 노출 → 비밀 정보 X
|
||||
// 또는 server-rendered (Next.js)
|
||||
const flags = await gb.evaluateAll();
|
||||
return { props: { flags } };
|
||||
```
|
||||
|
||||
### Type-safe flags
|
||||
```ts
|
||||
type Flags = {
|
||||
'new-checkout': boolean;
|
||||
'button-color': 'blue' | 'red' | 'green';
|
||||
'max-items': number;
|
||||
};
|
||||
|
||||
function flag<K extends keyof Flags>(key: K): Flags[K] {
|
||||
return gb.getFeatureValue(key, defaults[key]) as Flags[K];
|
||||
}
|
||||
```
|
||||
|
||||
### Cleanup (가장 어려움)
|
||||
```ts
|
||||
// 모든 flag = TODO 폴더
|
||||
// "100% rollout 후 14일 → 코드 cleanup"
|
||||
// CI 가 오래된 flag 알람
|
||||
```
|
||||
|
||||
```bash
|
||||
# .github/workflows/flag-audit.yml
|
||||
- run: gb-cli list-flags --age-gt 90d
|
||||
```
|
||||
|
||||
### Local override (dev / test)
|
||||
```ts
|
||||
// 환경변수로 override
|
||||
const overrides = JSON.parse(process.env.FLAG_OVERRIDES ?? '{}');
|
||||
gb.setForcedFeatures(overrides);
|
||||
```
|
||||
|
||||
```ts
|
||||
// 테스트
|
||||
gb.setForcedFeatures({ 'new-checkout': true });
|
||||
```
|
||||
|
||||
### Audit log
|
||||
- 누가 / 언제 / 무엇을 toggle 했는지.
|
||||
- Rollout 변화 history.
|
||||
- Compliance 요구.
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 규모 | 추천 |
|
||||
|---|---|
|
||||
| 작은 / 자체 호스트 | GrowthBook OSS / Unleash |
|
||||
| 큰 / 매니지드 | LaunchDarkly / Statsig |
|
||||
| 실험 자동 분석 | Statsig (자동 stats) |
|
||||
| 단순 on/off | env var + ConfigMap |
|
||||
| Edge runtime | Cloudflare Flags / GrowthBook Edge |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Flag 쌓기 + 정리 안 함**: 100+ flag = 코드 스파게티.
|
||||
- **Cleanup 정책 없음**: 누구도 안 지움.
|
||||
- **Random variant — sticky X**: 사용자가 매번 다른 variant.
|
||||
- **Attribute 안 정의 — global 만**: targeting 못 함.
|
||||
- **Flag 안에 비즈니스 결정**: dashboard 가 source of truth — 코드 의도 잃음.
|
||||
- **Critical path flag check 매번 fetch**: cache.
|
||||
- **Flag failure → 앱 다운**: default 명시 + 안전 fallback.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- 새 feature = flag 부터.
|
||||
- % rollout + segment + kill switch 3종.
|
||||
- 정리 정책 + audit log 필수.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[Feature_Flags_in_Practice]]
|
||||
- [[Backend_API_Gateway_BFF]]
|
||||
- [[AI_LLM_Eval_Patterns]]
|
||||
Reference in New Issue
Block a user