# Next.js

{% hint style="info" %}
Next.js는 `@hackler/react-sdk` 패키지를 이용하여 핵클을 연동할 수 있습니다.

핵클 SDK의 자세한 연동 및 사용 가이드는 [React](/development-guide/react.md) 가이드를 참고해주세요.
{% endhint %}

## 1. 설치

Next.js 에서 Hackle 을 사용하시려면 `@hackler/react-sdk`를 설치해야 합니다.

{% tabs %}
{% tab title="npm" %}

```shell
npm install @hackler/react-sdk
```

{% endtab %}

{% tab title="yarn" %}

```shell
yarn add @hackler/react-sdk
```

{% endtab %}

{% tab title="pnpm" %}

```shell
pnpm add @hackler/react-sdk
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
.env.development, .env.production 파일에 Hackle SDK 키를 개발환경, 운영환경에 맞게 각각 추가하세요.

* SDK 키는 핵클 서비스의 대시보드 안에 위치한 [SDK 연동 정보](https://dashboard.hackle.io/config/sdk-setting)에서 확인하실 수 있습니다.
  {% endhint %}

```
NEXT_PUBLIC_HACKLE_SDK_KEY=your-hackle-sdk-key
```

## 2. 연동 방법

Hackle은 Next.js 의 [Page Router](https://nextjs.org/docs/pages) 와 [App Router](https://nextjs.org/docs/app)를 모두 지원합니다. 각각 Hackle을 연동하는 방법에는 몇 가지 차이점이 있습니다.

#### 인스턴스 생성

{% tabs %}
{% tab title="App Router" %}

```typescript
// app/hackleClient.client.ts

"use client";

import { createInstance } from "@hackler/react-sdk";

export const hackleClient = createInstance(
  process.env.NEXT_PUBLIC_HACKLE_SDK_KEY!
);
```

{% endtab %}

{% tab title="Page Router" %}

```typescript
// app/hackleClient.client.ts

import { createInstance } from "@hackler/react-sdk";

export const hackleClient = createInstance(
  process.env.NEXT_PUBLIC_HACKLE_SDK_KEY!
);
```

{% endtab %}
{% endtabs %}

#### App Router - 프로바이더 생성

```typescript
// app/HackleClientProvider.tsx

"use client";

import { HackleProvider } from "@hackler/react-sdk";
import { hackleClient } from "@/app/hackleClient.client";

export function HackleClientProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <HackleProvider hackleClient={hackleClient} user={{userId: "a-user-id"}} supportSSR>
      {children}
    </HackleProvider>
  );
}
```

#### App Router - 루트 레이아웃 설정

```typescript
import { HackleClientProvider } from "@/app/HackleClientProvider";
import "./globals.css";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ko">
      <body>
        <HackleClientProvider>
          {children}
        </HackleClientProvider>
      </body>
    </html>
  );
}
```

#### Page Router - 프로바이더 사용

```typescript
// pages/_app.tsx

import type { AppProps } from "next/app"
import { HackleProvider } from "@hackler/react-sdk"
import { hackleClient } from "@/app/hackleClient.client"

export default function App({ Component, pageProps }: AppProps) {
  return (
    <HackleProvider hackleClient={hackleClient} user={{ userId: "a-user-id" }} supportSSR>
      <Component {...pageProps} />
    </HackleProvider>
  )
}
```

## 3. 주요 기능

#### A/B 테스트 그룹 분배

{% tabs %}
{% tab title="App Router" %}

```typescript
"use client"

import { useLoadableVariationDetail } from "@hackler/react-sdk"
import DecisionComponent from "./DecisionComponent"

interface ClientComponentProps {
  experimentKey: number
}

export default function ClientComponent({ experimentKey }: ClientComponentProps) {
  const { decision, isLoading } = useLoadableVariationDetail(experimentKey)
  if (isLoading) return <div>Loading...</div>
  const { variation, reason, experiment } = decision

  return (
    <div>
      <h3>Client Component</h3>
      <DecisionComponent
        variation={variation}
        reason={String(reason)}
        experimentKey={experiment?.key.toString() ?? "-"}
        experimentVersion={experiment?.version.toString() ?? "-"}
      />
    </div>
  )
}
```

{% endtab %}

{% tab title="Page Router" %}

```typescript
import { useLoadableVariationDetail } from "@hackler/react-sdk"
import DecisionComponent from "./DecisionComponent"

interface ClientComponentProps {
  experimentKey: number
}

