본문 바로가기
경일/Block-chain

[Block-chain] smart contract를 이용한 DApp counter 만들기

by dev_kong 2022. 7. 13.
728x90
728x90

0. 목차

1. 개요

2. 구성

3. truffle 이용하여 contract 배포 및 실행

4. react로 구현한 페이지에서 contract 실행

1. 개요

리액트와 스마트 컨트랙트를 이용해서 단순한 카운터 애플리케이션을 만들려고 한다.

이제껏 했던것의 총 정리의 느낌이다.

 

geth를 이용하지 않고 ganache를 이용해서 테스트 할것이다.

 

2. 구성

|- truffle

|- client

npx create-react-app cleint
mkdir truffle && cd truffle
truffle init

 

위 명령어를 차례대로 실행해서 디렉토리 구조를 잡아주었다.

 

3. truffle 이용하여 contract 배포 및 실행

3-1. truffle 설정

ganache를 이용하기 때문에 config 파일의 network 부분을 수정한다.

 

    development: {
      host: "127.0.0.1", // Localhost (default: none)
      port: 8545, // Standard Ethereum port (default: none)
      network_id: "*", // Any network (default: none)
    },

 

3-2. contract & migration 작성

 

smart contract 부터 작성하자.

 

pragma solidity ^0.8.15;

contract Counter{
  uint256 private _count;

  function current() public view returns(uint256){
    return _count;
  }

  function increment() public {
    _count += 1;
  }

  function decrement() public {
    _count -= 1;
  }
}

 

매우 간단한 코드이니 설명은 생략^^

 

이번엔 migration을 작성하자.

 

파일명은 2_deploy_Counter 로 정하고

내용은 기존 1_initial_migration 내용을 그대로 복붙 한뒤 Migration을 Counter로 변경해주면 된다.

 

const Counter = artifacts.require("Counter");

module.exports = function (deployer) {
  deployer.deploy(Counter);
};

 

3-3. contract 배포

저번 포스팅에서는 compile 을 먼지 실행한뒤 migration을 했지만,

사실 migration 할때 컴파일 된거 없으면 컴파일 부터 실행해준다.

 

truffle migration

 

또한 geth가 아닌 ganache를 이용하기 때문에,

mining을 해주지 않아도 된다 (가나슈 는 트랜잭션이 발생하면 바로 블록을 생성해주기때문)

배포가 잘 됐는지 확인을 해보자

 

truffle console
truffle(development)>

 

truffle console 을 실행해준 뒤

 

truffle(development)> Counter.deployed().then((instance)=>{it=instance}) 
truffle(development)> it.current.call()
#BN { negative: 0, words: [ 0, <1 empty item> ], length: 1, red: null }

 

위의 명령어를 실행했을 때 BN이 출력된다면 잘 배포 된거다.

 

3-4. 스마트 컨트랙트 실행

 

truffle(development)> it.increment()
truffle(development)> it.current.call()
#BN { negative: 0, words: [ 1, <1 empty item> ], length: 1, red: null }
truffle(development)> it.increment()
truffle(development)> it.current.call()
#BN { negative: 0, words: [ 2, <1 empty item> ], length: 1, red: null }
truffle(development)> it.decrement()
truffle(development)> it.current.call()
#BN { negative: 0, words: [ 1, <1 empty item> ], length: 1, red: null }

 

컨트랙트를 실행해보고 값도 다시 출력해보자.

아주 잘된다.

 

이제 react에서 작업을 진행할건데,

그전에 CA를 확인하고 어딘가에 잘 복붙 해두자.

 

truffle(development)> Counter.address
# '0xf4f8207A7fA7D553ded43c3982864F0434CAAe58'

 

4. react로 구현한 페이지에서 contract 실행

트랜잭션 발생시키기 위해선 private key 가 필요하다.

그런데 private key는 관리에 각별한 주의가 필요하기 때문에, Wallet 프로그램을 사용하는 데, 바로 Metamask 이다.

 

즉 화면 -> Metamask -> Ethereum client 순으로 통신이 진행되는데,

이건 이전의 포스팅에서 다룬 적이 있다.

 

복습의 느낌으로 다시 한번 해보자

우선 web3 부터 설치를 하자.

 

npm i web3

 

4-1. 커스텀 훅

web3가 설치 됐으면 web3를 사용할 커스텀 훅을 만들자.

 

import { useEffect, useState } from "react";
import Web3 from "web3/dist/web3.min";

export const useWeb3 = () => {
  const [account, setAccount] = useState();
  const [web3, setWeb3] = useState();

  useEffect(() => {
    (async function () {
      if (!window.ethereum) return;

      const [address] = await window.ethereum.request({
        method: "eth_requestAccounts",
      });

      setAccount(address);

      const web3 = new Web3(window.ethereum);
      setWeb3(web3);
    })();
  }, []);

  return [web3, account];
};

 

이렇게 설정을 해주면 현재 페이지에 연결 되어있는 계정이 없다면 메타마스크에서 계정을 연결하라는 화면이 나오고,
연결을 설정하면, 연결한 계정을 setAccount를 통해 account로 설정한다.


이미 연결이 되어 있다면, 연결된 계정을 setAccount를 통해 account로 설정한다.

 

