오픈씨 따라하기 (with REACT, web3.js)

박지헌·2021년 12월 20일
0

블록체인 개발

목록 보기
6/7

오픈씨(OpenSea) 는 현재 존재하는 NFT 마켓플레이스 중 가장 유명한 곳이다.
https://opensea.io/

이더리움 계열의 NFT 를 생성, 판매, 구매할 수 있는 곳이다~!!
(클레이튼, 폴리곤도 지원)

개인적으로도 몇 가지 NFT 를 오픈씨에서 구매한 적이 있다. 살짝 자랑해보면

이렇게 내 이더리움 지갑주소 소유의 ERC721 데이터의 이미지 URI 주소를 참조하여 화면에 보여주기도 한다.

이번 블로깅에서는 이러한 오픈씨처럼

  • 내 지갑주소(metamask) 를 연결하여
  • 내 소유의 NFT 를 화면에 보여주고
  • 새로운 NFT 를 mint (생성)

하는 애플리케이션을 만들어 보겠다.

1. 리엑트(REACT) 프로젝트 생성

우리는 node.js 와 react 를 활용하여 애플리케이션을 만들 것이다.

먼저 아래 명령어로 react 프로젝트를 생성해보자.

npx create-react-app web3-practice
code web3-practice

index.js

이 파일은 딱히 할게 없다. 아래와 같이 간단히 남겨둔다.

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

App.js

이번 실습에서 가장 중요한 파일이라 할 수 있겠다.
사용자 view 메인 화면이기도 하면서 많은 로직을 담을 것이다.

우선은 아래와 같이 뼈대를 만들어 보자.

import './App.css';

function App() {
  return (
    <div className="App">
      
    </div>
  );
}

export default App;

2. web3.js 및 메타마스크 연동

개인적으로 이 부분에서 거의 3일을 꼴딱 날려먹었다.
먼저 문제가 뭐였는지 살펴보자.

npm 으로 web3.js 라이브러리 모듈을 설치한다.

npm install web3

이 때 package.json 을 보면 v1.6.1 이 설치될 것이다.

그리고 App.js 를 다음과 같이 수정한다. web3 모듈을 불러오고 state 에 담는 로직이다.

import {useState} from 'react';
import Web3 from 'web3';

function App() {
		const [web3, setWeb3] = useState();
    useEffect(() => {
        if (typeof window.ethereum !== "undefined") {. // window.ethereum이 있다면
            try {
                const web = new Web3(window.ethereum);  // 새로운 web3 객체를 만든다
                setWeb3(web);
            } catch (err) {
                console.log(err);
            }
        }
    }, []);
}

그리고 애플리케이션의 메타마스크 지갑 연동을 위해 버튼을 하나 생성할 것이다.
버튼을 눌렀을 때 connectWallet() 함수가 실행되어 window.ethereum.request 를 수행한다.

function app() {
	const [account, setAccount] = useState('');
	// ...

	const connectWallet = async () => {
        accounts = await window.ethereum.request({
            method: "eth_requestAccounts",
        });

        setAccount(accounts[0]);
    };

	return (
        <div className="App">
            <button
                className="metaConnect"
                onClick={() => {
                    connectWallet();
                }}
            >
                connect to MetaMask
            </button>
            <div className="userInfo">주소: {account}</div>  // 연결된 계정 주소를 화면에 출력합니다
        </div>
    );
}

그리고 npm run start 를 해보자.

polyfills, webpack.. 뭐시라???
애플리케이션이 실행이 되지 않는다.

BREAKING CHANGE: webpack < 5 used to include polyfills for node.js core modules by default. This is no longer the case. Verify if you need this module and configure a polyfill for it.

이 문제를 해결하기 위해 내가 선택한 방법은 web3@0.20.5 이전 버전을 설치하는 것이었다.

결과적으로 아주 잘못된 선택이었다. 왜냐하면 개발을 진행할수록 다른 문제들이 계속 발생했기 때문이다.

결국 해매고 해매다가 아래와 같이 package.json 의 dependency 를 구성하니 해결이 되었다. 꼭 참고하길 바란다.

    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-scripts": "4.0.3",
    "reactstrap": "8.9.0",
    "web3": "^1.6.1"

