DApp 카운터 만들기

707·2022년 7월 18일
0

솔리디티

목록 보기
3/5
post-thumbnail

💬 DApp이란?

디앱(DApp) 또는 댑이란 Decentralized Application의 약자로서, 이더리움, 큐텀, 이오스 같은 플랫폼 코인 위에서 작동하는 탈중앙화 분산 애플리케이션을 말한다.

이전까지 이더리움의 스마트컨트랙트에 대해서 알아보았다. DApp은 이 스마트컨트랙트가 블록체인에 도입되면서 생겨나게 된 어플리케이션이다. 즉, 스마트컨트랙트를 올릴 수 없는 비트코인 네트워크에서는 이 DApp이 구현될 수 없다.

스마트 컨트랙트는 블록체인 네트워크에 코드를 심어 블록체인 상에서 실행될 수 있도록 하는 것을 말한다. 이렇게 여러 데이터들이 블록체인 상에서 상호작용하며 기록되고 불러내어지는 애플리케이션을 모두 디앱으로 부를 수 있다.

이 디앱의 작동방식을 간단히 요약하면 다음과 같다.

  1. 개발자는 컨트랙트의 배포를 통해 네트워크 상에 우리가 원하는 기능을 담은 코드를 넣어둔다.
  2. 사용자들은 해당 컨트랙트의 주소로 트랜잭션을 보내며 이 컨트랙트의 기능을 이용할 수 있다.

이를 확인할 수 있는 아주 간단한 예제인 카운터 만들기를 통해 디앱의 개념과 작동방식을 완벽히 이해해보자!

💬 카운터 만들기

우리가 만들 카운터로 한정해 디앱의 작동방식을 다시 설명하자면

  1. 먼저 카운터 기능 (더하기/빼기/현재값확인)을 담은 코드를 컨트랙트로 만들어 배포한다.
  2. 이용자가 접근할 수 있도록 클라이언트 웹을 만든다.
    2-1. 기능을 이용하려면 트랜잭션이 필요하다. 지갑을 연동하자.
    2-2. 버튼을 누르면 바로 트랜잭션이 보내지도록 설정해주자.
    2-3. 여러명의 사용자가 있는 경우 다른 사람이 카운트를 변경했을 때 그 값을 받아올 수 있도록 해주자.
    2-4. 트랜잭션을 만들 때 서버를 이용하여 단계를 세분화해보자.


1. 카운터 컨트랙트 배포하기

  1. _count 라는 이름의 상태변수를 만들어 준 뒤
    현재값을 가지고 오는 함수와 카운트를 1씩 올리고 내리는 함수를 만들어준다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

contract Counter {
  uint256 private _count;

  function current() public view returns(uint256) {
    return _count;
  }

  function increment() public {
    _count += 1;
  }

  function decrement() public {
    _count -= 1;
  }
}

  1. ganache를 이용해 간이 블록체인을 켜준다.
npx ganache-cli

truffle compile
  1. truffle을 이용해 컴파일과 마이그레이션을 실행한다.
// 2_deploy_counter.js
const Counter = artifacts.require("Counter");

module.exports = function (deployer) {
  deployer.deploy(Counter);
};
truffle migration

👍 배포 끝!



2. 리액트

2-1. useWeb3 커스텀훅으로 메타마스크 연결하기

create-react-app을 해준 뒤
src 폴더 내에 hooks 디렉토리 생성. useWeb3.jsx를 만든다.

이 커스텀 훅에서는 클라이언트 측의 메타마스크 월렛 정보를 가지고 오고, 메타마스크와 통신을 할 수 있도록 web3 객체를 만들어 리턴시키도록 할 것이다.

import React, { useEffect, useState } from "react";
import Web3 from "web3/dist/web3.min";

const useWeb3 = () => {
  // 커스텀훅의 리턴값으로 넘겨 줄 account와 web3객체를 state로 만듦.
  const [account, setAccount] = useState(null);
  const [web3, setWeb3] = useState(null);

  // component 렌더링이 완료되면 내부의 async 콜백함수가 실행.
  useEffect(() => {
    (async () => {
      // 메타마스크 설치 안된 경우 바로 리턴해서 함수 종료
      if (!window.ethereum) return; 
      
      // 메타마스크의 account정보를 가져온다.
      const [address] = await window.ethereum.request({
        method: "eth_requestAccounts",
      });
      setAccount(address);

      // web3로 메타마스크와 통신
      const web3 = new Web3(window.ethereum);
      setWeb3(web3);
    })();
  }, []);

  return [web3, account];
};

export default useWeb3;

