이번 포스팅에서는 메타마스크를 사용해서 스마트 컨트랙트를 실행시켜보는 작업을 진행해보고자 한다.
위에 보이는 것과 같이 스마트 컨트랙트에 의해 동작하는 Counter(카운터)를 만들어 볼 예정이다. 화면에 보이는 (+)버튼, (-)버튼을 클릭할 때마다 클라이언트는 메타마스크와 연결된 계정을 사용해 트랜잭션을 발생시키게 되고 스마트 컨트랙트 코드가 실행되어 상태변수의 값이 변경된다.
React를 사용해 프론트 화면을 구성할 예정이며 메타마스크는 Ganache 네트워크에 연결하고자 한다. 그리고 스마트 컨트랙트 작성 및 배포는 Truffle을 사용할 것이다.
$ # front 디렉토리
$ npx create-react-app front
$ # ganache 실행
$ npx ganache-cli
$ # truffle 생성
$ npx truffle init
메타마스크에서 Ganache를 통해 실행시킨 로컬 이더리움 네트워크를 연결하는 작업에 대해서는 이전 포스팅에서 다뤘던 내용이므로 여기서는 생략하도록 하겠다. 이전 글을 참고하길 바란다.
참고)
2022.06.29 - [Ethereum] - Ethereum/이더리움 - 메타마스크 연결하기
< 목차 >
- 스마트 컨트랙트 작성 및 배포
- front 작업하기
1. 스마트 컨트랙트 작성 및 배포
스마트 컨트랙트를 작성하고 배포하는 데에는 truffle을 사용하고자 한다. npx truffle init 으로 생성된 contracts/ 디렉토리 안에서 Counter.sol 파일을 만들어 카운터 스마트 컨트랙트 코드를 다음과 같이 작성해주었다.
/* contracts/ 디렉토리 Counter.sol 파일 */
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
contract Counter {
uint256 private _count;
// 상태변수가 private 이기 때문에 getter 함수를 직접 만들어줘야 한다.
function current() public view returns(uint256) {
return _count;
}
// 상태변수 변경 함수
function increment() public {
_count += 1;
}
function decrement() public {
_count -= 1;
}
}
주의할 점은 상태변수 _count를 private으로 만들어줬기 때문에 getter 함수가 자동으로 생성되지 않는다. 따라서 상태변수 값을 조회할 수 있는 current( ) 메소드를 따로 만들어주었다. increment( ) 메소드와 decrement( ) 메소드는 각각 상태변수의 값을 +1 증가시키고 -1 감소시키는 함수이다.
현재 Ganache를 사용해 로컬 이더리움 네트워크를 생성하였으므로 truffle-config.js 파일 안의 networks 속성값을 다음과 같이 만들어준다.
networks: {
development: {
host: '127.0.0.1', // Localhost (default: none)
port: 8545, // Standard Ethereum port (default: none)
network_id: '*', // Any network (default: none)
}
}
이제 truffle 명령어를 사용해 작성한 스마트 컨트랙트 코드를 컴파일 해주도록 하자.
$ npx truffle compile
컴파일이 완료되었다면 build/contracts/ 디렉토리 안에 Counter.json 파일이 생성될 것이고 배포를 위해 migrations/ 디렉토리 안에서 2_deploy_Counter.js 파일을 만들어 배포 관련 코드를 아래와 같이 작성해주었다.
/* migrations/ 디렉토리 2_deploy_Counter.js 파일 */
const Counter = artifacts.require('Counter');
module.exports = function (deployer) {
deployer.deploy(Counter);
};
migration 코드를 작성 완료하였다면 아래의 명령어를 통해 Ganache로 생성한 로컬 이더리움 네트워크에 스마트 컨트랙트를 배포해주면 된다.
$ npx truffle migration
이 때 메타마스크에 연결되어 있는 계정을 조회해보면 보유하고 있던 ETH에서 일정 수량만큼이 차감된 것을 확인할 수 있는데 이는 스마트 컨트랙트를 배포할 때 지불한 가스비만큼 ETH가 차감된 것이라고 보면 된다.
마지막으로 truffle console 창을 활용해서 배포가 잘 되었는지 확인해주자.
$ npx truffle console
Counter.deployed().then(instance => it = instance)
it.current.call( )을 실행할 경우 결과물이 BN으로 나오는 것을 볼 수 있는데 BN은 Big Number의 약자로 JavaScript에서 큰 숫자를 표현할 때 사용되는 객체이다. 스마트 컨트랙트에서 사용되는 integer는 기본적으로 그 값이 큰 경우가 많다. 1 ETH만 하더라도 10^18 wei를 의미하기 때문이다. 이에 JavaScript에서 call( ) 메소드를 사용해 상태변수를 가져올 때 BN 형태로 오는 경우가 많다. 그리고 it.increment( )를 실행할 경우 즉시 상태변수 값이 변경되는 것을 확인할 수 있는데 이는 Ganache로 실행한 로컬 이더리움 네트워크에서 트랜잭션이 발생할 때마다 자동으로 블록을 마이닝 해주기 때문이다.
2. front 작업하기
truffle을 사용해 Ganache로 생성한 로컬 이더리움 네트워크에 스마트 컨트랙트를 배포 완료하였고 truffle 콘솔창에서 제대로 배포가 되었는지 여부 또한 확인해주었다. 이제 프론트 쪽 작업을 진행해보고자 한다. React에 관한 설명은 생략하고 클라이언트 쪽에서 메타마스크에게 요청을 보내 트랜잭션을 발생시키는 부분에 대해서만 언급하도록 하겠다.
npx create-react-app 으로 생성한 front 디렉토리의 구조는 아래와 같다.
src/ 디렉토리 안에 hooks 폴더를 만들어 useWeb3 라는 커스텀 훅을 아래와 같이 만들어주었다.
/* hooks/ 디렉토리 useWeb3.jsx 파일 */
import React, { useEffect, useState } from 'react';
import Web3 from 'web3/dist/web3.min'; // NodeJs 환경에서만 쓸 수 있는 기능은 제외하고 최소기능만 가져오기
// 커스텀 훅의 역할은 상태를 담고 있는 컴포넌트
// 자주 사용하는 상태들은 커스텀 훅으로 빼서 사용 가능하다.
const useWeb3 = () => {
// 메타마스크에서 사용하고 있는 계정과 관련된 상태
const [account, setAccount] = useState(null);
// 클라이언트와 메타마스크가 통신하기 위한 web3
const [web3, setWeb3] = useState(null);
useEffect(() => {
// 즉시 실행 함수
(async () => {
if (!window.ethereum) return;
// rpc 통신 , Promise 객체 반환
const [address] = await window.ethereum.request({
method: 'eth_requestAccounts',
});
setAccount(address);
const web3 = new Web3(window.ethereum);
setWeb3(web3);
})();
}, []);
return [web3, account];
};
export default useWeb3;
account 라는 상태와 web3 라는 상태를 만들어준 다음 useEffect( )를 사용해서 useWeb3 훅이 실행되었을 때 account의 상태값을 메타마스크에서 현재 사용하고 있는 계정으로 web3의 상태값을 메타마스크와 통신하기 위한 인스턴스로 변경해주었다. useEffect( ) 안에 작성된 즉시 실행 함수 (async ( )=>{ })( ) 의 내용을 좀 더 살펴보도록 하자.
메타마스크는 Chrome Extension 프로그램으로 메타마스크를 설치할 경우 브라우저의 window 객체 안에 ethereum 객체가 만들어진다. window.ethereum은 메타마스크와 같은 이더리움 계열 지갑 프로그램을 설치했을 때 생성되는 객체로 window.ethereum을 사용해 메타마스크에 요청을 보내는 작업을 할 수 있다.
if (!window.ethereum) return 에 의해 클라이언트의 브라우저에 메타마스크가 설치되어 있지 않다면 즉시 실행 함수를 종료시키고 메타마스크가 설치되어 있다면 아래와 같이 window.ethereum.request( ) 메소드를 사용해 메타마스크에서 현재 연결되어 있는 계정을 가져와 account 상태값을 업데이트 해준다.
const [address] = await window.ethereum.request({
method: 'eth_requestAccounts',
});
setAccount(address)
그리고 new Web3(window.ethereum) 에 의해 생성된 web3 인스턴스로 web3 상태값을 업데이트 해준다. 이제 해당 web3 인스턴스를 통해서 메타마스크와 통신을 주고 받을 수 있게 된다.
const web3 = new Web3(window.ethereum);
setWeb3(web3);
다음은 useWeb3 훅이 호출되는 App.jsx 파일을 살펴보자.
/* App.jsx 파일 */
import React from 'react';
import useWeb3 from './hooks/useWeb3';
import Counter from './components/Counter';
import './App.css';
function App() {
const [web3, account] = useWeb3();
if (!account) return <h1>메타마스크를 연결해주세요.</h1>;
return (
<div className="App">
<h2>Account : {account}</h2>
<Counter web3={web3} account={account} />
</div>
);
}
export default App;
useWeb3 훅이 반환하는 account 값이 null 값이라면 "메타마스크를 연결해주세요" 라는 문구를 화면에 표시해주도록 하였고 <Counter /> 컴포넌트에게 web3 인스턴스와 account를 props로 전달하였다.
마지막으로 <Counter /> 컴포넌트를 살펴보도록 하자.
/* Counter.jsx 파일 */
import React, { useEffect, useState } from 'react';
import CounterContract from '../contracts/Counter.json';
const Counter = ({ web3, account }) => {
const [count, setCount] = useState(0);
const [deployed, setDeployed] = useState(null);
const increment = async () => {
// 인자값으로 트랜잭션을 발생시킬 계정을 넣어준다.
const result = await deployed.methods.increment().send({ from: account });
if (!result) return;
const current = await deployed.methods.current().call();
setCount(current);
};
const decrement = async () => {
const result = await deployed.methods.decrement().send({ from: account });
if (!result) return;
const current = await deployed.methods.current().call();
setCount(current);
};
// Truffle 에서는 deployed() 를 사용해 배포된 컨트랙트를 가져왔었다.
// Web3 에서는 new web3.eth.Contract() 를 사용한다.
useEffect(() => {
(async () => {
if (deployed) return;
// Contract를 호출할 때 필요한 값들을 인자값으로 전달
// 인자값 2개 , (abi, CA)
const Deployed = new web3.eth.Contract(CounterContract.abi, '0xB275b8A06f34d9428CB34e5c1be036c863215db8');
const count = await Deployed.methods.current().call();
setCount(parseInt(count));
setDeployed(Deployed);
})();
}, []);
return (
<div>
<h2>Counter : {count}</h2>
<button onClick={() => increment()}>+</button>
<button onClick={() => decrement()}>-</button>
</div>
);
};
export default Counter;
import CounterContract from '../contracts/Counter.json'; 으로 Counter.json 파일을 읽어온 것을 확인할 수 있는데 Counter.json 파일은 truffle 디렉토리의 build/ 디렉토리 안에 있는 Counter.json 파일과 동일한 파일이다. 해당 파일 안에 있는 abi 와 CA 정보를 이용해 트랜잭션을 발생시키거나 스마트 컨트랙트를 실행시키기 위해 동일한 파일을 복사해서 가져온 것이다.
useEffect( ) 안의 (async ( )=>{ })( ) 에서 아래와 같이 Deployed 변수에 abi 정보와 CA 정보를 갖고 있는 Contract 인스턴스를 할당해주었다.
const Deployed = new web3.eth.Contract(CounterContract.abi, '0xB275b8A06f34d9428CB34e5c1be036c863215db8');
const count = await Deployed.methods.current().call();
setCount(parseInt(count));
setDeployed(Deployed);
new web3.eth.Contract( )의 두번째 인자값으로 들어간 CA 값은 Counter.json 파일 안에 있는 CA 값을 직접 넣어준 것이다. 이제 Deployed 인스턴스를 사용해 스마트 컨트랙트 안의 getter 함수인 current( ) 함수를 호출해 상태변수 값을 가져온 다음 count 변수의 값으로 업데이트해주고 deployed 변수는 Deployed 인스턴스로 상태를 변경해주었다.
이후 (+)버튼과 (-)버튼에서 onClick 이벤트가 발생했을 때 실행시킬 increment( ) 와 decrement( ) 함수를 만들어 준 다음 Deployed 인스턴스가 할당된 deployed 변수를 사용해 다음과 같은 방법으로 트랜잭션을 발생시켜주었다.
const increment = async () => {
// 인자값으로 트랜잭션을 발생시킬 계정을 넣어준다.
const result = await deployed.methods.increment().send({ from: account });
if (!result) return;
const current = await deployed.methods.current().call();
setCount(current);
};
const decrement = async () => {
const result = await deployed.methods.decrement().send({ from: account });
if (!result) return;
const current = await deployed.methods.current().call();
setCount(current);
};
스마트 컨트랙트의 상태변수 값을 변경시켜주는 함수를 실행시키기 위해서는 수수료를 지불해야 하기 때문에 .send( ) 메소드를 사용해 트랜잭션을 발생시켜주었으며 인자값으로 { from: account } 를 전달해 메타마스크와 연결되어 있는 계정으로 트랜잭션을 일으키도록 하였다.
'Ethereum' 카테고리의 다른 글
Ethereum/이더리움 - 스마트 컨트랙트로 토큰 발행하기 (0) | 2022.07.17 |
---|---|
Ethereum/이더리움 - 스마트 컨트랙트 이벤트 등록 및 백엔드에서 트랜잭션 생성하기 (0) | 2022.07.14 |
Ethereum/이더리움 - Truffle (스마트 컨트랙트 개발 프레임워크) (0) | 2022.07.13 |
Ethereum/이더리움 - JavaScript로 스마트 컨트랙트 배포 및 실행 (0) | 2022.07.12 |
Ethereum/이더리움 - 스마트 컨트랙트 배포 및 실행 (3) | 2022.07.11 |