React에서 동적 구성 — Jank 없이 Feature Flags

발행: (2026년 1월 10일 오전 03:55 GMT+9)
9 min read
원문: Dev.to

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 대시보드에서 정의됩니다:

  • planpremium과 같으면 → 10000 반환
  • countryDE와 같으면 → 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 저장소를 확인해 주세요.

Back to Blog

관련 글

더 보기 »

향상된 환경 변수 UI

환경 변수 UI가 이제 공유 및 프로젝트 환경 변수 전반에 걸쳐 관리하기가 더 쉬워졌습니다. 스크롤에 소비하는 시간을 줄이고, 더 큰 hit targets를 사용할 수 있습니다,…

개발자? 아니면 그냥 Toolor?

번역할 텍스트를 제공해 주시겠어요? 현재는 이미지 링크만 있어 내용을 확인할 수 없습니다. 텍스트를 복사해서 알려주시면 한국어로 번역해 드리겠습니다.

Todo 앱

소개 첫 번째 논리 중심 프로젝트인 Counters를 완료한 후, 나는 UI를 개선하는 것이 아니라 복잡성의 다음 자연스러운 단계로 나아가고 싶었다 — ...