Claude Code로 Feature Flag 안전하게 구현하기: 릴리스, 실험, Kill Switch
Claude Code로 feature flag를 안전하게 설계합니다. 롤아웃, 실험, kill switch, 관측, 정리까지 다룹니다.
토글보다 먼저 운영 규칙을 정한다
Feature flag는 이미 배포된 코드의 기능을 런타임에 켜거나 끄는 스위치입니다. 초보자가 가장 자주 실수하는 지점은 if (flag) 문법이 아닙니다. 릴리스 flag, 실험 flag, kill switch를 같은 방식으로 만들고, 누가 언제 지울지 정하지 않는 것입니다.
Claude Code는 UI 분기를 빠르게 만들어 줍니다. 하지만 운영 가능한 구현에는 안전한 기본값, targeting context, 서버/클라이언트 평가 경계, 노출 로그, 가드레일 지표, 롤백 절차, 단기 flag의 삭제 날짜가 필요합니다. Masa가 콘텐츠 사이트와 작은 SaaS에서 얻은 교훈은 단순합니다. “기능을 켜는 코드”보다 “실패하면 무엇을 끄고 어떤 숫자를 볼 것인가”가 먼저입니다.
공식 문서를 기준으로 모델을 잡으세요. OpenFeature는 애플리케이션이 사용하는 evaluation API와 실제 provider를 분리하고, evaluation context로 사용자와 환경 정보를 전달합니다. LaunchDarkly는 release flag, experiment flag, kill switch 같은 사용 사례를 정리합니다. Unleash는 Define, Develop, Production, Cleanup, Archived로 이어지는 라이프사이클을 설명하며 오래된 flag를 남기지 않는 운영을 강조합니다.
참고한 1차 자료:
- OpenFeature introduction
- OpenFeature evaluation context
- LaunchDarkly creating flags
- LaunchDarkly targeting rules
- LaunchDarkly guarded rollouts
- Unleash feature flag lifecycle
- Claude Code best practices
Release, Experiment, Kill Switch를 분리한다
코드를 쓰기 전에 flag의 수명을 먼저 분류합니다. Release flag는 미완성 기능을 숨기고 일부 사용자에게만 점진적으로 공개한 뒤, 100% 안정화되면 제거합니다. Experiment flag는 가설을 검증하므로 노출 이벤트와 결과 지표가 필수입니다. Kill switch는 외부 API 장애, 비용 급증, 추천 시스템 지연, 위험한 자동화를 즉시 멈추기 위한 장기 안전장치입니다.
| 사용 사례 | 종류 | 성공 지표 | 실패 시 조치 |
|---|---|---|---|
| 새 checkout을 Pro 계정 25%에만 공개 | Release | 결제 완료율, 결제 오류율 | checkout_v2_release 끄기 |
| 가격 페이지 CTA 문구 비교 | Experiment | 가입 시작률, 유료 의향 클릭 | 실험 중지 후 control 고정 |
| 블로그 affiliate 블록을 본문 중간으로 이동 | Experiment | 상품 클릭, 읽기 완료율 | 블록을 글 하단으로 되돌림 |
| 공급사 장애 시 추천 모듈 중지 | Kill switch | p95 지연, 5xx 비율 | recommendations_enabled 끄기 |
측정이 없으면 flag는 추측이 됩니다. 함께 읽을 글로 Claude Code A/B 테스트와 Claude Code 분석 구현이 좋습니다. 수익형 사이트에서는 클릭뿐 아니라 AdSense 품질, 읽기 완료, affiliate 수익, 상담 의향을 같이 보호해야 합니다. 혼자 시작한다면 무료 자료를 받고, 팀 적용은 영문 상담 페이지로 이어갈 수 있습니다.
최소 설정과 Evaluator 패턴
처음부터 특정 벤더에 강하게 묶지 않는 것이 좋습니다. 애플리케이션은 key, 기본값, context로 flag를 평가하고, 뒤쪽 provider는 LaunchDarkly, Unleash, OpenFeature provider, JSON 파일, 내부 서비스로 바꿀 수 있어야 합니다. 아래 코드는 flag-demo.ts로 저장한 뒤 npx tsx flag-demo.ts로 실행할 수 있는 최소 예시입니다.
type FlagValue = boolean | string | number;
type FlagKind = "release" | "experiment" | "kill_switch";
type Plan = "free" | "pro" | "enterprise";
type Role = "user" | "admin";
type Operator = "equals" | "in";
type FlagContext = {
targetingKey: string;
plan: Plan;
country: string;
role: Role;
appVersion: string;
};
type FlagRule = {
attribute: keyof Omit<FlagContext, "targetingKey">;
operator: Operator;
values: string[];
value: FlagValue;
percentage?: number;
};
type FlagConfig = {
key: string;
kind: FlagKind;
enabled: boolean;
defaultValue: FlagValue;
offValue: FlagValue;
owner: string;
removeAfter?: string;
rules: FlagRule[];
};
const registry: Record<string, FlagConfig> = {
checkout_v2_release: {
key: "checkout_v2_release",
kind: "release",
enabled: true,
defaultValue: false,
offValue: false,
owner: "growth-platform",
removeAfter: "2026-07-15",
rules: [
{
attribute: "role",
operator: "equals",
values: ["admin"],
value: true,
},
{
attribute: "plan",
operator: "in",
values: ["pro", "enterprise"],
value: true,
percentage: 25,
},
],
},
pricing_copy_2026_06: {
key: "pricing_copy_2026_06",
kind: "experiment",
enabled: true,
defaultValue: "control",
offValue: "control",
owner: "monetization",
removeAfter: "2026-06-30",
rules: [
{
attribute: "country",
operator: "in",
values: ["JP", "US", "DE"],
value: "simple",
percentage: 50,
},
],
},
recommendations_enabled: {
key: "recommendations_enabled",
kind: "kill_switch",
enabled: true,
defaultValue: true,
offValue: false,
owner: "sre",
rules: [],
},
};
function bucketFor(flagKey: string, targetingKey: string): number {
const input = `${flagKey}:${targetingKey}`;
let hash = 0;
for (const char of input) {
hash = (hash * 31 + char.charCodeAt(0)) >>> 0;
}
return hash % 100;
}
function ruleMatches(
flagKey: string,
rule: FlagRule,
context: FlagContext,
): boolean {
const actual = String(context[rule.attribute]);
const matched =
rule.operator === "equals"
? actual === rule.values[0]
: rule.values.includes(actual);
if (!matched) return false;
if (rule.percentage === undefined) return true;
return bucketFor(flagKey, context.targetingKey) < rule.percentage;
}
export function evaluateFlag<T extends FlagValue = FlagValue>(
key: string,
context: FlagContext,
): T {
const flag = registry[key];
if (!flag) return false as T;
if (!flag.enabled) return flag.offValue as T;
for (const rule of flag.rules) {
if (ruleMatches(flag.key, rule, context)) {
return rule.value as T;
}
}
return flag.defaultValue as T;
}
const demoContexts: FlagContext[] = [
{
targetingKey: "user_001",
plan: "pro",
country: "JP",
role: "user",
appVersion: "1.8.0",
},
{
targetingKey: "user_002",
plan: "free",
country: "BR",
role: "admin",
appVersion: "1.8.0",
},
];
for (const context of demoContexts) {
console.log(context.targetingKey, {
checkout: evaluateFlag<boolean>("checkout_v2_release", context),
pricingCopy: evaluateFlag<string>("pricing_copy_2026_06", context),
recommendations: evaluateFlag<boolean>(
"recommendations_enabled",
context,
),
});
}
핵심은 단순합니다. 알 수 없는 flag는 안전한 fallback으로 닫고, percentage rollout은 Math.random()이 아니라 안정적인 targetingKey를 사용합니다. 단기 flag에는 owner와 removeAfter가 있어야 합니다.
서버 평가와 클라이언트 표시를 나눈다
결제, 권한, quota, 재고, backend 비용과 관련된 판단은 서버에서 평가합니다. 클라이언트 flag는 이미 허용된 UI 문구, 레이아웃, onboarding 힌트, 낮은 위험의 시각 변화에만 사용합니다. targeting 규칙 전체를 브라우저에 보내거나 숨겨진 버튼으로 접근 제어를 대체하지 마세요.
type User = {
id: string;
plan: "free" | "pro" | "enterprise";
role: "user" | "admin";
};
type RequestLike = {
headers: {
get(name: string): string | null;
};
};
export function buildFlagContext(
user: User,
request: RequestLike,
): FlagContext {
return {
targetingKey: user.id,
plan: user.plan,
role: user.role,
country: request.headers.get("x-country") ?? "US",
appVersion: process.env.NEXT_PUBLIC_APP_VERSION ?? "dev",
};
}
export function getServerFlagSnapshot(context: FlagContext) {
return {
checkoutV2: evaluateFlag<boolean>("checkout_v2_release", context),
pricingCopy: evaluateFlag<string>("pricing_copy_2026_06", context),
};
}
type PricingFlags = {
pricingCopy: string;
};
export function PricingCta({ flags }: { flags: PricingFlags }) {
const label =
flags.pricingCopy === "simple"
? "무료 플랜으로 시작하기"
: "무료 체험 시작하기";
return <a href="/signup">{label}</a>;
}
React 컴포넌트는 서버가 계산한 snapshot을 표시할 뿐입니다. Claude Code에게도 “권한과 결제는 서버, 클라이언트는 평가된 결과만 사용”이라고 명시해야 합니다.
관측 없이 롤아웃하지 않는다
안전한 rollout은 단순히 1%에서 시작하는 것이 아닙니다. 언제 10%, 25%, 50%, 100%로 올릴지, 어떤 숫자가 나빠지면 멈출지 정해져 있어야 합니다. Unleash의 gradual rollout은 percentage, stickiness, constraint를 조합합니다. LaunchDarkly guarded rollout은 지표를 보고 회귀가 생기면 일시 중지나 rollback으로 이어집니다. 자체 evaluator를 써도 이 운영 모델은 가져오세요.
노출, 주요 지표, 가드레일을 분리합니다. 노출은 누가 어떤 flag 값을 봤는지 알려 줍니다. 주요 지표는 목표 행동이 좋아졌는지 보여 줍니다. 가드레일은 속도, 오류, 수익 품질, 지원 부담, 신뢰가 망가지지 않았는지 확인합니다.
type FlagExposure = {
flagKey: string;
value: FlagValue;
targetingKey: string;
route: string;
evaluatedAt: string;
};
export function trackFlagExposure(event: FlagExposure) {
console.log(
JSON.stringify({
event_name: "feature_flag_exposure",
...event,
}),
);
}
Checkout에서는 5xx, 결제 실패, 문의 증가를 봅니다. 수익형 블로그는 affiliate 클릭만 보지 말고 읽기 완료율, bounce, Core Web Vitals, 유료 상담 클릭도 봅니다. AI 기능은 token 비용, p95 latency, 사용자별 quota를 같이 봅니다. 클릭이 늘어도 구매 품질이 떨어지면 성공이 아닙니다.
실제 실패 모드
첫째, 매 페이지 로드마다 랜덤 배정하는 실험입니다. 새로고침으로 A와 B가 바뀌면 노출과 전환 데이터가 깨집니다. 안정적인 targeting key로 bucket을 만드세요.
둘째, premium 기능을 클라이언트에서만 숨기는 방식입니다. React 버튼이 없어도 API가 열려 있으면 보호가 아닙니다. Feature flag는 UX 스위치이지 authorization이 아닙니다.
셋째, 위험한 기본값입니다. 미정의 release flag는 보통 false로 닫아야 합니다. 오타가 true로 해석되면 의도치 않은 전체 출시가 됩니다.
넷째, 임시 flag를 지우지 않는 것입니다. 몇 달 뒤 checkout_v2_release는 아무도 모르는 분기가 됩니다. 결정이 끝나면 cleanup PR로 제거합니다.
다섯째, 규칙이 너무 중첩되는 것입니다. 부모 flag, 자식 flag, 겹치는 percentage rollout은 실제 대상 비율을 설명하기 어렵게 만듭니다.
Claude Code에 안전하게 요청하는 법
Claude Code는 파일을 읽고 수정하고 테스트를 실행할 수 있습니다. 그래서 처음부터 범위, fallback, 검증 명령을 넣어야 합니다.
이 저장소에 feature flag workflow를 추가해 주세요.
첫 flag는 checkout_v2_release이며 staged rollout용입니다.
제약:
- 결제와 권한 flag는 서버에서 평가한다
- 알 수 없는 release flag는 false를 반환한다
- percentage rollout은 안정적인 targetingKey를 사용한다
- registry에는 owner와 removeAfter를 넣는다
- unrelated files는 수정하지 않는다
필요한 출력:
- 최소 flag registry와 evaluateFlag 함수
- 노출 이벤트 타입
- 3개 이상의 제품 use case
- 실패 예시와 rollback 절차
- 실행한 테스트 명령
merge 전 review prompt도 따로 사용합니다.
이 feature flag 구현을 review해 주세요.
기본값, 서버/클라이언트 경계, 안정적인 bucketing,
노출 이벤트 누락, cleanup 날짜, rollback 동작을 봐 주세요.
심각도 순서로 파일 위치와 함께 지적해 주세요.
이렇게 요청하면 Claude Code가 단순 UI 토글보다 운영 가능한 설계에 가까운 결과를 냅니다.
Cleanup까지가 출시다
모든 flag는 만드는 순간부터 유지보수 비용이 됩니다. Release flag는 전체 rollout 후 삭제하고, experiment flag는 승자를 정한 뒤 삭제합니다. Kill switch는 남겨도 되지만 owner, runbook, alert가 있어야 합니다. PR 템플릿에 owner, removeAfter, 모니터링 지표, 제거 예정 PR을 넣으면 나중의 팀원이 이해하기 쉽습니다.
이 글의 evaluator는 TypeScript demo로 실행 가능한 형태로 확인했습니다. 같은 targetingKey는 같은 bucket에 들어가고, 알 수 없는 flag는 안전 fallback을 반환하며, kill switch는 명확한 off value를 가집니다. Masa의 수익형 콘텐츠 운영 메모는 클릭만 보지 말고 읽기 품질과 유료 의향까지 함께 보라는 것입니다. 먼저 release flag 하나, experiment flag 하나, kill switch 하나로 작게 시작하세요.
무료 PDF: Claude Code 치트시트
이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.
개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.
작성자 소개
Masa
Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.
관련 글
Claude Code 권한 세이프티 래더: 통제력을 잃지 않고 allow 넓히기
read-only에서 제한 편집, 검증 명령, deploy 확인까지 권한을 단계적으로 넓히는 방법.
Claude Code Small PR Proof Pack: 작은 PR을 리뷰 가능한 상태로 만드는 증거 세트
Claude Code의 작은 PR에 diff, 검증, 공개 URL, CTA 경로, rollback을 붙이는 실무 체크리스트.
Claude Code 커밋 전 리뷰 게이트: diff, 테스트, 공개 URL, CTA 확인
Claude Code 작업을 커밋하기 전에 diff 범위, build, 공개 URL, Gumroad 링크, 상담 CTA, 테스트 누락과 무관한 파일을 확인하는 방법입니다.