본문 바로가기

카테고리 없음

65강_231004_Blockchain(solidity 컨트랙트 구조, Contract 코드 작성, Truffle - 리엑트 & 트러플을 사용한 카운터 제작)

728x90

 

 

 

 


Solidity 컨트랙트 구조

솔리디티는 이더리움에서 제공하는 스마트 컨트랙트 개발 언어이며, 절차적 프로그래밍 언어이자 컴파일 언어이다.

솔리디티의 스마트 컨트랙트 구조는 라이센스를 나타내주는 식별자솔리디티 버전, 배포하고자 하는 컨트랙트 코드가 있다. 스마트 컨트랙트의 기본적인 구조는 다음과 같다.

// 1. SPDX 라이센스 식별자
// SPDX-License-Identifier: MIT

// 2. Version Pragma
pragma solidity ^0.8.13;

// 3. 배포할 컨트랙트
contract Counter{
	.....
}

SPDX License Identifier

솔리디티 코드를 작성할 때 모든 소스 파일의 최상단에는 라이센스를 나타내는 주석으로 시작한다. 컴파일러는 기계 판독이 가능한 SPDX 라이센스 식별자의 사용을 권장한다. 스마트 컨트랙트의 신뢰성을 보장하고, 저작권 문제를 방지한다.

Pragma

솔리디티 버전을 선언한다. 새로운 컴파일러 버전이 나와도 기존의 코드가 깨지는 것을 방지하며, 향후 컴파일러 버전과 이전 컴파일러 버전 사이에 호환되지 않은 변경 사항이 생기는 것을 차단한다.

Contract

배포할 컨트랙트 코드를 작성한다. 객체 지향 언어의 class와 유사하며, countract의 내부에 상태 변수를 보관한다. 상태변수를 조회 또는 변경을 하기 위한 함수도 포함한다.

 

 

 


Contract 코드 작성

이전에 확인해 본 솔리디티의 컨트랙트 구조에는 배포할 컨트랙트 코드를 작성하는 부분이 있었다. 해당 부분을 작성하기 위한 구조를 파헤쳐보려 한다.

import

외부 파일의 모듈화된 코드를 가져올 수 있는 코드이다. 솔리디티에서는 export할 필요 없이 선언한 constract를 바로 사용할 수 있다. import 코드 작성 방법은 다음과 같다.

import "파일의 경로";

import {"Contract 이름"} from "파일의 경로";

아래 코드는 import 사용 예시이다.

// ----- Counter.sol -------------------------
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Counter{
	.....
    constructor(){}

    function setValue(uint256 _value) public { ..... }

    function getValue() public view returns(uint256) { ..... }
}
// -------------------------------------------

import "./Counter.sol";

contract Counter2{
    Counter counter;    // Counter 컨트랙트의 구조를 가지고 있는 counter 상태변수를 선언
    
    constructor(){
        // Counter 인스턴스를 하나 생성.
        counter = new Counter();
    } 

    function setValue(uint256 _value) public {
        value = _value;
        counter.setValue(_value);
    }
}

 

상태 변수

contract 내부에 선언한 변수이다. 해당 변수는 contract storage에 저장된다.

💡 Storage??
블록체인에 기록되는 영구적인 값이 유지되는 공간이다.

💡 Memory??
프로그램이 동작하는 동안에만 값을 기억한다. 프로그램이 종료되면 해제시키는 데이터 공간이다. 
예) function 등

 

데이터의 타입

상태 변수에는 다양한 데이터 타입이 존재한다. 상태 변수를 선언할 때 타입을 지정해주어야 한다. 상태변수를 선언하는 형식은 ' [타입] [변수명] ' 이다.

bool

  • 참과 거짓의 값을 저장하는 변수이다.
  • 값을 지정해주지 않는다면 기본값은 true로 설정된다.
constract Types {
    bool _bool;
    // 값을 지정해주지 않아 _bool의 값은 'true'이다.
    bool _bool2 = false;
    // 값 false로 선언하였으므로, _bool의 값은 'false'이다.
}

 

int / uint

int

  • 부호가 있는 정수형 변수이다.
  • ' - '가 붙을 수 있어 음수가 될 수 있다.

uint

  • 부호가 없는 정수형 변수이다.
  • ' - '가 붙지 않는 정수형이라 음수가 될 수 없다.
👩‍🏫 int / uint 타입은 데이터 범위를 지정할 수 있다.
작업을 할 때 어떤 코드를 작성하느냐에 따라 효율적으로 데이터 공간을 줄일 수 있다. 자료형(uint 또는 int)뒤에 숫자를 붙이면 메모리 영역의 크기를 지정할 수 있다. 8 ~ 256bit 까지 지원한다. 

예시 )
int8 === -128 ~ 127
uint8 === 0 ~ 255

 

enum

개발자가 사용하면서 가독성을 높이기 위해서 사용하는 자료형이다. 여러 상수 값을 그룹화하여 사용하는 데이터 형식이다. 다음은 enum의 예시이다.

enum Status {
	Pending,    // 0
	Accapted,   // 1
	Rejected,   // 2
}

