본문으로 건너뛰기

빠른 시작

이 가이드를 완료하면 브라우저에서 지갑을 선택해 연결하고, 연결된 계정 ID와 체인 ID를 화면에 표시할 수 있습니다. 이 예제는 Sepolia 테스트넷을 대상으로 하며, 프로젝트 콘솔에서 발급받은 Client ID가 필요합니다 — clientId는 AppKit 생성자의 필수 옵션입니다.

이 빠른 시작은 프레임워크 없는 TypeScript 앱을 다룹니다. React·Next.js 앱이라면 UI 없는 코어 대신 React 바인딩(@scope-connect/appkit-react)의 Provider와 기본 제공 UI(<ScopeConnect />)로 더 적은 코드로 시작할 수 있습니다.

사전 준비

  • Node.js
  • npm
  • 브라우저에 설치된 EVM 확장 지갑 하나
  • Sepolia 테스트넷 연결 요청을 승인할 테스트 계정
  • 프로젝트 콘솔에서 발급받은 Client ID (공개값, X-App-Key로 전송)

프로젝트에서는 공식 배포 채널에서 안내받은 @scope-connect/appkit 버전을 고정합니다.

1. TypeScript 프로젝트 만들기

Vite의 vanilla-ts 템플릿으로 빈 브라우저 앱을 만듭니다.

터미널
npm create vite@latest scope-connect-quickstart -- --template vanilla-ts
cd scope-connect-quickstart
npm install
npm install "@scope-connect/appkit@<VERSION>"

<VERSION>을 배포 안내에서 제공한 버전으로 바꿉니다. 설치가 끝나면 package.json@scope-connect/appkit이 추가됩니다.

2. 지갑 선택 화면 구현

src/main.ts를 아래 코드로 교체합니다. 프로젝트 설정과 현재 환경을 반영한 지갑 목록을 만들고, 사용자가 버튼을 클릭하면 해당 지갑으로 연결합니다. 지갑 승인은 반드시 사용자의 버튼 클릭 안에서 요청합니다.

src/main.ts
import "./style.css";
import {
AppError,
AppKit,
AppResultCode,
isWalletNetworkSupported,
type SupportWalletInfo,
walletSupportsRelayOn,
type WalletId,
} from "@scope-connect/appkit";

// 이 빠른 시작은 Sepolia 테스트넷을 대상으로 합니다.
const CHAIN_ID = "eip155:11155111";

// 필수 옵션은 clientId 하나입니다 — 없거나 공백이면 생성자가 INVALID_CONFIG를 던집니다.
// clientId는 콘솔에서 발급받은 공개 앱 키를 .env(VITE_SCOPE_CLIENT_ID)로 주입합니다.
// userHash(사용자 단위 지표용 식별값)는 선택입니다 — 개인정보를 포함하지 않는 값만 필요할 때 넘깁니다.
// 텔레메트리는 기본 비활성이므로 별도로 끄지 않아도 전송되지 않습니다.
const appKit = new AppKit({
clientId: import.meta.env.VITE_SCOPE_CLIENT_ID,
// 사용자 단위 조회·집계가 필요하면 getOrCreateUserHash()를 함께 import해 userHash로 넘깁니다.
});
appKit.init();

// 이 데스크톱 빠른 시작은 주입형(EIP-6963) 지갑만 버튼으로 표시합니다.
// 모바일 SDK·App2App·유니버설 링크와 릴레이 지갑은 별도 연결 흐름에서 다룹니다.
const isInjected = (wallet: SupportWalletInfo) => wallet.eip155?.rdns !== undefined;

