[CosmWasm 101 와씀] 3편: 프론트엔드 연결하기— 간단한 Clicker 게임 구현

Suji Yoon
DSRV
Published in
37 min readJul 4, 2022

DSRV Dev Guild에서는 더 많은 개발자들과 Web3 인프라를 만들어가기 위해, 다양한 메인넷과 스마트 컨트랙트에 대한 가이드를 연재합니다.

Disclaimer: 이 글은 정보 전달을 위한 목적으로 작성되었으며, 특정 프로젝트에 대한 투자 권고, 법률적 자문 등 목적으로 하지 않습니다. 모든 투자의 책임은 개인에게 있으며, 이로 발생된 결과에 대해 어떤 부분에서도 DSRV는 책임을 지지 않습니다. 본문이 포괄하는 내용들은 특정 자산에 대한 투자를 추천하는 것이 아니며, 언제나 본문의 내용만을 통한 의사결정은 지양하시길 바랍니다.

[CosmWasm 101 시리즈]

  1. Counter 컨트랙트 톺아보기
  2. Counter 컨트랙트 배포하기 (Go CLI 및 CosmJS 활용)
  3. Frontend와 통신하기

지난 번 ‘[CosmWasm 101 시리즈] 2편: Go CLI와 CosmJS를 활용하여 Counter 컨트랙트 배포하기’ 에서는 wasm 컨트랙트 코드를 컴파일하고, 생성된 동일한 wasm 바이너리 파일을 활용하여 다양한 테스트넷에 Go CLI와 CosmJS를 활용하여 배포하는 과정을 실습해보았습니다. 이번 시간에는 배포한 컨트랙트와 프론트엔드(Front-end)가 통신하는 방법을 간단한 Clicker 게임 예제를 통해 설명하고자 합니다.

소개

⭐️ 해당 문서는 CosmWasm 101 시리즈 2편을 읽은 독자 중 프론트엔드 작업을 하고 싶은 분들을 위한 문서로, 다음의 선수 지식을 필요로 합니다.

1. CosmWasm 101 시리즈 2편
2. JavaScript 문법에 대한 기본적인 이해
3. React.js 코드에 대한 기본적인 이해

이번 튜토리얼에서는 간단한 Clicker 게임을 만들어보고자 합니다. 이번 컨텐츠를 통해 독자 여러분들께서는 하기한 내용들을 얻어가실 수 있습니다.

  • CosmJS의 사용법: 이더리움 스마트 컨트랙트와 프론트엔드가 통신할 때 Web3.jsEther.js를 사용하는 것처럼 CosmWasm 스마트 컨트랙트와 프론트엔드가 통신할 때는 CosmJS를 사용합니다.
  • Keplr 지갑 object의 사용법: Keplr 지갑 설치 여부를 확인한 후 네트워크를 추가하고 지갑에 연결하여 정보를 가져옵니다.

CosmJS는 작은 npm 패키지들로 구성되어 있는 라이브러리로, 본 글에서는 @cosmjs/cosmwasm-stargate (^0.28.0) 패키지를 사용합니다. (2022년 6월 기준)
우리가 이번에 실습해 볼 Clicker 예제는, 15초 동안 화면에 나타나는 CosmWasm 아이콘을 클릭하여 점수를 얻고, 게임이 종료된 후 컨트랙트에 트랜잭션을 날려서 점수를 저장하는 간단한 게임입니다.

다음 링크에서 게임을 직접 플레이할 수 있습니다. 또한, AllThatNode Docs에서도 해당 튜토리얼을 따라하실 수 있습니다.

구현 요구사항

구현 단계는 크게 세 단계로 나눌 수 있으며 ,각각의 단계에서 구현해야 할 주요 요구 사항은 다음과 같습니다.

1단계: Keplr 지갑 연결하기

  • 요구사항 1: Keplr 지갑에 테스트넷 네트워크를 추가하고 연결할 수 있다.
  • 요구사항 2: CONNECTDISCONNECT 버튼의 UI 컴포넌트를 구현하고, 버튼을 클릭했을 때 지갑을 연결하고 연결을 해지할 수 있다.

2단계: 스마트 컨트랙트와 통신하기

  • 요구사항 1: 스마트 컨트랙트를 리팩토링할 수 있다.
  • 요구사항 2: get_count 메소드를 실행하면, 컨트랙트의 현재 count 값을 조회할 수 있다.
  • 요구사항 3: reset 메소드를 실행하면, 컨트랙트의 count 값을 0으로 초기화 할 수 있다.
  • 요구사항 4: increment 메소드를 실행하면, 컨트랙트의 count 값에 획득한 점수를 저장할 수 있다.

3단계: Clicker 게임 구현하기

  • 요구사항 1: 플레이 화면 UI 를 구현할 수 있다.
  • 요구사항 2: GAME START 버튼을 눌러 get_countreset 메소드를 차례로 실행할 수 있다.
  • 요구사항 3: 게임이 시작되면 15초 동안 화면에 랜덤하게 나타나는 CosmWasm 아이콘을 클릭하여 점수를 획득할 수 있다.
  • 요구사항 4: 게임 종료 후 나타나는 TRANSACTION 버튼의 UI 컴포넌트를 구현하고, 이를 클릭하면 increment 메소드를 실행할 수 있다.

구현 시작하기

다음의 명령어를 통해 아래의 저장소를 로컬에 clone 받아보도록 합시다.

git clone https://github.com/DSRV-DevGuild/cosmwasm-clicker-game.git

우리는 main 브랜치에서 아래의 단계들을 따라가며 함께 Clicker 게임을 구현할 것입니다. 각각의 단계는 브랜치별로 정리되어 있으며 단계별 요구사항은 커밋 로그를 통해 확인할 수 있습니다.

