TOKTOKHAN.DEV — TEAM

똑똑한개발자는 디지털 프로덕트 에이전시 입니다. 프로젝트를 진행하면서 얻게되는 기술 또는 디자인 관점에서의 인사이트를 공유하기 위해 콘텐츠를 발행하고 있습니다. We publish content to share insights gained from a technical or design perspective while working on projects.

Follow publication

Reown AppKit를 사용한 Counter Example(with. Anchor Framework)

--

들어가며

본 프로젝트는 Solana 블록체인에서 Reown AppKit을 사용하여 구현된 Counter dApp 예제입니다. Anchor 프레임워크를 기반으로 구축되었으며, 프론트엔드는 Next.js와 React를 사용하여 개발되었습니다. 또한 상태 관리를 위해 react-query를 활용하고, 애니메이션을 위해 GSAP를 사용했습니다.

프로그램을 실행하기 위해서는 git clone 이후 anchor build 및 deploy를 새롭게 진행한 다음 anchor/src/counter-exports.ts에 programId를 변경해주고 Next.js를 실행해주시면 됩니다.

Anchor Framework 란?

Anchor 프레임워크는 Solana 프로그램을 구축하는 과정을 단순화하는 도구입니다. 블록체인 개발이 처음이거나 경험이 많은 프로그래머라도 Anchor를 사용하면 Solana 프로그램을 작성, 테스트 및 배포하는 과정이 훨씬 쉬워집니다.

Anchor의 주요 특징

  1. 복잡성 추상화
    Anchor는 Solana의 프로그래밍 모델에서 발생하는 낮은 수준의 복잡성을 추상화하여, 개발자가 블록체인의 세부 사항보다 비즈니스 로직에 집중할 수 있도록 합니다.
  2. Rust 기반 프레임워크
    Anchor는 Rust 언어를 기반으로 구축되어 안전성과 성능을 보장합니다.
  3. 테스트 및 배포 간소화
    Anchor는 테스트 환경과 배포 프로세스를 간소화하여 개발자가 빠르게 반복하고 프로그램을 배포할 수 있도록 지원합니다.

Anchor는 Solana 생태계에서 강력한 도구로, 개발자들이 더 효율적으로 블록체인 애플리케이션을 구축할 수 있도록 돕습니다.

Reown(前. WalletConnect) AppKit 란?

Reown AppKit은 개발자들이 디지털 소유권을 직관적이고 안전하게 관리할 수 있는 사용자 경험을 구축할 수 있도록 돕는 도구입니다.

가장 핵심적인 기능으로는 지갑 로그인, 이메일 로그인, 소셜 로그인을 지원하여 다양한 환경에서 원활한 인증 경험을 제공합니다. 이를 통해 개발자는 복잡한 인증 시스템을 직접 구현할 필요 없이, 쉽고 빠르게 Web3 및 기존 웹 서비스와의 연결을 구축할 수 있습니다.

전체 폴더 구조

.
├── anchor // anchor 프로젝트 폴더
├── public
├── src // next.js 프로젝트 폴더
├── .env
└── package.json

anchor 폴더 구조

.
├── anchor
│ ├── migrations
│ ├── prgrams
│ │ └── count
│ │ ├── src
│ │ │ └── lib.rs // Solana Program 매인 로직
│ │ ├── Cargo.toml
│ │ └── Xargo.toml
│ ├── src
│ │ ├── counter-exports.ts // 빌드한 program ts로 가져오기
│ │ └── index.ts
│ ├── target
│ ├── test
│ ├── Anchor.toml
│ ├── Cargo.toml
│ └── tsconfig.json
...

next.js 폴더 구조

.
├── public
├── src
│ ├── apis
│ ├── components
│ ├── configs
│ ├── ...
│ │ └── appkit.ts // appkit provider
│ ├── containers
│ │ ├── Count
│ │ │ ├── hooks
│ │ │ │ └── useCounter.ts // Program 호출
│ │ │ ├── Count.tsx
│ │ │ └── index.ts
│ │ ...
│ ├── hoc
│ ├── hooks
│ │ ├── useAnchor.ts // anchor hoooks
│ │ ...
│ └── pages
│ ├── _app.tsx
│ ├── _document.tsx
│ └── index.tsx
└── package.json

솔라나 프로그램 구축

이더리움에서는 Solidity라는 언어를 활용해서 스마트 컨트랙트를 구축한다면, 솔라나에서는 Rust라는 언어로 프로그램을 구축합니다.

