Next.js + Typescript + Web3 세팅

707·2022년 7월 28일
1
post-thumbnail

Next와 Typescript, Web3를 사용하여 NFT 민팅 및 거래 기능의 웹사이트 제작하기

세팅

👉 truffle

ganache

가나쉬 네트워크를 이용하여 테스트 배포를 진행할 것이므로 배포 전 ganache를 켜주자

ganache-cli

ganache를 킨 후 생성된 계정의 2~3개 정도를 미리 메타마스크에 가져와 등록해주자.
메타마스크에 연결된 네트워크가 ganache인지 확인하고, 등록한 계정의 잔액이 100ETH씩 있는지 확인 해주기

contract

mkdir truffle
cd truffle
truffle init
npm init -y
npm install openzeppelin-solidity

ERC721 토큰의 mint, transfer, approve 등의 기능을 담은 HbToken.sol
토큰의 판매, 구매 등의 기능을 담은 SaleToken.sol
두가지의 컨트랙트 작성


migration

2_deploy_minting.js

const HbToken = artifacts.require("HbToken");
const SaleToken = artifacts.require("SaleToken");

module.exports = async (deployer) => {
  await deployer.deploy(HbToken, "test", "tst", "http://localhost:3500");
  const tokenInstance = await HbToken.deployed();
  await deployer.deploy(SaleToken, tokenInstance.address);
};

SaleToken 컨트랙트가 Token 컨트랙트의 ca 값을 알아야 하므로 async, await 구문을 이용하여 순차적으로 배포를 처리해준다.

❗️ 배포 후에는 각 컨트랙트의 ca값을 확인하여 미리 기록해두는 것이 편하다.

👉 front

넥스트와 타입스크립트, chakra-ui를 이용할 것이다. 필요한 라이브러리를 설치한다. web3도 미리 설치해주자.

npx create-next-app@latest --typescript front
cd front
npm i @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^6
npm i web3

chakra UI

chakra UI는 리액트 컴포넌트 라이브러리이다. 미리 디자인과 기능적인 작업이 완성 된 컴포넌트를 손쉽게 가져와서 이용할 수 있게 해준다.
이를 사용하기 위해서는 먼저 _app.tsx에서 provider를 설정해주면 된다.

_app.tsx

import type { AppProps } from "next/app";
import { ChakraProvider } from "@chakra-ui/react";

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <ChakraProvider>
      <Component {...pageProps} />
    </ChakraProvider>
  );
}

export default MyApp;

Component를 ChakraProvider로 감싸준다.

Chackra UI에서는 div와 같은 역할을 하는 컴포넌트를 기본적으로 제공해준다.
Box(div), Flex(flex를 넣은 div), Text, Button 등이 있다.
RN에서 View, Text 등을 사용하는 것과 같은 개념이다.

Hook

클라이언트 앱에서 블록체인 네트워크와 통신을 하기 위해서는 web3를 사용한다. 미리 브라우저에 메타마스크를 설치하고, web3로는 이 메타마스크에 요청을 보내면 메타마스크가 블록체인네트워크와 통신을 해준다.

우리는 이를 위해

  1. 메타마스크에서 현재 연결된 account를 가지고 오기 : useAccount
  2. web3를 이용하여 통신하기 : useWeb3

이 두가지 역할을 할 함수를 hook으로 따로 미리 만들어 둘 것이다.
그 후에 web3가 필요한 컴포넌트에 이 훅을 가지고 오면 손쉽게 활용할 수 있다.


useAccount.ts

import { useEffect, useState } from "react";

const useAccount = () => {
  const [account, setAccount] = useState<string>("");

  const getAccount = async () => {
    try {
      if (!window.ethereum) throw new Error("Error : no metamask");
      const accounts = await window.ethereum.request({
        method: "eth_requestAccounts",
      });

      if (accounts && Array.isArray(accounts)) {
        setAccount(accounts[0]);
      }
    } catch (e) {
      console.log(e);
    }
  };

  useEffect(() => {
    getAccount();
  }, []);

  return { account };
};