그런데 현재 코드에서는 메타마스크에 계정이 2개가 있고,
메타마스크에서 연결된 계정을 변경하면 리액트는 계정이 변경된 걸 알수 없다.

 

그래서 메타마스크에서 Events를 만들어 두었다.
공식문서에서 쉽게 확인이 가능하다.

 

import { useEffect, useState } from "react";
import Web3 from "web3/dist/web3.min";

export const useWeb3 = () => {
  const [account, setAccount] = useState();
  const [web3, setWeb3] = useState();

  const getUsedAccount = async () => {
    if (!window.ethereum) return;

    const [address] = await window.ethereum.request({
      method: "eth_requestAccounts",
    });

    setAccount(address);

    const web3 = new Web3(window.ethereum);
    setWeb3(web3);
  };

  useEffect(() => {
    if (!window.ethereum) return;
    getUsedAccount();
    window.ethereum.on("accountsChanged", getUsedAccount);
  }, []);

  return [web3, account];
};

 

이렇게 수정을 해주면, componentDidMount 시점에 연결된 account를 state에 저장하고,
accountChanged 이벤트를 등록한다, 이벤트가 발생하면 on의 두번째 인자값인 콜백함수가 실행되는데,
해당 함수 역시 연결된 account를 state에 저장하는 함수이기 때문에,
metamask에서 연결된 계정을 변경할때마다 state가 변경된다.

 

그런데 공식문서에 따르면 componentWillUnmount 시점에 등록한 evenvt를 remove해주라고 나와있다.

 

import React, { useEffect, useState } from "react";
import Web3 from "web3/dist/web3.min";

export const useWeb3 = () => {
  const [account, setAccount] = useState();
  const [web3, setWeb3] = useState();

  const getUsedAccount = async () => {
    if (!window.ethereum) return;

    const [address] = await window.ethereum.request({
      method: "eth_requestAccounts",
    });

    setAccount(address);

    const web3 = new Web3(window.ethereum);
    setWeb3(web3);
  };

  useEffect(() => {
    getUsedAccount();
    window.ethereum.on("accountsChanged", getUsedAccount);
  }, []);

  useEffect(
    () => () => {
      window.ethereum.removeListener("accountsChanged", getUsedAccount);
    },
    []
  );

  return [web3, account];
};

 

useEffect를 하나 더 추가해서 willUnmount 시점에 removeListener를 해준다.

 

4-2. app.jsx

방금만든 따끈따끈 custom hook 을 이용해서 app.jsx를 만들자.

 

// app.jsx

import { useWeb3 } from "./hooks/useWeb3";
import { Counter } from "./components/Counter.jsx";

function App() {
  const [web3, account] = useWeb3();

  if (!account) return <>강해져서 돌아와라 애송이</>;
  return (
    <div>
      <span> Account : {account}</span>
      <Counter web3={web3} account={account} />
    </div>
  );
}

export default App;

 

별거없다.

 

4-3. Counter.jsx

이제 제일 중요한 Counter 컴포넌트를 만들자.

 

import { useState, useEffect } from "react";
import CounterContract from "../contracts/Counter.json";

export const Counter = ({ web3, account }) => {
  const [count, setCount] = useState();
  const [deployed, setDeployed] = useState();

  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);
  };

  useEffect(() => {
    (async function () {
      if (deployed) return;
      const deployedContract = new web3.eth.Contract(
        CounterContract.abi,
        "0xf4f8207A7fA7D553ded43c3982864F0434CAAe58"
      );

      const count = await deployedContract.methods.current().call();
      setCount(count);
      setDeployed(deployedContract);
    })();
  });

  return (
    <div>
      <h2>Counter : {count}</h2>
      <button onClick={increment}>증가</button>
      <button onClick={decrement}>감소</button>
    </div>
  );
};

 

useEffect를 통해 배포된 컨트랙트를 deploy state로 넣고,
contract 내의 함수가 들어있는 객체 methods 에서 current를 통해 현재 값을 count state에 넣어준다.

 

그리고, 두개의 버튼에 각각 onClick 이벤트를 달아두었다.

 

각각 increment와 decrement 함수를 달아주면 된다.

버튼을 클릭하면 메타마스크가 동작하고 확인버튼을 누르면 화면의 값이 바뀌는걸 볼 수 있다.

 

4-4. CA를 Counter.json에서 가져오는 법

 

Counter.json 에 networks 프로퍼티에 현재 연결된(기존에 연결됐던) network 에 대한 내용이 있다.

그리고 그 안에 현재 배포된 contract에 대한 내용도 들어있다.

즉, CA도 있다는 얘기

 

그리고 web3 라이브러리에서 현재 연결된 networkId를 가져오는 메서드가 있다.

이 두가지를 조합하면 하드코딩한 CA를 유동적으로 처리할 수 있다.

 

//Counter.jsx

useEffect(() => {
    (async function () {
      if (deployed) return;

      const networkId = await web3.eth.net.getId();
      const { address: CA } = CounterContract.networks[networkId];
      const { abi } = CounterContract;

      const deployedContract = new web3.eth.Contract(abi, CA);

      const count = await deployedContract.methods.current().call();
      setCount(count);
      setDeployed(deployedContract);
    })();
  });

 

이렇게 해주면 기존의 코드와 동작은 동일하게 하지만

개발 단계에서 contract를 수정할 때, 파일만 옮기면 된다.

728x90
728x90

댓글