본문 바로가기

BlockChain Developer/Public Blockchain

[Ethereum] Geth Dapp (with Node.js & React.js) 구축하기

React js에서, Nodejs에서 각각 사용법을 분할해 적었으므로, 둘 중 한가지만 진행하면 됩니다.

 

1. puppeth를 통한 제네시스 블록 생성하기.

 

genesis.json 파일 생성을 원하는 디렉토리안에서 puppeth를 실행시켜 제네시스 파일을 만든다.

cd mydirectory
puppeth //기본적으로 geth를 설치했다면 puppeth가 존재한다.

 

puppeth를 통한 genesisblock 생성결과화면

 

2. mynetwork.json 파일을 초기화(Init)하자

geth --datadir . init mynetwork.json

 

geth 폴더가 생성되면서, 체인 데이터가 저장될 디렉토리를 생성해준다.

그럼 총 2개의 폴더가 생기는데 각 폴더의 용도는 다음과 같다.

 

geth = 체인 데이터 저장

keystore = 계정데이터 저장

 

3. Geth 실행

geth --datadir . --rpc --rpcapi "web3,eth,net" --rpccorsdomain "*" --allow-insecure-unlock

실행화면

 

이런 화면이 뜬다면, Geth서버가 열린것이다. 이제 새 터미널을 열어 geth console에 접속해보자.

 

3. 새 터미널을 열어 geth console에 접속해보자.

위 geth 실행화면의 결과창에서 자신만의 ipc endpoint를 찾는다.

그리고 새 터미널을 열고 아래 명령어를 수행해 콘솔을 연다.

 

//geth attach (복사한 ipc endpoint)
geth attach /Users/handonglee/Desktop/mydapp/block/geth.ipc

 

 

4. 콘솔에서 명령어 수행하기 (어카운트 생성 && 마이닝)

**단 모든 명령어의 앞 web3는 생략이 가능하다.

//콘솔명령어

web3.personal.newAccount(); = 새 계정생성 (실행하면 비밀번호 입력하라 나옴)

web3.eth.accounts = 계정확인

web3.eth.accounts[0] = 계정 확인

web3.eth.coinbase = 코인베이스 계정확인

<만약, 코인베이스 계정이 존재하지 않는다면? miner.setEtherbase(eth.accounts[1]) 명령어를 통해 설정이 가능하다.>

web3.eth.getBalance(eth.accounts[0]) = 0번째 계정(1번째  계정)의 잔고확인

miner.start(1) = mining을 시작한다. ()안의 수는 가동할 쓰레드의 갯수다.

<miner.start 명령어 사용시 null을 반환하게 된다. 이런경우, eth.mining으로 채굴여부를 확인이 가능하다.>

 

 

5-1. Nodejs에 Web3 연결하기 (Step 5에서 택1)

 

1. 기본적인 express 서버 설정 및 web3.js파일을 생성했다.

(getBalance는 테스트코드로, 첫번째 인자로 주소값을 가지고, 두번째 인자로 콜백함수를 가진다.)

index.js

2. web3.js에 다음과 같이 작성해서 위에서 구동한 Geth서버와 붙여준다.

web3.js

 

5-2. React.js 와 연결하기 (Step 5에서 택1)

 

Reactjs에서는 window객체가 있어 다음과 같이 메타마스크로 연결할 수 있다.

ComponentDidMount부분에서 아래 코드를 붙여넣으면 페이지에 접속했을 때 메타마스크와 연결될 것이다.

    if (typeof window.ethereum !== 'undefined') {
      window.web3 = new Web3(window.web3.currentProvider);
      try {
        await window.ethereum.enable();
        console.log(`✅ Connected Properly`)
      } catch (err) {
        console.log(`❌ ETH NONO!`,err)
      }
    } else {
      console.log("no !!!!!")
    }

 

6. Web 3와 연결했다면, 이제 컨트렉트를 만들어야한다. Truffle을 구동하자.

 

아래 명령어를 수행했다면, truffle로인해 contract & migration & test & truffleconfig.js 파일 및 폴더가 생성된다.

 

contract 폴더는 Solidity 스마트컨트렉트 코드를 작성하는 곳이며,

migration 폴더는 스마트컨트렉트 배포코드이며,

test는 말그대로 테스트 코드 작성 폴더이며,

truffle-config파일은 truffle 설정파일이다.

truffle init

 

 

7.  스마트컨트렉트 배포하기 (Student)

1. Contract 폴더에 Student.sol파일을 만들고 간단한 학생 등록, 및 Read해오는 스마트 컨트렉트를 작성한다.

pragma solidity >=0.4.22 <0.9.0;

contract Student {
    
    struct Student {
        string name;
        uint age;
    }

    mapping(uint256 => Student) studentInfo;

    function setStudentInfo(uint256 _studentId, string memory name, uint age) public {
        Student storage student = studentInfo[_studentId];
        student.name = name;
        student.age = age;
    }

    function getStudentInfo(uint256 _studentId) public view returns (string memory, uint) {
        return (studentInfo[_studentId].name, studentInfo[_studentId].age);
    }

}

 

