세상을 바꿀 Web 3.0 - Web3.js 예제로 실습하기(1)

Jeenie·2022년 8월 22일
6

세상을 바꿀 web 3.0

목록 보기
3/7
post-thumbnail
post-custom-banner

이 포스트는 Cryptozombies의 레슨을 따라하며 정리한 글입니다.

기초부터 차근차근 따라해보자.
크립토좀비 프로젝트를 진행하면서 개념들을 함께 정리하면, 모호했던 개념 이해에 도움이 될 듯하다.

1. 시작하기

우선 프로젝트를 하나 생성한다.

mkdir web3-project
cd web3-react
code .

vscode로 해당 프로젝트를 열고,
index.html 파일을 만든다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>CryptoZombies front-end</title>
    <script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
  </head>
  <body>

  </body>
</html>

이제 web3.js를 설치해보자.

// NPM을 사용할 때
npm install web3

// Yarn을 사용할 때
yarn add web3

나는 yarn을 사용해서 아래와 같이 설치했다.

2. web3 Provider

2-1. Web3 Provider가 왜 필요할까?


출처 : web3.js 마이크로소프즈웨어 393호 발췌 (web3.js 구조도)

이 그림이 아주 잘 설명하고 있다!

이더리움은 똑같은 데이터의 복사본을 공유하는 노드들로 구성되어 있다.
Web3.js에서 Web3 Provider를 설정하는 것은,
우리 코드에 읽기와 쓰기를 처리하려면 어떤 노드와 통신을 해야하는지 설정하는 것이다.

이것은 전통적인 클라이언트-서버 웹앱에서 API 호출을 위한 웹 서버의 URL을 설정하는 것과 같다.

나만의 이더리움 노드를 Provider로 운영할 수도 있다.
하지만 더 편리한 서비스가 있다. 바로 Infura!
Infura를 사용하면, 내 DApp 사용자들을 위해 나만의 이더리움 노드를 운영할 필요가 없다.

2-2. Infura

캐시 계층을 포함(빠른 읽기를 위함)한 다수의 이더리움 노드를 운영하는 서비스.
접근을 위한 API를 무료로 사용할 수 있다.
Infura를 Provider로 사용하면, 나만의 이더리움을 설치하고 유지할 필요 없이 이더리움 블록체인과 메세지를 확실히 주고받을 수 있다.

var web3 = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"));

이렇게 web3 provider를 생성할 때 infura를 쓰도록 설정할 수 있다.

하지만,
나의 DApp에 방문하는 많은 사용자들은 단순히 공개 키로 읽기만 하지 않을 것이다.
블록체인에 무언가를 쓰기도 할 것이기 때문에,
이 사용자들이 그들의 개인 키로 트랜잭션에 서명할 수 있도록 해야한다.

이더리움(일반적인 블록체인)은 트랜잭션전자 서명을 하기 위해 공개/개인 키 쌍을 사용한다.
이런 방식으로 만약 블록체인에서 어떤 데이터를 변경하면,
나의 공개 키를 통해 내가 거기 서명을 한 사람이라고 증명 할 수 있다.
하지만 아무도 내 개인 키를 모르기 때문에, 내 트랜잭션을 누구도 위조할 수 없는 것.

Q. 나는 아직 이 방식이 많이 헷갈린다........
개인키와 공개키, 트랜잭션의 검증(비대칭 암호화 방식)을 참조해서 조금 더 이해하자

Web3.givenProvider가 null이면 원격/로컬 노드에 연결해야한다.

또는 메인넷에 연결하려면, 아래와 같이 직접 연결하면 된다.

var web3 = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"));

하지만 이렇게 사용자들의 개인키를 관리하려하는 것은 정말 좋지 않다.

우리는 암호학 전문가도, 보안 전문가도 아니기 때문에....
다행히 사용자들이 이더리움 계정과 개인키를 안전하게 관리할 수 있게 해주는 서비스가 있다!

