본문으로 건너뛰기

React 바인딩

@scope-connect/appkit-reactAppKit 개요의 UI 없는 @scope-connect/appkit에 React 화면 구성을 더하는 패키지입니다. 프레임워크에 종속되지 않는 코어는 모달·QR·딥링크 데이터만 반환하고, 이 패키지가 그 데이터를 React로 렌더링합니다.

두 개의 계층으로 구성됩니다.

  1. 연결 계층ScopeAppKitProvider가 설정을 주입한 AppKit 인스턴스를 React 컨텍스트로 제공하고, 세션 훅(useWalletConnection, useAppKitEvmSession, useSolanaSession, useRelaySession)이 그 인스턴스를 공유합니다.
  2. 기본 제공 UI(선택)<ScopeConnect /> 하나로 지갑 목록 → 릴레이 QR → 연결 카드 → 오류까지 표시할 수 있습니다. 화면을 직접 구성할 때는 WalletList, PairingQr, ConnectedCard를 따로 배치합니다.

연결·요청·서명 같은 동작 자체는 코어의 것이 그대로 유지됩니다. useScopeAppKit()이 반환하는 AppKit 인스턴스로 지갑 연결, 요청 보내기, 서명의 API를 동일하게 사용할 수 있습니다.

사전 준비

  • Node.js
  • React 18 이상 (react, react-dom)
  • viem 2.x
  • Next.js를 사용한다면 App Router 프로젝트

react, react-dom, viem은 선택 의존성이므로 앱에 직접 설치해야 합니다. @scope-connect/appkit은 이 패키지와 함께 설치됩니다.

1. 설치

공식 배포 채널에서 안내받은 버전을 고정합니다.

터미널
npm install "@scope-connect/appkit-react@<VERSION>"

<VERSION>을 배포 안내에서 제공한 버전으로 바꿉니다.

2. Next.js 설정

이 패키지는 각 파일 상단에 "use client" 지시문을 포함하며, 지갑 커넥터가 window에 접근하므로 클라이언트에서만 동작합니다. Next.js App Router에서는 두 가지를 설정합니다.

먼저 패키지를 transpile 대상에 추가합니다. "use client" 지시문과 클라이언트 전용 코드가 올바르게 처리됩니다.

next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
transpilePackages: ["@scope-connect/appkit-react"],
};

export default nextConfig;

다음으로 Provider는 SSR을 끄고 마운트합니다. 커넥터가 window를 참조하므로 서버 렌더링 시점에 실행되면 안 됩니다.

3. 앱에 ScopeAppKitProvider 추가

ScopeAppKitProviderScopeAppKitConfig를 주입합니다. 이 패키지는 환경 변수를 직접 읽지 않습니다. 앱이 런타임 환경에서 값을 읽어 설정으로 전달합니다.

dynamic(ssr:false)로 감싸는 클라이언트 래퍼를 하나 만듭니다.

app/providers.tsx
"use client";

import dynamic from "next/dynamic";
import type { ReactNode } from "react";
import type { ScopeAppKitConfig } from "@scope-connect/appkit-react";

// 커넥터가 window에 접근하므로 클라이언트에서만 마운트합니다.
const ScopeAppKitProvider = dynamic(
() => import("@scope-connect/appkit-react").then((m) => m.ScopeAppKitProvider),
{ ssr: false },
);

export function AppKitProviders({ children }: { children: ReactNode }) {
const clientId = process.env.NEXT_PUBLIC_APPKIT_CLIENT_ID;
if (!clientId) {
throw new Error("NEXT_PUBLIC_APPKIT_CLIENT_ID is required");
}

const config: ScopeAppKitConfig = {
dappName: "My dApp",
clientId,
};

return <ScopeAppKitProvider config={config}>{children}</ScopeAppKitProvider>;
}

루트 레이아웃에서 앱 전체를 감쌉니다.