용어의 차이일 뿐 같은 개념이라고 보면 됩니다!

1. 프로그램 개요

이 스마트 컨트랙트는 카운터(Counter) 계정을 생성하고, 해당 카운터를 증가시키는 기능을 포함하고 있습니다.

  • initialize: 카운터를 초기화하는 함수 (시작값을 설정)
  • increment: 카운터 값을 1 증가시키는 함수

2. 주요 코드 설명

1) initialize 함수

pub fn initialize(ctx: Context<Initialize>, start: u64) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.authority = *ctx.accounts.authority.key;
counter.count = start;
Ok(())
}
  • 새로운 카운터 계정을 생성하고 초기값(start)을 설정합니다.
  • 카운터의 소유자를 authority로 설정합니다.

2) increment 함수

pub fn increment(ctx: Context<Increment>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count += 1;
Ok(())
}
  • 기존의 counter 값을 1 증가시킵니다.
  • has_one = authority 속성을 통해, 소유자만 카운터를 수정할 수 있도록 제한합니다.

3. 계정 구조

1) Initialize 구조체

#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init, payer = authority, space = 48, seeds = [b"counter", authority.key().as_ref()], bump)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
  • 새로운 counter 계정을 생성합니다.
  • seeds를 사용하여 **고유한 PDA(Program Derived Address)**를 생성합니다.
  • authority는 트랜잭션을 수행하는 사용자로, 초기화 비용을 지불합니다.

2) Increment 구조체

#[derive(Accounts)]
pub struct Increment<'info> {
#[account(mut, has_one = authority)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
  • counter의 값을 변경하기 위해 소유자(authority) 검증을 수행합니다.

3) 데이터 구조

#[account]
pub struct Counter {
pub authority: Pubkey,
pub count: u64,
}
  • authority: 카운터 계정의 소유자 (이 사람이 변경 가능)
  • count: 현재 카운트 값

Next.js 연동 파트

1. AppKit 기본 설정

먼저, Reown AppKit을 설정하여 지갑 인증을 위한 Provider를 제공합니다.

// config/appkit.ts
import { createAppKit } from '@reown/appkit/react'

export const modal = createAppKit({
adapters: [solanaAdapter],
networks: [solana, solanaTestnet, solanaDevnet],
metadata,
features: {
email: true,
analytics: true,
socials: ['google', 'x', 'github'],
emailShowWallets: true,
onramp: false,
},
projectId,
themeMode: 'dark',
})
export function AppKitProvider({ children }: { children: React.ReactNode }) {
return children
}

이제 프로젝트 내에서 AppKitProvider를 사용하면 지갑 인증 및 계정 상태 관리가 가능해집니다.

2. 로그인 여부 확인 HOC

HOC(High-Order Component)를 활용해 사용자의 로그인 여부를 확인하고, 미로그인 상태일 경우 로그인 페이지로 이동하도록 합니다.

// hocs/withAuthGuard.tsx
import { ComponentProps, ComponentType, useEffect } from 'react'
import { useRouter } from 'next/router'
import { useToast } from '@chakra-ui/react'
import { useAppKitAccount } from '@reown/appkit/react'

export default function withAuthGuard<T extends ComponentType<any>>(AppComponent: T) {
return function WrappedAppComponent(props: ComponentProps<T>) {
const { isConnected } = useAppKitAccount()
const router = useRouter()
const toast = useToast()
useEffect(() => {
if (!isConnected) {
const currentPath = router.asPath
router.replace(`/?returnUrl=${encodeURIComponent(currentPath)}`)
if (!toast.isActive('wallet-connect-toast')) {
toast({
title: 'Please connect your wallet',
description: 'Please connect your wallet to continue',
status: 'error',
position: 'top',
id: 'wallet-connect-toast',
})
}
}
}, [isConnected, router, toast])
return isConnected ? <AppComponent {...props} /> : null
}
}
  • useAppKitAccount()를 사용하여 지갑 연결 여부 확인
  • 미연결 시 현재 페이지 URL을 저장 후 로그인 페이지(/?returnUrl=)로 이동
  • useToast()를 이용해 "Please connect your wallet" 메시지 출력

3. Anchor hook 설정

Anchor 기반으로 Solana 스마트 컨트랙트와 연동하기 위한 hook을 설정합니다.