1단계 — Keplr 지갑 연결하기

요구사항 1: Keplr 지갑에 테스트넷 네트워크를 추가하고, 연결할 수 있다.

📝 작업 포인트

[✓] keplr 오브젝트 확인을 통한 keplr 익스텐션 설치 여부 확인하기
[ ]
experimentalSuggestChain 메소드를 통해 지갑에 네트워크 추가하기
[ ]
window.keplr.enable(chainId:string) 메소드로 지갑에 접근 요청하기

Keplr 지갑은 Cosmos 생태계의 인터체인을 지원하는 지갑입니다. 우리가 배포한 스마트 컨트랙트와 프론트엔드가 통신하기 위해서는 컨트랙트를 배포한 블록체인 네트워크와 연결해 줄 인터페이스가 필요한데, 지갑이 바로 그 역할을 수행하게 됩니다.

Keplr 지갑 익스텐션을 설치한 후 계정을 생성해 봅시다. Keplr 지갑을 실행해보면 기본적으로 연결되어 있는 네트워크에 테스트넷이 없습니다. 이처럼 기본적으로 제공하지 않는 네트워크를 추가하기 위해서는 프론트엔드에서 별도의 메소드를 호출하여 네트워크의 configuration 값을 매개변수로 제공한 후, 추가를 요청해야 합니다.

우리는 Malaga, Osmosis Testnet, Juno Testnet, Archway Testnet 을 지갑에 추가할 것입니다. 각 네트워크의 configuration 값은 src/wallet/network_info.js 파일에서 확인할 수 있습니다. 해당 파일에 대해서는 나중에 다시 설명하도록 하겠습니다.

먼저, src/wallet 폴더 안에 connect.js 파일을 새로 생성합니다. 이 곳에서 지갑에 네트워크를 추가하고 연결하는 connectWallet 함수를 구현할 것입니다. 아래와 같이 connectWallet 함수를 선언하고 export 해줍니다.

src/wallet/connect.js 출처: Yoon-Suji of DSRV

지갑에 네트워크를 추가하기 전에 먼저 Keplr 지갑(크롬 익스텐션)이 사용자의 브라우저에 설치되어 있는지 확인을 해야 합니다. Keplr 지갑은 CosmJS와 상호운용이 가능한 OfflineSigner 를 제공하는데요. window.getOfflineSigner(chainId:string) 을 통해 OfflineSigner 오브젝트를 가져올 수 있습니다. OfflineSigner는 CosmJS가 제공하는 서명자(Signers) 로 클라이언트를 생성할 때 사용됩니다.

또한 Keplr 지갑은 window.keplr 함수를 제공합니다. 만약 window.getOfflineSigner 또는 window.keplr 의 값이 null 인 경우에는 Keplr 지갑이 브라우저에 설치되지 않은 경우이므로 Keplr 지갑을 설치해달라는 알람창을 띄우도록 합니다.

src/wallet/connect.js 출처: Yoon-Suji of DSRV

📝 작업 포인트

[✓] keplr 오브젝트 확인을 통한 keplr 익스텐션 설치 여부 확인하기
[✓] experimentalSuggestChain 메소드를 통해 지갑에 네트워크 추가하기
[ ]
window.keplr.enable(chainId:string) 메소드로 지갑에 접근 요청하기

Keplr 지갑이 브라우저에 설치된 것을 확인하면, 이제 네트워크를 추가해 봅시다. 새롭게 Keplr 지갑에 추가하려는 네트워크 정보(chainInfo)를 window.keplr.experimentalSuggestChain 으로 전달합니다.

해당 메소드는 Promise를 반환 하기 때문에 async-await 패턴을 이용합니다. Keplr 지갑에 새 네트워크를 추가하는 window.keplr.experimentalSuggestChain 메소드는 Keplr v0.6.4부터 사용가능합니다.

src/wallet/connect.js 출처: Yoon-Suji of DSRV
💡 DSRV’s Tip: JavaScript의 'async-await' 에 대해 간단히 알아보기

'aync-await'은 Promise 객체를 반환하는 함수를 처리할 때 사용하는 패턴입니다. 비동기 처리 코드 앞에 'await'를 붙이고 해당 코드를 호출하는 함수에는 'async'를 붙여 사용할 수 있습니다.

window.keplr.experimentalSuggestChain의 인자로 전달해야 하는 chainInfosrc/wallet/network_info.js 에서 확인할 수 있습니다. Javascript의 Factory Pattern을 사용해 네 개의 테스트넷에 대한 chainInfo를 생성하고 networkInfo 객체에 chainId를 키값으로 하여 저장했습니다. Optional 주석이 처리된 것을 제외하고는 필수로 전달해야하는 정보로, 전달하지 않을 경우 에러가 발생합니다.

src/wallet/network_info.js 출처: Yoon-Suji of DSRV

window.keplr.experimentalSuggestChain 메소드가 실행되면, 아래 사진과 같이 체인 추가 요청 팝업이 표시됩니다. 사용자가 허락 버튼을 누르면, 사용자의 Keplr 지갑이 새로운 네트워크를 추가합니다. 사용자가 거부하거나 추가하려는 체인의 필수 정보 요구 사항이 충족되지 않으면 에러를 발생시킵니다. 만약 같은 체인이 이미 등록되어 있는 경우에는, 사용자의 상호작용을 요구하지 않고 넘어갑니다.

Keplr 체인 추가 요청 화면. 출처: DSRV

📝 작업 포인트

[✓] keplr 오브젝트 확인을 통한 keplr 익스텐션 설치 여부 확인하기
[✓] experimentalSuggestChain 메소드를 통해 지갑에 네트워크 추가하기
[✓] window.keplr.enable(chainId:string) 메소드로 지갑에 접근 요청하기