export default function ClientComponent({ experimentKey }: ClientComponentProps) {
  const { decision, isLoading } = useLoadableVariationDetail(experimentKey)
  if (isLoading) return <div>Loading...</div>
  const { variation, reason, experiment } = decision

  return (
    <div>
      <h3>Client Component</h3>
      <DecisionComponent
        variation={variation}
        reason={String(reason)}
        experimentKey={experiment?.key.toString() ?? "-"}
        experimentVersion={experiment?.version.toString() ?? "-"}
      />
    </div>
  )
}
```

{% endtab %}
{% endtabs %}

#### 기능플래그 결정

{% tabs %}
{% tab title="App Router" %}

```typescript
"use client"

import { useLoadableFeatureDetail } from "@hackler/react-sdk"
import { FEATURE_FLAG_KEY } from "@/app/constants"
import DecisionComponent from "./DecisionComponent"

export default function ClientComponent() {
  const { decision, isLoading } = useLoadableFeatureDetail(FEATURE_FLAG_KEY)
  if (isLoading) return <div>Loading...</div>
  const { isOn, reason, experiment } = decision

  return (
    <div>
      <h3>Client Component</h3>
      <DecisionComponent
        isOn={isOn}
        reason={String(reason)}
        experimentKey={experiment?.key.toString() ?? "-"}
        experimentVersion={experiment?.version.toString() ?? "-"}
      />
    </div>
  )
}
```

{% endtab %}

{% tab title="Page Router" %}

```typescript
import { useLoadableFeatureDetail } from "@hackler/react-sdk"
import { FEATURE_FLAG_KEY } from "@/app/constants"
import DecisionComponent from "./DecisionComponent"

export default function ClientComponent() {
  const { decision, isLoading } = useLoadableFeatureDetail(FEATURE_FLAG_KEY)
  if (isLoading) return <div>Loading...</div>
  const { isOn, reason, experiment } = decision

  return (
    <div>
      <h3>Client Component</h3>
      <DecisionComponent
        isOn={isOn}
        reason={String(reason)}
        experimentKey={experiment?.key.toString() ?? "-"}
        experimentVersion={experiment?.version.toString() ?? "-"}
      />
    </div>
  )
}
```

{% endtab %}
{% endtabs %}

#### 원격구성 적용

{% tabs %}
{% tab title="App Router" %}

```typescript
"use client";

import useRemoteConfigWithParsing from "@/app/hooks/useRemoteConfigWithParsing";
import { REMOTE_CONFIG_KEY } from "@/app/constants";

const defaultConfig = {
  isDemo: false,
};

export default function ClientComponent() {
  const config = useRemoteConfigWithParsing(REMOTE_CONFIG_KEY, defaultConfig);
  if (config.isLoading) return <div>Loading...</div>

  return (
    <div>
      <h3>Client Component</h3>
      <dl>
        <dt>defaultConfig</dt>
        <dd>{JSON.stringify(defaultConfig, null, 2)}</dd>
        <dt>config</dt>
        <dd>{JSON.stringify(config.value, null, 2)}</dd>
      </dl>
    </div>
  );
}
```

{% endtab %}

{% tab title="Page Router" %}

```typescript
import useRemoteConfigWithParsing from "@/app/hooks/useRemoteConfigWithParsing";
import { REMOTE_CONFIG_KEY } from "@/app/constants";

const defaultConfig = {
  isDemo: false,
};

export default function ClientComponent() {
  const config = useRemoteConfigWithParsing(REMOTE_CONFIG_KEY, defaultConfig);
  if (config.isLoading) return <div>Loading...</div>

  return (
    <div>
      <h3>Client Component</h3>
      <dl>
        <dt>defaultConfig</dt>
        <dd>{JSON.stringify(defaultConfig, null, 2)}</dd>
        <dt>config</dt>
        <dd>{JSON.stringify(config.value, null, 2)}</dd>
      </dl>
    </div>
  );
}
```

{% endtab %}
{% endtabs %}

```typescript
// app/hooks/useRemoteConfigWithParsing.ts

import { useLoadableRemoteConfig } from "@hackler/react-sdk"

export default function useRemoteConfigWithParsing<T>(key: string, defaultValue: T): { value: T; isLoading: boolean } {
  const { remoteConfig, isLoading } = useLoadableRemoteConfig()
  if (isLoading) return { value: defaultValue, isLoading }

  try {
    const configValue = remoteConfig.get(key, JSON.stringify(defaultValue))
    return { value: JSON.parse(configValue), isLoading }
  } catch (error) {
    console.warn("Failed to parse remote config:", error)
    return { value: defaultValue, isLoading }
  }
}
```

#### 이벤트 전송

```typescript
// app/example.ts

"use client"

import { useTrack } from "@hackler/react-sdk";

