React에서 동적 구성 — Jank 없이 Feature Flags
Source: Dev.to
위에 제공된 내용 외에 번역할 텍스트가 없습니다. 번역이 필요한 전체 글이나 추가적인 텍스트를 제공해 주시면 한국어로 번역해 드리겠습니다.
Source: …
React에서 기능 플래그의 문제점
대부분의 React 앱은 구성(configuration)을 다음 세 가지 방법 중 하나로 처리합니다:
1. 빌드 시점 환경 변수
const isNewCheckout = process.env.REACT_APP_NEW_CHECKOUT === "true";
function Checkout() {
return isNewCheckout ? : ;
}
변경이 필요하면? 전체 앱을 다시 빌드하고 재배포합니다.
2. 최상위에서 내려주는 Props
function App() {
const [flags, setFlags] = useState(null);
useEffect(() => {
fetch("/api/flags")
.then((r) => r.json())
.then(setFlags);
}, []);
if (!flags) return ;
return ;
}
동작은 하지만 이제 많은 컴포넌트를 통해 props를 전달해야 합니다. Context를 사용하면 전달 문제는 해결되지만, 플래그가 바뀔 때마다 모든 것이 다시 렌더링됩니다.
3. 자체 패턴을 가진 서드파티 SDK
import { useFlags } from "some-feature-flag-sdk";
function Checkout() {
const { newCheckout } = useFlags();
// …
}
조금 나아졌지만 이제 월 $300 / 청구서와 벤더‑전용 API를 사용하게 됩니다.
동적 구성은 이렇게 느껴져야 합니다
function Checkout() {
const isNewCheckout = useConfig("new-checkout");
return isNewCheckout ? : ;
}
When someone flips that value in a dashboard:
- 컴포넌트가 자동으로 다시 렌더링됩니다
- 페이지 새로고침이 필요 없습니다
- prop drilling이 없습니다
- 완전한 TypeScript 지원
Replane으로 구축하기
Replane은 오픈소스(MIT)이며 바로 이것을 수행합니다.
1. 앱을 감싸기
import { ReplaneProvider } from "@replanejs/react";
function App() {
return (
}
>
);
}
프로바이더는 Server‑Sent Events를 통해 연결됩니다. 설정은 한 번 로드된 후 실시간으로 업데이트를 스트리밍합니다.
2. 어디서든 설정 읽기
import { useConfig } from "@replanejs/react";
function Checkout() {
const isNewCheckout = useConfig("new-checkout");
const discountBanner = useConfig("checkout-banner-text");
return (
{discountBanner && }
{isNewCheckout ? : }
);
}
이 훅은 요청된 설정만 구독하므로, 특정 키를 사용하는 컴포넌트만 해당 키가 변경될 때 다시 렌더링됩니다.
3. 컨텍스트를 사용해 타깃 지정 추가
function Checkout() {
const { user } = useAuth();
const rateLimit = useConfig("api-rate-limit", {
context: {
userId: user.id,
plan: user.subscription,
country: user.country,
},
});
// Premium users might get 10 000, free users get 100
}
오버라이드 규칙은 Replane 대시보드에서 정의됩니다:
plan이premium과 같으면 →10000반환country가DE와 같으면 →500반환- 기본값 →
100
새 규칙을 추가할 때 코드 변경이 필요하지 않습니다.
타입 안전하게 만들기
제네릭 훅은 작동하지만, 계약을 더 엄격히 할 수 있습니다:
// config.ts
import { createConfigHook } from "@replanejs/react";
interface AppConfigs {
"new-checkout": boolean;
"checkout-banner-text": string | null;
"api-rate-limit": number;
"pricing-tiers": {
free: { requests: number };
pro: { requests: number };
};
}
export const useAppConfig = createConfigHook();
// Checkout.tsx
import { useAppConfig } from "./config";
function Checkout() {
// Autocomplete works, type is inferred
const isNewCheckout = useAppConfig("new-checkout");
// ^? boolean
const pricing = useAppConfig("pricing-tiers");
// ^? { free: { requests: number }; pro: { requests: number } }
}
설정 이름에 오타가 있나요? TypeScript가 잡아냅니다.
잘못된 타입 가정인가요? TypeScript가 잡아냅니다.
로딩 상태 처리
앱에 맞는 전략을 선택하세요:
옵션 1 – Loader prop (default)
}
>
모든 설정이 로드될 때까지 로더를 표시합니다. 간단하지만 전체 앱을 차단합니다.
옵션 2 – Suspense
}>
React의 Suspense와 통합됩니다. 이미 데이터 페칭에 사용하고 있다면 이상적입니다.
옵션 3 – Async mode with defaults
제공된 기본값으로 즉시 렌더링됩니다. 실제 값은 연결이 설정된 후 교체됩니다. 로딩 UI는 없지만 초기 렌더링 후 값이 “플립”될 수 있습니다.
서버‑사이드 렌더링 (SSR) 하이드레이션
// On server
import { Replane, getReplaneSnapshot } from "@replanejs/react";
const replane = new Replane();
await replane.connect({ baseUrl: "...", sdkKey: "..." });
const snapshot = replane.getSnapshot(); // ← server‑fetched snapshot
// Pass `snapshot` to the client via props or serialize it into HTML
// On client
클라이언트는 스냅샷으로부터 즉시 하이드레이트한 뒤, 실시간 업데이트를 위해 연결합니다.
언제 사용해야 할까
적합한 경우
- 점진적 롤아웃을 위한 기능 플래그
- 간단한 A/B‑테스트 변형
- 사용자별 또는 테넌트별 맞춤화
- 마케팅에서 조정하고 싶은 UI 텍스트
- 운영 제한 (속도 제한, 최대 항목 수, 타임아웃)
- 사고 대응을 위한 킬 스위치
빌드 시점 설정으로 유지
- API 엔드포인트 (런타임에 변경 금지)
- 분석 키 (런타임에 변경 금지)
- 빌드 결과에 영향을 주는 모든 것
Source: …
일반적인 실수
1. 모든 것을 동적 구성에 넣기
실시간 업데이트가 필요하지 않은 값도 있습니다. 앱이 실행되는 동안 값이 변하지 않는다면 정적으로 유지하세요.
2. 기본값이 없음
// Bad – crashes if the config server is down
const limit = useConfig("rate-limit");
// Good – works even before the connection is established
{/* ... */}
3. 잘못된 위치에 컨텍스트 사용
// Bad – creates a new object each render, breaks memoization
const value = useConfig("limit", { context: { userId: user.id } });
// Better – stable reference
const context = useMemo(() => ({ userId: user.id }), [user.id]);
const value = useConfig("limit", { context });
4. 에러 경계 무시
import { ErrorBoundary } from "react-error-boundary";
Config failed to load}>
연결 실패는 예외를 발생시킵니다; 에러 경계로 잡아 처리하세요.
시작하기
npm install @replanejs/react
Replane을 직접 호스팅하는 경우 baseUrl을 여러분의 인스턴스로 지정하세요. 그렇지 않다면 무료 티어를 cloud.replane.dev에서 이용할 수 있습니다.
import { ReplaneProvider, useConfig } from "@replanejs/react";
function App() {
return (
Loading…}
>
);
}
function Main() {
const isEnabled = useConfig("feature-enabled");
return Feature is {isEnabled ? "on" : "off"};
}
그 결제 차단 스위치? 이제 대시보드에서 토글로 바뀌었습니다. 제품 팀이 직접 켤 수 있습니다. 10 % 롤아웃? 규칙 하나만 바꾸면 배포 없이 적용됩니다. 그리고 새벽 2시에 문제가 생기면 휴대폰으로 바로 비활성화합니다.
질문이 있나요? 댓글을 남기거나 GitHub 저장소를 확인해 주세요.