트러플(Truffle)을 사용하여 리액트(React) 기반의 스마트 컨트랙트(Smart Contract) 기능이 들어간 DApp 구현 실습, Web3와 메타마스크 연결

2021. 10. 15. 03:27Blockchain/Truffle

실습 전 이론 학습

- 리액트에서 배포(deploy)한 스마트 컨트랙트 내용을 가져 올 때 web3를 사용

- Web3를 메타마스크에 연결하는 방법은? 트러플이 알아서 해줌

truffle unbox react로 작업 환경 세팅 시 자동으로 생기는 파일, /client/src/getWeb3.js
npm run start로 서버 실행 후 localhost:3000에서 콘솔에 window.ethereum 입력 시 연결 확인 가능
메타마스크와 연결하여 사용하는 방식 = injected web3 방식

 


DApp 구현 실습 (과일 상점)

 

  1. npm install -g truffle // 트러플 설치
  2. truffle unbox react // 리액트 작업 환경 자동 세팅
  3. ganache 실행 후 metamask 연결 (이전 포스트 참조, 링크 : https://hwan91.tistory.com/11)
  4. truffle-config.js 코드 수정
  5. truffle compile // contract 코드 컴파일
  6. truffle migrate // contract 코드 배포
  7. cd client
  8. npm run start // 리액트 실행
  9. localhost:3000에서 콘솔에 window.ethereum // 메타마스크와 연결 되어있는지 확인 
  10. 테스트용으로 사전 빌드(컴파일과 배포)를 했기 때문에 자동 생성 된 파일 삭제
    1: /contracts/SimpleStorage.sol
    2: /migrations/2_deploy_contracts.js
  11. truffle create contract Fruitshop // 파일 생성
  12. truffle create migration Fruitshop // 파일 생성
  13. /migrations/1634177248_fruitshop.js 코드 수정
    let Fruitshop = artifacts.require('./Fruitshop.sol');
    
    module.exports = function(_deployer) {
      // Use deployer to state migration tasks.
      _deployer.deploy(Fruitshop);
    };​
  14. /contracts/Fruitshop.sol 코드 작성 // 배포 할 새 contract 코드
    // SPDX-License-Identifier: MIT
    pragma solidity >=0.4.22 <0.9.0;
    
    /*
      구현 할 기능 목록
    
      1. 보낸 사람의 계정에서 사과를 총 몇개 가지고있는지 확인하는 코드
      2. 사과 구매 시 해당 계정(주소)에 사과를 추가해주는 코드
      3. 사과 판매 시 내가 가지고있는 [사과*사과구매가]만큼 토큰을 반환해주고 사과를 0개로 변환하는 코드
      4. 내 사과를 반환해주는 코드
    */
    
    contract Fruitshop {
      mapping(address=>uint) myApple;
    
      constructor() public {
    
      }
    
      function buyApple() payable public {
        myApple[msg.sender]++; // 사과 개수, uint 반환
      }
    
      function getMyApple() public view returns(uint) {
        return myApple[msg.sender];
      }
    
      function sellApple(uint _applePrice) payable public { // payable == 토큰 거래가 가능한 함수임을 선언
        uint totalPrice = (myApple[msg.sender] * _applePrice); // 사과 개수 * 사과 가격 = 총 가격
        myApple[msg.sender] = 0; // 사과를 0개로 변환, 초기화
        msg.sender.transfer(totalPrice); // 환불
      }
    }​
  15. truffle compile // contract 코드 컴파일
  16. truffle migrate // contract 코드 배포
    ** truffle migrate --reset --all // 배포 중 오류 발생 시 --reset 플래그를 주어 첫번째 스크립트부터 다시 실행
    오류 발생, 배포가 제대로 완료되지 않음
    오류 해결, 배포 완료 시 정상적인 터미널 화면
  17. npm run start // 리액트 재실행
  18. /client/src/App.js 코드 수정 // 기존 코드 주석 후 새 코드 작성
  19. 기본 화면을 구성하는 코드 작성
    import React from "react";
    import FruitshopContract from "./contracts/Fruitshop.json";
    import getWeb3 from "./getWeb3";
    
    import "./App.css";
    
    const App = () => {
        return (
            <div>
                <h1>사과 가격 : 10 ETH</h1>
                <button>구매</button>
                <p>내가 가진 사과 : 0</p>
                <button>판매 (판매 가격은 : {0 * 10} ETH)</button>
            </div>
        );
    }
    
    export default App;

    화면 구성
  20.  버튼 클릭 시 >> 내가 가지고 있는 사과 1씩, 판매 가격 10씩 증가
    판매 버튼 클릭 시 >> 내가 가지고 있는 사과 개수와 판매 가격 0으로 초기화
    import React, { useState } from "react";
    import FruitshopContract from "./contracts/Fruitshop.json";
    import getWeb3 from "./getWeb3";
    
    import "./App.css";
    
    const App = () => {
        const [myApple, setMyApple] = useState(0);
    
        // 구매
        const buyApple = () => {
            setMyApple(prev => prev + 1);
        }
    
        // 판매
        const sellApple = () => {
            setMyApple(0);
        }
    
        return (
            <div>
                <h1>사과 가격 : 10 ETH</h1>
                <button onClick={() => buyApple()}>구매</button>
                <p>내가 가지고있는 사과 : {myApple}</p>
                <button onClick={() => sellApple()}>판매 (판매가격은 : {myApple * 10} ETH)</button>
            </div>
        );
    }
    
    export default App;​
  21. Web3를 가져와서 메타마스크를 연결하는 코드 작성
    import React, { useState, useEffect } from "react";
    import FruitshopContract from "./contracts/Fruitshop.json";
    import getWeb3 from "./getWeb3";
    
    import "./App.css";
    
    const App = () => {
        const [myApple, setMyApple] = useState(0);
    
        // 구매
        const buyApple = () => {
            setMyApple(prev => prev + 1);
        }
    
        // 판매
        const sellApple = () => {
            setMyApple(0);
        }
    
        // web3 가져와서 메타마스크 연결
        const getweb = async () => {
            let web3 = await getWeb3(); // == const web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545'));
            console.log(web3);
        }
    
        useEffect(() => {
            getweb();
        }, []);
    
        return (
            <div>
                <h1>사과 가격 : 10 ETH</h1>
                <button onClick={() => buyApple()}>구매</button>
                <p>내가 가지고있는 사과 : {myApple}</p>
                <button onClick={() => sellApple()}>판매 (판매가격은 : {myApple * 10} ETH)</button>
            </div>
        );
    }
    
    export default App;​
    console.log(web3);
  22. contract 파일에 있는 함수를 쉽게 연결하고 사용하기 위해 서버 종료 후 새 패키지 설치 후 코드 작성
    npm install @truffle/contract // 패키지 설치
    import React, { useState , useEffect} from "react";
    import FruitshopContract from "./contracts/Fruitshop.json";
    import getWeb3 from "./getWeb3";
    
    import "./App.css";
    
    const App = () => {
        const [myApple, setMyApple] = useState(0);
    
        // 구매
        const buyApple = () => {
            setMyApple(prev => prev + 1);
        }
    
        // 판매
        const sellApple = () => {
            setMyApple(0);
        }
    
        // web3 가져와서 메타마스크 연결
        const getweb = async () => {
            const contract = require('@truffle/contract');
    
            let web3 = await getWeb3(); // == const web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545'));
            let fruitshop = contract(FruitshopContract);
            
            fruitshop.setProvider(web3.currentProvider);
    
            let instance = await fruitshop.deployed();
            
            // console.log(web3);
            console.log(instance);
        }
    
        useEffect(() => {
            getweb();
        }, []);
    
        return (
            <div>
                <h1>사과 가격 : 10 ETH</h1>
                <button onClick={() => buyApple()}>구매</button>
                <p>내가 가지고있는 사과 : {myApple}</p>
                <button onClick={() => sellApple()}>판매 (판매가격은 : {myApple * 10} ETH)</button>
            </div>
        );
    }
    
    export default App;​
    console.log(instance);
    console.log(instance)에 출력 된 address는 /client/src/contracts/Fruitshop.json 파일에서 같은 내용을 찾을 수 있음
  23. 연결 된 계정 가져오는 코드 작성
    reducer와 dispatch를 사용하여 거래 결과에 따라 실제 거래 내역을 변수에 담아 출력하는 코드 작성
    import React, { useState, useEffect, useReducer } from "react";
    import FruitshopContract from "./contracts/Fruitshop.json";
    import getWeb3 from "./getWeb3";
    
    import "./App.css";
    
    const App = () => {
        let initialState = {
            web3: null,
            instance: null,
            accounts: null,
        }
    
        function reducer(state, action) {
            switch (action.type) {
                case "INIT":
                    let { web3, instance, accounts } = action;
                    return {
                        ...state,
                        web3,
                        instance,
                        accounts,
                    }
            }
        }
    
        const [myApple, setMyApple] = useState(0);
        const [state, dispatch] = useReducer(reducer, initialState);
    
        // 구매
        const buyApple = async () => {
            let { instance, accounts, web3 } = state;
    
            await instance.buyApple({
                from: accounts,
                // value: 10000000000000000000, // 10 ETH == 10의 18승, wei 단위
                value: web3.utils.toWei("10", "ether"), // 인자값 1: 숫자, 2: 단위
                gas: 90000,
            });
    
            setMyApple(prev => prev + 1);
        }
    
        // 판매
        const sellApple = async () => {
            let { instance, accounts, web3 } = state;
            await instance.sellApple(web3.utils.toWei("10", "ether"), {
                from: accounts,
                gas: 90000,
            });
    
            setMyApple(0);
        }
    
        // 현재 보유한 사과 개수를 리턴
        const getApple = async (instance) => {
            if (instance == null) return;
            let result = await instance.getMyApple();
    
            setMyApple(result.toNumber()); // toNumber(0) == integer로 변환
        }
    
        // web3 가져와서 메타마스크 연결
        const getweb = async () => {
            const contract = require('@truffle/contract');
    
            let web3 = await getWeb3(); // == const web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545'));
            let fruitshop = contract(FruitshopContract);
    
            fruitshop.setProvider(web3.currentProvider);
    
            let instance = await fruitshop.deployed();
            let accounts = await web3.eth.getAccounts(); // 계정 가져오기
            
            // console.log(web3);
        	// console.log(instance);
            console.log(accounts); // 0xDDf1e0003aFF5DDa63dc400A4bF7FA7e48550604
    
            let initActions = {
                type: 'INIT',
                web3,
                instance,
                accounts: accounts[0],
            }
    
            dispatch(initActions);
            getApple(instance);
        }
    
        useEffect(() => {
            getweb();
        }, []);
    
        return (
            <>
                <div>
                    <h1>사과 가격 : 10 ETH</h1>
                    <button onClick={() => buyApple()}>구매</button>
                    <p>내가 가지고있는 사과 : {myApple}</p>
                    <button onClick={() => sellApple()}>판매 (판매가격은 : {myApple * 10} ETH)</button>
                </div>
            </>
        );
    }
    
    export default App;​​
    console.log(accounts);
    클립보드에 복사 된 내용은 0xDDf1e0003aFF5DDa63dc400A4bF7FA7e48550604 로 console.log(accounts)에 출력 된 내용과 동일함
  24. 작동 확인
    구매 버튼 클릭 시
    확인을 눌러 거래가 성사 되었을 경우 ETH가 10씩 차감 됨, 현재 2회 구매
    거부를 누를 경우 거래는 취소되며 이와 같은 내용이 콘솔에 출력 됨
    판매 버튼 클릭 시 (0이 출력 되는 이유는 value값 설정을 해주지 않아서)
    확인을 눌러 거래가 성사 되었을 경우 구매에 사용했던 ETH가 다시 돌아왔음을 확인