요구사항 1–1의 완성본을 보고 싶다면 ➡️ git checkout 51cb919

Keplr 지갑에 네트워크를 추가했으면, 이제 해당 네트워크의 정보를 가져올 수 있습니다. Keplr 지갑에 연결하기 위해서는 먼저 window.keplr.enable(chainId:string) 메소드를 통해 웹 사이트가 Keplr에 지갑 접근을 요청해야 합니다. 해당 메소드를 실행하면 아래와 같이 사용자에게 동의를 구하는 팝업이 표시됩니다.

src/wallet/connect.js 출처: Yoon-Suji of DSRV
Keplr 네트워크 접근 요청 화면. 출처: DSRV

이렇게 사용자의 허락을 받은 후에 window.getOfflineSigner() 를 통해 OfflineSigner를 가져옵니다. OfflineSignergetAccounts() 메소드는 주소와 퍼블릭 키가 담긴 배열을 반환합니다. 하지만 현재 Keplr 지갑은 체인 아이디 당 하나의 ‘주소 / 퍼블릭 키’ 쌍만을 지원합니다.

따라서 사용자의 주소는 accounts[0].address 를 통해 가져올 수 있습니다. 즉, accounts 배열의 첫 번째 원소에 접근하면 사용자의 현재 주소와 퍼블릭 키를 얻을 수 있습니다.

SigningCosmWasmClient.connectWithSigner() 메소드는 RPC 엔드포인트 주소와 Keplr에서 가져온 OfflineSigner을 인자로 받아 SigningCosmWasmClient 를 반환합니다. SigningCosmWasmClient 클래스를 이용하기 위해서는 해당 클래스를 import 해야 합니다.

이렇게 생성한 client 오브젝트에서 getBalance() 메소드를 호출하여 해당 주소의 balance를 가져올 수 있습니다.

src/wallet/connect.js 출처: Yoon-Suji of DSRV

이제 마지막으로 client, accounts[0].address (사용자의 지갑 주소), balance, chainId 값을 함수를 호출한 부모 컴포넌트로 전달해야 합니다. React.js 에서 자식 컴포넌트에서 부모로 값을 전달할 때는 부모에서 props 로 함수를 내려주고 자식이 해당 함수의 인자로 값을 전달할 수 있습니다.

connectWallet 함수를 호출할 부모 컴포넌트인 src/App.js 로 이동해서 props 로 전달할 함수 getInfo를 먼저 구현합니다. App.js 에서는 useState를 이용해서 client, address, balance, chainId 상태를 관리하고, getInfo 함수는 인자로 해당 값을 받아 setState를 이용해 저장합니다.

connectWallet 함수를 호출할 때는 아래와 같이 연결하고자 하는 네트워크의 정보와 getInfo 함수를 함께 넘겨줍니다.

src/App.js 출처: Yoon-Suji of DSRV
💡 DSRV’s Tip: React.js의 useState에 대해 간단히 알아보기

React Hook의 useState는 함수 컴포넌트 안에서 상태 변화를 추적하고 관리하는 메소드 입니다. useState를 호출하면 [state 변수, 해당 변수를 갱신할 수 있는 함수]를 반환합니다.

이제 다시 src/wallet/connect.js 로 돌아와서 connectWallet 함수에 getInfo 로 값을 넘겨주는 코드를 추가합니다.

src/wallet/connect.js 출처: Yoon-Suji of DSRV

여기까지 구현이 완료되었다면 yarn start 혹은 npm start 명령어를 이용해 프로젝트를 실행시켜 봅시다. Keplr 네트워크 추가 요청 창과 접근 요청 창이 차례로 뜨고 콘솔에 지갑주소(address)가 정상적으로 출력된다면 Keplr 지갑에 네트워크를 추가하고 연결하는 코드가 잘 동작하는 것입니다.

요구사항 1–1 구현 화면. 출처: DSRV

지금까지 구현한 connect.jsApp.js 의 전체 코드는 다음과 같습니다.

src/wallet/connect.js 출처: Yoon-Suji of DSRV
src/App.js 출처: Yoon-Suji of DSRV

요구사항 2: CONNECT와 DISCONNECT 버튼의 UI 컴포넌트를 구현하고, 버튼을 클릭했을 때 지갑을 연결하고 연결을 해지할 수 있다.

📝 작업 포인트

[✓] 네트워크 별로 chainId에 따라 DISCONNECT와 CONNECT 버튼 나타내기
[ ] CONNECT 버튼을 눌렀을 때 지갑이 연결되고 DISCONNECT 버튼을 눌렀을 때 지갑과의 연결이 해제되도록 구현하기
[ ] 지갑과 연결되면 PLAY 버튼과 연결된 지갑의 주소와 잔액을 출력하기
[ ] PLAY 버튼을 눌렀을 때 /play 주소로 연결하기

이번 요구사항은, 연결하고 싶은 네트워크의 버튼을 누르면 해당 버튼은 DISCONNECT 로 바뀌고 게임 플레이 화면으로 넘어갈 수 있는 PLAY 버튼이 생기며, 연결된 지갑의 주소와 잔액을 아래에 출력하는 것입니다. 지갑과 연결하는 버튼은 네트워크 별로 chainId 값에 따라서 버튼 컴포넌트가 표시되도록 구현합니다.

우리가 구현할 메인 화면은 다음과 같습니다. 메인 화면 구현은 src/App.js 에서 진행됩니다. (앞선 과정에서 주석으로 (debug) 라고 표시한 코드는 삭제해주세요.)

Clicker Game 메인 화면 — 1. 출처: DSRV
Clicker Game 메인 화면 — 2. 출처: DSRV

