SDK ์—ฐ๋™

๐Ÿ“˜

Next.js๋Š” @hackler/react-sdk ํŒจํ‚ค์ง€๋ฅผ ์ด์šฉํ•˜์—ฌ ํ•ตํด์„ ์—ฐ๋™ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•ตํด SDK์˜ ์ž์„ธํ•œ ์—ฐ๋™ ๋ฐ ์‚ฌ์šฉ ๊ฐ€์ด๋“œ๋Š” React ๊ฐ€์ด๋“œ๋ฅผ ์ฐธ๊ณ ํ•ด์ฃผ์„ธ์š”.

1. ์„ค์น˜

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 ํ‚ค๋ฅผ ๊ฐœ๋ฐœํ™˜๊ฒฝ, ์šด์˜ํ™˜๊ฒฝ์— ๋งž๊ฒŒ ๊ฐ๊ฐ ์ถ”๊ฐ€ํ•˜์„ธ์š”.

  • 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-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();
}