app/layout.tsx
import type { ReactNode } from "react";
import { AppKitProviders } from "./providers";

export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
<AppKitProviders>{children}</AppKitProviders>
</body>
</html>
);
}

ScopeAppKitConfig 필드

필드타입필수설명
dappNamestringMetaMask 연결 프롬프트와 릴레이 페어링 메타데이터에 표시되는 dApp 이름
dappUrlstringMetaMask 프롬프트용 dApp URL. 미지정 시 window.location.origin
clientIdstringConnect 백엔드 공개 앱 키. 없거나 공백이면 AppKit 생성자가 INVALID_CONFIG를 던집니다
userHashstringAppKit에 전달하는 선택적 사용자 식별값. 이 값만으로 텔레메트리가 활성화되지는 않습니다
debugboolean모바일 연결 진단 로그. 기본값은 false이며 URL·지갑 주소가 포함될 수 있습니다

기본 주입형 연결이 지갑과 직접 통신하더라도 clientId는 항상 필요합니다. React 바인딩은 이 값으로 Phantom 모바일 세션과 주입형 EVM 연결 기록을 활성화합니다. Connect 백엔드 루트와 릴레이 WSS 엔드포인트는 @scope-connect/appkit에 빌드 시점에 고정됩니다.

현재 ScopeAppKitConfig는 텔레메트리 활성화 옵션을 제공하지 않습니다. userHash만 전달해도 텔레메트리 이벤트는 전송되지 않습니다. 텔레메트리가 필요한 경우 AppKit 설정의 주의사항을 확인하고 UI 없는 AppKit을 직접 구성합니다.

4. <ScopeConnect /> 기본 UI 사용

<ScopeConnect />는 지갑 목록 → 릴레이 QR → 연결 카드 → 오류 표시까지 연결 흐름 전체를 담은 기본 컴포넌트입니다. ScopeAppKitProvider 하위에 두면 props 없이 동작합니다.

app/page.tsx
"use client";

import { ScopeConnect } from "@scope-connect/appkit-react";

export default function Home() {
return (
<main>
<h1>Scope Connect</h1>
<ScopeConnect />
</main>
);
}

기본 제공 UI는 CSS 변수로 테마를 조정합니다(--fg, --accent, --muted 등, 미지정 시 기본값 사용). 부모 요소에 값을 선언하면 그대로 반영됩니다.

5. 훅과 컴포넌트로 연결 화면 구성

디자인을 직접 제어하려면 <ScopeConnect /> 대신 useWalletConnection 훅과 하위 컴포넌트를 조합합니다. 하위 컴포넌트는 전달받은 값을 화면에 표시합니다.

useWalletConnection은 EVM·Solana·릴레이 세션을 독립적으로 관리하고, 사용자가 마지막에 연결한 지갑에 해당하는 활성 세션을 노출합니다.

app/MyConnect.tsx
"use client";

import { isMobileEnv } from "@scope-connect/appkit";
import {
useScopeAppKit,
useWalletConnection,
WalletList,
PairingQr,
ConnectedCard,
} from "@scope-connect/appkit-react";

export function MyConnect() {
const appkit = useScopeAppKit();
const conn = useWalletConnection();

const wallets = appkit.getSupportedWallets({ mobile: isMobileEnv() });

// 연결됨 → 연결 카드
if (conn.connectionStatus === "connected" && conn.connectedAddress) {
return (
<ConnectedCard
walletName={conn.connectedWalletId ?? "Wallet"}
displayAddress={conn.displayAddress}
chainLabel={conn.currentEvmChainId ? `eip155:${conn.currentEvmChainId}` : "Solana"}
onDisconnect={() => void conn.disconnect()}
/>
);
}

// 릴레이 페어링 중 → QR
if (conn.pairingUri) {
return <PairingQr uri={conn.pairingUri} />;
}

// 그 외 → 지갑 목록
return (
<WalletList
wallets={wallets}
onConnect={(info) => void conn.connect(info.walletId)}
isConnecting={conn.connectionStatus === "connecting"}
selectedWalletId={conn.selectedWalletId}
/>
);
}

