React.js 로 프론트엔드를 구성합니다.
Web3.js 를 활용해 dApp 을 완성시킵니다.
실제 완성물은 조금 더 못생겼습니다. css 를 다루기 귀찮아서...
우리가 받아온 react box 에는 미리 작성되어있는 코드가 존재합니다. 이제 그 부분은 필요가 없으므로 제거합니다.
src/App.js 를 아래 소스코드로 바꿔주세요:
import React, { Component } from "react";
import getWeb3 from "./utils/getWeb3";
import "./App.css";
class App extends Component {
constructor(props) {
super(props);
this.state = {
web3: null
};
}
componentWillMount() {
getWeb3
.then(results => {
this.setState({
web3: results.web3
});
this.instantiateContract();
})
.catch(() => {
console.log("Error finding web3.");
});
}
instantiateContract() {
const contract = require("truffle-contract");
}
render() {
return <div className="App">Fruit shop</div>;
}
}
export default App;
이제 App.js 는
componentWillMount 에서 Web3 를 불러오고 state 에 저장합니다. 앞으로 우리는 this.state.web3 를 통해 Web3.js 를 사용합니다.
또한 instantiateContract 함수를 실행하고 truffle-contract 모듈을 불러옵니다. 이 모듈은 Truffle 에서 작성한 컨트랙트를 Javascript 로 손쉽게 불러오고 사용하도록 만들어줍니다.
우리가 컴파일한 Shop.json 파일을 불러옵니다:
import React, { Component } from "react";
import ShopContract from "../build/contracts/Shop.json";
import getWeb3 from "./utils/getWeb3";
instantiateContract 함수를 수정합니다:
const contract = require("truffle-contract");
const shop = contract(ShopContract);
shop.setProvider(this.state.web3.currentProvider);
truffle-contract 를 통해 우리가 불러온 Shop.json 파일을 const shop 에 저장했습니다.
Provider 를 우리가 현재 사용하는 MetaMask 로 설정합니다.
state 에 shopInstance, myAccount 를 추가해주세요. 우리가 배포한 컨트랙트의 인스턴스와 구매, 판매에 사용할 계정을 할당할 state 입니다:
class App extends Component {
constructor(props) {
super(props);
this.state = {
shopInstance: null, // shopInstance 추가
myAccount: null, // myAccount 추가
web3: null
};
}
이제 instantiateContract 함수를 마저 완성합시다:
instantiateContract() {
const contract = require("truffle-contract");
const shop = contract(ShopContract);
shop.setProvider(this.state.web3.currentProvider);
/* 이것을 추가하세요. */
this.state.web3.eth.getAccounts((error, accounts) => {
if (!error) {
shop.deployed().then(instance => {
this.setState({ shopInstance: instance, myAccount: accounts[0] });
});
}
});
}
web3 의 getAccounts 를 통해 계정을 받아오면, shop 에 배포된 인스턴스를 shopInstance 에 저장합니다.
accounts 에는 Ganache 에서 생성한 10 개의 계정이 들어있습니다. 우리는 그 중 첫번째 계정인 accounts[0] 을 사용합니다.
Shop 컨트랙트를 사용할 준비는 모두 마쳤습니다. 마지막으로 App.js 코드를 확인해주세요:
import React, { Component } from "react";
import ShopContract from "../build/contracts/Shop.json";
import getWeb3 from "./utils/getWeb3";
import "./App.css";
class App extends Component {
constructor(props) {
super(props);
this.state = {
shopInstance: null,
web3: null
};
}
componentWillMount() {
getWeb3
.then(results => {
this.setState({
web3: results.web3
});
this.instantiateContract();
})
.catch(() => {
console.log("Error finding web3.");
});
}
instantiateContract() {
const contract = require("truffle-contract");
const shop = contract(ShopContract);
shop.setProvider(this.state.web3.currentProvider);
this.state.web3.eth.getAccounts((error, accounts) => {
if (!error) {
shop.deployed().then(instance => {
this.setState({ shopInstance: instance });
});
}
});
}
render() {
return <div className="App">Fruit shop</div>;
}
}
export default App;
buyApple() {
this.state.shopInstance.buyApple({
from: this.state.myAccount,
value: this.state.web3.toWei(10, "ether"),
gas: 900000
});
}
우리가 Shop.sol 에 작성한 buyApple 함수를 호출합니다.
구매자(from)는 앞서 만든 myAccount 입니다.
가격(value)은 10 ether 입니다. value 는 ether 보다 작은 단위인 wei 이기 때문에 toWei 함수로 10 ether 를 wei 로 환산합니다.
gas 는 900000 로 설정합니다.
우리가 Solidity 로 작성한 sellMyApple 함수에서 uint 형 파라미터를 받던거 기억 하시죠?:
// Shop.sol 의 sellMyApple 함수
function sellMyApple(uint _applePrice) payable external
Web3.js 에서 파라미터를 넘기면서 호출해주면 됩니다:
sellApple() {
this.state.shopInstance.sellMyApple(this.state.web3.toWei(10, "ether"), {
from: this.state.myAccount,
gas: 900000
});
}
sellMyApple 은 트랜잭션을 실행해야 하므로 이더리움을 누구에서 전송해야 할지 알려줘야 합니다. 따라서 from 정보도 함께 넘겨줍니다.
gas 는 900000 로 설정합니다.
먼저 내 사과 개수를 보관할 myApples 를 state 에 새롭게 추가해야겠죠?:
constructor(props) {
super(props);
this.state = {
shopInstance: null,
myAccount: null,
myApples: 0, // myApples 추가
web3: null
};
}
구매와 판매 기능은 작성했지만 현재 내가 몇 개의 사과를 가지고 있는지 모릅니다. 다행히 우리는 Shop.sol 에 getMyApples 함수를 작성했습니다:
// Shop.sol 의 getMyApples 함수
function getMyApples() view external returns(uint16) {
return myApple[msg.sender];
}
Web3.js 로 이 함수를 호출하고 state 에 내가 가진 사과의 개수를 저장합시다. 이때 객채로 전달되는 result 에서 우리는 사과의 개수만 필요하므로 toNumber 를 사용합니다:
updateMyApples() {
this.state.shopInstance.getMyApples().then(result => {
this.setState({ myApples: result.toNumber() });
});
}
내가 가진 사과 개수를 업데이트 하기 위해 updateMyApples 함수를 적절한 시점에 호출해야합니다.
instantiateContract 에 코드 한 줄을 추가해주세요:
this.state.web3.eth.getAccounts((error, accounts) => {
if (!error) {
shop.deployed().then(instance => {
this.setState({ shopInstance: instance, myAccount: accounts[0] });
this.updateMyApples(); // 여기서 updateMyApples 호출하기
});
}
});
이제 프론트엔드를 작성합시다!
사실 저도 React.js 를 이제 막 사용했습니다. 그래서 최대한 간단히 만들겠습니다.
우리가 필요한 컴포넌트는 다음과 같습니다:
render 함수를 수정해주세요:
render() {
return (
<div className="App">
<h1>사과의 가격: 10 ETH</h1>
<button onClick={() => this.buyApple()}>구매하기</button>
<p>내가 가진 사과: {this.state.myApples}</p>
<button onClick={() => this.sellApple()}>
판매하기 (판매 가격: {10 * this.state.myApples})
</button>
</div>
);
}
크롬 브라우져를 실행하고 MetaMask 로 우리가 띄워놓은 Ganache 에 접속합시다.
MetaMask 를 설치 및 실행하고 크롬 익스텐션에서 여우 모양 아이콘을 눌러주세요:
Custom RPC 를 눌러주세요.http://127.0.0.1:7545 를 입력하고 Save 를 눌러주세요.Ganache 의 첫번째 계정 Private Key 를 받아와 MetaMask 계정에 추가합시다:
Ganache 를 열어 첫번째 계정의 열쇠 모양 버튼을 눌러주세요.
Private Key 를 복사해주세요.
Import Account 버튼을 눌러 복사한 Private Key 를 입력해주세요.
성공적으로 계정을 불러오면 IMPORTED 라는 빨간 태그가 있는 계정이 생성됩니다.
Ganache 의 첫번째 계정과 같은량의 이더리움이 들어있겠죠?이제 사과를 직접 구매하고 판매해봅시다!
서버를 실행시켜주세요:
> npm run start
http://localhost:3000 에 접속합니다.
구매하기 버튼을 눌러 트랜잭션을 진행합니다.판매하기 버튼으로 우리가 가진 사과를 과일 가게에 팔아봅시다.10 ether 가 다시 들어왔습니다!