import { AnchorProvider } from '@coral-xyz/anchor'
import { useAppKitNetwork, useAppKitProvider } from '@reown/appkit/react'
import { AnchorWallet } from '@solana/wallet-adapter-react'
import { Connection, clusterApiUrl } from '@solana/web3.js'

const NETWORK = {
'Solana Mainnet': 'mainnet-beta',
'Solana Testnet': 'testnet',
'Solana Devnet': 'devnet',
}
export function useAnchorProvider() {
const connection = new Connection(clusterApiUrl('devnet'), 'confirmed')
const { walletProvider } = useAppKitProvider<any>('solana')
const { caipNetwork } = useAppKitNetwork()
const cluster = NETWORK[caipNetwork?.name as keyof typeof NETWORK]
const provider = new AnchorProvider(
connection,
walletProvider as AnchorWallet,
{
commitment: 'confirmed',
},
)
return { provider, cluster }
}

4. Anchor를 이용한 Solana 스마트 컨트랙트 연동

Solana의 Anchor 프레임워크를 활용하여, 스마트 컨트랙트의 카운터 값을 읽고, 증가시키는 기능을 구현합니다.

// containers/Count/hooks/useCounter.ts
import { useMutation, useQuery } from 'react-query'
import { getCountProgram, getCountProgramId } from '@/anchor'
import { useAnchor } from '@/hooks/useAnchor'

export function useCounter() {
const [countAddress, setCountAddress] = useState()
const { address } = useAppKitAccount()
const { provider, cluster } = useAnchor()
const programId = useMemo(
() => getCountProgramId(cluster as Cluster),
[cluster],
)
const program = useMemo(
() => getCountProgram(provider, programId),
[provider, programId],
)

// 자동 생성되는 PDA를 수기로 생성해서 state로 저장
useEffect(() => {
const updateState = async () => {
if (!countAddress && address) {
const [counterPDA] = await PublicKey.findProgramAddress(
[Buffer.from('counter'), new PublicKey(address).toBuffer()],
programId,
)
setCountAddress(counterPDA.toBase58())
}
}
updateState()
}, [address, programId, countAddress])
const getCount = useQuery({
queryKey: ['count', { cluster }],
queryFn: () => program.account.counter.fetch(countAddress),
})

const initialize = useMutation({
mutationKey: ['test', 'initialize', { cluster }],
mutationFn: () =>
program.methods
.initialize(new BN(0))
.accounts({
authority: address,
})
.rpc(),
onSuccess: (signature) => {
...
},
onError: (error) =>
...
})
const incrementCount = useMutation({
mutationFn: () =>
program.methods.increment().accounts({ counter: countAddress }).rpc(),
onSuccess: (signature) => {
...
getCount.refetch()
},
onError: (error) =>
...
})
return { getCount, initialize, incrementCount }
}
  • getCount: 스마트 컨트랙트에서 현재 카운터 값 조회
  • initialize: 카운터 값을 초기화하는 트랜잭션 실행
  • incrementCount: 카운터 값을 증가시키는 트랜잭션 실행

5. 카운터 UI 구현

UI를 구현하여 카운터 값을 표시하고, 버튼 클릭으로 값을 증가시키는 기능을 추가합니다.

// containers/Count/Count.tsx
import { Button, Center, Text, VStack } from '@chakra-ui/react'
import { useAppKitAccount } from '@reown/appkit/react'
import { useCounter } from './hooks/useCounter'

function Count() {
const { initialize, incrementCount, getCount } = useCounter()
const { address } = useAppKitAccount()
const count = getCount.data?.count.toNumber() || 0
return (
<Center flexDirection="column" pt={'100px'}>
<VStack spacing={'12px'}>
<Text textStyle={'pre-heading-01'}>Counter</Text>
<Text>{address?.slice(0, 4)}...{address?.slice(-4)}</Text>
<Text textStyle={'pre-display-01'}>{count}</Text>
<Button onClick={() => incrementCount.mutate()}>Increment</Button>
<Button onClick={() => initialize.mutate()}>Initialize</Button>
</VStack>
</Center>
)
}
export default Count

참고문서

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Published in TOKTOKHAN.DEV — TEAM

똑똑한개발자는 디지털 프로덕트 에이전시 입니다. 프로젝트를 진행하면서 얻게되는 기술 또는 디자인 관점에서의 인사이트를 공유하기 위해 콘텐츠를 발행하고 있습니다. We publish content to share insights gained from a technical or design perspective while working on projects.

No responses yet

Write a response