바로 메타마스크(MetaMask)

2-3. MetaMask

메타마스크는 사용자들이 이더리움 계정과 개인키를 안전하게 관리할 수 있게 해주는,
크롬/파이어폭스 브라우저 확장 프로그램이다.
해당 계정으로 Web3.js를 사용하는 웹사이트들과 상호작용을 할 수 있게 해준다!

DApp을 메타마스크와 호환시킨다는 것은,
사용자들이 웹 브라우저를 통해 내 DApp과 상호작용할 수 있다는 것.

메타마스크는 내부적으로 Infura의 서버를 Web3 프로바이더로 사용한다.
사용자들이 그들만의 Web3 프로바이더를 선택할 수 있는 옵션 또한 있다.
따라서 메타마스크의 Web3 Provider를 사용하도록 하자.

메타마스크는 web3 라는 전역 자바스크립트 객체를 통해,
브라우저에 Web3 Provider를 주입한다.

따라서 내 앱에서는 web3 객체가 존재하는지 확인하고,
존재한다면 web3.currentProvider를 Provider로 사용하면 되는 것.

메타마스크가 제공하는 템플릿 코드

사용자가 메타마스크를 설치했는지 확인하고,
설치가 안되었다면 메타마스크를 설치해야한다고 알려주는 코드다.

이로써 메타마스크의 Web3 프로바이더로 Web3.js를 초기화된다.

window.addEventListener('load', function() {

  // Web3가 브라우저에 주입되었는지 확인(Mist/MetaMask)
  if (typeof web3 !== 'undefined') {
    // Mist/MetaMask의 프로바이더 사용
    web3js = new Web3(web3.currentProvider);
  } else {
    // 사용자가 Metamask를 설치하지 않은 경우에 대해 처리
    // 사용자들에게 Metamask를 설치하라는 등의 메세지를 보여줄 것
  }

  // 앱을 시작하고 web3에 자유롭게 접근할 수 있다
  startApp()

})

미스트(Mist)란?
메타마스크 말고도 사용자들이 쓸 수 있는 다른 개인 키 관리 웹브라우저.
이또한 web3 변수를 주입하는 동일한 형태를 사용한다.
그러니 사용자들이 다른 프로그램을 쓰더라도 여기서 설명하는 방식으로 사용자의 Web3 프로바이더를 인식할 수 있다.

3. 컨트랙트와 통신하기

이제 메타마스크의 Web3 프로바이더로 Web3.js를 초기화했으니,
Web3.js가 우리의 스마트 컨트랙트와 통신할 수 있도록 만들어보자.

Web3.js와 스마트 컨트랙트의 통신을 위해서는,
컨트랙트의 주소ABI가 필요하다.

ABI(Application Binary Interface)란?
기본적으로 JSON 형태로 컨트랙트의 메소드를 표현하는 것.
어떤 형태로 함수 호출을 해야 컨트랙트가 이해할 수 있는지 알려준다.

스마트 컨트랙트를 모두 작성하고 컴파일 한 후에 이더리움에 배포한다.

컨트랙트를 배포 하면, 해당 컨트랙트는 이더리움 상에 고정된 영원히 존재하는 주소 를 얻는다.
이더리움에 배포하기 위해 컨트랙트를 컴파일할 때, 솔리디티 컴파일러가 ABI를 준다.
그 ABI와, 배포로 얻은 컨트랙트 주소를 복사해서 따로 저장해둔다.

다음 포스트에서 다룰 배포순서 미리보기 ✨

  • Contract 배포 순서
  1. Contract 작성
  2. 컴파일
  3. Contract 객체 생성(ABI포함) -> 다음 포스트에서 다룰 것
  4. Contract 인스턴스를 이용해 deploy 생성 후 send
  • Contract 사용 순서
  1. Contract 객체 생성(ABI, 배포된 Contract Address)
  2. Method 호출

