
-> 지난 블록체인 Block-Chain - 이더리움 DApp - Counter 만들기 ( Feat. Ganache ) (1) 포스트에서 이어집니다.
Ganache network를 재실행하게 되면 배포한Smart Contract가 초기화되기 때문에 자동으로json파일의network에 배포된CA값을 가져오도록 세팅하였습니다.migration --reset으로 컴파일 내용을 재배포 한뒤FrontServer의 기존의json파일을 제거하고 재배포한json파일을 가져와주었습니다.
web3.eth.net.getId() : 현재 연결된 네트워크의 ID를 가져옵니다.
ID를 가지고 Counter.json 파일에서 Contract Address값을 가져옵니다.
SmartContract Event란SolidityCode로 작성한Contract의 함수들이 호출될 때 배포하고 난뒤TransactionReceipt에 해당 기록을 남김으로써 후에 기록을 찾아볼 수 있도록log를 삽입하는 것입니다.Event를 등록하는 이유는 현재 코드에서+버튼을 누르는 사람이A라고 했을때A의 화면에는 바뀐count값을 가져와서 화면에 띄워주겠지만B라는 사람의 화면에는 상태변수가 변했을때 다시 바뀐count값을 가져와서 화면에 띄워주는 코드가 없습니다.
즉 본인이+버튼을 눌렀을 경우에는 화면에 바로 바뀐 값이 렌더되겠지만 본인이+버튼을 누르지 않았을때에는network상에서는count값이 변했겠지만 이 변한 값을 받아오는 부분이 빠져있기 때문에Event를 추가하여 본인이+버튼을 누르지 않아도log를 추적하여 새로운log가 발생되면 즉시 해당log를 읽어 값을 변경하여 내가 버튼을 누르지 않아도 화면에 바뀐 상태변수를 보여주도록 할 것입니다.
Solidity Code 수정 contract Counter {
uint256 private _count;
// 이벤트 선언 => 이벤트명은 Count이고 uint256에 대한 내용으로 log를 찍을것입니다.
event Count(uint256 count);
// public이 아닌 private기 때문에 _count 상태변수를 가져오는 함수를 만들어주었습니다.
function current() public view returns (uint256) {
return _count;
}
function increment() public {
_count += 1;
// 이벤트를 실행 => log를 찍음
emit Count(_count);
}
function decrement() public {
_count -= 1;
// 이벤트를 실행 => log를 찍음
emit Count(_count);
}
}
Count Event추가 및 함수 실행시 emit을 사용하여 Count Event를 실행하도록 하였습니다.Solidity Code를 수정했으니 재배포 하고Counter.json파일을 다시FrontServer로 가져와야 합니다.truffle migration --reset후 배포한json파일FrontServer로 가져옵시다.
useEffect(() => {
(async () => {
if (deployed) return;
// netwokrId 가져오기
const networkId = await web3.eth.net.getId();
// Counter.json 파일에서 network의 CA 가져오기
const ContractAddress = CounterContract.networks[networkId].address;
// 2가지 인자값 abi ,CA
const deploy = await new web3.eth.Contract(CounterContract.abi, ContractAddress);
const current = await deploy.methods.current().call();
// logs의 두번째 인자값의 내용은 어디 Contract의 log를 가져올 것인가 CA값을 적습니다.
web3.eth
.subscribe('logs', { address: ContractAddress }) //
.on('data', (log) => {
// on 메서드로 연결한 CA에서 data를 받을때 log를 console.log로 찍을것입니다.
console.log(log);
});
setDeployed(deploy);
setCounte(current);
})();
}, []);
Front에서useEffect부분에log를 읽는 코드를 추가합니다.+혹은-버튼을 누르고console.log(log)를 확인해보면 아래와 같은 값이 출력됩니다.data값에 해당 숫자가 찍힌것을 확인할 수 있는데0x0000...2이런식으로 나오는 이유는Contract를 작성할 때count를uint256으로 타입을 선언해주었기 때문에 메모리 공간을uint256만큼 공간을 만들어주었기 때문입니다.

