# 브라우저와 앱에서 기능 플래그 쓰기

사용자의 기기에서 실행되는 코드는 플래그 규칙을 가질 수 없습니다. 규칙에는 고객, 내부 계정, 아직 발표하지 않은 요금제가 들어갈 수 있기 때문입니다. 브라우저와 앱에서는 Mocco가 직접 플래그를 평가하고 `new-checkout = true` 같은 답만 돌려줍니다. 코드는 그대로 표준 [OpenFeature](https://openfeature.dev) API를 쓰고, 플래그가 바뀌면 Mocco의 프로바이더가 즉시 알려 줍니다.

이 가이드는 [기능 플래그 빠른 시작](https://www.mocco.club/docs/ko/articles/f3cruw-quickstart)처럼 기능 플래그와 환경이 있는 프로젝트가 있다고 가정합니다.

## 1. 기기에 보여 줄 플래그 고르기

플래그를 열고 **Available to browsers and apps**를 체크합니다. 이렇게 표시한 플래그만 publishable 키에 응답합니다. 다른 플래그는 존재하지 않는 것처럼 보이므로(`FLAG_NOT_FOUND`) 내부 플래그의 이름이 기기에 전달되지 않습니다.

!["Available to browsers and apps"가 체크된 플래그 페이지](https://fi-workers-mocco-storage.s3.ap-northeast-2.amazonaws.com/pub/w/93e11fe8-0494-4e3d-b1cb-fe4dffd3ee7e/p/d64785b8-9a23-4fa1-a81d-550af30e203a/helpcenter/7a4f921f-eeae-4986-a6e7-f3cd36eded68/flags-client-visible.png)

## 2. 사이트의 오리진 허용하기

publishable 키는 페이지의 오리진이 프로젝트의 **Web** 앱 중 하나에 속할 때만 웹 페이지에서 동작합니다. 프로젝트의 **Overview** 페이지에서 Web 앱을 추가하거나(또는 기존 앱에서 **Edit origins**를 선택하고), 페이지를 제공하는 모든 오리진을 입력합니다. 예를 들면 `https://app.acme.com`이고, 로컬 개발에서는 `http://localhost:5173`입니다. 오리진은 스킴과 호스트로 이루어지며 경로는 없습니다.

![프로젝트 개요에서 Web 앱의 오리진 편집하기](https://fi-workers-mocco-storage.s3.ap-northeast-2.amazonaws.com/pub/w/93e11fe8-0494-4e3d-b1cb-fe4dffd3ee7e/p/d64785b8-9a23-4fa1-a81d-550af30e203a/helpcenter/07491c20-fec1-4e47-9593-528ef33162c0/flags-origins.png)

앱(React Native)은 오리진을 보내지 않으므로 이 단계는 웹 페이지에만 해당합니다.

## 3. 환경 하나를 위한 publishable 키 만들기

**API keys** 페이지에서 **flags:read** 권한이 있는 **Publishable** 키를 만들고 읽을 환경을 고릅니다. 서버 키와 마찬가지로 정확히 환경 하나만 읽으므로, 앱의 환경마다 자체 키로 빌드하세요. publishable 키는 앱에 넣어 배포해도 안전합니다. 1단계에서 표시한 플래그의 답만 읽을 수 있습니다.

![Production용 publishable flags:read 키 만들기](https://fi-workers-mocco-storage.s3.ap-northeast-2.amazonaws.com/pub/w/93e11fe8-0494-4e3d-b1cb-fe4dffd3ee7e/p/d64785b8-9a23-4fa1-a81d-550af30e203a/helpcenter/ec3f43eb-16b0-45be-a68a-d3e678442536/flags-pub-key.png)

## 4a. 웹 페이지 또는 React 앱

OpenFeature 웹 SDK와 Mocco의 웹 프로바이더를 설치합니다.

```bash
npm install @openfeature/web-sdk @mocco/openfeature-web
```

사용자 컨텍스트와 함께 프로바이더를 한 번 등록합니다.

```ts
import { MoccoWebProvider } from '@mocco/openfeature-web';
import { OpenFeature } from '@openfeature/web-sdk';

await OpenFeature.setContext({ targetingKey: user.id, plan: user.plan });
await OpenFeature.setProviderAndWait(new MoccoWebProvider({ publishableKey: 'mk_pub_…' }));

const showNewCheckout = OpenFeature.getClient().getBooleanValue('new-checkout', false);
```

평가는 프로바이더가 이미 가진 답을 읽으므로 네트워크를 기다리지 않습니다. 프로바이더는 모든 답을 한 번의 요청으로 가져오고 Mocco의 변경 스트림을 구독합니다. 콘솔에서 플래그를 바꾸면 열려 있는 페이지가 1~2초 안에 갱신되고 OpenFeature가 `PROVIDER_CONFIGURATION_CHANGED`를 발생시킵니다. 스트림에 연결할 수 없으면 대신 60초마다 확인합니다(`pollInterval`). 탭이 다시 보일 때도 확인하며, 마지막 답을 `localStorage`에 보관해 새로고침했을 때 네트워크가 응답하기 전에 그 답을 보여 줍니다.

사용자가 로그인하거나 요금제를 바꾸면 `OpenFeature.setContext(...)`를 다시 호출하세요. 프로바이더가 새 컨텍스트의 답을 가져옵니다.

### React

[`@openfeature/react-sdk`](https://openfeature.dev/docs/reference/technologies/client/web/react)를 쓰면 앱을 한 번 감싸고 훅으로 플래그를 읽습니다. 플래그가 바뀌면 컴포넌트가 다시 렌더링됩니다.

```tsx
import { MoccoWebProvider } from '@mocco/openfeature-web';
import { OpenFeature, OpenFeatureProvider, useBooleanFlagValue } from '@openfeature/react-sdk';

OpenFeature.setProvider(new MoccoWebProvider({ publishableKey: 'mk_pub_…' }));

export function App() {
  return (
    <OpenFeatureProvider>
      <Checkout />
    </OpenFeatureProvider>
  );
}

function Checkout() {
  const isNew = useBooleanFlagValue('new-checkout', false);
  return isNew ? <NewCheckout /> : <OldCheckout />;
}
```

## 4b. React Native 앱

OpenFeature SDK, Mocco의 React Native 프로바이더, 프로바이더가 쓰는 저장소 및 이벤트 스트림 라이브러리를 설치합니다.

```bash
npm install @openfeature/web-sdk @openfeature/react-sdk @mocco/openfeature-react-native \
  @react-native-async-storage/async-storage react-native-sse
```

React Native에는 `localStorage`, 페이지 가시성, `EventSource`가 없으므로 직접 넘겨 줍니다.

```tsx
import AsyncStorage from '@react-native-async-storage/async-storage';
import { MoccoReactNativeProvider } from '@mocco/openfeature-react-native';
import { OpenFeature, OpenFeatureProvider, useBooleanFlagValue } from '@openfeature/react-sdk';
import { AppState } from 'react-native';
import EventSource from 'react-native-sse';

OpenFeature.setProvider(
  new MoccoReactNativeProvider({
    publishableKey: 'mk_pub_…',
    storage: AsyncStorage,
    appState: AppState,
    EventSource,
  }),
);
```

그다음에는 위 React 예제처럼 `OpenFeatureProvider`와 훅을 씁니다.

각 항목의 역할은 다음과 같습니다.

- **`storage`**는 마지막 답을 기기에 보관합니다. 다음 실행 때 앱은 오프라인이어도 그 답으로 바로 시작하고 백그라운드에서 갱신합니다. Mocco에 연결할 수 없는 동안 프로바이더는 `PROVIDER_STALE`을 보고하며 저장된 사본으로 계속 응답합니다.
- **`appState`**는 앱이 포그라운드로 돌아올 때마다 새 답을 가져오고, 백그라운드에서는 확인을 멈춥니다.
- **`EventSource`**는 앱이 열려 있는 동안 Mocco의 변경 스트림을 구독하므로 변경이 몇 초 안에 도착합니다. 없으면 프로바이더는 앱이 활성 상태인 동안 60초마다 확인합니다(`pollIntervalMs`).

저장된 답도 없고 네트워크도 없는 맨 처음 실행만 답 없이 시작합니다. 이때는 모든 플래그가 코드의 기본값을 반환합니다.

## 서버 측: Vercel Flags SDK

[Flags SDK](https://flags-sdk.dev)를 쓰는 Next.js 앱에서는 **secret** 키와 Mocco의 서버 프로바이더로 서버에서 평가합니다. Flags SDK의 OpenFeature 어댑터를 거칩니다.

```bash
npm install flags @flags-sdk/openfeature @openfeature/server-sdk @mocco/openfeature-server
```

```ts
import { createOpenFeatureAdapter } from '@flags-sdk/openfeature';
import { MoccoProvider } from '@mocco/openfeature-server';
import { OpenFeature } from '@openfeature/server-sdk';
import { flag } from 'flags/next';

const mocco = createOpenFeatureAdapter(async () => {
  await OpenFeature.setProviderAndWait(new MoccoProvider({ secretKey: process.env.MOCCO_FLAGS_KEY! }));
  return OpenFeature.getClient();
});

export const newCheckout = flag<boolean>({
  key: 'new-checkout',
  defaultValue: false,
  adapter: mocco.booleanValue(),
  identify: async () => ({ targetingKey: (await currentUser()).id }),
});
```

그러면 서버 컴포넌트나 라우트에서 `await newCheckout()`이 [기능 플래그 빠른 시작](https://www.mocco.club/docs/ko/articles/f3cruw-quickstart#7-evaluate-flags-from-node)에서 설명한 대로 메모리에서 평가됩니다.