// status 초기값은 0
Status public status;

// enum의 상태를 조회
function get() public view returns(Status){
    return status;
}
// enum의 상태를 변경
function set(Status _status) public {
    status = _status;
}

Status라는 enum을 정의해 pending, Accepted, Rejected 를 순서대로 상수 0, 1, 2로 정의한다. enum으로 정의한 상수의 값은 고정된 값이므로 변경할 수 없는 값이다.

get 함수로 status를 반환하면, 초기 값은 0으로 지정되어있기 때문에 인 0인 pending의 값을 반환한다.

set 함수로 Status 형식의 매개변수인 _status를 받는다. 이 매개변수를 통해 스마트 컨트랙트의 상태변수인 status의 값을 변경할 수 있다. 만약 set(Status.Accepted)와 같이 함수를 호출하면, status의 값이 Accepted로 변경되어 1인 Accepted를 반환하게 된다. 

 

string

  • 문자열 자료형이다.
string Str = "hello sol";

 

address

  • 주소형 변수이다. 지갑의 주소라고 생각하면 된다.
  • 주소의 크기는 20byte 크기의 자료형이다. 컨트랙트 주소를 저장할 때 사용하는 변수이다.
address sender = 0x0000000000000000000000000000000000060000;
// sender.balance <= 이더의 잔액을 조회할 수 있다.
// sender.transfer("보낼 금액");
// sender.send("보낼 금액");
// transfer, send 메서드를 통해 이더 전송이 가능하다.

 

Array

  • 배열의 타입이다. 
  • 배열의 크기를 선언 시 크기를 지정하면 고정된 크기의 배열을 선언한다. 배열의 크기를 입력하지 않으면 실행 중 변경이 가능한 배열을 선언하게 된다.
// 배열의 크기가 실행중에 변경이 가능하다.
string[] strArr = ["1", "2", "3"];

// 배열의 크기가 선언 시 지정이 된다. 해당 크기와 맞지 않으면 오류.
string[2] strArr2 = ["1", "2"];

 

struct

  • 구조를 정의하는 타입이다.
struct Struct {
	string name;
	uint number;
}

 

mapping

  • 키와 값을 저장할 때 사용하는 데이터 타입이다.
    • Key - Value
mapping (address => uint256) tokens;
tokens {
	address : 10000
}
// adress => key
// uint256 => value


// string이 key이고 mapping(adress => uint256)이 value이다.
// address가 key이고 uint256이 value이다.
// 따라서,
token2{
    string : {
        address : 10000,
        adress2 : 10000
    }
    string2 : {
        address : 10000,
        adress2 : 10000
    }
}

 

global

  • 글로벌 변수. 즉 전역 변수이다.
  • 글로벌한 블록체인 안에 있는 변수이며 블록체인 및 트랜잭션의 속성을 가져올 수 있다.
  • 크게, block, msg, address로 나뉜다.
    • block : 이더리움 블록체인의 정보를 가지고 있는 글로벌 변수이다.
      • coinbase, difficulty, gaslimit. number. timestamp ...
    • msg : 컨트랙트에서 message call 했을 때, 컨트랙트에 전달된 메시지 정보를 갖고 있는 객체이다.
      • sender, value, data, sig ...
    • address
      • balance, transfer, send ...
// 글로벌 변수
// address payable _to => address payable(타입 선언식) _to(매개변수 이름)
function global(address payable _to) public payable {
    // payable : 이더리움을 보낼건지, 결제를 할건지. 결제 처리를 한다는 처리문이다.
    
    // 이더리움 블록체인의 정보를 가지고 있는 글로벌 변수가 있다.
    // block
    block.coinbase;     // 현재 블록을 채굴한 account의 주소
    block.difficulty;   // 현재 블록의 난이도
    block.gaslimit;     // 현재 블록이 사용 가능한 최대 gas값
    block.number;       // 블록의 높이
    block.timestamp;    // 블록 생성 시간
    
    // msg : 컨트랙트에서 message call 했을 때, 컨트랙트에 전달된 메시지 정보를 가지고 있는 객체
    msg.sender;     // 컨트랙트를 호출한 account의 주소
    msg.value;      // 메시지로 전달받은 이더리움의 단위. wei단위로 담겨있다.
    msg.data;       // 컨트랙트 call로 실행할 때 보낸 데이터의 내용
    msg.sig;        // 전달받은 데이터의 첫 4바이트 === 어떤 메소드를 실행시켰는지 확인할 수 있다.
    
    // address
    _to.balance;            // 계정의 잔고
    uint amount = 10**18;
    _to.transfer(amount);   // 이더를 해당 주소에 보냄
    _to.send(amount);       // 이더를 해당 주소에 보냄
}

 

함수의 구조

솔리디티에서 함수의 구조는 일반적으로 다음과 같이 사용된다.

// 함수의 구조
function name(uint a) public view returns (uint) {
  .....  
}