다시 애플리케이션을 실행하면 아래와 같이 메타마스크(metamask) 지갑 연동이 정상적으로 동작함을 확인할 수 있을 것이다.

3. ERC721 스마트컨트랙트 배포

우리가 이 블로깅에서 하고자 하는 것은 오픈씨(OpenSea) 와 같이 내 지갑주소와 연결된 NFT 들을 보여주는 것이다.

따라서 아래 것들이 필요하다.

  • NFT 를 발행할 스마트컨트랙트
  • NFT 민팅(mint)

하나씩 해보자~!

먼저 특정 계정의 ERC721 토큰 목록을 가져오기 위해서는 ERC721 표준에 정의되어 있는 totalsupply 라는 함수를 이용하면 된다.

Remix 에서 아래와 같이 ERC721 스마트 컨트랙트를 작성한다.

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

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";

contract cozNFTs is ERC721URIStorage, Ownable, ERC721Enumerable {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    constructor() public ERC721("cozNFT", "NFT"){}

    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal
      override(ERC721, ERC721Enumerable) {
        super._beforeTokenTransfer(from, to, tokenId);
    }

    function _burn(
        uint256 tokenId
    ) internal
      override(ERC721, ERC721URIStorage) {
        super._burn(tokenId);
    }

  function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721, ERC721Enumerable)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
    function tokenURI(
        uint256 tokenId
    ) public view
      override(ERC721, ERC721URIStorage) returns (string memory) {
        return super.tokenURI(tokenId);
    }
    
    function mintNFT(address recipient, string memory tokenURI) public onlyOwner returns (uint256) {
        _tokenIds.increment();

        uint256 newItemId = _tokenIds.current();
        _mint(recipient, newItemId);
        _setTokenURI(newItemId, tokenURI);

        return newItemId;
    }
}


시원하게 컴파일(compile) 을 해주고, 롭스텐 네트워크에 deploy 를 수행한다.

컨트랙트를 배포하는 것이니 가스비가 들것이다. 승인도 해주고~~

이제 NFT 를 발행할 준비를 끝냈다.

4. NFT mint

내 스마트 컨트랙트가 배포되었다면 이제 mintNFT 함수로 NFT를 만들 차례이다. 나중에 애플리케이션에서 NFT 의 소유권을 변경하는 기능도 추가할 것이니 넉넉하게 NFT 를 2개 민팅해두자

  • recipient : 리액트 어플리케이션과 연결한 계정 주소
  • tokenURI : NFT 이미지 주소 URI

remix 에서 DEPLOY 부문 하단을 보면 우리가 배포한 smart contract 의 다양한 함수를 실행할 수 있다.

mintNFT 부문의 인자들을 입력하고, 트랜잭션을 발생시키자.

2개를 mint 하였다면, 드디어 우리는 애플리케이션에서 내가 가진 NFT 를 볼 준비가 되었다.

5. web3.js 로 NFT 확인하기

내 지갑주소와 연결된 ERC721 토큰을 화면에 가져오려면 어떤 정보가 필요할까?

  • 해당 스마트컨트랙트 정보 (ABI)
  • 스마트컨트랙트 계약 주소
  • 내 지갑주소

ABI 정보는 src/erc721api.js 에 아래와 같은 형식으로 담는다.
let erc721Abi = []; //ABI 정보

export default erc721Abi;

지갑주소는 메타마스크를 연동하여 그 주소를 곧장 사용할 것이다.
마지막으로 계약 주소는 text 폼을 생성하여 입력하도록 하고, 버튼을 클릭하면 조회 함수를 실행시키도록 해보자.

function App() {
	const [newErc721addr, setNewErc721Addr] = useState();

	// 생략

	const addNewErc721Token = () => {}

	return (
	// 생략
	<div className="newErc721">
	    <input
	        type="text"
	        onChange={(e) => {
	            setNewErc721addr(e.target.value);  // 입력받을 때마다 newErc721addr 갱신
	        }}
	    ></input>
	    <button onClick={addNewErc721Token}>add new erc721</button>
	</div>
	)
}