export default function Example() {
  const track = useTrack();

  return <button onClick={() => track({ key: "test" })}>test</button>;
```

### 고급설정

Next.js 에서 서버사이드 분배를 위해서는 추가 설정이 필요합니다. 서버사이드에서 분배를 하는 경우 분배 식별자와 서버 사이드와 클라이언트 사이드에서 실행되는 코드를 주의 깊게 관리해야 합니다.

#### 설치

서버사이드(Node.js 환경)에서 사용될 sdk 를 별도로 설치해야 합니다.

{% tabs %}
{% tab title="npm" %}

```shell
npm install @hackler/javascript-sdk
```

{% endtab %}

{% tab title="yarn" %}

```shell
yarn add @hackler/javascript-sdk
```

{% endtab %}

{% tab title="pnpm" %}

```shell
pnpm add @hackler/javascript-sdk
```

{% endtab %}
{% endtabs %}

#### SDK 키 추가

.env.development, .env.production 파일에 Hackle SDK 키를 개발환경, 운영환경에 맞게 각각 추가하세요.

```
HACKLE_SDK_KEY_SERVER=your-hackle-sdk-key
```

SDK 키는 <https://dashboard.hackle.io/config/sdk-setting> 에서 Server 키를 찾을 수 있습니다.

#### 연동 방법

```typescript
// app/hackleClient.server.ts

import { createInstance } from "@hackler/javascript-sdk";

export const hackleClient = createInstance(
  process.env.NEXT_PUBLIC_HACKLE_SDK_KEY!
);
```

#### 서버사이드 사용 방법

**A/B 테스트 그룹 분배**

{% tabs %}
{% tab title="App Router" %}

```typescript
import { hackleClient } from "@/app/HackleClient.server";
import DecisionComponent from "./DecisionComponent";

interface ServerComponentProps {
  experimentKey: number;
}

export default async function ServerComponent({
  experimentKey,
}: ServerComponentProps) {
  const userId = "a-user-id";
  await hackleClient.onInitialized();
  const { variation, reason, experiment } = hackleClient.variationDetail(
    experimentKey,
    { userId }
  );

  return (
    <div>
      <h3>Server Component</h3>
      <DecisionComponent
        variation={variation}
        reason={String(reason)}
        experimentKey={experiment?.key.toString() ?? "-"}
        experimentVersion={experiment?.version.toString() ?? "-"}
      />
    </div>
  );
}
```

{% endtab %}

{% tab title="Page Router" %}

```typescript
import { Decision } from "@hackler/javascript-sdk";
import { hackleClient } from "@/app/HackleClient.server";
import DecisionComponent from "./DecisionComponent";

interface ServerComponentProps {
  decison: Decision;
}

export default async function ServerComponent({
  decison
}: ServerComponentProps) {
  return (
    <div>
      <h3>Server Component</h3>
      <DecisionComponent
        variation={decison.variation}
        reason={String(decison.reason)}
        experimentKey={decison.experiment?.key.toString() ?? "-"}
        experimentVersion={decison.experiment?.version.toString() ?? "-"}
      />
    </div>
  );
}

export const getServerSideProps = async () => {
  const userId = "a-user-id";
  await hackleClient.onInitialized();
  const decision = hackleClient.variationDetail(experimentKey, { userId });
  return {
    props: {
      decision,
    },
  };
};
```

{% endtab %}
{% endtabs %}

**기능플래그 결정**

{% tabs %}
{% tab title="App Router" %}

```typescript
import { hackleClient } from "@/app/HackleClient.server";
import DecisionComponent from "./DecisionComponent";

interface ServerComponentProps {
  featureFlagKey: number;
}

export default async function ServerComponent({
  featureFlagKey,
}: ServerComponentProps) {
  const userId = "a-user-id";
  await hackleClient.onInitialized()
  const featureFlagDetail = hackleClient.featureFlagDetail(featureFlagKey, {
    userId
  });

  return (
    <div>
      <h3>Server Component</h3>
      <DecisionComponent
        isOn={featureFlagDetail.isOn}
        reason={String(featureFlagDetail.reason)}
        experimentKey={featureFlagDetail.experiment?.key.toString() ?? "-"}
        experimentVersion={
          featureFlagDetail.experiment?.version.toString() ?? "-"
        }
      />
    </div>
  );
}
```

{% endtab %}

{% tab title="Page Router" %}

```typescript
import { FeatureFlagDecision } from "@hackler/javascript-sdk";
import { hackleClient } from "@/app/HackleClient.server";
import DecisionComponent from "./DecisionComponent";

interface ServerComponentProps {
  featureFlagDecision: FeatureFlagDecision;
}

export default async function ServerComponent({
  featureFlagDecision,
}: ServerComponentProps) {
  return (
    <div>
      <h3>Server Component</h3>
      <DecisionComponent
        isOn={featureFlagDecision.isOn}
        reason={String(featureFlagDecision.reason)}
        experimentKey={featureFlagDecision.experiment?.key.toString() ?? "-"}
        experimentVersion={
          featureFlagDecision.experiment?.version.toString() ?? "-"
        }
      />
    </div>
  );
}

export const getServerSideProps = async () => {
  const userId = "a-user-id";

  await hackleClient.onInitialized();
  const featureFlagDecision = hackleClient.featureFlagDetail(featureFlagKey, {
    userId,
  });

  return {
    props: {
      featureFlagDecision,
    },
  };
};
```

{% endtab %}
{% endtabs %}

**원격구성 적용**

{% tabs %}
{% tab title="App Router" %}

```typescript
import { hackleClient } from "@/app/HackleClient.server";

const defaultConfig = {
  isDemo: false,
};

interface ServerComponentProps {
  remoteConfigKey: string;
}

export default async function ServerComponent({
  remoteConfigKey,
}: ServerComponentProps) {
  const userId = "a-user-id";
  await hackleClient.onInitialized()
  const remoteConfig = hackleClient.remoteConfig({
    userId
  });

  const config = JSON.parse(
    remoteConfig.get(remoteConfigKey, JSON.stringify(defaultConfig))
  );

  return (
    <div>
      <h3>Server Component</h3>
      <dl>
        <dt>defaultConfig</dt>
        <dd>{JSON.stringify(defaultConfig, null, 2)}</dd>
        <dt>config</dt>
        <dd>{JSON.stringify(config, null, 2)}</dd>
      </dl>
    </div>
  );
}
```

{% endtab %}

{% tab title="Page Router" %}

```typescript
import { hackleClient } from "@/app/HackleClient.server";

const defaultConfig = {
  isDemo: false,
};

interface ServerComponentProps {
  config: typeof defaultConfig;
}

export default async function ServerComponent({
  config,
}: ServerComponentProps) {
  return (
    <div>
      <h3>Server Component</h3>
      <dl>
        <dt>defaultConfig</dt>
        <dd>{JSON.stringify(defaultConfig, null, 2)}</dd>
        <dt>config</dt>
        <dd>{JSON.stringify(config, null, 2)}</dd>
      </dl>
    </div>
  );
}

export const getServerSideProps = async () => {
  const userId = "a-user-id";
  await hackleClient.onInitialized();
  const remoteConfig = hackleClient.remoteConfig({
    userId,
  });

  const config = JSON.parse(
    remoteConfig.get(remoteConfigKey, JSON.stringify(defaultConfig))
  );

  return {
    props: {
      config: config,
    },
  };
};
```

{% endtab %}
{% endtabs %}

**이벤트 전송**

{% tabs %}
{% tab title="App Router" %}

```typescript
import { hackleClient } from "@/app/HackleClient.server";

export default async function Example() {
	hackleClient.track({key: "test"}, {userId: "a-user-id"});
}
```

{% endtab %}

{% tab title="Page Router" %}

```typescript
import { hackleClient } from "@/app/HackleClient.server";

export default async function Example() {
	hackleClient.track({key: "test"}, {userId: "a-user-id"});
}
```

{% endtab %}
{% endtabs %}

#### instrumentation

HackleClient 는 초기화시 Hackle Server 로 부터 설정 정보를 받아오고 이후 주기적으로 동기화를 합니다. 따라서 instrumentation.ts 를 활용하면 await hackleClient.onInitialized() 와 같은 코드를 매 페이지에서 사용하지 않고 코드를 더 간단하게 유지할 수 있습니다. 단 instrumentationHook 이 기본설정이 되는 Next.js v15 이상에서 권장 합니다.

{% tabs %}
{% tab title="hackleClient.server.ts" %}

```typescript
// app/hackleClient.server.ts

import { createInstance } from "@hackler/javascript-sdk";

declare global {
  var __hackleClient: ReturnType<typeof createInstance> | undefined;
  var __hackleInitialized: boolean | undefined;
}

if (!global.__hackleClient) {
  global.__hackleClient = createInstance(
    process.env.NEXT_PUBLIC_HACKLE_SDK_KEY!
  );
}

export async function initializeHackle() {
  if (global.__hackleInitialized) {
    return;
  }

  try {
    await global.__hackleClient!.onInitialized({
      timeout: 10000,
    });

    global.__hackleInitialized = true;
  } catch (error) {
    console.error("❌ Hackle SDK initialization failed:", error);
    throw error;
  }
}

export const hackleClient = global.__hackleClient;
```

{% endtab %}

{% tab title="instrumentation.ts" %}

```typescript
// instrumentation.ts

import { initializeHackle } from "@/app/hackleClient.server";

export async function register() {
  await initializeHackle();
}
```

{% endtab %}
{% endtabs %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.hackle.io/development-guide/nextjs-index.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