web3.eth.subscribe() 코드 수정 web3.eth
.subscribe('logs', { address: ContractAddress }) //
.on('data', (log) => {
// on 메서드로 연결한 CA에서 data를 받을때 log를 console.log로 찍을것입니다.
console.log(log.data);
// 첫번째 인자값은 16진수로 받아온 log를 어떤 type으로 파싱을 할것인가와 파싱한값을 어떤 name으로 받을것인가를 적습니다.
// 두번째 인자값은 파싱할 데이터가 들어갑니다.
const value = web3.eth.abi.decodeLog([{ type: 'uint256', name: '_count' }], log.data);
// return Obj 값이 객체로 나오는 이유는 Event에서 여러개의 data를 넣는다면 여러개의 data값이 출력되기 때문에 객체 형태로 return 됩니다.
console.log(value);
});
- 위와 같이
log.data를decodeLog메서드를 사용하여 16진수에서 사람이 읽을 수 있도록 파싱을 하면Object로 아래와 같은 값이 나옵니다.

- 객체의
0은data값의index를 의미하고length는 길이_count는 파싱한data값을 의미합니다.
web3.eth
.subscribe('logs', { address: ContractAddress }) //
.on('data', (log) => {
// on 메서드로 연결한 CA에서 data를 받을때 log를 console.log로 찍을것입니다.
console.log(log.data);
// 첫번째 인자값은 16진수로 받아온 log를 어떤 type으로 파싱을 할것인가와 파싱한값을 어떤 name으로 받을것인가를 적습니다.
// 두번째 인자값은 파싱할 데이터가 들어갑니다.
const value = web3.eth.abi.decodeLog([{ type: 'uint256', name: '_count' }], log.data); // return Obj 값이 객체로 나오는 이유는 Event에서 여러개의 data를 넣을수도 있기 때문에
console.log(value);
setCounte(value._count);
});
--- 중략 ---
const increment = async () => {
await deployed.methods.increment().send({ from: account });
};
const decrement = async () => {
await deployed.methods.decrement().send({ from: account });
};
- 마지막으로 위와 같이 이벤트
log를 추적하여 받은data값을 파싱하고 이 파싱한count값을setCounte로 변경하면 내가 아닌 다른 누군가가+버튼을 눌러도 내 화면에서도event의log를 받기 때문에 내 화면에도count가 변경될 것입니다.- 내가
+혹은-버튼을 누른다고 하더라도subscribe메서드로log를 추적할 것이니 내 화면에도count가 변경될 것입니다.