여기서 name은 '함수명'을 의미하며, 함수의 매개변수의 uint는 '매개변수의 타입'을, a는 '매개변수의 이름'을 의미한다. 그렇다면 그 뒤에 위치한 ' public view returns(uint) '는 무엇을 의미할까? 좀 더 자세히 살펴보고자 한다.

 

접근자 타입 (visibility modifier)

앞서 얘기한 함수의 구조에서 public은 접근자의 타입을 의미한다. 접근 제어자는 해당 함수가 어디서 호출될 수 있는지를 제어할 수 있다.

  • public : 외부에서 호출이 가능하다. 외부 컨트랙트나 계정에서 호출이 가능하고 EOA나 CA에서 호출이 가능하다. 따라서, 어디서든지 호출이 가능하다.
  • private : 현재 컨트랙트(contract) 내부에서만 호출이 가능하다.
  • Internal : 현재 컨트택트(contract) 내부의 함수에서는 접근이 가능하다. 외부에서는 직접 접근을 할 순 없지만, 상속을 받아서 호출이 가능하다.
  • External : 자신이 선언된 컨트랙트(contract) 내부에서는 사용이 불가하며, 오직 컨트랙트 외부에서만 호출될 수 있다.

 

접근 지정자 ( 상태 제어자, state modifier)

앞에서 확인한 솔리디티 함수의 일반적인 구조에서 view에 해당한다. 접근 지정자는 상태 변수 접근 부분이나 변경 선언이며, 솔리디티 언어의 특징이다. 

  • view : 읽기 전용 상태변수를 의미한다. 상태 변수를 변경할 수 없다.
  • pure : 상태 변수를 읽을 수도, 변경할 수도 없다. 말 그대로 순수하게 전달받은 매개변수로만 함수를 동작하고 싶은 경우에만 사용한다.
  • payable : 결제를 처리할 수 있다는 선언이다. 이더(ETH)를 전송하는데 선언하지 않으면 요청이 거부된다. (reject)

 

조건문

솔리디티 에러 함수라고도 불리는 require를 설명하고자 한다. 솔리디티의 에러 핸들러는 require 말고도 revert, assert 가 존재한다. 먼저 require를 살펴보고, 추가적으로 revert와 assert를 확인해보고자 한다.

require

  • 주어진 조건을 검사해서 조건에 만족하면 구문을 통과하고, 만족하지 않으면 요청 이전 상태로 돌아간다. (reject 된다.)
  • 작업이 이전 상태로 돌아가기 때문에, 가스비를 반환받게 되어 소비되는 가스비는 없다.
  • if문과 같이 조건 처리를 할 때 사용된다.
require(조건문, (optional)"에러 메시지");
조건문이 잘 통과되면 동작해야 할 구문;

require 사용 예시를 살펴보자. 컨트랙트 배포자가 계약을 파기하고 싶은 경우 아래와 같이 require를 사용할 수 있다.

// 상태변수 sender 선언 시, 접근지정자 payable을 사용해 이더를 거레할 수 있도록 한다.
address payable sender;

// 현재 컨트랙트를 배포한 계정이, sender가 맞을 경우 요청을 진행한다.
require(msg.sender == sender);

// 지갑의 주소를 입력해 배포자에게 CA의 잔액을 전송한다.
selfdestruct(sender);

 


👩‍🏫 selfdestruct

selfdestruct 함수는 블록체인의 스마트 계약에서 컨트랙트를 삭제하고 해당 컨트랙트의 잔액을 매개변수로 입력된 지갑의 주소로 전달한다. selfdestruct 함수는 매개변수로 지갑의 주소나 컨트랙트의 주소(CA)를 받을 수 있다. 

selfdestruct("지갑 주소")	// : 현재 계약을 파기하고, 전달받은 매개변수 주소로 CA의 잔액을 전송한다.
selfdestruct("CA 주소")	// : 계약을 파기하고 전달된 CA에 잔액을 전송할 수 있다.

 

revert

  • 조건없이 에러를 발생기키고 가스비를 환불해준다.
  • revert는 특정한 조건없이 에러를 실행시켜 컨트랙트를 실행하며 발생되었던 가스비를 돌려준다.
revert("에러 메시지");

 

assert

  • 주어진 조건을 검사해서 조건에 만족하면 구문을 통과하고, 만족하지 않으면 계약의 현재 상태를 이전 상태로 되돌린다.
  • 절대로 발생해서는 안되는, 실제로 복구할 수 없는 오류를 검출할 때 사용한다.
  • 논리적으로 복구할 수 없는 오류를 검출하기 때문에 오류가 발생하면 가스비를 소비하고 계약을 중단한다.
assert(조건문);

 

 

 


Truffle

Truffle이란, Dapps 개발을 쉽게할 수 있도록 도와주는 프레임워크이다. 스마트 컨트랙트 컴파일, 배포 테스트 기능을 쉽게 할 수 있도록 도와준다.

트러플을 사용하여 간단한 카운터 트랜잭션을 제작하고자 한다. 프론트로는 react를 사용하고자 한다.

 

#. 폴더의 경로는 다음과 같다.

 

1. 리엑트, truffle을 설치한다.

먼저 VSC 터미널에서 명령어를 입력해 리엑트를 설치한다.