스마트 컨트랙트 배포를 해야 스마트 컨트랙트 주소가 생성된다.
배포하기 위해 컨트랙트를 컴파일하면 솔리디티 컴파일러가 ABI를 준다.
이 과정은 스마트 컨트랙트 객체를 실제로 블락 내용에 포함시켜서, 채굴되어 블락체인에 포함되도록 하는 과정이다.

3-1. Web3.js 컨트랙트 인스턴스화하기

컨트랙트의 주소ABI를 얻으면 Web3에서 인스턴스화할 수 있다.

3-1-1. 인스턴스화를 왜 하는걸까?

인스턴스란?
추상적인 개념인 클래스에서 실제 객체를 생성하는 일

지금 스마트 컨트랙스 객체는, 클래스와 같이 아직 실제 데이터에 접근할 수 있는 데이터가 없는 상태.
(왜인지는 다음 포스트에서 배포를 진행하면 이해할 수 있다)

그 객체로부터 하나의 인스턴스(실제 객체)를 만드는 것.

인스턴스 형태에 대한 자세한 내용은 [JavaScript] - 객체와 인스턴스, 붕어빵 틀과 슈크림붕어빵에서
나는 인스턴스가 확실히 뭔지도 몰랐다!

// myContract 인스턴스화
var myContract = new web3js.eth.Contract(myABI, myContractAddress);

솔리디티 컴파일러가 준 ABI컨트랙트 지갑 주소를 넣어서 Contract를 인스턴스화 한다.

    var cryptoZombies;

      function startApp() {
        var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
        // 메인넷에서의 내 컨트랙트 주소
        cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);
      }

      window.addEventListener('load', function() {
        if (typeof web3 !== 'undefined') {
          web3js = new Web3(web3.currentProvider);
        } else {
        }
        startApp()
      })

앱을 실행시키는 함수를 만들고,
cryptoZombies라는 변수에 인스턴스화된 컨트랙트를 할당한다.

이제 컨트랙트 설정이 모두 끝났다!
Web3.js로 컨트랙트와 통신할 수 있게 되었다.

4. 컨트랙트 함수 호출하기

Web3.js는 컨트랙트 함수를 호출하기 위해 우리가 사용할 두 개의 메소드를 가지고 있다.

4-1. method

4-1-1. Call

call은 viewpure 함수를 위해 사용한다.
로컬 노드에서만 실행하고, 블록체인에 트랜잭션을 만들지 않는다.

viewpure 함수는 읽기 전용으로, 블록체인에서 상태를 변경하지 않는다.
가스소모도 없고, 트랜잭션을 만들지 않기 때문에 메타마스크에서 사용자에게 트랜잭션에 서명하라고 창을 띄우지도 않는다.

myContract.methods.myMethod(123).call()

매개 변수 123로 myMethod 함수를 call한다.

4-1-1. Send

send는 트랜잭션을 만들고, 블록체인 상의 데이터를 변경한다.
view와 pure가 아닌 모든 함수에 대해 send를 사용해야 하는 것!

트랜잭션을 send한다 === 사용자에게 가스를 지불한다.
메타마스크에서 트랜잭션에 서명하라고 창을 띄워야한다.
Web3 프로바이더로 메타마스크를 사용할 때, send()를 호출하면 자동으로 이 작업이 이루어진다.

myContract.methods.myMethod(123).send()

4-2. 데이터 받기

Zombie[] public zombies;

솔리디티에서 public으로 변수를 선언하면 자동으로 같은 이름의 퍼블릭 getter 함수를 만들어낸다.
ID 15인 좀비를 찾길 원한다면, 변수를 함수인 것처럼 호출할 수 있다 : zombies(15) 이렇게!

function getZombieDetails(id) {
  return cryptoZombies.methods.zombies(id).call()
}
// 함수를 호출하고, 그 결과를 가지고 특정 작업을 수행해라
getZombieDetails(15).then(function(result) {
  console.log("Zombie 15:" + JSON.stringify(result));
});