버튼을 클릭하면 실행되는 함수는 web3.js 를 이용하여 web3.eth.Contract() 라는 함수를 호출한다.

그 코드를 짜보면

function App () {
	const [erc721list, setErc721list] = useState([]);  // 자신의 NFT 정보를 저장할 토큰

	const addNewErc721Token = async () => {
		// 생략
			let arr = [];
		  for (let i = 1; i <= totalSupply; i++) {
		      arr.push(i);
		  }
		  
		  for (let tokenId of arr) {
		      let tokenOwner = await tokenContract.methods
		          .ownerOf(tokenId)
		          .call();
		      if (String(tokenOwner).toLowerCase() === account) {
		          let tokenURI = await tokenContract.methods
		              .tokenURI(tokenId)
		              .call();
		          setErc721list((prevState) => {
		              return [...prevState, { name, symbol, tokenId, tokenURI }];
		          });
		      }
		  }
	}
}

이제 남은 것은 호출 후 결과값을 화면에 뿌려주는 것이다. 결과값 view 콤포넌트는 별도의 콤포넌트로 분리해주자.

src/component 디렉토리를 생성하고, TokenList.js 파일을 아래와 같이 작성한다.

import Erc721 from "./Erc721";

function TokenList({erc721list }) {
    return (
        <div className="tokenlist">
            <Erc721 erc721list={erc721list}  />
        </div>
    );
}

export default TokenList;

Erc721.js 파일도 함께 작성해주자. 이 파일은 TokenList.js 에서 재호출하는 콤포넌트이다. NFT 토큰이 여러개 존재할 수 있으므로 이렇게 분리하는 것이다.

function Erc721({ erc721list }) {
    return (
        <div className="erc721list">
            {erc721list.map((token) => {
                return (
                    <div className="erc721token">
                        Name: <span className="name">{token.name}</span>(
                        <span className="symbol">{token.symbol}</span>)
                        
                        <div className="nft">id: {token.tokenId}</div>
                        <img src={token.tokenURI} width={300} />
                    </div>
                );
            })}
        </div>
    );
}

export default Erc721;

뭔가 빠뜨린 것 같다. App.js 의 return 문에 TokenList.js 를 콤포넌트로 추가해야겠져??

// 생략
import TokenList from "./components/TokenList";

function App () {
	// 생략
	return (
		<div className="App">
			// 생략
			<TokenList erc721list={erc721list} />
		</div>
	)
}

자 이제 테스트를 해보자. 아래와 같이 동작하면 우리는 오픈씨의 기본을 만든 것이나 다름없다!!

6. NFT 전송하기

오픈씨(OpenSea) 의 핵심은 NFT 의 소유권을 변경하는 기능에 있다. 즉, 사용자들 간에 P2P 로 NFT 를 판매하고 구매하는 것이다.

이 파트에서는 위에서 mint 했던 NFT 의 소유권을 이전(NFT 전송) 하는 코드를 짜보자.

전송을 위한 화면 구성은 TokenList.js 에서 수행한다.
그 전에 state 값을 TokenList 콤포넌트에 보내기 위해 App.js 를 수정해준다.

// App.js
function App () {
	return (
	//...
	<TokenList web3={web3} account={account} erc721list={erc721list} />
	)
}

TokenList.js 와 Erc721.js 는 아래와 같이 수정한다.

// TokenList.js
function TokenList({ web3, account, erc721list }) {
    return (
        <div className="tokenlist">
            <Erc721 web3={web3} account={account} erc721list={erc721list} />
        </div>
    );
}

진짜는 Erc721.js 에 있으니.. 전체 코드를 첨부한다.

import erc721Abi from "../erc721Abi";
import {useState, useEffect} from 'react';

