4.8 KiB
4.8 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | |||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| backend-feature-flags-deep | Feature Flags 심화 — 점진 / A/B / 격리 | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
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)
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
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
# Dashboard (GrowthBook)
flag: new-checkout
rules:
- condition: { plan: 'pro' }
rollout: 100%
- condition: { country: 'US' }
rollout: 10%
- default: 0%
A/B 실험
flag: button-color
experiment:
hypothesis: red converts higher than blue
variants:
- { id: blue, weight: 50 }
- { id: red, weight: 50 }
metric: checkout_completed
const color = gb.getFeatureValue('button-color', 'blue');
analytics.track('button_seen', { variant: color });
// CTA 클릭 시
analytics.track('checkout_completed', { variant: color });
Kill switch
if (gb.isOn('use-new-payment-provider')) {
await stripeNew.charge(...);
} else {
await stripeOld.charge(...);
}
// 새 provider 가 깨지면 → flag off → 즉시 fallback
Stable hash (consistent variant)
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
// 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
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 (가장 어려움)
// 모든 flag = TODO 폴더
// "100% rollout 후 14일 → 코드 cleanup"
// CI 가 오래된 flag 알람
# .github/workflows/flag-audit.yml
- run: gb-cli list-flags --age-gt 90d
Local override (dev / test)
// 환경변수로 override
const overrides = JSON.parse(process.env.FLAG_OVERRIDES ?? '{}');
gb.setForcedFeatures(overrides);
// 테스트
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 필수.