cryptoZombies에는 위에서 인스턴스화한 컨트랙트가 들어있다.

cryptoZombies.methods.zombies(id).call()
Web3 프로바이더와 통신해서 우리 컨트랙트의 Zombie[] public zombies에서 인덱스가 전달받은 id와 같은 좀비를 반환할 것이다.

이는 외부 서버로 API 호출을 하는 것처럼 비동기적으로 일어난다.
Web3는 단순한 콜백이 아닌 Promise를 사용하기 때문에.

Web3 프로바이더로부터 응답을 받으면,(Promise가 만들어지면)
then 문장을 실행하고 여기서 result를 콘솔에 로그로 기록하는 예제.

찍힌 result는 아래와 같은 형태이다

{
  "name": "H4XF13LD MORRIS'S COOLER OLDER BROTHER",
  "dna": "1337133713371337",
  "level": "9999",
  "readyTime": "1522498671",
  "winCount": "999999999",
  "lossCount": "0" // Obviously.
}

4-3. 따라하기

이미 배포되어있는 우리 컨트랙트(cryptoZombies라는 변수에 할당되어있음)의 안에는 두 함수가 있다.

  • 매핑된 zombieToOwner 함수
mapping (uint => address) public zombieToOwner;
  • getZombiesByOwner함수
function getZombiesByOwner(address _owner)

따라서,

  var cryptoZombies;

  function startApp() {
    var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
    cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);
  }

  function getZombieDetails(id) {
    return cryptoZombies.methods.zombies(id).call()
  }

  function zombieToOwner(id) {
    return cryptoZombies.methods.zombieToOwner(id).call()
  }
// 이 함수는 `id`를 매개 변수로 받고,
// 우리 컨트랙트의 zombieToOwner에 대한 Web3.js call을 반환시킨다.

  function getZombiesByOwner(owner) {
    return cryptoZombies.methods.getZombiesByOwner(owner).call()
  }
// 이 함수는 `owner`를 매개 변수로 받고,
// 우리 컨트랙트의 getZombieByOwner에 대한 Web3.js call을 반환시킨다.

  window.addEventListener('load', function() {
    if (typeof web3 !== 'undefined') {
      web3js = new Web3(web3.currentProvider);
    } else {
    }
    startApp()

  })

Web3.js의 call 메소드로는 컨트랙트 내부의 함수를 호출할 수 있다 ✨

따라서 우리가 지금 정의한 zombieToOwner 함수는
📌 (cryptoZombies에 할당된) 컨트랙트 안에 있는 zombieToOwner 함수와 통신해서,
전달된 id로 컨트랙트 내부에 정의된 작업을 처리한 뒤 그 결과 값을 반환시킨다.

정의한 getZombiesByOwner 함수도 마찬가지로,
📌 (cryptoZombies에 할당된) 컨트랙트 안에 있는 getZombiesByOwner 함수와 통신해서,
전달된 id로 컨트랙트 내부에 정의된 작업을 처리한 뒤 그 결과 값을 반환시킨다.

4-3-1. 매핑(mapping)이란?

컨트랙트 내부에 되어있다는 매핑은 뭘까?

매핑은 솔리디티에서 구조화된 데이터를 저장하는 또다른 방법이다.

매핑은 기본적으로 키-값 (key-value) 저장소로, 데이터를 저장하고 검색하는 데 이용된다.

// 금융 앱용으로, 유저의 계좌 잔액을 보유하는 uint를 저장한다: 
mapping (address => uint) public accountBalance;

// 혹은 userID로 유저 이름을 저장/검색하는 데 매핑을 쓸 수도 있다 
mapping (uint => string) userIdToName;

첫번째 예시에서 address 이고 은 uint이다.
두번째 예시에서 는 uint이고 은 string이다.

아래는 이름과 연봉을 mapping하여 저장하는 컨트랙트의 예제.