function Erc721({ web3, account, erc721list }) {
    const [to, setTo] = useState("");
	
    const sendToken = async (tokenAddr, tokenId) => {
      const tokenContract = await new web3.eth.Contract(
          erc721Abi,
          tokenAddr,
          {
              from: account,
          }
      );
      console.log(tokenContract);
      console.log(`보내는주소: ${account}`);
      console.log(`받는주소: ${to}`);
      console.log(tokenContract.options.address);
      
      tokenContract.options.address = to;

      tokenContract.methods
          .transferFrom(account, to, tokenId)
          .send({
              from: account,
          })
          .on("receipt", (receipt) => {
              setTo("");
          });
    };

    return (
        <div className="erc721list">
            {erc721list.map((token) => {
                return (
                    <div className="erc721token">
                        Name: <span className="name">{token.name}</span>(
                        <span className="symbol">{token.symbol}</span>)
                        <div className="nft">id: {token.tokenId}</div>
                        <img src={token.tokenURI} width={300} />
                        
                                        /* nft 전송 관련 */
                                        <div className="tokenTransfer">
                            To:{" "}
                            <input
                                type="text"
                                value={to}
                                onChange={(e) => {
                                    setTo(e.target.value);
                                }}
                            ></input>
                            <button
                                className="sendErc20Btn"
                                onClick={sendToken.bind(
                                    this,
                                    token.address,
                                    token.tokenId
                                )}
                            >
                                send Token
                            </button>
                        </div>
                    </div>
                );
            })}
        </div>
    );
}

export default Erc721;

그런데 이것을 테스트해보면 가스 수수료 설정을 해야 한다고 뜬다.

처음에는 해당 Contract 에 address 가 설정되지 않았다고 하여 아래 코드를 추가시키니 정상적으로 동작하였다.

tokenContract.options.address = to;

아마 가스비 또한 options 에 추가해야 될 것 같다. 시작부족으로 이번 블로깅에서 마치지 못하는 것이 한이다. 작업이 완료되면 추가 블로깅을 작성토록 하겠다.

개발 회고

추천 받은 회고 방법론을 통해 지금까지 한 내용을 정리해 보겠다.
회고 방법론 : KPT (KEEP, PROBLEM, TRY)

Keep (장점, 유지할 점) :

보잘것 없었던 나에게 Keep 할 점이 있기는 할까? 그래도 희망의 끈을 놓지 않고 프로젝트 팀원들에게 피해를 끼치지 않으려 노력했다.

처음에 이 개발을 시작할 때 다짐했던 것이 잘하든 못하든 '커뮤니케이션' 을 잘하자였다.

그리고 삽질을 포기하지 않았기 때문에(?!) 결국 마지막에 팀원들에게 도움을 줄 수 있었다.
앞으로 남은 프로젝트에서도 꼭 '커뮤니케이션' 과 '성실한 자세' 를 잊지 말아야겠다.

Problem (단점, 변경 또는 버릴 점) :

web3.js 를 처음에 설치할 때 너무너무너무나 많은 시간을 소모한 것이 이번 과제를 어렵게 한 요인이 되었다. 이 문제는 모듈의 문제였는데, 괜히 라이브러리를 다운그레이드하여 진행을 한 것이 나를 더욱 어렵고 지치게 만들지 않았나 싶다.

잘못된 길을 빠르게 수정하고 옳은 길로 가려고 노력하는 태도가 필요할 것 같다.

그리고 사실 구글링으로도 찾기가 어렵다면, 같은 문제를 겪었을 가능성이 있는 다른 조 사람들에게 빨리 물어보는 것도 시간을 단축하기에 큰 도움이 되었을 것이다.

혼자 열심히 고민하는 것도 중요하지만 그 선을 유지하고, 일정에 문제가 생기지 않도록 고집을 버리는 습관을 들이자!

Try (시도할 점, 앞으로의 행동) :

NFT 마켓플레이스는 현재 블록체인 업계에서 가장 핫한 영역이다. 너도나도 NFT 를 발행하고, 그 판매시장을 형성하기 위해 혈안이 되어 있다.

따라서 내가 블록체인 개발자가 되려면 NFT 마켓플레이스를 만들어보는 것은 돈으로도 살 수 없는 자산이 될 것이다.

정말 그럴싸한, 정말 오픈씨(OpenSea) 같은 애플리케이션을 만들어 봐야겠다.

읽어주셔서 감사합니다.

profile
나는야 박지헌

0개의 댓글