SDK ์ฐ๋
Next.js๋
@hackler/react-sdkํจํค์ง๋ฅผ ์ด์ฉํ์ฌ ํตํด์ ์ฐ๋ํ ์ ์์ต๋๋ค.ํตํด SDK์ ์์ธํ ์ฐ๋ ๋ฐ ์ฌ์ฉ ๊ฐ์ด๋๋ React ๊ฐ์ด๋๋ฅผ ์ฐธ๊ณ ํด์ฃผ์ธ์.
1. ์ค์น
Next.js ์์ Hackle ์ ์ฌ์ฉํ์๋ ค๋ฉด @hackler/react-sdk๋ฅผ ์ค์นํด์ผ ํฉ๋๋ค.
npm install @hackler/react-sdkyarn add @hackler/react-sdkpnpm add @hackler/react-sdk.env.development, .env.production ํ์ผ์ Hackle SDK ํค๋ฅผ ๊ฐ๋ฐํ๊ฒฝ, ์ด์ํ๊ฒฝ์ ๋ง๊ฒ ๊ฐ๊ฐ ์ถ๊ฐํ์ธ์.
- SDK ํค๋ ํตํด ์๋น์ค์ ๋์๋ณด๋ ์์ ์์นํ SDK ์ฐ๋ ์ ๋ณด์์ ํ์ธํ์ค ์ ์์ต๋๋ค.
NEXT_PUBLIC_HACKLE_SDK_KEY=your-hackle-sdk-key
2. ์ฐ๋ ๋ฐฉ๋ฒ
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>
)
}
3. ์ฃผ์ ๊ธฐ๋ฅ
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-sdkyarn add @hackler/javascript-sdkpnpm add @hackler/javascript-sdkSDK ํค ์ถ๊ฐ
.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
