Feature Flag
기능 플래그
코드 배포와 기능 릴리스를 분리하는 기법. A/B 테스트에 활용.
기능 플래그
코드 배포와 기능 릴리스를 분리하는 기법. A/B 테스트에 활용.
Feature Flag(기능 플래그)는 코드 배포와 기능 릴리스를 분리하는 소프트웨어 개발 기법입니다. 코드를 배포하되, 특정 기능은 플래그(on/off 스위치)로 제어하여 원하는 시점에 활성화할 수 있습니다. Feature Toggle, Feature Switch라고도 불립니다.
핵심 개념:
주요 활용 사례:
Feature Flag 관리 도구:
Trunk-Based Development과의 관계: Feature Flag는 장기 기능 브랜치를 없애고, 모든 개발자가 메인 브랜치에 자주 병합할 수 있게 합니다. 미완성 기능은 플래그로 숨기면 되므로 병합 충돌이 줄고 CI/CD 파이프라인이 단순해집니다.
// TypeScript - 간단한 Feature Flag 서비스
interface FeatureFlags {
newDashboard: boolean;
darkMode: boolean;
betaFeatures: boolean;
maxUploadSize: number;
}
class FeatureFlagService {
private flags: FeatureFlags;
private userOverrides: Map<string, Partial<FeatureFlags>> = new Map();
constructor() {
// 기본값 (환경변수나 설정 파일에서 로드)
this.flags = {
newDashboard: process.env.FF_NEW_DASHBOARD === 'true',
darkMode: true,
betaFeatures: false,
maxUploadSize: 10 * 1024 * 1024, // 10MB
};
}
// 글로벌 플래그 확인
isEnabled(flag: keyof FeatureFlags): boolean {
return Boolean(this.flags[flag]);
}
// 사용자별 플래그 확인 (오버라이드 지원)
isEnabledForUser(flag: keyof FeatureFlags, userId: string): boolean {
const userFlags = this.userOverrides.get(userId);
if (userFlags && flag in userFlags) {
return Boolean(userFlags[flag]);
}
return this.isEnabled(flag);
}
// 퍼센트 기반 롤아웃 (Canary)
isEnabledForPercentage(flag: keyof FeatureFlags, userId: string, percentage: number): boolean {
const hash = this.hashUserId(userId);
return hash % 100 < percentage && this.isEnabled(flag);
}
private hashUserId(userId: string): number {
let hash = 0;
for (let i = 0; i < userId.length; i++) {
hash = ((hash << 5) - hash) + userId.charCodeAt(i);
hash |= 0;
}
return Math.abs(hash);
}
// 특정 사용자에게 기능 활성화 (베타 테스터 등)
enableForUser(userId: string, flags: Partial<FeatureFlags>) {
const existing = this.userOverrides.get(userId) || {};
this.userOverrides.set(userId, { ...existing, ...flags });
}
}
// 사용 예시
const featureFlags = new FeatureFlagService();
// React 컴포넌트에서 사용
function Dashboard({ userId }: { userId: string }) {
// 10% 사용자에게만 새 대시보드 노출
if (featureFlags.isEnabledForPercentage('newDashboard', userId, 10)) {
return <NewDashboard />;
}
return <LegacyDashboard />;
}
// React Feature Flag Context
import { createContext, useContext, ReactNode } from 'react';
interface FeatureFlagContextType {
isEnabled: (flag: string) => boolean;
getVariant: (flag: string) => string | null;
}
const FeatureFlagContext = createContext<FeatureFlagContextType | null>(null);
export function FeatureFlagProvider({
children,
flags
}: {
children: ReactNode;
flags: Record<string, boolean | string>;
}) {
const value: FeatureFlagContextType = {
isEnabled: (flag) => Boolean(flags[flag]),
getVariant: (flag) => typeof flags[flag] === 'string' ? flags[flag] : null,
};
return (
<FeatureFlagContext.Provider value={value}>
{children}
</FeatureFlagContext.Provider>
);
}
// Custom Hook
export function useFeatureFlag(flag: string): boolean {
const context = useContext(FeatureFlagContext);
if (!context) throw new Error('useFeatureFlag must be used within FeatureFlagProvider');
return context.isEnabled(flag);
}
// 컴포넌트에서 사용
function CheckoutButton() {
const newCheckoutEnabled = useFeatureFlag('new-checkout-flow');
if (newCheckoutEnabled) {
return <NewCheckoutButton />;
}
return <LegacyCheckoutButton />;
}
// A/B 테스트 변형 확인
function PricingPage() {
const context = useContext(FeatureFlagContext);
const variant = context?.getVariant('pricing-experiment');
switch (variant) {
case 'control': return <PricingA />;
case 'variant-b': return <PricingB />;
case 'variant-c': return <PricingC />;
default: return <PricingA />;
}
}
// Node.js - LaunchDarkly SDK
import * as ld from 'launchdarkly-node-server-sdk';
const ldClient = ld.init(process.env.LAUNCHDARKLY_SDK_KEY!);
// 서버 시작 시 초기화 대기
await ldClient.waitForInitialization();
// 사용자 컨텍스트 정의
const user = {
key: 'user-123',
email: 'user@example.com',
custom: {
plan: 'premium',
country: 'KR',
signupDate: '2024-01-15',
},
};
// Boolean 플래그 확인
const showNewFeature = await ldClient.variation('new-feature', user, false);
// 다변형 플래그 (A/B/C 테스트)
const checkoutVariant = await ldClient.variation('checkout-experiment', user, 'control');
// 플래그 변경 실시간 감지
ldClient.on('update:new-feature', async () => {
const newValue = await ldClient.variation('new-feature', user, false);
console.log('Feature flag updated:', newValue);
});
// Express 미들웨어
function featureFlagMiddleware(flagKey: string, defaultValue: boolean) {
return async (req, res, next) => {
const user = { key: req.user?.id || 'anonymous' };
req.featureFlags = req.featureFlags || {};
req.featureFlags[flagKey] = await ldClient.variation(flagKey, user, defaultValue);
next();
};
}
// 라우트에서 사용
app.get('/api/checkout', featureFlagMiddleware('new-checkout', false), (req, res) => {
if (req.featureFlags['new-checkout']) {
return handleNewCheckout(req, res);
}
return handleLegacyCheckout(req, res);
});
PM: "새 결제 시스템 언제 출시해요? QA가 2주는 필요하다던데."
개발자: "코드는 이번 주에 배포하고 Feature Flag로 막아둘게요. QA는 스테이징에서 플래그 켜고 테스트하면 되고, 준비되면 프로덕션에서 1% 사용자부터 시작해서 점진적으로 올려요."
PM: "문제 생기면?"
개발자: "대시보드에서 플래그만 끄면 즉시 롤백돼요. 코드 롤백 필요 없이 3초 만에 원복됩니다."
면접관: "Feature Flag 사용 시 주의점은 뭐가 있나요?"
지원자: "첫째, 플래그가 쌓이면 기술 부채가 됩니다. 완전 롤아웃 후엔 플래그 코드를 반드시 제거해야 해요. 둘째, 플래그 조합이 많아지면 테스트 케이스가 기하급수적으로 늘어요. 셋째, 성능에 영향을 줄 수 있어서 플래그 평가는 캐싱하고, 핫패스에서는 플래그 호출을 최소화해야 합니다."
면접관: "플래그 정리는 어떻게 하세요?"
지원자: "플래그 생성 시 만료일을 정하고, CI에서 만료된 플래그 사용을 경고하는 린트 룰을 추가했어요. 분기마다 '플래그 정리의 날'을 정해서 레거시 플래그를 일괄 제거합니다."
온콜: "에러율 급등했어요! 새 추천 알고리즘 배포 후부터인 것 같아요."
개발자: "LaunchDarkly 대시보드에서 'new-recommendation-v2' 플래그 끌게요. 3초 후 반영돼요."
온콜: "에러율 정상으로 돌아왔어요!"
개발자: "일단 급한 불 끄고, 내일 로그 분석해서 원인 파악할게요. 플래그 덕분에 코드 롤백 없이 1분 만에 해결됐네요."