이제 Malaga, Osmosis 테스트넷, Juno 테스트넷, 그리고 Archway 테스트넷에 연결하는 버튼을 구현합시다. 연결하고 싶은 네트워크의 버튼을 누르면 위에서 구현한 connectWallet 를 호출해서 client, address, balance, chainId 정보를 가져옵니다. 버튼을 누를 때마다 함수가 실행되도록 하기 위해 src/wallet/connect.js 파일 안의 connectWallet 함수의 인자에 event를 추가해줍니다.

src/wallet/connect.js 출처: Yoon-Suji of DSRV

Object.keys() 메소드는 networkInfo의 key 값 즉, chainId 값만을 가져와 배열로 반환하는 메소드 입니다. networkInfo에 담긴 네트워크 별로 chainId에 따라서 DISCONNECT와 CONNECT 버튼이 나타나도록 구현합니다.

src/App.js 출처: Yoon-Suji of DSRV

📝 작업 포인트

[✓] 네트워크 별로 chainId에 따라 DISCONNECT와 CONNECT 버튼 나타내기
[✓] CONNECT 버튼을 눌렀을 때 지갑이 연결되고 DISCONNECT 버튼을 눌렀을 때 지갑과의 연결이 해제되도록 구현하기
[ ] 지갑과 연결되면 PLAY 버튼과 연결된 지갑의 주소와 잔액을 출력하기
[ ] PLAY 버튼을 눌렀을 때 /play 주소로 연결하기

disconnect 메소드는 connectWallet 메소드를 통해 연결했던 체인 정보를 초기화하는 방식으로 구현해보도록 하겠습니다. 이 과정에서 React.js의 useState 메소드가 사용됩니다.

src/App.js 출처: Yoon-Suji of DSRV

📝 작업 포인트

[✓] 네트워크 별로 chainId에 따라 DISCONNECT와 CONNECT 버튼 나타내기
[✓] CONNECT 버튼을 눌렀을 때 지갑이 연결되고 DISCONNECT 버튼을 눌렀을 때 지갑과의 연결이 해제되도록 구현하기
[✓] 지갑과 연결되면 PLAY 버튼과 연결된 지갑의 주소와 잔액을 출력하기
[ ] PLAY 버튼을 눌렀을 때 /play 주소로 연결하기

우리는 client 오브젝트가 전역에 존재하는 지 확인하여, 웹 페이지가 지갑과 연결되어 있는 지 알 수 있습니다. 만약 client 오브젝트가 존재하는 경우, addressbalance 정보를 반환하도록 구현해 봅시다.

src/App.js 출처: Yoon-Suji of DSRV

📝 작업 포인트

[✓] 네트워크 별로 chainId에 따라 DISCONNECT와 CONNECT 버튼 나타내기
[✓] CONNECT 버튼을 눌렀을 때 지갑이 연결되고 DISCONNECT 버튼을 눌렀을 때 지갑과의 연결이 해제되도록 구현하기
[✓] 지갑과 연결되면 PLAY 버튼과 연결된 지갑의 주소와 잔액을 출력하기
[✓] PLAY 버튼을 눌렀을 때 /play 주소로 연결하기

요구사항 1–2의 완성본을 보고 싶다면 ➡️ git checkout 7339029

이제 PLAY 버튼을 누르면 /play 주소로 이동하고, stateaddressdenom, 그리고 chainId 정보를 함께 전달하도록 구현해보겠습니다. useNavgiate 메소드를 사용해서 navigate 기능을 구현할 수 있습니다.

버튼의 style 속성에 visibility 태그 값을 “visible” 로 설정하여 지갑과 연결된 경우에만 PLAY 버튼이 나타나도록 구현할 수 있습니다.

src/App.js 출처: Yoon-Suji of DSRV

여기서 visible 변수를 지갑과 연결되었을 때는 ‘visible’, 지갑과 연결되지 않았을 때는 ‘hidden’ 값을 가지도록 상태를 관리하기 위해

const [visible, setVisible] = useState("hidden");

코드를 추가하고, getInfo 메소드와 disconnect 메소드에도 아래와 같이 코드를 추가해줍니다.

src/App.js 출처: Yoon-Suji of DSRV

마지막으로 /play 주소로 접속하면 플레이 화면이 나오도록 라우터를 추가해야 합니다. src/pages 라는 폴더를 생성하고 그 안에 play.js 파일을 다시 생성합니다. 플레이 화면은 다음 단계에서 구현할 것이기 때문에 지금은 함수만 선언합니다.

src/pages/play.js 출처: Yoon-Suji of DSRV

src/index.js 파일에서 아래와 같이 라우터를 추가합니다.

src/index.js 출처: Yoon-Suji of DSRV

여기까지 구현이 완료되었다면, yarn start 또는 npm start 로 프로젝트를 실행했을 때 다음과 같은 화면이 나타날 것입니다.

요구사항 1–2 구현 화면 — 1. 출처: DSRV
요구사항 1–2 구현 화면 — 2. 출처: DSRV

완성된 App.js의 전체 코드는 다음과 같습니다.

src/App.js 출처: Yoon-Suji of DSRV
❗ 주의사항 - Faucet 요청하기

게임을 이용하기 위해서는 계정에 잔액이 있어야 합니다! CosmWasm 101 2편을 참고해서 faucet을 요청해주세요.

2. 스마트 컨트랙트와 통신하기

요구사항 1: 스마트 컨트랙트를 리팩토링할 수 있다.

📝 작업 포인트

[✓] reset 트랜잭션의 authorized 조건 삭제하기
[ ]
increment 트랜잭션이 count 값을 받아서 그 값만큼 더하도록 수정하기

이번 요구사항은, CosmWasm 101 2편에서 배포에 사용했던 스마트 컨트랙트의 코드를 리팩토링하는 것입니다. 기존의 코드는 다음의 저장소에서 확인할 수 있고 다음의 명령어를 통해 가져올 수 있습니다.