async function render(): Promise<void> {
// 콘솔 설정(활성 지갑·네트워크)을 가져옵니다. 실패하면 SDK 내장 목록을 그대로 씁니다.
await appKit.loadConfig();

// 콘솔 설정과 현재 환경이 반영된 목록에서 이 네트워크를 지원하는 지갑만 남깁니다.
const wallets = appKit
.getSupportedWallets()
.filter((wallet) => isWalletNetworkSupported(wallet.walletId, CHAIN_ID));
const injected = wallets.filter(isInjected);
const relayOnly = wallets.filter(
(wallet) => !isInjected(wallet) && walletSupportsRelayOn(wallet, "eip155"),
);

document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
<main>
<h1>SCOPE Connect 빠른 시작</h1>
<p>연결할 지갑을 선택하세요.</p>
<div id="wallet-list">
${injected
.map(
(wallet) =>
`<button class="wallet" type="button" data-wallet-id="${wallet.walletId}">${wallet.displayName}</button>`,
)
.join("")}
</div>
${
relayOnly.length > 0
? `<p class="relay-hint">${relayOnly
.map((wallet) => wallet.displayName)
.join(", ")}는 QR·딥링크 릴레이로 연결하며 pair()가 필요합니다 — 이 빠른 시작에서는 다루지 않습니다.</p>`
: ""
}
<pre id="connection-result">아직 연결되지 않았습니다.</pre>
</main>
`;

const output = document.querySelector<HTMLPreElement>("#connection-result")!;
const buttons = document.querySelectorAll<HTMLButtonElement>(".wallet");

buttons.forEach((button) => {
button.addEventListener("click", async () => {
const walletId = button.dataset.walletId as WalletId;
buttons.forEach((other) => (other.disabled = true));
output.textContent = `${button.textContent} 승인을 기다리는 중입니다…`;

try {
const connection = await appKit.selectWallet(walletId, {
chainId: CHAIN_ID,
});

output.textContent = JSON.stringify(
{
account: connection.account,
chainId: connection.chainId,
},
null,
2,
);
} catch (error) {
if (error instanceof AppError && error.code === AppResultCode.USER_REJECTED) {
output.textContent = "연결 요청이 취소되었습니다. 다시 시도할 수 있습니다.";
} else if (error instanceof AppError && error.code === AppResultCode.WALLET_NOT_FOUND) {
output.textContent = "선택한 지갑을 찾지 못했습니다. 설치 또는 실행 상태를 확인하세요.";
} else {
output.textContent = "연결에 실패했습니다. 브라우저 콘솔에서 원인을 확인하세요.";
console.error(error);
}
} finally {
buttons.forEach((other) => (other.disabled = false));
}
});
});
}

void render();

init()에는 전달할 값이 없습니다(선택 인자 config는 현재 예약된 빈 타입). 체인 허용 목록은 더 이상 여기에 전달하지 않고, 프로젝트 콘솔 설정을 백엔드에서 가져오는 loadConfig()로 로드합니다. clientId는 생성자 필수 옵션이라 loadConfig()는 항상 콘솔 설정을 조회합니다. 요청이 실패하거나 프로젝트가 활성화한 항목이 없으면 전체 내장 목록을 사용합니다. AppKit 설정와 같은 방식으로 .env에 값을 넣습니다.

.env
VITE_SCOPE_CLIENT_ID=your_public_client_id

getSupportedWallets()loadConfig()로 가져온 프로젝트 설정과 현재 환경(데스크톱·모바일)을 반영한 목록을 반환하고, isWalletNetworkSupported(walletId, chainId)로 목표 네트워크를 지원하는 지갑만 남깁니다. 이 목록에는 연결 경로가 다른 지갑이 함께 들어갈 수 있습니다. 그래서 위 예제는 eip155.rdns(주입형 EIP-6963 발견 정보) 유무로 데스크톱 주입형 지갑만 버튼에 표시합니다. rdns가 없다고 selectWallet()을 사용할 수 없는 것은 아닙니다. 모바일 MetaMask SDK·유니버설 링크, Klip App2App, Phantom 모바일 연결은 selectWallet()이 실행 환경에 맞는 커넥터를 선택하고, Scope Connect 같은 릴레이 전용 지갑만 pair() 흐름을 사용합니다. 지갑별 조건은 지원 지갑과 네트워크에서 확인합니다.

3. 실행과 성공 확인

개발 서버를 시작하고 터미널에 표시된 로컬 URL을 브라우저에서 엽니다.

터미널
npm run dev
  1. 연결할 지갑 버튼을 선택합니다.
  2. 확장 지갑에서 계정 연결(필요하면 Sepolia로 네트워크 전환)을 승인합니다.
  3. 화면에 다음과 같은 결과가 나타나는지 확인합니다.
기대 결과 예시
{
"account": "eip155:11155111:0x8ba1…",
"chainId": "eip155:11155111"
}

account{namespace}:{reference}:{address} 형식의 CAIP-10이고 chainIdeip155:{number} 형식이면 첫 연결이 완료된 것입니다. 실제 주소 값은 선택한 지갑 계정에 따라 달라집니다.

4. 다른 네트워크와 지갑으로 확장

Solana 지갑도 같은 selectWallet()으로 연결합니다. chainId를 생략하면 지갑의 기본 네임스페이스에 맞는 커넥터가 자동으로 선택되고 기본 클러스터(solana:mainnet)로 연결하며, 다른 클러스터가 필요하면 solana: 체인을 전달합니다.

Solana 지갑 연결 예시
// Phantom을 주입형 Solana 지갑으로 연결합니다(기본 클러스터: solana:mainnet).
const solana = await appKit.selectWallet("phantom");

// Devnet을 대상으로 연결하려면 chainId를 전달합니다.
const devnet = await appKit.selectWallet("phantom", { chainId: "solana:devnet" });

사용자가 네트워크를 먼저 고르게 하려면 appKit.getSupportedNetworks()로 목록을 만든 뒤, 선택한 chainId를 위 예제의 isWalletNetworkSupported() 필터에 그대로 넘깁니다. 프로젝트 설정과 무관하게 SDK 내장 목록만 조회하면 되는 화면이라면 getWalletsForNetwork(chainId)를 사용해도 됩니다 — 대신 콘솔 설정은 반영되지 않습니다. Solana 모바일 경로는 별도의 세션 어댑터가 필요하며, 지갑별 데스크톱·모바일 조건은 지원 지갑과 네트워크에서 확인합니다.

clientId는 이미 필수로 구성했으므로 백엔드 잔액 조회는 추가 설정 없이 쓸 수 있고, 릴레이 연결도 추가 설정이 필요 없습니다 — Connect 백엔드 루트와 릴레이 엔드포인트는 모두 SDK에 빌드 시점에 고정됩니다. 텔레메트리는 기본 비활성이므로 위 예제처럼 telemetry를 생략하면 아무것도 전송하지 않습니다. 콘솔 모니터링이 필요하면 AppKit 설정에 따라 telemetry: true로 켭니다.

다음 단계