# test라는 폴더에 리엑트 라이브러리를 설치한다.
npx create-react-app test

# test 폴더로 이동한다.
cd test

이렇게 리엑트 라이브러리를 설치하고 해당 폴더로 터미널 경로를 이동한 후, Truffle을 설치한다. 트러플은 리엑트 폴더 밖에다가 설치해도 무관하다. VSC 터미널에 트러플 설치 명령어를 입력한다.

# 트러플 프레임워크를 설치한다.
npm i truffle

# 트러플의 초기세팅을 할 수 있는 폴더와 파일을 생성한다.
npx truffle init

' npx truffle init ' 명령어를 입력하면 트러플의 초기세팅을 위한 파일과 폴더가 설치된다. 'contracts', 'migrations', 'test'파일과 'truffle-config.js'파일이 설치된다. 어떤 역할을 수행할 수 있는 폴더인지 살펴보고자 한다.

  • contracts
    • 솔리디티 코드를 작성한 .sol 파일을 담을 폴더이다.
    • 컴파일을 진행하면 이 폴더에 있는 .sol 파일을 읽어서 컴파일을 진행한다.
    • 컴파일 후, build 폴더가 생기고 컴파일된 내용이 .json 파일로 생성된다.
  • migrations
    • 컨트랙트 배포를 진행할 JS 코드를 생성한다.
    • 이더리움 네트워크에 배포하는 내용을 작성할 JS를 이 폴더에 생성한다.
  • test
    • 테스트 파일을 작성할 폴더이다.
  • truffle-config.js
    • 네트워크 연결정보솔리디티 컴파일 버전 정보 등을 관리한다.

2. contracts/Counter.sol_솔리디티 코드를 작성한다.

Counter 컨트랙트를 생성한다. 카운터되는 값을 담을 uint256자료형의 상태변수 value를 선언한다. value의 접근지시자는 private으로 현재 컨트랙트에서만 사용할 수 있다.

increment 함수는 카운터 값을 증가시키는 함수이다. 접근지시자가 public으로, 어느 곳에서든 호출하여 사용이 가능하다. 함수가 실행되면 value 값이 1 증가한다.

decrement 함수는 카운터 값을 감소시키는 함수이다. increment 함수와 마찬가지로 public 타입으로 호출이 가능한 함수이며, 함수가 실행되면 value 값이 1 감소한다.

getValue 함수는 현재 카운터의 값을 반환하는 함수이다. public 타입으로 어디서든 호출이 가능하며, 상태 제어자가 view로 선언되어 상태변수의 값을 읽을 수만 있다. 반환되는 값은 uint256 타입으로 반환된다.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract Counter{
    uint256 private value;

    // 누르면 1증가 함수
    function increment() public {
        value += 1;
    }
    
    // 누르면 1감소 함수
    function decrement() public {
        value -= 1;
    }

	// 카운터의 값을 반환하는 함수
    function getValue() public view returns(uint256) {
        return value;
    }
}

 

3. test/truffle-config.js_네트워크와 솔리디티 컴파일을 설정한다.

Truffle의 이더리움 스마트 컨트랙트 설정를 작성한다.

networks 객체는 트러플 프로젝트에서 사용할 수 있는 네트워크 구성을 정의한다. 지금 코드에서는 development 네트워크만을 정의하는데 development 네트워크는 로컬 개발 환경을 의미한다. 해당 환경에서 스마트 계약 개발 및 테스트가 가능하다.

host는 스마트 계약을 실행할 이더리움 노드의 호스트 주소를 나타낸다. 지금의 경우엔 '127.0.0.1'로 로컬 노드 환경을 사용한다.

port는 이더리움 노드에 연결할 포트 번호를 나타낸다. 지금의 경우, '8545'를 사용하는데 테스트를 진행할 ganache의 포트번호이다.

network_id는 스마트 계약을 배포할 네트워크 식별자를 설정한다. '*'값은 모든 네트워크에서 사용할 수 있음을 의미한다.

compilers 객체는 스마트 컨트랙트 컴파일 설정을 정의한다. 지금의 코드에서는 솔리디티 컴파일러인 'solc'를 정의하고 있다.

solc 컴파일러는 솔리디티 컴파일 버전이며, 스마트 계약을 컴파일할 때 사용된다. 지금의 경우 '0.8.13' 버전을 사용하고 있으며, 이 버전은 이전에 입력한 솔리디티 코드에서 언급한 버전과 동일해야 한다.

module.exports = {
  networks: {
    development: {
      host: "127.0.0.1",
      port: 8545,
      network_id: "*",
    },
  },

  compilers: {
    solc: {
      version: "0.8.13",
    },
  },
};

 

4. 솔리디티 코드를 컴파일한다.

솔리디티 코드를 컴파일해 build된 파일을 생성하고자 한다. VSC 터미널에서 컴파일 명령어를 입력하여 코드를 컴파일한다.

npx truffle compile

컴파일이 완료되면 build/contracts 폴더가 생성되고 그 안에 Counter.json 파일이 생성된다. 해당 파일에서 abi 배열 부분을 사용할 예정이다.

 

