Next.js - Solana Kit
Solana Kit을 사용하여 Next.js에서 최소한의 Solana 지갑 통합을 설정하세요.
이 가이드는 @solana/kit
을 사용하여 Next.js 애플리케이션에서 Solana 지갑 기능을
구현하는 최소한의 예시를 제공합니다. 지갑 연결 버튼과 트랜잭션을 보내는
컴포넌트를 만들게 됩니다.
Nextjs Kit 애플리케이션
React 애플리케이션에서 @solana/kit
을 사용하는 더 포괄적인 예시는 Solana Kit
저장소의
React 앱 예시를
참조하세요.
리소스
사전 요구 사항
- Node.js 설치
Next.js 프로젝트 생성
UI 컴포넌트를 위한 shadcn이 포함된 새로운 Next.js 프로젝트를 생성하고 필요한 Solana 의존성을 설치하세요.
$npx shadcn@latest init
프로젝트 디렉토리로 이동하세요:
$cd <project-name>
UI 컴포넌트 설치
다음 shadcn UI 컴포넌트를 설치하세요:
$npx shadcn@latest add button dropdown-menu avatar
Solana 의존성 설치
다음 Solana 의존성을 설치하세요:
$npm install @solana/kit @solana/react @wallet-standard/core @wallet-standard/react @solana-program/memo
구현 안내
아래 단계를 따르고 제공된 코드를 프로젝트에 복사하세요.
1. Solana 컨텍스트 생성
먼저, 애플리케이션 전체의 지갑 상태를 관리하는 React 컨텍스트를 생성합니다.
components/solana-provider.tsx
를 생성하고 제공된 코드를 추가하세요. 이
프로바이더 컴포넌트는 다음과 같은 기능을 수행합니다:
- Solana의 devnet RPC 엔드포인트에 연결
- 사용자 브라우저에 설치된 사용 가능한 Solana 지갑 필터링
- 현재 연결된 지갑과 계정 추적
- 자식 컴포넌트에 지갑 상태 제공
"use client";import React, { createContext, useContext, useState, useMemo } from "react";import {useWallets,type UiWallet,type UiWalletAccount} from "@wallet-standard/react";import { createSolanaRpc, createSolanaRpcSubscriptions } from "@solana/kit";import { StandardConnect } from "@wallet-standard/core";// Create RPC connectionconst RPC_ENDPOINT = "https://api.devnet.solana.com";const WS_ENDPOINT = "wss://api.devnet.solana.com";const chain = "solana:devnet";const rpc = createSolanaRpc(RPC_ENDPOINT);const ws = createSolanaRpcSubscriptions(WS_ENDPOINT);interface SolanaContextState {// RPCrpc: ReturnType<typeof createSolanaRpc>;ws: ReturnType<typeof createSolanaRpcSubscriptions>;chain: typeof chain;// Wallet Statewallets: UiWallet[];selectedWallet: UiWallet | null;selectedAccount: UiWalletAccount | null;isConnected: boolean;// Wallet ActionssetWalletAndAccount: (wallet: UiWallet | null,account: UiWalletAccount | null) => void;}const SolanaContext = createContext<SolanaContextState | undefined>(undefined);export function useSolana() {const context = useContext(SolanaContext);if (!context) {throw new Error("useSolana must be used within a SolanaProvider");}return context;}export function SolanaProvider({ children }: { children: React.ReactNode }) {const allWallets = useWallets();// Filter for Solana wallets only that support signAndSendTransactionconst wallets = useMemo(() => {return allWallets.filter((wallet) =>wallet.chains?.some((c) => c.startsWith("solana:")) &&wallet.features.includes(StandardConnect) &&wallet.features.includes("solana:signAndSendTransaction"));}, [allWallets]);// State managementconst [selectedWallet, setSelectedWallet] = useState<UiWallet | null>(null);const [selectedAccount, setSelectedAccount] =useState<UiWalletAccount | null>(null);// Check if connected (account must exist in the wallet's accounts)const isConnected = useMemo(() => {if (!selectedAccount || !selectedWallet) return false;// Find the wallet and check if it still has this accountconst currentWallet = wallets.find((w) => w.name === selectedWallet.name);return !!(currentWallet &¤tWallet.accounts.some((acc) => acc.address === selectedAccount.address));}, [selectedAccount, selectedWallet, wallets]);const setWalletAndAccount = (wallet: UiWallet | null,account: UiWalletAccount | null) => {setSelectedWallet(wallet);setSelectedAccount(account);};// Create context valueconst contextValue = useMemo<SolanaContextState>(() => ({// Static RPC valuesrpc,ws,chain,// Dynamic wallet valueswallets,selectedWallet,selectedAccount,isConnected,setWalletAndAccount}),[wallets, selectedWallet, selectedAccount, isConnected]);return (<SolanaContext.Provider value={contextValue}>{children}</SolanaContext.Provider>);}
2. 레이아웃 업데이트
다음으로, 전체 Next.js 애플리케이션을 Solana 프로바이더로 감싸줍니다.
app/layout.tsx
를 제공된 코드로 업데이트하세요. 이 단계에서는:
SolanaProvider
컴포넌트를 가져옵니다- 애플리케이션의 자식 컴포넌트를
SolanaProvider
로 감쌉니다 - 모든 페이지와 컴포넌트가 지갑 기능에 접근할 수 있도록 합니다
import { SolanaProvider } from "@/components/solana-provider";import "./globals.css";export default function RootLayout({children}: Readonly<{children: React.ReactNode;}>) {return (<html lang="en"><body><SolanaProvider>{children}</SolanaProvider></body></html>);}
3. 지갑 연결 버튼 생성
이제 지갑을 연결하고 연결 해제하는 버튼을 만들어 보겠습니다.
components/wallet-connect-button.tsx
를 생성하고 제공된 코드를 추가하세요. 이
드롭다운 버튼은:
- 클릭 시 사용 가능한 지갑을 표시합니다
- 지갑 표준을 사용하여 지갑 연결 흐름을 처리합니다
"use client";import { useState } from "react";import { useSolana } from "@/components/solana-provider";import { Button } from "@/components/ui/button";import {DropdownMenu,DropdownMenuContent,DropdownMenuItem,DropdownMenuLabel,DropdownMenuSeparator,DropdownMenuTrigger} from "@/components/ui/dropdown-menu";import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";import { ChevronDown, Wallet, LogOut } from "lucide-react";import {useConnect,useDisconnect,type UiWallet} from "@wallet-standard/react";function truncateAddress(address: string): string {return `${address.slice(0, 4)}...${address.slice(-4)}`;}function WalletIcon({wallet,className}: {wallet: UiWallet;className?: string;}) {return (<Avatar className={className}>{wallet.icon && (<AvatarImage src={wallet.icon} alt={`${wallet.name} icon`} />)}<AvatarFallback>{wallet.name.slice(0, 2).toUpperCase()}</AvatarFallback></Avatar>);}function WalletMenuItem({wallet,onConnect}: {wallet: UiWallet;onConnect: () => void;}) {const { setWalletAndAccount } = useSolana();const [isConnecting, connect] = useConnect(wallet);const handleConnect = async () => {if (isConnecting) return;try {const accounts = await connect();if (accounts && accounts.length > 0) {const account = accounts[0];setWalletAndAccount(wallet, account);onConnect();}} catch (err) {console.error(`Failed to connect ${wallet.name}:`, err);}};return (<buttonclassName="flex w-full items-center justify-between px-2 py-1.5 text-sm outline-none hover:bg-accent focus:bg-accent disabled:pointer-events-none disabled:opacity-50"onClick={handleConnect}disabled={isConnecting}><div className="flex items-center gap-2"><WalletIcon wallet={wallet} className="h-6 w-6" /><span className="font-medium">{wallet.name}</span></div></button>);}function DisconnectButton({wallet,onDisconnect}: {wallet: UiWallet;onDisconnect: () => void;}) {const { setWalletAndAccount } = useSolana();const [isDisconnecting, disconnect] = useDisconnect(wallet);const handleDisconnect = async () => {try {await disconnect();setWalletAndAccount(null, null);onDisconnect();} catch (err) {console.error("Failed to disconnect wallet:", err);}};return (<DropdownMenuItemclassName="text-destructive focus:text-destructive cursor-pointer"onClick={handleDisconnect}disabled={isDisconnecting}><LogOut className="mr-2 h-4 w-4" />Disconnect</DropdownMenuItem>);}export function WalletConnectButton() {const { wallets, selectedWallet, selectedAccount, isConnected } = useSolana();const [dropdownOpen, setDropdownOpen] = useState(false);return (<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}><DropdownMenuTrigger asChild><Button variant="outline" className="min-w-[140px] justify-between">{isConnected && selectedWallet && selectedAccount ? (<><div className="flex items-center gap-2"><WalletIcon wallet={selectedWallet} className="h-4 w-4" /><span className="font-mono text-sm">{truncateAddress(selectedAccount.address)}</span></div><ChevronDown className="ml-2 h-4 w-4" /></>) : (<><Wallet className="mr-2 h-4 w-4" /><span>Connect Wallet</span><ChevronDown className="ml-2 h-4 w-4" /></>)}</Button></DropdownMenuTrigger><DropdownMenuContent align="end" className="w-[280px]">{wallets.length === 0 ? (<p className="text-sm text-muted-foreground p-3 text-center">No wallets detected</p>) : (<>{!isConnected ? (<><DropdownMenuLabel>Available Wallets</DropdownMenuLabel><DropdownMenuSeparator />{wallets.map((wallet, index) => (<WalletMenuItemkey={`${wallet.name}-${index}`}wallet={wallet}onConnect={() => setDropdownOpen(false)}/>))}</>) : (selectedWallet &&selectedAccount && (<><DropdownMenuLabel>Connected Wallet</DropdownMenuLabel><DropdownMenuSeparator /><div className="px-2 py-1.5"><div className="flex items-center gap-2"><WalletIcon wallet={selectedWallet} className="h-6 w-6" /><div className="flex flex-col"><span className="text-sm font-medium">{selectedWallet.name}</span><span className="text-xs text-muted-foreground font-mono">{truncateAddress(selectedAccount.address)}</span></div></div></div><DropdownMenuSeparator /><DisconnectButtonwallet={selectedWallet}onDisconnect={() => setDropdownOpen(false)}/></>))}</>)}</DropdownMenuContent></DropdownMenu>);}
4. 트랜잭션 전송 컴포넌트 생성
메모 프로그램을 호출하여 트랜잭션 로그에 메시지를 추가하는 트랜잭션을 보내는 컴포넌트를 만듭니다.
이 컴포넌트의 목적은 연결된 지갑으로 트랜잭션을 보내는 방법을 보여주는 것입니다.
components/memo-card.tsx
를 만들고 제공된 코드를 추가하세요. 이 컴포넌트는:
- 사용자가 메시지를 입력할 수 있게 합니다
- 메모 프로그램을 호출하는 명령어가 포함된 Solana 트랜잭션을 생성합니다
- 연결된 지갑에 트랜잭션 서명 및 전송을 요청합니다
- Solana 익스플로러에서 트랜잭션을 볼 수 있는 링크를 표시합니다
"use client";import { useState } from "react";import { useSolana } from "@/components/solana-provider";import { useWalletAccountTransactionSendingSigner } from "@solana/react";import { type UiWalletAccount } from "@wallet-standard/react";import {pipe,createTransactionMessage,appendTransactionMessageInstruction,setTransactionMessageFeePayerSigner,setTransactionMessageLifetimeUsingBlockhash,signAndSendTransactionMessageWithSigners,getBase58Decoder,type Signature} from "@solana/kit";import { getAddMemoInstruction } from "@solana-program/memo";// Component that only renders when wallet is connectedfunction ConnectedMemoCard({ account }: { account: UiWalletAccount }) {const { rpc, chain } = useSolana();const [isLoading, setIsLoading] = useState(false);const [memoText, setMemoText] = useState("");const [txSignature, setTxSignature] = useState("");const signer = useWalletAccountTransactionSendingSigner(account, chain);const sendMemo = async () => {if (!signer) return;setIsLoading(true);try {const { value: latestBlockhash } = await rpc.getLatestBlockhash({ commitment: "confirmed" }).send();const memoInstruction = getAddMemoInstruction({ memo: memoText });const message = pipe(createTransactionMessage({ version: 0 }),(m) => setTransactionMessageFeePayerSigner(signer, m),(m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),(m) => appendTransactionMessageInstruction(memoInstruction, m));const signature = await signAndSendTransactionMessageWithSigners(message);const signatureStr = getBase58Decoder().decode(signature) as Signature;setTxSignature(signatureStr);setMemoText("");} catch (error) {console.error("Memo failed:", error);} finally {setIsLoading(false);}};return (<div className="space-y-4"><div><label className="block text-sm mb-1">Memo Message</label><textareavalue={memoText}onChange={(e) => setMemoText(e.target.value)}placeholder="Enter your memo message"className="w-full p-2 border rounded min-h-[100px]"maxLength={566}/></div><buttononClick={sendMemo}disabled={isLoading || !memoText.trim()}className="w-full p-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:bg-gray-400">{isLoading ? "Sending..." : "Send Memo"}</button>{txSignature && (<div className="p-2 border rounded text-sm"><p className="mb-1">Memo Sent</p><ahref={`https://explorer.solana.com/tx/${txSignature}?cluster=devnet`}target="_blank"rel="noopener noreferrer"className="text-blue-500 hover:underline">View on Solana Explorer →</a></div>)}</div>);}// Main memo componentexport function MemoCard() {const { selectedAccount, isConnected } = useSolana();return (<div className="space-y-4 p-4 border rounded-lg"><h3 className="text-lg font-semibold">Send Memo</h3>{isConnected && selectedAccount ? (<ConnectedMemoCard account={selectedAccount} />) : (<p className="text-gray-500 text-center py-4">Connect your wallet to send a memo</p>)}</div>);}
5. 앱 페이지 업데이트
마지막으로, 메인 앱 페이지를 업데이트합니다.
app/page.tsx
를 제공된 코드로 업데이트하세요. 이 페이지는:
WalletConnectButton
와MemoCard
컴포넌트를 가져와 사용합니다
"use client";import { WalletConnectButton } from "@/components/wallet-connect-button";import { MemoCard } from "@/components/memo-card";export default function Home() {return (<div className="min-h-screen flex items-center justify-center p-4"><div className="w-full max-w-md bg-card rounded-lg border shadow-lg p-6 space-y-6"><div className="flex justify-center"><WalletConnectButton /></div><MemoCard /></div></div>);}
6. 애플리케이션 실행
이제 애플리케이션을 실행하여 지갑 통합을 테스트합니다.
$npm run dev
연결된 지갑은 devnet 클러스터에 연결되도록 구성되어 있어야 하며 트랜잭션을 보내기 위해 devnet SOL로 충전되어 있어야 합니다.
Is this page helpful?