export default useAccount;

타입스크립트로 위와 같은 코드를 작성하면
window객체 내에 기본적으론 ethereum이 포함되지 않기 때문에 찾을 수 없다는 에러가 발생한다.

이를 해결하기 위해 index.d.ts를 작성해준다.

index.d.ts

import { MetaMaskInpageProvider } from "@metamask/providers";

declare global {
  interface Window {
    ethereum?: MetaMaskInpageProvider;
  }
}

MetaMaskInpageProvider 를 라이브러리를 설치해 가지고 올 수 있다. window내의 ethereum이 이 타입을 가질 수 있도록 위처럼 넣어준다.


또한 window.ethereum.request()로 account목록을 가지고 오는 rpc요청을 보내면 리턴값의 타입이 Maybe<unknown>으로 지정되어 있다. 리턴값이 account의 배열이라는 것을 작성자는 알지만 타입스크립트는 모르기때문에 단순히 accounts[0]으로 첫번째 계정의 주소를 가지고 오려고 하면 타입에러가 발생한다.

때문에 if문으로 accounts가 존재하며 accounts가 배열이라면 이라는 조건을 붙여준 뒤에야 타입에러 없이 해당 값을 가져올 수 있기때문에

if (accounts && Array.isArray(accounts)) {
  setAccount(accounts[0]);
}

이런 코드를 넣어주었다.



useWeb3.ts

import { useEffect, useState } from "react";
import Web3 from "web3";
import { Contract } from "web3-eth-contract"; // ❗️ 컨트랙트 타입
import { AbiItem } from "web3-utils";

const useWeb3 = () => {
  const [web3, setWeb3] = useState<Web3 | undefined>(undefined);
  const [tokenContract, setTokenContract] = useState<Contract>();
  const [saleContract, setSaleContract] = useState<Contract>();

  const getWeb3 = async () => {
    try {
      if (window.ethereum) {
        setWeb3(new Web3(window.ethereum as any));
      }
    } catch (e) {
      console.log(e);
    }
  };

  const getToken = (networkId: number) => {
    if (!web3) return;
    const tokenJSON = require("../contracts/HbToken.json");
    const abi: AbiItem = tokenJSON.abi;
    const ca: string = tokenJSON.networks[networkId].address;
    const instance = new web3.eth.Contract(abi, ca);
    setTokenContract(instance);
  };

  const getSale = (networkId: number) => {
    if (!web3) return;
    const saleJSON = require("../contracts/SaleToken.json");
    const abi: AbiItem = saleJSON.abi;
    const ca: string = saleJSON.networks[networkId].address;
    const instance = new web3.eth.Contract(abi, ca);
    setSaleContract(instance);
  };

  useEffect(() => {
    if (!web3) getWeb3();
    else {
      (async () => {
        const networkId: number = await web3.eth.net.getId();
        getToken(networkId);
        getSale(networkId);
      })();
    }
  }, [web3]);

  return { web3, tokenContract, saleContract };
};

export default useWeb3;

이 useWeb3 훅에서는 3가지 작업을 처리해 준다.

  1. Web3와 메타마스크 연결해주기 : getWeb3
  2. Token 컨트랙트 인스턴스 생성 : getToken
  3. Sale 컨트랙트 인스턴스 생성 : getSale

위 세가지 함수를 만들고,
useEffect로 랜더링 시 시점을 잡아 실행되도록 코드를 작성해주었다.

컨트랙트의 인스턴스를 생성함으로써 우리는 해당 컨트랙트 내의 메소드를 간단하게 호출할 수 있게 되었다. web3와 컨트랙트의 인스턴스 2가지를 훅을 사용한 컴포넌트로 리턴해준다.

컴포넌트에서는

const { account } = useAccount();
const { web3, tokenContract, saleContract } = useWeb3();

로 가지고 와서 사용하면 된다.

1개의 댓글

comment-user-thumbnail
2023년 4월 6일

안녕하세요. 좋은글 감사드립니다. 혹시 깃헙에 예제 공유 부탁드려도될까요?

답글 달기