5. Ganache 실행 및 컨트랙트 배포

컨트랙트 배포를 하기 전, 배포자를 설정해야 하기 때문에 가나슈 먼저 실행하도록 한다. VSC 터미널에서 가나슈를 설치 및 실행한다.

# ganache 설치
npm i ganache-cli

# ganache 실행
npx ganache-cli

가나슈를 실행하면 터미널에 실행 결과로 다음과 같은 이더리움 주소와 private Key가 뜬다.

Ganache CLI v6.12.2 (ganache-core: 2.13.2)

Available Accounts
==================
(0) 0xc749895DE43CFee67B987d89f96b189Fa26f0633 (100 ETH)
(1) 0x37Aa989E9c2fDD75Cd1A0B0a451fe03DD36856B6 (100 ETH)
(2) 0x716d99dc9C9372549FE6F88c7f35feDbe128e34d (100 ETH)
(3) 0x79F6a9a396ce843698D90ebbDF5aCB80BD2316b8 (100 ETH)
(4) 0x622Fc42FDF5CA780298B8a0b5CA90b3FD3aC1517 (100 ETH)
(5) 0x1e186C0D7F98F3E9f9a13C35779E82ee9799aCd8 (100 ETH)
(6) 0x008839Bbb1714B3E09455b1a5382b8C5Ea77C90a (100 ETH)
(7) 0xe1A1454B2e2Afd697F57bF9DeC6cf1e839133f36 (100 ETH)
(8) 0x14C911A3525A967b6C2435158a6F9695e5608118 (100 ETH)
(9) 0x90eE99500B7b6cB6B1beff65e8e6299a665BD4c1 (100 ETH)

Private Keys
==================
(0) 0x16e4d5f692f61c47f81efa87587ccb6de86ef0b4b8bc2e016d6f552e94ceb63f
(1) 0x6dcd38ed0a536eaefca4cc8a0a5fd7d177e1fa5708c111e4e8e9b821712161ba
(2) 0xb366ad0d56b1ba66fff0e27552f012d77a78ddda478d012849db98b688f9198c
(3) 0x504277d766af6016fb389af002c8f541bc28ce7520b103bcf8fe4bf7987a2231
(4) 0x18779f4f4b0eee203a86d4e31f1a7a5e88d25843b6ca617ce68c9e8da3e969e4
(5) 0xd6d7d73e0b97290eabe43f0e1d71670a4cfd4052402c16e48f69701d5ec255d2
(6) 0x0c1f4413bdb7e757dc7631fdec0720ff886fdb476b3906cbf57e1d1b61269c1d
(7) 0xf3f80d3668485bc6af06a966476e0cca7cec6bcc441038bd305a9e2ad4714a71
(8) 0xd4258b0777557e32b235bbf2240cf62eb8d158f9799d518c8b66aa1a716c52f9
(9) 0x4642b9e886b5ed23b9e305876c46bcfac303fda584bad10e658e0d1af679a0f6

HD Wallet
==================
Mnemonic:      expect inform object insect lucky drastic bronze vanish bone truly accuse potato
Base HD Path:  m/44'/60'/0'/0/{account_index}

Gas Price
==================
20000000000

Gas Limit
==================
6721975

Call Gas Limit
==================
9007199254740991

Listening on 127.0.0.1:8545

 

이제 컨트랙트 배포를 위해서 migrations 폴더에서 배포 코드를 생성한다. migrations폴더 안에 파일을 생성하는데 생성하는 파일의 이름에는 규칙이 있다. 배포 파일 규칙은 다음과 같다.

// 파일명 규칙
// [번호]_[내용]_[컨트랙트 이름].js

1_deploy_Counter.js

 

이 규칙을 참고해서 배포코드를 작성한다. migrations 폴더 안에 1_deploy_Counter.js 파일을 생성한 후 그 안에 배포코드를 작성했다. 

먼저, 솔리디티 파일을 컴파일한 내용을 담는 객체 Counter를 정의한다. Counter 객체는 'artifacts.require()' 메서드를 사용해, 컴파일된 계약의 내용을 로드한다. 매개변수로는 스마트 컨트랙트의 이름을 입력한다. 이전에 우리가 컴파일한 파일은 build/Counter.json 파일을 살펴보면 "contractName" : "Counter"인것을 확인할 수 있다.

'module.exports'로 내보낼 내용을 정의한다. 매개변수로 'deployer'를 입력했는데, 이 객체는 Truffle에서 배포 관련 도구를 제공하는 객체이다. deployer를 통해 스마트 계약을 배포하고 관리할 수 있다.

'deployer.deploy()' 메서드를 호출해 스마트 컨트랙트를 이더리움 블록체인 네트워크에 배포한다. 해당 메서드를 호출하면 다음과 같은 일이 발생한다.