위와 같은 useWeb3 커스텀 훅을 만들어주었다.
여러 컴포넌트에서 web3를 이용해야하므로 이렇게 커스텀훅으로 만들어두면 편리하게 가져와 쓸 수 있게된다.



2-2. counter 컴포넌트 만들기

src 폴더 내에 components 디렉토리 생성 후 Counter.jsx를 만들었다.

import { useState, useEffect } from "react";
import CounterContract from "../contracts/Counter.json";
import axios from "axios";

const Counter = ({ web3, account }) => {
  const [count, setCount] = useState(0);
  const [deployed, setDeployed] = useState();

  const increment = async () => {
    const result = await deployed.methods.increment().send({ from: account }); 
    if (!result) return;
    const current = await deployed.methods.current().call();
    setCount(current);
  };

  const decrement = async () => {
    const result = await deployed.methods.decrement().send({ from: account });
    if (!result) return;
    const current = await deployed.methods.current().call();
    setCount(current);
  };

  useEffect(() => {
    (async () => {
      if (deployed) return; 
      // 재렌더링 시 deployed가 없을 때에만 아래 코드 실행.

      const networkId = await web3.eth.net.getId();
      const ca = CounterContract.networks[networkId].address;
      const abi = CounterContract.abi;

      const Deployed = new web3.eth.Contract(abi, ca);
      const count = await Deployed.methods.current().call();

      setCount(count);
      setDeployed(Deployed);
    })();
  }, []);

  return (
    <div>
      <h2>Counter: {count} </h2>
      <button onClick={increment}>증가</button>
      <button onClick={decrement}>감소</button>
    </div>
  );
};

export default Counter;

useEffect를 이용하여 componentDidMount인 상태일 때 deployed라는 state가 비어있는 경우 컨트랙트의 인스턴스를 생성하여 담아준다.
이렇게 컨트랙트의 인스턴스를 생성하기 위해서는 CA와 abi가 필요하다.

이 값은 truffle을 이용하여 Counter.sol 솔리디티 파일을 컴파일하고 배포했을 때 생성되는 Counter.json 파일을 복사해 리액트로 가져온 뒤 객체 내에 있는 정보 중 ca와 abi를 찾아 이용하면 된다.

이 deployed를 이용하여 컴포넌트 내에서 current와 increment, decrement 메소드를 쓸 수 있게된다.

최초 렌더링시에 Deployed.methods.current().call()로 컨트랙트의 상태변수값을 받아오고

증가, 감수 버튼을 누르면 실행되는 함수에서 deployed.methods.increment().send({from: account})로 상태변수값을 변경하고, 변경된 상태변수를 다시 current().call()로 받아온다.

여기서 중요한 점은 컨트랙트의 상태를 변수하는 increment, decrement 메소드를 실행할 떄에는 트랜잭션이 발생하므로 send에 트랜잭션을 발생시키는 account를 입력해주어야한다는 점이다.

그러면 해당 account에서 deployed 인스턴스를 만들 때 적었던 CA로 트랜잭션이 보내지고, 마이닝될 때 EVM에서 해당 트랜잭션에 담긴 코드를 실행하면서 소모할 가스비가 수수료 개념으로 빠져나가게 된다.

2-3. 이벤트 이용하기.

(1) Counter.sol에 이벤트 만들기

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

contract Counter {
  uint256 private _count;
  event Count(uint256 count); 
  // 로그이름 Count. 로그내용 count인 이벤트 등록 

  function current() public view returns(uint256) {
    return _count;
  }

  function increment() public {
    _count += 1;
    emit Count(_count); // 👈 로그 생성 후 이벤트를 emit함
  }

  function decrement() public {
    _count -= 1;
    emit Count(_count); // 👈 로그 생성 후 이벤트를 emit함
  }
}

이벤트는 블록체인 상의 컨트랙트와 그 컨트랙트로 만든 앱의 사용자 단에서 무언가 액션이 발생했을 때 서로 의사소통하는 방법이다.

컨트랙트 상에서 메소드가 실행되며 emit이 일어날 때마다 클라이언트는 해당 로그를 인지하고 이벤트 처리를 하게된다.

아래에서 클라이언트가 이벤트를 알 수 있도록 하는 코드를 확인해보자

(2) 이벤트

리액트의 Counter 컴포넌트의 useEffect훅의 내부에서 web3를 사용해 로그를