contract SalaryStorage {
   mapping(string => uint) salary;

   function set(string name, uint amount) public {
       salary[name] = amount;
   }

   function get(string name) public constant returns (uint) {
       return  salary[name];
   }
}

문자열 key - 자연수 value 형식의 salary라는 변수를 정의한 것.

5. 메타마스크 & 계정

이로써 처음으로 스마트 컨트랙트와 성공적으로 상호작용을 했다!
박수박수👏👏

이제 홈페이지에 유저가 가진 전체 좀비 군대를 보여주기 위해서,
각 요소들을 하나로 합칠 것이다.

현재 유저가 가지고 있는 모든 좀비들의 ID를 찾기 위해서는 어떻게 해야할까?

당연히 컨트랙트에 정의된 getZombiesOwner(owner) 함수를 호출해야할 것이다.

그런데, 우리의 컨트랙트 안에는 이 getZombiesOwner 함수에 전달해야할 매개변수 "owner"로
솔리디티 address를 보내라고 정의가 되어있다.

그럼 유저의 주소를 어떻게 알아내서 보낼 수 있을까?

5-1. 메타마스크에서 사용자 계정 가져오기

메타마스크는 확장 프로그램 안에서, 사용자들이 다수의 계정을 관리할 수 있도록 한다.

우리는 주입되어있는 web3 변수를 통해 현재 활성화된 계정이 뭔지 확인할 수 있다.

var userAccount = web3.eth.accounts[0]

사용자가 언제든 메타마스크에 활성화된 계정을 바꿀 수 있기 때문에,
이 변수의 값이 바뀌었는지 계속 감시하고
값이 바뀌면 그에 따라 UI를 업데이트 해야한다.

예를들어 홈페이지에 유저가 가진 모든 좀비 군대를 보여주고 싶다면,
메타마스크에서 계정을 바꾸었을 때 바뀐 계정의 좀비 군대를 보여주기위해 업데이트를 해줘야한다.

var accountInterval = setInterval(function() {
  // 계정이 바뀌었는지 확인
  if(web3.eth.account[0] !== userAccount) {
    userAccount = web3.eth.accounts[0];
    // 계정이 이전과 같지 않다면 계정을 바꿔주고
    updateInterface();
    // 새 계정에 대한 UI로 업데이트하기 위한 함수를 호출
  }
}, 100);

100밀리초마다 사용자가 계정을 바꿨는지 확인하고,
바꿨다면 해당 계정할당 + 화면 업데이트 함수를 호출한다.

5-2. 따라하기

  var cryptoZombies;
  var userAccount;

  function startApp() {
    var cryptoZombiesAddress = "YOUR_CONTRACT_ADDRESS";
    cryptoZombies = new web3js.eth.Contract(cryptoZombiesABI, cryptoZombiesAddress);
    
	var accountInterval = setInterval(function() {
      if(web3.eth.accounts[0] !== userAccount) {
        userAccount = web3.eth.accounts[0];
        getZombiesByOwner(userAccount)
          .then(displayZombies);
      }
    },100);
  }

  function getZombieDetails(id) {
    return cryptoZombies.methods.zombies(id).call()
  }

  function zombieToOwner(id) {
    return cryptoZombies.methods.zombieToOwner(id).call()
  }

  function getZombiesByOwner(owner) {
    return cryptoZombies.methods.getZombiesByOwner(owner).call()
  }

  window.addEventListener('load', function() {
    if (typeof web3 !== 'undefined') {
      web3js = new Web3(web3.currentProvider);
    } else {
    }
    startApp()

  })

계정에 대한 좀비를 불러오는 getZombiesByOwner 함수를 실행하고,
성공하면 displayZombies라는 함수를 실행한다.

아직 displayZombies 함수는 없지만, 다음 포스트에서 이어서 만들어보자.

profile
Web Front-end developer
post-custom-banner

0개의 댓글