1. Truffle이 배포할 스마트 계약에 대한 배포 트랜잭션을 생성한다. 이 트랜잭션에는 스마트 계약의 생성자 인수, 코드 및 다른 정보가 포함된다.
2. 생성된 배포 트랜잭션은 이더리움 블록체인 네트워크에 브로드캐스트 된다.
3. 이더리움 블록테인에서 배포 트랜잭션이 마이닝되고 블록에 포함된다.
4. 배포가 완료되면 스마트 계약은 네트워크에 배포되고, 그 결과로 스마트 계약의 주소가 생성된다.

따라서 'deployer.deploy(Counter)'를 작성해, 설정한 트랜잭션 내용을 이더리움 블록체인 네트워크에 배포하는 것이다.

// artifacts : 컴파일한 내용에서 파일을 찾아서 불러온다.
const Counter = artifacts.require("Counter");

module.exports = (deployer) => {
    // deployer : 배포 내용이 포함된 객체를 전달받고, deploy메서드가 해당 컴파일된 내용을 네트워크에 배포를 진행한다.
  deployer.deploy(Counter);
};

 

배포 작업을 수행하기 위해, VSC 터미널에 배포 명령어를 입력한다.

npx truffle migrate

명령어를 입력하면 컴파일된 컨트랙트 내용을 확인하고, 배포가 진행된다. 배포가 완료되면 배포를 위한 일정부분의 가스비가 지불되고, CA(컨트랙트 주소), account(컨트랙트 배포자 주소)등의 정보가 실행결과로 표시된다.

Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.


Starting migrations...
======================
> Network name:    'development'
> Network id:      1696648659800
> Block gas limit: 6721975 (0x6691b7)


1_deploy_Counter.js
===================

   Deploying 'Counter'
   -------------------
   > transaction hash:    0x12e9d6c615e05f71c66142363d1887833dc4ae87a3fd409f134e4a06e104cf4d
   > Blocks: 0            Seconds: 0
   > contract address:    0x678E253B06bDd784f3F81694E7736c79d913011E
   > block number:        1
   > block timestamp:     1696648800
   > account:             0xc749895DE43CFee67B987d89f96b189Fa26f0633
   > balance:             99.99689726
   > gas used:            155137 (0x25e01)
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.00310274 ETH

   > Saving artifacts
   -------------------------------------
   > Total cost:          0.00310274 ETH

Summary
=======
> Total deployments:   1
> Final cost:          0.00310274 ETH

해당 결과물에서 CA를 사용해 call, send 작업을 수행, 원격 프로시저를 호출할 수 있다.

 

6. test/Counter.test.js_테스트 코드 작성 및 실행

테스트 코드는 test 폴더에서 테스트코드를 작성해 수행하지만, 그 전에 truffle 콘솔로도 작성한 솔리디티 파일을 간단하게 테스트 해볼 수 있다. 테스트코드로 코드의 적합성을 확인해야 하는 이유는, 테스트를 위해 개발 과정 중에 트랜잭션을 생성하는 등의 기능을 수행하면 가스비가 그대로 지불되기 때문에, 테스트코드로 코드의 적합성을 확인하는 것이다. 


👩‍🏫 Truffle 콘솔로 call, send를 테스트하자!

VSC 터미널에서 명령어를 입력해, truffle의 콘솔로 접근할 수 있다.

# Truffle 콘솔 접근 명령어
npx truffle console

명령어를 입력하면 터미널 창이 다음과 같이 변경된다.

 

Counter.depolyed() 메서드를 호출해 배포된 Counter 스마트 계약의 인스턴스를 매개변수(instance)로 받는다. '.then()'을 사용해 비동기적으로 인스턴스의 값을 받아 변수 counter를 선언함과 동시에 instance의 값을 담아준다. 변수 counter에는 배포된 컨트랙트의 인스턴스가 담겨있고, call과 send가 메서드로 포함되어 있다.

헤당 코드를 트러플 콘솔이 열린 터미널에 입력한다.

Counter.deployed().then((instance) => (counter = instance));

실행 결과는 다음과 같다.

TruffleContract {
	.....
  increment: [Function (anonymous)] {
    call: [Function (anonymous)],
    sendTransaction: [Function (anonymous)],
    estimateGas: [Function (anonymous)],
    request: [Function (anonymous)]
  },
  decrement: [Function (anonymous)] {
    call: [Function (anonymous)],
    sendTransaction: [Function (anonymous)],
    estimateGas: [Function (anonymous)],
    request: [Function (anonymous)]
  },
  getValue: [Function (anonymous)] {
    call: [Function (anonymous)],
    sendTransaction: [Function (anonymous)],
    estimateGas: [Function (anonymous)],
    request: [Function (anonymous)]
  },
  sendTransaction: [Function (anonymous)],
  estimateGas: [Function (anonymous)],
  call: [Function (anonymous)],
  send: [Function (anonymous)],
  allEvents: [Function (anonymous)],
  getPastEvents: [Function (anonymous)]
}

 

call 요청을 보내보도록 하자. 터미널에서 getValue 함수를 호출해, 스마트 계약의 상태를 읽어온다. 

counter.getValue();