Counter.jsx

  useEffect(() => {
    (async () => {
      if (deployed) return; 
      // deployed가 없을 때 한번만 실행할 함수이므로 조건문 달아줌.

      const networkId = await web3.eth.net.getId();
      const ca = CounterContract.networks[networkId].address;
      const abi = CounterContract.abi;

      const Deployed = new web3.eth.Contract(abi, ca);
      const count = await Deployed.methods.current().call();

      // ❗️ 로그 
      web3.eth.subscribe("logs", { address: ca }).on("data", (log) => {
        console.log(log.data); 
        const params = [{ type: "uint256", name: "count" }]; 
        const value = web3.eth.abi.decodeLog(params, log.data); 
        console.log(value); // u {0: '4', __length__: 1, _count: '4'} 
        setCount(value.count);
      });

      setCount(count); 
      // 페이지 처음 진입 시에는 기존의 count값을 가지고 와야 하므로 얘는 살려두고 증가/감소 함수 내부의 setCount는 삭제 => 로그 subscribe가 대신함
      setDeployed(Deployed);
    })();
  }, []);

web3.eth.subscribe를 이용하여 솔리디티에서 emit했던 이벤트를 클라이언트단에서 확인할 수 있다.
"logs"나 "data"는 web3에서 제공하는 이벤트명이다. ( 👉 공식문서 )

우리는 solidity에서 이벤트의 인자값에 대한 별도의 변수명 지정 없이 타입만을 설정해두었다. 이를 우리가 원하는 변수명으로 받아오기 위해서 params 배열을 만들어준다. 만약 이벤트의 인자가 n개라면 length가 n인 params 배열을 만들면 될 것이다. params에서는 솔리디티에서 설정했던 데이터타입과 변수명을 속성으로 가진 객체를 순서대로 담고 있다.

web3.eth.abi.decodeLog 메소드에서 첫번째 인자값으로는 위에서 만든 params 배열, 두번째 인자값으로는 콜백함수의 인자인 객체 중 data 속성을 넣어준다.

이렇게 하여 블록체인 상에서 바이트코드로 존재했던 로그를 위 메소드로 디코딩하여 볼 수 있다.

useEffect에서 컴포넌트가 처음 렌더링 되었을 때 subscribe는 실행되고, 컴포넌트가 렌더링되는 내내 자동으로 블록체인 네트워크 상에서 발생한 모든 이벤트를 받아올 수 있는 상태가 된다.

이렇게 하면 내가 보낸 트랜잭션으로 상태변수의 값을 바꿨을 때 뿐만 아니라,
나와 동시에 접속한 다른 사용자가 보낸 트랜잭션으로 인해 상태변수의 값이 바뀌었을 때도 바로바로 화면에 그 변화를 확인할 수 있게되었다.

또한, 기존에는 increment, decrement함수에서 매번 보냈던 current 요청도 더이상 할 필요가 없어졌다. 상태를 바꾸기만 하면 자동으로 이벤트가 발생되어 로그가 전해지기 때문이다.
두 함수를 아래와 같이 수정할 수 있다.

const increment = async () => {
  	const result = await deployed.methods.increment().send({ from: account }); 
  	if (!result) return;
    const current = await deployed.methods.current().call();
    setCount(current);
};

// 👇
const increment = async () => {
  	const result = await deployed.methods.increment().send({ from: account }); 
};



const decrement = async () => {
    const result = await deployed.methods.decrement().send({ from: account });
    if (!result) return;
    const current = await deployed.methods.current().call();
    setCount(current);
};

// 👇
const decrement = async () => {
  	const result = await deployed.methods.decrement().send({ from: account }); 
};


💬 정리

이상으로 카운터 DApp을 만들어보며 디앱 개발의 전체적인 흐름을 확인해보았다. 웹개발과 다른 점은 데이터를 단순히 중앙의 서버 하나에 보관하는 것이 아니라 분산화된 블록체인 네트워크를 이용한다는 점이지, 많은 차이가 있지는 않다고 느꼈다. 다만 매번 요청을 보낼 때 트랜잭션을 이용해야하므로 가스비가 든다는 점에서 이러한 어플리케이션을 어떤 분야에서 어떻게 활용하면 좋을지에 대한 것이 아직은 잘 와닿지가 않는 것 같다.

👉 Avoiding the Pointless Blockchain Project
구축하고자 하는 앱이 블록체인을 이용하는 것이 적합한지를 판단하는 기준에 대해서 알려주는 글이다. 여기서는

  1. 공유 데이터베이스가 필요할 것
  2. 다수의 사용자가 데이터베이스에 접근하고 있을 것.
  3. 사용자 간 신뢰가 없을 것.
  4. 탈중개화를 원하거나 필요로 할 것.
  5. 사용자들의 거래 간에 상호작용이 있을 것.

이상의 다섯가지를 충족하지 못한다면 기존의 중앙집중식 데이터베이스를 사용하기를 권장하고 있다.

0개의 댓글