cargo generate --git https://github.com/DSRV-DevGuild/cw-template.git --name PROJECT_NAME

src/contract.rs 파일의 try_reset 함수를 보면, reset 트랜잭션을 실행할 때 아래와 같이 owner를 확인하는 조건이 있어서 컨트랙트를 배포한 계정이 아니면 Unauthorized 에러가 발생하며 해당 트랜잭션을 실행하지 못합니다. 해당 조건은 아래 코드의 3~5열에서 확인할 수 있습니다.

src/contract.rs 출처: Yoon-Suji of DSRV

우리는 게임에서 누구나 reset 트랜잭션을 실행할 수 있어야 하기 때문에 해당 조건을 아래와 같이 삭제하고, 인자로 받는 값 중 info: MessageInfo 도 사용하지 않기 때문에 삭제해줍니다. 그러면 동일한 파일 안의 execute 함수에서 try_reset 함수를 실행할 때 info 를 전달하기 때문에 에러가 발생할 것입니다. 해당 부분에서도 info를 삭제해줍니다.

src/contract.rs 출처: Yoon-Suji of DSRV

그리고 cargo test를 통과하기 위해서는 테스트 코드인 reset 함수도 수정해야 합니다. 아래의 10~16열을 살펴보면 컨트랙트의 소유자가 아닌 경우 에러가 발생하는지 확인하는 코드가 있습니다.

src/contract.rs 출처: Yoon-Suji of DSRV

우리는 Unauthorized 에러를 발생시키는 코드를 삭제했기 때문에 아래와 같이 소유자가 아닌 사람도 컨트랙트를 reset할 수 있는지 확인하는 코드로 변경합니다.

src/contract.rs 출처: Yoon-Suji of DSRV

📝 작업 포인트

[✓] reset 트랜잭션의 authorized 조건 삭제하기
[✓] increment 트랜잭션이 count 값을 받아서 그 값만큼 더하도록 수정하기

요구사항 2–1의 완성본을 보고 싶으시면 ➡ git checkout f580124

이제 다음으로 try_increment 함수를 수정해보겠습니다. 기존에 배포한 컨트랙트의 함수는 아래와 같이 increment 트랜잭션을 보내면 count를 1씩 증가시킵니다.

src/contract.rs 출처: Yoon-Suji of DSRV

우리는 사용자가 획득한 점수만큼 count를 증가시키기를 원하기 때문에 아래의 16열처럼 인자로 count를 받고 해당 값만큼 컨트랙트의 count에 더해주도록 코드를 수정합니다. 그리고 9열의 execute 함수에서도 try_increment를 실행할 때 count를 전달하도록 수정합니다.

src/contract.rs 출처: Yoon-Suji of DSRV

그러면 “variant msg::ExecuteMsg::Increment does not have a field named count” 라는 에러가 발생할 것입니다. 에러를 해결하기 위해 src/msg.rs 로 이동해서 ExecuteMsg 안의 Incrementcount를 정의해주어야 합니다.

src/msg.rs 출처: Yoon-Suji of DSRV

그리고 cargo schema 명령어를 입력하여 JSON Schema 파일을 새로 생성해줍니다. 명령어가 성공적으로 실행 완료되었다면 schema 폴더 아래에 5개의 새로운 json 파일이 생성되는 것을 확인할 수 있습니다. 그 중 schema/execute_msg.json 파일을 확인하면 아래와 같이 increment 부분이 변경된 것을 확인할 수 있습니다.

schema/execute_msg.json 출처: Yoon-Suji of DSRV

다음으로 src/integration_tests.rs 에서 에러가 발생하는 부분을 아래의 9열과 같이 수정합니다.

src/integration_tests.rs 출처: Yoon-Suji of DSRV

이제 마지막으로 다시 src/contract.rs 로 돌아와서 increment 테스트 코드를 수정합니다. 기존의 테스트 코드는 다음과 같이 Increment를 실행하고 1이 증가했는지 확인하는 코드였습니다.

src/contract.rs 출처: Yoon-Suji of DSRV

우리는 Incrementcount 값을 전달하고 해당 값만큼 증가시키도록 코드를 수정했기 때문에 인자로 5를 주고 5만큼 늘어났는지 확인하도록 코드를 수정하겠습니다. 11열과 17열의 변경된 코드를 확인해주세요.

src/contract.rs 출처: Yoon-Suji of DSRV

이제 터미널에 cargo test 를 실행하여 테스트 코드가 잘 동작하는지 확인해봅시다. 터미널에 아래와 같은 메시지가 출력되면 수정한 코드가 잘 동작하는 것입니다.

요구사항 2–1 구현 화면 — cargo test. 출처: DSRV

수정된 스마트 컨트랙트의 전체 코드는 다음의 저장소에서 확인할 수 있습니다.

다시 프론트엔드 코드로 돌아와 src/contract/address.js 파일을 확인하면, 수정한 스마트 컨트랙트를 Malaga, Osmosis Testnet, Juno Testnet, Archway Testnet에 새로 배포한 주소를 다음과 같이 저장해 놓았습니다.

src/contract/address.js 출처: Yoon-Suji of DSRV

만약 직접 컨트랙트를 배포하여 사용하고 싶다면, CosmWasm 101 2편을 참고해서 컨트랙트를 배포한 후 contractAddress 의 주소를 새로운 주소로 변경해주세요.

요구사항 2: get_count 메소드를 실행하면, 컨트랙트의 현재 count 값을 조회할 수 있다.

📝 작업 포인트

[✓] get_count 쿼리를 실행하여 컨트랙트의 현재 count 값을 조회하기

요구사항 2–2의 완성본을 보고 싶다면 ➡ ️git checkout 20423ea