결과 값이다. BN객체는 매우 큰 숫자를 명시한다. 특히 블록체인과 같은 분산 원장 기술에서 자주 사용된다. '[ 0, <1 empty item> ]'는 스마트 계약에서 반환된 숫자를 나타낸다. 배열의 첫 번째 요소인 '0'은 BN객체나 나타내는 숫자의 값이다. 반환된 값이 0임을 나타낸다. 두 번째 요소인 '<1 empty item>'는 배열 내의 요소의 개수를 나타낸다. 요소가 하나만 있음을 의미한다.

BN { negative: 0, words: [ 0, <1 empty item> ], length: 1, red: null }

 

send 요청을 보내보도록 하자. 터미널에서 setValue 함수를 호출해, 스마트 계약의 상태를 변경한다.

(setValue는 위에 설명한 솔리디티 코드에는 작성되지 않은 함수이다. setValue 함수가 다음과 같이 솔리디티 코드로 정의되어 있다는 가정하에 테스트를 진행하도록 한다.)

counter.setValue 함수

counter.setValue(20);

실행 결과이다. send 요청을 보내기 때문에 트랜잭션이 생성되고 이더리움 블록체인에 브로드캐스트된다. 결과로 나온 정보에 트랜잭션의 해시(tx)와 트랜잭션 리셉트(receipt)가 포함된다.

  tx: '0xec2e2be4aa1cf39fa5703d15c20eaf6b64f679e19dbe63fc80e203530a1418ac',
  receipt: {
    transactionHash: '0xec2e2be4aa1cf39fa5703d15c20eaf6b64f679e19dbe63fc80e203530a1418ac',
    transactionIndex: 0,
    blockHash: '0xae61e83a8c269f3d51c75050c8d56683975cd71a3d459f16c44116e22f759a26',
    blockNumber: 2,
    from: '0xee41fad846200a51ca27edd9b760ca949eb4f5fb',
    to: '0xa7314bd3e8df84cfbe95b775167564b163e44cee',
    gasUsed: 41624,
    cumulativeGasUsed: 41624,
    contractAddress: null,
    logs: [],
    status: true,
    logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
    rawLogs: []
  },
  logs: []

 

setValue로 값이 변경된 내용을 확인하기 위해서 다시 'counter.getValue()'를 입력하면 다음과 같이 '[ 20, <1 empty item> ]'로 값이 변경된 것을 확인할 수 있다.

BN { negative: 0, words: [ 20, <1 empty item> ], length: 1, red: null }

 

이제 test 폴더에 테스트 코드를 작성하여 솔리디티 파일을 테스트하고자 한다. 방금 전 콘솔로 한 테스트와 마찬가지로 counter.setValue가 설정되어 있다는 가정하에 테스트를 진행한다. 

counter.setValue 함수

test/Counter.test.js 파일을 생성해 테스트 코드를 작성했다. 

Counter는 'artifacts.require()' 메서드를 사용하여 스마트 계약 'Counter'를 Javascript 객체로 불러온 값을 담는다.

contract는 테스트 케이스를 정의하기 위한 최상위 구조로, 이 함수는 두 개의 매개변수를 받는다.

첫 번째 매개변수
- 테스트 케이스의 이름을 나타내는 문자열을 받는다. 이 값으로 테스트 케이스를 식별할 수 있다.
두 번째 매개변수
- 콜백 함수를 받는다. 테스트 케이스의 실제 테스트 코드가 포함되는 값이다.

contract의 첫 번째 매개변수로 'Counter'를 받아, 테스트 케이스의 이름을 Counter로 설정하고, 두 번째 매개변수로 실제 테스트 코드가 진행될 콜백함수를 입력했다. 이 콜백함수의 매개변수로 'account'를 입력했는데 Truffle에서 자동으로 제공해주는 기능으로, 테스트 케이스에서 사용할 수 있는 계정의 목록들이 매개변수로 들어올 수 있는 것이다.

describe로 테스트 그룹 단위를 지정한다. it 으로 선언한 'counter 1' 테스트 단위에서 스마트 계약을 배포한 내용을 테스트 그룹에서 선언한 counter 객체에 담는다.

'counter 2' 테스트 단위에서 스마트 계약(counter)의 getValue를 호출하고 그 결과값을 콘솔로 출력한다.

'counter 2' 테스트 단위는 스마트 계약(counter)의 setValue를 호출해 계역의 상태를 변경한다. 변경된 상태값을 확인하기 위해 콘솔로 getValue함수를 호출해 현재 계약의 상태 값을 불러온다.

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

// contract : 테스트 케이스를 정의하기 위한 최상위 구조
contract("Counter", (account) => {
  // account : 네트워크에 있는 계정들이 매개변수로 들어온다.
  console.log(account);
  // describe : 테스트 그룹 단위
  describe("counter contract", () => {
    let counter;
    // it : 테스트 단위
    it("counter 1", async () => {
      // 테스트 내용
      counter = await Counter.deployed();
    });

    it("counter 2", async () => {
      console.log(await counter.getValue.call());
    });

    it("counter 3", async () => {
      await counter.setValue(20);
      console.log(await counter.getValue.call());
    });
  });
});

 

작성한 테스트 코드를 실행하기 위해서 VSC 터미널에 명령어를 입력한다.