useWalletConnection이 노출하는 주요 값입니다.

타입설명
connectionStatus"idle" | "connecting" | "connected" | "failed"활성 세션의 연결 상태
connectedAddressstring | null연결된 계정 주소 (전체)
displayAddressstring | null축약 표시용 주소
connectedWalletIdstring | null연결된 지갑 ID
selectedWalletIdstring | null연결 시도 중인 지갑 ID
currentEvmChainIdnumber | undefinedEVM 연결의 현재 체인 ID
pairingUristring | null릴레이 페어링 QR URI (페어링 중에만 null이 아닌 값)
lastError{ code, message } | null활성 세션의 마지막 연결 실패 — code는 SDK의 AppResultCode 값(예: USER_REJECTED, WALLET_NOT_FOUND)이라 안내 문구를 원인별로 분기할 수 있습니다. 재시도 시 초기화
connect(walletId, chainId?)Promise<void>지갑 연결. 릴레이·Solana·EVM 경로를 자동 라우팅
switchToEvmChain(chainId)Promise<void>EVM 체인 전환
disconnect()Promise<void>활성 세션 연결 해제
reset()void모든 세션 초기화

더 낮은 계층이 필요하면 useAppKitEvmSession, useSolanaSession, useRelaySession을 개별적으로 사용할 수 있습니다.

6. Phantom 모바일에서 앱 복귀 처리

일반 모바일 브라우저에서 Phantom 연결 딥링크를 열면 Phantom 앱으로 전환되고, 승인 후 redirect_link(${origin}/wallet-callback?reqUuid=..&phase=connect)로 페이지를 리로드하며 돌아옵니다. 이 복귀를 처리하는 콜백 라우트를 하나 두고 handleSolanaMobileCallback(appkit, query)를 호출합니다.

app/wallet-callback/page.tsx
"use client";

import { useEffect, useRef, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useScopeAppKit, handleSolanaMobileCallback } from "@scope-connect/appkit-react";

export default function WalletCallbackPage() {
const router = useRouter();
const search = useSearchParams();
const appkit = useScopeAppKit();
const didRun = useRef(false);
const [failed, setFailed] = useState(false);

useEffect(() => {
if (didRun.current) return;
didRun.current = true;

void (async () => {
// 복귀 쿼리를 파싱하고 백엔드 콜백 POST·폴링·커넥터 복원을 수행합니다.
const result = await handleSolanaMobileCallback(appkit, search.toString());
if (!result) {
setFailed(true);
return;
}
router.replace("/");
})();
// 단일 실행 가드(didRun) — 의존성 변경 시 재실행하지 않습니다.
}, []);

if (failed) {
return <p>지갑 연결에 실패했습니다. 이전 화면으로 돌아가 다시 시도해 주세요.</p>;
}
return <p>지갑 연결을 확인하고 있습니다…</p>;
}

성공 후 홈으로 돌아오면 useWalletConnection(내부의 useSolanaSession)이 저장된 SDK 상태에서 연결을 복원합니다. handleSolanaMobileCallback은 실패하면 오류를 던지지 않고 null을 반환하므로, 실패 상태를 표시하고 사용자가 다시 시도할 수 있게 합니다.

패키지가 제공하는 API

카테고리제공 API
기본 제공 UIScopeConnect, WalletList, PairingQr, ConnectedCard, PaymentRequest
Provider·컨텍스트ScopeAppKitProvider, useScopeAppKit, useScopeAppKitConfig
세션 훅useWalletConnection, useAppKitEvmSession, useSolanaSession, useRelaySession, useNetworkFee
팩토리·유틸createScopeAppKit, handleSolanaMobileCallback, truncateAddress, parseEvmChainId

다음 단계