이제 다시 프론트엔드 코드로 돌아와서 배포한 스마트 컨트랙트와 통신을 해보겠습니다. 이번 요구사항은 get_count 쿼리를 실행하여, 컨트랙트의 현재 count 값을 조회하는 메소드를 구현하는 것입니다. src/contract 폴더 안에 get_count.js 파일을 만듭니다.

CosmWasm 101 시리즈 2편에서 언급했듯이, CosmJS로 쿼리를 보낼 때에는 queryContractSmart() 메소드를 사용합니다. 메소드에 컨트랙트의 주소와 메시지를 인자로 함께 보내서 쿼리를 실행하고, 결과로 받아오는 count값은 result.count로 접근할 수 있습니다.

src/contract/get_count.js 출처: Yoon-Suji of DSRV

메소드가 잘 동작하는 지 확인하기 위해 src/App.js 로 이동해서 getInfo안에서 get_count를 다음과 같이 실행해볼 수 있습니다.

src/App.js 출처: Yoon-Suji of DSRV

yarn start 혹은 npm start를 통해 프로젝트를 실행시키고 메인 화면에서 네트워크의 버튼을 눌렀을 때 콘솔에 아래와 같이 count 값이 출력된다면 메소드가 잘 동작하는 것입니다.

요구사항 2–2 구현 화면. 출처: DSRV

요구사항 3: reset 메소드를 실행하면, 컨트랙트의 count 값을 0으로 초기화 할 수 있다.

📝 작업 포인트

[✓] reset 트랜잭션을 실행하여, 컨트랙트의 count 값을 초기화하기

요구사항 2–3의 완성본을 보고 싶다면 git checkout 356ff82

이번 요구사항은 reset 트랜잭션을 실행하여, 컨트랙트의 count값을 원하는 값으로 초기화하는 메소드를 구현하는 것입니다. src/contract/reset.js 파일을 만들어주세요.

reset 트랜잭션은 컨트랙트의 내부 상태를 변경하기 때문에 gas 비용을 지불해야 합니다.

GasPrice 는 단일 가스 단위(single unit of gas)의 가격으로 GasPrice.fromString() 메소드를 통해 생성할 수 있습니다. 또한, calculateFee() 메소드는 gasLimitgasPrice 를 인자로 받아 가스 비용을 계산합니다. 통신하려는 컨트랙트가 배포된 네트워크에 알맞은 denom을 지정하여 가스 비용을 계산합니다.

execute() 메소드에 가스 비용을 지불할 지갑의 주소와, 컨트랙트의 주소, 메시지, 가스 비용을 인자로 함께 보내 트랜잭션을 실행할 수 있습니다.

src/contract/reset.js 출처: Yoon-Suji of DSRV

메소드가 잘 동작하는 지 확인하기 위해 src/App.js 로 이동해서 getInfo 안에서 reset를 다음과 같이 실행해볼 수 있습니다.

src/App.js 출처: Yoon-Suji of DSRV

yarn start 혹은 npm start를 통해 프로젝트를 실행시키고 메인 화면에서 네트워크의 버튼을 누르면 아래와 같이 컨트랙트 실행을 요청하는 창이 뜰 것입니다.

Keplr 컨트랙트 실행 요청 화면 — reset. 출처: DSRV

허락 버튼을 누르고 콘솔에 아래와 같이 트랜잭션 로그가 출력된다면 메소드가 성공적으로 구현된 것입니다. 구현이 완료된 것을 확인한 후 debug 코드는 삭제해주세요.

요구사항 2–3 구현 화면. 출처: DSRV

요구사항 4: increment 메소드를 실행하면, 컨트랙트의 count 값에 획득한 점수를 저장할 수 있다.

📝 작업 포인트

[✓] increment 트랜잭션을 실행하여, 컨트랙트의 count 값에 원하는 값을 더하기

요구사항 2–4의 완성본을 보고 싶으시면 — git checkout 9033aca

이번 단계의 마지막 요구사항은 increment 트랜잭션을 실행하여, 컨트랙트의 count 값에 원하는 값만큼 더하는 메소드를 구현하는 것입니다. src/contract/increment.js 파일을 만들어주세요.

트랜잭션 메시지를 제외하고 코드는 reset.js 와 동일합니다.

src/contract/increment.js 출처: Yoon-Suji of DSRV

메소드가 잘 동작하는 지 확인하기 위해 src/App.js 로 이동해서 getInfo 안에서 increment를 다음과 같이 실행해볼 수 있습니다.

src/App.js 출처: Yoon-Suji of DSRV

yarn start 혹은 npm start를 통해 프로젝트를 실행시키고 메인 화면에서 네트워크의 버튼을 누르면 아래와 같이 컨트랙트 실행을 요청하는 창이 뜰 것입니다.

Keplr 컨트랙트 실행 요청 화면 — increment. 출처: DSRV

허락 버튼을 누르고 콘솔에 아래와 같이 트랜잭션 로그가 출력된다면 메소드가 성공적으로 구현된 것입니다.

요구사항 2–4 구현 화면. 출처: DSRV

3단계. Clicker 게임 구현하기

이제 플레이 화면을 만들고, 버튼과 위에서 구현한 메소드를 연결해서 게임을 완성시켜보겠습니다. 플레이 페이지의 코드는 이전에 만들었던 src/pages/play.js 에서 구현할 것입니다. 우리가 구현할 최종 플레이 페이지는 다음과 같습니다.

Clicker Game 플레이 화면. 출처: DSRV

요구사항 1: 플레이 화면 UI 를 구현할 수 있다.

📝 작업 포인트

[✓] 플레이 화면 UI 구현하기
[ ]
useState 로 변수 상태 관리하기