npx truffle test

실행 결과는 다음과 같다.

Using network 'development'.


Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.
[
  '0xeE41fad846200a51ca27eDD9b760Ca949eb4f5fB',
  '0x6D0387dE1Bc1EAF61c003F8706530975F0A6F031',
  '0xCD771ceA03c56E723C06654f574e03EDF7d8DC41',
  '0xBC903032f7e1de34b285cAabEF330B5428A7C2Df',
  '0x5a103E3D0FaE640D0b3f4c7305d1c9C982B5cD18',
  '0xf21336AD1AaD23471ae2e96f45EC9f649eB8D054',
  '0xAfE0E9b9Ad301448d7dbF3D94FeD48544bEDa365',
  '0xeE068dF741cA1113C6a4eca1eaaBC5a9e6791E58',
  '0x81660A66F896B8f7487745C5d285D5b4340980bb',
  '0xD6c1dBB3C909251A82F43F6ab5a31b7950665A51'
]


  Contract: Counter
    counter contract
      ✔ counter 1
# console.log()로 확인한 값
BN { negative: 0, words: [ 0, <1 empty item> ], length: 1, red: null }
      ✔ counter 2
# console.log()로 확인한 값
BN { negative: 0, words: [ 20, <1 empty item> ], length: 1, red: null }
      ✔ counter 3 (39ms)


  3 passing (69ms)

 

이제 실제 카운터 기능을 수행하는 코드를 작성하고자 한다. 이전에 솔리디티 파일에서 작성한 함수를 사용하는 코드를 작성하려 한다.

7. src/abi/Counter.json_컴파일된 파일에서 abi를 추출한다.

클라이언트 애플리케이션에서 스마트 계약과 상호작용하기 위해서 계약의 abi가 필요하다. abi는 스마트 걔약의 함수와 이벤트에 대한 정보를 제공하는 json 형식의 데이터로, 클라이언트는 이 데이터를 사용하여 스마트 계약의 함수를 호출하고 이벤트를 수신할 수 있기 때문이다.

컴파일된 json의 파일에서 abi를 추출하는 방법은 간단하다. 'build/contracts/Counter.json' 파일에서 abi 배열을 복사해 'src/abi/Counter.json' 파일을 생성한 후, 붙여넣기만 하면 된다.

다음은 'src/abi/Counter.json' 파일의 데이터이다.

[
  {
    "inputs": [],
    "name": "increment",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "decrement",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "getValue",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function",
    "constant": true
  }
]

 

8.src/hooks/web3.hook.js_web3 라이브러리 사용과 메타마스크 연동등을 모듈화하는 커스텀 훅을 생성한다.

리엑트와 이더리움의 상호작용을 관리하기 위한 커스텀 훅 'useWeb3'를 생성한다. 커스텀 훅을 생성할 때는 해당 모둘이 커스텀훅임을 명시하기 위해 훅 이름 앞에 'use'를 붙여준다. (암묵적인 룰)

블록체인과 상호작용하기 위해서는 web3 라이브러리를 사용해야 하므로, VSC 터미널에 명령어를 입력해 web3 라이브러리를 설치받는다.

npm i web3

 

useWeb3에서 현재 접속한 메타마스크 지갑의 정보를 담을 변수 user와 네트워크에 연결한 web3 인스턴스를 담을 변수 web3를 useState()상태변수로 선언한다. 변수 user의 account는 지갑의 주소를 담고, balance는 지갑의 잔액을 wei단위로 받는다.

useEffect로 컴포넌트가 마운트될 때, 메타마스크로부터 로그인을 요청해 사용자 계정을 불러오고, web3 인스턴스를 생성한다. 전달받은 사용자 정보로 setUser() 함수를 사용해 사용자 정보를 설정한다.

 

 

 

 

 

 

 

 


👩‍🏫 만약, 컴파일과 배포를 진행한 후에 sol 파일을 수정했다면??

우리가 블록체인 개발 과정 중에서 솔리디티 파일의 컴파일과 배포를 진행한 후에 기능적인 문제로 솔리디티 파일을 수정해야하는 경우는 매우 흔하게 발생하는 일이다. 그럴 땐 수정한 sol파일을 다시 컴파일 한 뒤, abi파일을 수정한 후, 다시 배포를 작업하여 수정된 CA의 주소를 코드 상에서 변경해주면 된다! 

작업할 내용을 확인하기 쉽게 정리를 하자면, 

1. sol파일을 수정한다.
2. VSC 터미널에서 'npx truffle compile'을 입력해 컴파일 한다.
3. 컴파일된 파일(build 폴더의 json파일)에서 abi 부분을 복사해, 'src/abi' 폴더의 json 파일에 복사한 내용을 붙여넣어 수정한다.
4. VSC 터미널에서 'npx truffle migrate'을 입력해 배포 한다.
5. 실행결과에서 CA(contract address)의 결과 값을 복사해 작업이 트랜잭션 작업이 수행되는 곳에 입력되어있는 CA의 값을 수정한다.

 

 

 

 

 

728x90