2. Migration폴더에 2_deploy_student.js 파일을 생성해 아래 코드를 넣는다.

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

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

 

3.truffle-config 파일로 이동해, network부분의 development 객체를 통해 배포를 진행할 예정이므로 설정해주자.

development 객체 부분의 주석을  풀고 아래 명령어를 넣어보자.

truffle migrate --network development

 

아래와 같은 에러가 날 것이다 !

 

"Migrations" -- Returned error: authentication needed: password or unlock."

(코인베이스 계정의 암호 때문에 컨트렉트 배포가 안된다는 의미다.)

 

다음 명령어를 통해 에러를 해결해주자.

personal.unlockAccount(web3.eth.coinbase, "1234")

 

다시 첫번째 명령어를 통해 재배포 해본다. 아, 이번엔 될것같이 잘 돌아가다가 에러가 난다. (안나면 그냥 해도된다)

가스비가 초과했다는 에러다. truffle config파일의 development 부분에 gas를 추가해주자.

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

이제 정상적으로 migration이 될 것이다.

 

 

8. 배포한 스마트 컨트렉트 사용하기

스마트컨트렉트를 정상적으로 배포했다면 아래와같은 deploy부분이 보일 것이다. 이 부분 중, contract address부분을 가져온다.

또한, build라는 폴더가 생성되었을텐데 폴더 안에 들어가 우리가 만든 Student.json파일에 들어가 abi를 가져오고 저장해둔다.

 

이제 가져온 abi 와 contract address를 통해 스마트컨트렉트를 가져오자. 일단 변수로 할당한다.

 

 

# Node.js에서는,

const StudentContract = new web3.eth.Contract(ContractABI, ContractAddress);

# React.js에서는,

const StudentContract = new window.web3.eth.Contract(student_contract_ABI, student_contract_ADRS)

 

위와 같이 연결해서 사용할 수 있다. 리액트에서는 State값으로 할당해두고 쓰길 권장한다. 자꾸 안그러면 사라진다 ^오^.. 아래처럼..

setStudentContract(new window.web3.eth.Contract(student_contract_ABI, student_contract_ADRS));

 

그럼 이제 다음 명령어를 통해, 컨트렉트를 실행할 수 있다.

 

1. setStudentInfo Contract 호출 (Send방식 = 데이터에 변동이 생기는 방식으로 from옵션이 최소한 반드시 필요하다)

StudentContract.methods.setStudentInfo(1, "Blockmonkey", 29).send({"from": "0x1e4ceafbfded02b9d607c89485f94a58819025d1" })

 

2. getStudentInfo 호출 (Call방식 = 단순히 읽어오는 것으로, 별다른 옵션이 필요없다)

StudentContract.methods.getStudentInfo(2).call()

위 코드를 통해 실행하고 , promise처리가 반드시 필요하다.

필자는 .then을 통해 했다.

 

 

9. 전체코드 Nodejs