이번 요구사항에서는 플레이 화면 UI 를 구현해보도록 하겠습니다. 우리가 만들 플레이 화면은 Previous Score와 Current Score를 좌측 상단에 표시하고, 우측 상단에는 남은 시간을 보여줍니다. 가운데는 GAME START 버튼이 있고 게임이 종료되면 TRANSACTION 으로 버튼이 바뀝니다.

아이콘은 게임 컨테이너 안에 존재하고 게임이 시작될 때 나타나며 게임이 종료되면 사라집니다. 트랜잭션이 실행되는 동안에는 “Loading…”을 보여주도록 합니다. play.js 파일의 Play 함수 안에서 다음의 코드를 추가합니다.

src/pages/play.js 출처: Yoon-Suji of DSRV

📝 작업 포인트

[✓] 플레이 화면 UI 구현하기
[✓] useState 로 변수 상태 관리하기

요구사항 3–1의 완성본을 보고 싶다면 git checkout 7807660

위의 코드를 저장한 후 실행하려 하면 변수가 지정되지 않았다는 오류가 뜰 것 입니다.

playTime은 15초로 지정해주고, 그 외의 변수들은 useState 메소드를 사용하여 상태를 관리해줍니다.

src/pages/play.js 출처: Yoon-Suji of DSRV

프로젝트를 실행하고 localhost:3000/play 주소로 접속했을 때 아래와 같은 화면이 나타난다면 여기까지 구현에 성공한 것입니다.

요구사항 3–1 구현 화면. 출처: DSRV

지금까지 구현한 play.js 의 전체 코드는 다음과 같습니다.

src/pages/play.js 출처: Yoon-Suji of DSRV

요구사항 2: Game Start 버튼을 눌러 get_countreset 메소드를 차례로 실행할 수 있다.

📝 작업 포인트

[✓] 메인 화면에서 전달한 정보를 이용해 client 생성하기
[ ]
GAME START 버튼을 눌렀을 때 get_count, reset 메소드 실행하기

이번 요구사항에서는 GAME START 버튼을 눌렀을 때 get_count 메소드를 실행해서 가져온 count 값을 Previous Score 에 보여주고, reset 메소드를 실행해서 컨트랙트의 count 값을 0으로 초기화할 것입니다.

먼저 스마트 컨트랙트와 통신하기 위해서는 client 가 필요합니다. useEffect 메소드를 통해 화면이 처음 렌더링될 때 App.js 화면에서 전달받은 정보인 chainId 를 이용해서 지갑에 연결한 후 client를 생성합니다. App.js 에서 state 로 전달한 정보를 사용하려면 useLocation 메소드를 이용해야 합니다.

SigningCosmWasmClient.connectWithSigner() 메소드는 RPC 엔드포인트 주소와 Keplr에서 가져온 OfflineSigner을 인자로 받아 SigningCosmWasmClient 를 반환합니다. client 는 네트워크와 통신하는 인터페이스의 역할을 합니다.

src/pages/play.js 출처: Yoon-Suji of DSRV

📝 작업 포인트

[✓] 메인 화면에서 전달한 정보를 이용해 client 생성하기
[✓] GAME START 버튼을 눌렀을 때 get_count, reset 메소드 실행하기

요구사항 3–2의 완성본을 보고 싶다면 git checkout 9d7f85b

이제 GAME START 버튼을 눌렀을 때 실행되는 메소드를 작성합니다. 앞서 작성한 get_count 메소드를 이용해서 컨트랙트의 count 값을 읽어와 previousScore에 저장합니다. 그 후 reset 메소드를 이용해 컨트랙트의 count 값을 0으로 초기화합니다.

컨트랙트와의 통신이 끝나면 게임을 시작합니다. 게임이 시작되면 15초로 설정되어있던 time이 1초마다 1씩 줄어들어야 합니다. 해당 기능을 구현하기 위해 setInterval 메소드를 사용합니다.

src/pages/play.js 출처: Yoon-Suji of DSRV
💡DSRV’s Tip: setInterval은 어떻게 사용할까?

'setTimeout'이 일정시간이 지난 후 함수를 한 번 실행시키는 것과 달리 'setInterval'은 함수를 일정한 간격을 두고 주기적으로 실행시킵니다.
'setInterval'은 해당 타이머의 식별자인 timerId를 반환하고 'clearInterval(timerId)'를 이용해 함수의 실행을 멈출 수 있습니다.

여기까지 구현이 완료되었다면 yarn start 혹은 npm start 를 이용해 프로젝트를 실행하고 메인화면에서 플레이버튼을 눌러 플레이 화면으로 넘어갑니다.

그리고 GAME START 버튼을 눌렀을 때 reset 트랜잭션 승인을 요청하는 팝업이 표시됩니다. 이후, 게임이 시작되면 화면에 CosmWasm 아이콘이 나타나고 시간이 1초씩 줄어드는 것을 확인할 수 있습니다. 이 때, 지갑을 연결하지 않고 /play 링크에 바로 접속하면 에러가 발생하므로, 먼저 메인 화면에서 지갑을 연결해야 합니다.

요구사항 3–2 구현 화면. 출처: DSRV

지금까지 구현한 play.js 의 전체 코드는 다음과 같습니다.

src/pages/play.js 출처: Yoon-Suji of DSRV

요구사항 3: 게임이 시작되면 15초 동안 화면에 랜덤하게 나타나는 CosmWasm 아이콘을 클릭하여 점수를 획득할 수 있다.

📝 작업 포인트

[✓] CosmWasm 아이콘을 클릭하면 현재 점수를 1씩 증가시키고, 다음 위치를 랜덤하게 지정한다.
[ ] 15초가 지나서
time 이 0이 되면 게임 종료한다.

