Next.js
설치
Next.js 에서 Hackle 을 사용하시려면 @hackler/react-sdk를 설치해야 합니다.
npm install @hackler/react-sdk
yarn add @hackler/react-sdk
pnpm add @hackler/react-sdk
.env.development, .env.production 파일에 Hackle SDK 키를 개발환경, 운영환경에 맞게 각각 추가하세요.
NEXT_PUBLIC_HACKLE_SDK_KEY=your-hackle-sdk-key
SDK 키는 https://dashboard.hackle.io/config/sdk-setting 에서 App/Browser 키를 찾을 수 있습니다.

연동 방법
Hackle은 Next.js 의 Page Router 와 App Router를 모두 지원합니다. 각각 Hackle을 연동하는 방법에는 몇 가지 차이점이 있습니다.
인스턴스 생성
// app/hackleClient.client.ts
"use client";
import { createInstance } from "@hackler/react-sdk";
export const hackleClient = createInstance(
process.env.NEXT_PUBLIC_HACKLE_SDK_KEY!
);
// app/hackleClient.client.ts
import { createInstance } from "@hackler/react-sdk";
export const hackleClient = createInstance(
process.env.NEXT_PUBLIC_HACKLE_SDK_KEY!
);
App Router - 프로바이더 생성
// 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 - 루트 레이아웃 설정
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 - 프로바이더 사용
// 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>
)
}
주요 기능
A/B 테스트 그룹 분배
"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>
)
}
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>
)
}
기능플래그 결정
"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>
)
}
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>
)
}
원격구성 적용
"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>
);
}
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>
);
}
// 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 }
}
}
이벤트 전송
// 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 를 별도로 설치해야 합니다.
npm install @hackler/javascript-sdk
yarn add @hackler/javascript-sdk
pnpm add @hackler/javascript-sdk
SDK 키 추가
.env.development, .env.production 파일에 Hackle SDK 키를 개발환경, 운영환경에 맞게 각각 추가하세요.
HACKLE_SDK_KEY_SERVER=your-hackle-sdk-key
SDK 키는 https://dashboard.hackle.io/config/sdk-setting 에서 Server 키를 찾을 수 있습니다.
연동 방법
// app/hackleClient.server.ts
import { createInstance } from "@hackler/javascript-sdk";
export const hackleClient = createInstance(
process.env.NEXT_PUBLIC_HACKLE_SDK_KEY!
);
서버사이드 사용 방법
A/B 테스트 그룹 분배
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>
);
}
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,
},
};
};
기능플래그 결정
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>
);
}
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,
},
};
};
원격구성 적용
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>
);
}
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,
},
};
};
이벤트 전송
import { hackleClient } from "@/app/HackleClient.server";
export default async function Example() {
hackleClient.track({key: "test"}, {userId: "a-user-id"});
}
import { hackleClient } from "@/app/HackleClient.server";
export default async function Example() {
hackleClient.track({key: "test"}, {userId: "a-user-id"});
}
instrumentation
HackleClient 는 초기화시 Hackle Server 로 부터 설정 정보를 받아오고 이후 주기적으로 동기화를 합니다. 따라서 instrumentation.ts 를 활용하면 await hackleClient.onInitialized() 와 같은 코드를 매 페이지에서 사용하지 않고 코드를 더 간단하게 유지할 수 있습니다. 단 instrumentationHook 이 기본설정이 되는 Next.js v15 이상에서 권장 합니다.
// 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;
// instrumentation.ts
import { initializeHackle } from "@/app/hackleClient.server";
export async function register() {
await initializeHackle();
}
Updated 1 day ago