- 이런식으로
Transaction이 발생되었을때 블록이mining이 되며Smart Contract가 실행되고 이때Event가 발동하여log를 남길 경우TransactionReceipt의logs부분에 해당log의 내용들을 확인할 수 있습니다.
- 지금까지는
Front에서MetaMask와 연결하여SmartContract를 실행시켰다면 이제는Back서버에서 서명을 제외한Transaction을 생성하여Front로 전달한후Front에서 해당Transaction을 가지고MetaMask에Transaction을 보내도록 처리할 것입니다.- 이렇게 작업해야하는 이유는 지금과 같이
Counter같이 간단한 작업들은Front에서 처리해도 되지만, 예를 들어 쇼핑몰 같은 경우에DataBase에 연동하여 작업을 해야할 경우가 있기 때문에 미리 연습해 보는것입니다.
Back 서버 세팅/* server.js */
const express = require('express');
const app = express();
const cors = require('cors');
const Web3 = require('web3');
const web3 = new Web3(new Web3.providers.HttpProvider('http://127.0.0.1:8545')); // Ganache 네트워크 연결
app.use(
cors({
origin: true,
credential: true,
}),
);
app.post('/counter', (req, res) => {
res.json('Hello World!');
});
app.listen(4000, () => {
console.log('Back Server On');
});
Back서버에서 서명을 제외한Transaction을 만들어서front로 보내주면front에서MetaMask로 연결하여MetaMask에서 서명을 만들어Transaction을 발생시킬것 입니다.- 배포한
Contract에 대한 내용이 필요할 것이니front에서 사용중이던Counter.json파일을Back서버에도 그대로 가져와서 작업을 할 것입니다.
app.post(counter) 코드 작성/* server.js */
app.post('/counter', async (req, res) => {
const { from, type } = req.body; // 0xb81cc9fa0ccd058f5a1669f719bf67e4b97bac56
const transaction = await new Txobj().init(from, type);
console.log(transaction);
res.json(transaction);
});
- 위와 같이
req.body로Front에서 요청한 내용의body내용을 가져와Transaction의from내용, 즉address값과count를+할것인지-할것인지에 대한type을 받아오도록 하겠습니다.
/* txobj.js */
const Web3 = require('web3');
const web3 = new Web3(new Web3.providers.HttpProvider('http://127.0.0.1:8545')); // Ganache 네트워크 연결
const CounterContract = require('./contracts/Counter.json');
class Txobj {
async init(_from, _type) {
this.nonce = await Txobj.getNonce(_from);
this.from = _from;
this.to = await Txobj.getCA(_from); // Truffle 디렉토리의 Counter.json 파일을 복사해서 가져오고 CA 계정을 구합니다.
this.data = await Txobj.getData(this.to, _type); // 함수를 실행시킬수 있는 bytecode가 들어갑니다.
return this;
}
//nonce 구해오기
static getNonce = async (_from) => {
return await web3.eth.getTransactionCount(_from);
};
// CA 계정 구해오기
static getCA = async (_from) => {
const networkId = await web3.eth.net.getId();
return CounterContract.networks[networkId].address;
};
// Data 구해오기
static getData = async (_ca, _type) => {
const abi = CounterContract.abi;
const deployed = new web3.eth.Contract(abi, _ca);
switch (_type) {
case 'increment':
return await deployed.methods.increment().encodeABI(); // increment 메서드 자체를 코드를 bytecode로 변환합니다.
case 'decrement':
return await deployed.methods.decrement().encodeABI();
}
};
/*
{
"nonce": 16,
"from": "0xb81cc9fa0ccd058f5a1669f719bf67e4b97bac56",
"to": "0x1a305591E65a493A042Fa445cE44F237DdAC2550",
"data": "0xd09de08a"
}
*/
}
module.exports = { Txobj };
Transaction이란 객체이니class Txobj를 생성하여 인자값으로req.body로 받아온from과type을 넣고new키워드로 새로운Txobj를 생성하려고 하면 자동으로nonce,from,to,data를 가진 인스턴스를 생성하도록 해주었습니다.nonce는web3.eth의getTransactionCount메서드를 사용하여 해당 계정으로 몇번의Transaction이 발동되었는지에 대한nonce값을 가져왔습니다.from은 말그대로 인자값으로 받은address값을 넣어줍니다.to에는Contract Address즉Contract에 대한 주소 값이 들어가는데 먼저networkId를web3.eth의getId메서드를 사용하여 가져온후networkId를 가지고 배포한Contract즉Counter.json파일에서 해당 네트워크의 배포된Contract의address값을 찾아와서 넣어주었습니다.- 마지막으로
data에는 미리 구한CA값과 인자값으로 받은type을 사용할 것인데, 배포한Contract의abi 값을 가져온후 가져온abi값과 인자값으로 받은CA를 사용하여 새로운deployed인스턴스를 생성해주고, 인자값으로 받은type에 따라increment일경우+decrement일 경우-를 하도록deployed인스턴스안에methods에서increment메서드 혹은derement메서드 자체를 컴퓨터가 읽을 수 있도록bytecode`로 변환하여 넣어주었습니다.

- 이런식으로
class를 활용하여increment혹은decrement를 처리하는Transaction을 만들어서front로 보내줄 것입니다.
const increment = async () => {
const response = await axios.post('http://localhost:4000/counter', { from: account, type: 'increment' });
// MetaMask에 Transaction send
await web3.eth.sendTransaction(response.data);
};
const decrement = async () => {
const response = await axios.post('http://localhost:4000/counter', { from: account, type: 'decrement' });
// MetaMask에 Transaction send
await web3.eth.sendTransaction(response.data);
};
- 마지막으로
Front의increment,decrement함수를 수정하여 모두 같은Back서버의API로 요청을 보내고type값만 다르게 주어Back서버에서increment혹은decrementContract내용이 포함된Transaction을 받고 해당Transaction을 연결된MetaMask에게 보내 서명을 추가한 뒤 해당Transaction을 블록에 넣어Smart contract를 실행할 것입니다.


Counter의+혹은-버튼을 누를때마다Smart Contract를 실행하여 상태변수를 바꿔줄것이니 가스비를 지불하여Transaction이txpool에 담기고 블록이mining되면transaction의contract가 실행되며 상태변수를 바꿔줍니다.