이번 요구사항에서는 CosmWasm 아이콘을 클릭하면 점수를 1씩 증가하고 다음 위치를 랜덤하게 지정하는 메소드가 실행되도록 할 것입니다. 그리고 15초가 모두 지나 시간이 0이 되면 게임이 종료되게 만들어 봅시다.

먼저 CosmWasm 아이콘을 클릭했을 때 실행할 메소드를 작성합니다. setScore 를 이용해 현재 점수를 1씩 증가시키고, setTargetPositionMath.random 메소드를 이용해서 아이콘의 위치를 랜덤으로 설정합니다.

src/pages/play.js 출처: Yoon-Suji of DSRV

📝 작업 포인트

[✓] CosmWasm 아이콘을 클릭하면 현재 점수를 1씩 증가시키고, 다음 위치를 랜덤하게 지정한다.
[✓] 15초가 지나서 time 이 0이 되면 게임 종료한다.

요구사항 3–3의 완성본을 보고 싶다면 ➡git checkout 15cfd0b

다음으로 1초마다 1씩 줄어드는 time 값이 0이 될 때 게임이 종료되도록 하는 코드를 구현해봅시다.

time 값이 변하는 것을 감지하는 useEffect 함수를 이용해서 time이 0이 될 때 아이콘이 사라지도록 설정하고 게임 종료 알람창을 띄웁니다.

그리고 clearInterval 메소드를 이용해 계속 실행되고 있는 setInterval 함수를 중지합니다.

src/pages/play.js 출처: Yoon-Suji of DSRV

여기까지 구현이 완료되었다면 프로젝트를 실행해서 플레이 버튼을 누르고 GAME START 버튼을 눌렀을 때 나타나는 CosmWasm 아이콘을 누르면 점수가 올라가고 아이콘의 위치가 랜덤하게 바뀌는 것을 확인할 수 있습니다. 또한 시간이 0초가 되면 알람창이 뜨면서 아이콘이 사라집니다.

요구사항 3–3 구현 화면. 출처: DSRV

지금까지 구현한 play.js 의 전체 코드는 다음과 같습니다.

src/pages/play.js 출처: Yoon-Suji of DSRV

요구사항 4: 게임 종료 후 나타나는 Transaction 버튼의 UI 컴포넌트를 구현하고, 이를 클릭하면 increment 메소드를 실행할 수 있다.

📝 작업 포인트

[✓] TRANSACTION 버튼을 눌렀을 때 increment 메소드 실행하기

요구사항 3–4의 완성본을 보고 싶다면 ➡️ git checkout 0c7059d

이번 요구사항에서는 게임이 종료한 후에 TRANSACTION 버튼을 클릭했을 때, increment 메소드를 실행해서 컨트랙트의 count 값에 더할 것인데요. 앞선 요구사항 2번에서 reset 메소드를 통해 컨트랙트의 count 값을 0으로 초기화해줬기 때문에 increment 메소드를 실행하면 컨트랙트의 count 값이 사용자가 획득한 점수가 됩니다.

먼저 TRANSACTION 버튼을 클릭했을 때 실행되는 submitScore 메소드를 작성합니다. 사용자가 얻은 점수인 score 만큼 increment를 실행하고, increment 트랜잭션이 완료되면 get_count 메소드를 통해 다시 컨트랙트의 count 값을 읽어와서 previousScore에 업데이트 합니다.

컨트랙트와의 모든 통신이 끝난 후에는 게임을 다시 시작할 수 있도록 gameOver 값을 false로 설정하고 0이 된 time 을 다시 15초로 바꿔줍니다.

src/pages/play.js 출처: Yoon-Suji of DSRV

이렇게 Clicker 게임의 모든 요구사항을 구현해보았습니다.

yarn start 혹은 npm start 로 프로젝트를 실행하고 게임을 즐겨보세요. 게임이 종료된 후 TRANSACTION 버튼을 눌렀을 때 increment 트랜잭션을 허락하는 창이 뜨고 허락 버튼을 누르면 Previous Score가 업데이트 되는 것을 확인할 수 있습니다.

요구사항 3–4 구현 화면. 출처: DSRV

play.js의 최종 코드는 다음과 같습니다.

src/pages/play.js 출처: Yoon-Suji of DSRV

완성된 전체 코드는 다음 저장소Step3 브랜치에서 확인할 수 있습니다.

글을 마치며..

이번 시간에는 간단한 Clicker 게임을 통해 wasm 컨트랙트와 프론트엔드가 통신하는 방법을 알아보았습니다. wasm 컨트랙트는 CosmJS를 지원하기 때문에 자바스크립트를 이용하여 프론트엔드와 통신할 수 있습니다.

이로써 “1편: Counter 컨트랙트 톺아보기”, “2편: Go CLI와 CosmJS를 활용하여 Counter 컨트랙트 배포하기”, “3편: 프론트엔드와 통신하기” 에 이르는 CosmWasm 101 시리즈가 끝을 맺게 되었습니다.

이 글이 WebAssembly 공부를 시작하고자 했던 많은 개발자분들께서 CosmWasm에 쉽게 입문하는데 도움이 되었길 바라며, 다음 글로 또 찾아오도록 하겠습니다. 이 글을 읽는데 귀중한 시간을 할애해주셔서 감사합니다.

Written by
Suji Yoon of DSRV, Developer Evangelist Intern (Twitter @suji_forcrypto)

Reviewed by
Sigrid Jin of DSRV, Technical Writer & Developer Evangelist (Twitter @sigridjin_eth)
Heesung Bae of DSRV, Software Developer (Twitter @BaeHeesung25)
Heesu Shin of DSRV, Backend Developer (Twitter @LucasShin8)
Owen Hwang of DSRV, Research Manager (Twitter @journeywith_eth)

--

--

Suji Yoon
DSRV
Writer for

Software Engineer Intern @DSRV / Ewhachain / Twitter: @suji_forcrypto