const express = require("express");
const app = express();
const PORT = 5000;
const web3 = require("./web3");
const ContractAddress = "0xBED998843D2BfAaeD28508e8D6983486F638c267";
const ContractABI = [
    {
      "constant": false,
      "inputs": [
        {
          "internalType": "uint256",
          "name": "_studentId",
          "type": "uint256"
        },
        {
          "internalType": "string",
          "name": "name",
          "type": "string"
        },
        {
          "internalType": "uint256",
          "name": "age",
          "type": "uint256"
        }
      ],
      "name": "setStudentInfo",
      "outputs": [],
      "payable": false,
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "constant": true,
      "inputs": [
        {
          "internalType": "uint256",
          "name": "_studentId",
          "type": "uint256"
        }
      ],
      "name": "getStudentInfo",
      "outputs": [
        {
          "internalType": "string",
          "name": "",
          "type": "string"
        },
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "payable": false,
      "stateMutability": "view",
      "type": "function"
    }
  ];
const StudentContract = new web3.eth.Contract(ContractABI, ContractAddress);


app.get("/", (req, res)=> {
    // web3.eth.getAccounts().then(user => {
    //     console.log(user[0]);
    //     StudentContract.methods.setStudentInfo(2, "Minseo", 24).send({"from": user[0] })
    //         .then(result => {
    //             console.log(result);
    //         });
    // });

    StudentContract.methods.getStudentInfo(2).call()
        .then(result => {
            console.log(result);
        })
    
    res.send("Success!");
});

app.listen(PORT, ()=> console.log(`✅ Server is Running At : ${PORT}`));

 

 

10. 전체 코드 React

import React, { useEffect, useState } from "react";
import Web3 from "web3";

function App(props) {
  let student_contract_ADRS = "0x8Def2096F8cfBd0C2d7FAA58eE3c3d595A47Dca3";
  let student_contract_ABI = [
    {
      "constant": false,
      "inputs": [
        {
          "internalType": "uint256",
          "name": "_studentId",
          "type": "uint256"
        },
        {
          "internalType": "string",
          "name": "name",
          "type": "string"
        },
        {
          "internalType": "uint256",
          "name": "age",
          "type": "uint256"
        }
      ],
      "name": "setStudentInfo",
      "outputs": [],
      "payable": false,
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "constant": true,
      "inputs": [
        {
          "internalType": "uint256",
          "name": "_studentId",
          "type": "uint256"
        }
      ],
      "name": "getStudentInfo",
      "outputs": [
        {
          "internalType": "string",
          "name": "",
          "type": "string"
        },
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "payable": false,
      "stateMutability": "view",
      "type": "function"
    }
  ]

  const [StudentContract, setStudentContract] = useState({});
  const [User, setUser] = useState("");
  const [View_Id, setView_Id] = useState(0);
  const [Add_Id, setAdd_Id] = useState(0);
  const [Add_Name, setAdd_Name] = useState("");
  const [Add_Age, setAdd_Age] = useState(0);
  const [Balance, setBalance] = useState(0);

  useEffect(() => {
    initWeb3();
  }, [])

  const initWeb3 = async () => {
    if (typeof window.ethereum !== 'undefined') {
      window.web3 = new Web3(window.web3.currentProvider);
      try {
        await window.ethereum.enable();
        console.log(`✅ Connected Properly`)
      } catch (err) {
        console.log(`❌ ETH NONO!`,err)
      }
    } else {
      console.log("no !!!!!")
    }

    window.web3.eth.getAccounts().then(res => {
      console.log(`현재 사용자 : ${res[0]}`);
      setUser(res[0]);
    });

    console.log("CP:", window.web3.currentProvider);




    // //가장 중요한.. 연결!..
    // if (typeof window.ethereum !== 'undefined') {
    //   window.web3 = new Web3(window.ethereum);
    //   try {
    //     await window.ethereum.enable();
    //     console.log(`✅ Connected Properly`)
    //   } catch (err) {
    //     console.log(`❌ ETH NONO!`,err)
    //   }
    // } else {
    //   console.log("no !!!!!")
    // }

    // //현재 사용자의 Account를 메타마스크로부터 가져온 뒤, User State값에 저장한다.
    // window.web3.eth.getAccounts().then(res => {
    //   setUser(res[0]);
    //   console.log(`사용자 주소: `, res[0]);
    // })

    //컨트렉트를 가져오고, StudentContract State값에 저장한다.
    setStudentContract(new window.web3.eth.Contract(student_contract_ABI, student_contract_ADRS));
  };


  //스마트 컨트렉트 호출
  //call(단순호출) & send(값 변경);
  const setStudentInfo = () => {
    StudentContract.methods.setStudentInfo(Add_Id, Add_Name, Add_Age).send({ "from": User })
      .then(result => {
        console.log(result);
        //칸 초기화
        setAdd_Id(0);
        setAdd_Name("");
        setAdd_Age(0);
      })
  };

  const getStudentInfo = () => {
    StudentContract.methods.getStudentInfo(View_Id).call()
      .then(result => {
        console.log(result);

        //칸 초기화
        setView_Id(0);
    })
  };

  const handleViewStudent_Id = (e) => {
    setView_Id(e.currentTarget.value)
  }

  const handleAddStudent_Id = (e) => {
    setAdd_Id(e.currentTarget.value);
  }
  const handleAddStudent_Name = (e) => {
    setAdd_Name(e.currentTarget.value);
  }
  const handleAddStudent_Age = (e) => {
    setAdd_Age(e.currentTarget.value);
  }

  const getBalance = () => {
    window.web3.eth.getBalance(User, function(err, wei){
      setBalance(window.web3.utils.fromWei(wei, "ether"));
    })
  }

  return (
    <div className="App">
      <div>
        <button onClick={getBalance}>이더잔고 가져오기</button>
        <div>{Balance} ETH</div>
      </div>
      
      
      <h1>학생조회서비스</h1>
      <div>
        <label>ID</label>
        <input type="number" placeholder="Student_ID" value={View_Id} onChange={handleViewStudent_Id} />
        <button onClick={getStudentInfo}>학생보기</button>
      </div>

      <h1>학생추가서비스</h1>
      <div>
        <div>
          <label>ID</label>
          <input type="number" placeholder="ID" value={Add_Id} onChange={handleAddStudent_Id} />
        </div>

        <div>
          <label>Name</label>
          <input type="text" placeholder="Name" value={Add_Name} onChange={handleAddStudent_Name} />
        </div>

        <div>
          <label>Age</label>
          <input type="number" placeholder="Age" value={Add_Age} onChange={handleAddStudent_Age} />
        </div>
        
        <button onClick={setStudentInfo}>학생추가하기</button>
      </div>
      
    </div>
  );
}

export default App;