
[이번 콘텐츠에서는 리액트로 어플리케이션 뼈대를 만들고 메타마스크 지갑과 연결한 후 최종적으로 Web3.js로 NFT를 받고 전송하기 단계까지 실습해 보는 시간을 갖겠습니다.]
리액트로 어플리케이션 뼈대 만들기
먼저 리액트 어플리케이션 뼈대는 다음과 같습니다.

프로젝트 구성은 아래 그림과 같습니다.

src 폴더 내에 App.test.js, logo.svg, repostWebVitals.js, setupTests.js를 삭제합니다. 그리고 App.js와 Index.js는 아래와 같이 남겨둡니다.


메타마스크 지갑과 연결하기
1. 이더리움 객체 가져오기
먼저, web3를 npm에서 설치합니다.
공급자 객체 연결은 처음 한번만 하면 되기 때문에 useEffect()를 사용해 컴포넌트가 처음 마운트 되었을 때 web3 객체를 연결하도록 하겠습니다.

2. 지갑연결하기
"Connect to Metamask" 버튼을 만들어, 해당 버튼을 누르면 어플리케이션에 메타마스크 지갑을 연결하도록 해야 합니다. 메타마스크 지갑을 연결하여, 메타마스크를 통해 web3를 사용해보겠습니다.
먼저, 다음과 같이 버튼을 하나 만들고 버튼을 누르면 connectWallet() 함수를 실행하는 이벤트를 추가해 보았습니다.

connectWallet()은 메타마스크로부터 계정을 연결하고, 계정 주소를 상태로 저장해야 합니다.
window.ethereum.request({method: 'eth_requestAccounts'}) 는 메타마스크 지갑과 연결된 계정 정보를 받는 JSON-RPC Call API입니다.

그러면 이제 어플리케이션을 실행해볼까요?

이제 메타마스크의 계정과 연결을 시도해보겠습니다.
계정을 연결하면 다음과 같이 주소가 출력됩니다.

짜잔!
ERC721 스마트 컨트랙트 수정하기
Web3.js를 사용해 특정 계정의 ERC721 토큰 목록을 가져오기 위해서는 ERC721에 정의된 totalSupply 함수를 사용해야 합니다. totalSupply 함수는 해당 스마트 컨트랙트에서 발행된 토큰의 갯수를 반환하는 것을 알 수 있었습니다.
openzeppelin에서 totalSupply 함수를 사용하기 위해서는 ERC721Enumerable 컨트랙트를 사용해야 합니다.
ERC721Enumerable 를 추가한 ERC721 스마트 컨트랙트를 새롭게 발행해보겠습니다.
Remix를 열고 다음과 같이 코드를 작성합니다.
// 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;
}
}
openzeppelin을 통해 상속받은 컨트랙트 중 겹치는 함수에 대해 override를 하였습니다. 위의 코드가 완전히 이해되지는 않아 일단은 패쓰하겠습니다.
특정 컨트랙트에 있는 함수를 사용하기 위해서는 컨트랙트의 ABI가 필요합니다.
코드를 컴파일하고, 컨트랙트의 ABI를 복사합니다. src 폴더 안에 erc721Abi.js 파일을 생성하고, 다음과 같이 작성하도록 하겠습니다.
// src/erc721Abi.js
let erc721Abi = [...] // 복사한 ABI를 할당
export default erc721Abi;
App.js에서 import 해야합니다.
// src/App.js
import erc721Abi from "./erc721Abi"
function App() {
// ..
}
메타마스크 지갑과 연동하고, Ropsten 네트워크에 컨트랙트를 배포합니다.
배포 후, mintNFT 함수로 NFT를 2개 민팅합니다. recipient는 리액트 어플리케이션과 연결한 계정의 주소, tokenURI는 자신의 블로그나 노션에 올린 이미지의 링크를 담습니다.
Web3.js로 NFT 받기
먼저 ERC721 컨트랙트 주소를 입력받아, 해당 컨트랙트에서 발행받은 토큰을 가져와보겠습니다.
주소를 입력받고, newErc721addr 상태는 입력받은 컨트랙트 주소를 저장합니다. 컨트랙트 주소를 입력하고 버튼을 누르면 addNewErc721Token() 이 실행됩니다.
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>
)
}
addNewErc721Token() 함수에서는 web3.js를 사용해 컨트랙트 객체를 생성하고, 컨트랙트 객체를 통해 컨트랙트 함수를 호출하여 계정이 가지고 있는 토큰의 목록을 저장하도록 합니다.
web3.eth.Contract()를 사용해 컨트랙트 객체를 만들고, 컨트랙트 객체를 만들기 위해 컨트랙트의 ABI와 주소가 필요합니다.
const addNewErc721Token = async () => {
const tokenContract = await new web3.eth.Contract(
erc721Abi,
newErc721addr
);
}
이제 컨트랙트 객체를 통해 컨트랙트 함수를 실행합니다. 컨트랙트의 이름, 심볼, 총 발행량을 가져옵니다.
Contract 객체를 통해 함수를 실행할 때는 뒤에 call()을 붙여야 합니다.
const addNewErc721Token = async () => {
const tokenContract = await new web3.eth.Contract(
erc721Abi,
newErc721addr
);
const name = await tokenContract.methods.name().call();
const symbol = await tokenContract.methods.symbol().call();
const totalSupply = await tokenContract.methods.totalSupply().call();
}
사용자의 토큰을 찾는 로직은 다음과 같습니다.
1) 토큰의 총 발행량만큼 반복문을 돌게 합니다.
2) Contract.methods.ownerOf() 를 통해 각 토큰의 오너 주소를 받아옵니다.
3) 해당 주소가 dApp으로 연결한 계정 주소와 같은지 확인하고 같다면, Contract.methods.tokenURI() 를 사용해 해당 토큰의 URI 값을 가져오면 됩니다.
4) 토큰 정보를 저장합니다.
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 }];
});
}
}
}
}
컨트랙트 함수 호출은 비동기적으로 실행되기 때문에, 반복문을 사용하기 위해서는 for...of 를 사용해야 합니다.
그다음 src 폴더 안에 components 폴더를 만들고, TokenList.js 파일을 생성하고 다음과 같이 작성합니다.
import Erc721 from "./Erc721";
function TokenList({erc721list }) {
return (
<div className="tokenlist">
<Erc721 erc721list={erc721list} />
</div>
);
}
export default TokenList;
그리고, Erc721.js 파일을 생성하고 다음과 같이 작성합니다.
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에서 컴포넌트를 추가합니다.
// 생략
import TokenList from "./components/TokenList";
function App () {
// 생략
return (
<div className="App">
// 생략
<TokenList erc721list={erc721list} />
</div>
)
}
npm run start를 입력하고,
컨트랙트 주소를 입력하면 해당 컨트랙트를 통해 발행된 토큰 중 연결된 지갑 주소로 민팅된 NFT가 출력되는 것을 확인할 수 있습니다.
Web3.js로 NFT 전송하기
이번 콘텐츠에서는 NFT를 다른 주소로 전송해야합니다. 컴포넌트에서 eth.methods.Contract 로 컨트랙트 객체를 생성하고, ERC721 컨트랙트의 transferFrom 함수를 사용해 토큰을 이동시켜 보겠습니다.
먼저, 컴포넌트에서 web3js를 사용해야 하기 때문에, App.js로부터 web3과 account를 가져옵니다.
// App.js
function App () {
return (
//...
)
}
// TokenList.js
function TokenList({ web3, account, erc721list }) {
return (
<div className="tokenlist">
<Erc721 web3={web3} account={account} erc721list={erc721list} />
</div>
);
}
컨트랙트 객체를 생성하기 위해서는 ABI가 필요하기 때문에 erc721Abi를 import 합니다.
// Erc721.js
import erc721Abi from "../erc721Abi";
이제, 각 토큰마다 전송을 위한 input 박스와 버튼을 추가합니다.
function Erc721({ web3, account, erc721list }) {
const [to, setTo] = useState("");
const sendToken = async (tokenAddr, tokenId) => { }
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>
);
}
input 에는 해당 nft 토큰을 전송할 주소를 넣으며, 주소는 to 상태에 저장됩니다. "send Token" 버튼을 누르면 sendToken() 함수가 실행됩니다.
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,
}
);
tokenContract.methods
.transferFrom(account, to, tokenId)
.send({
from: account,
})
.on("receipt", (receipt) => {
setTo("");
});
};
return (...)
}
sendToken() 함수에서는 web3.js를 이용해 컨트랙트 객체를 만들고, 컨트랙트의 transferFrom 함수를 실행하여 토큰을 to 주소로 전송합니다.
contract.methods().send() 는 컨트랙트 함수를 실행하는 트랜잭션을 만들어 올리는 web3.js 함수입니다. send 함수는 이벤트 프로미스 객체를 반환하며, .on() 로 받을 수 있습니다.
가령, .on('receipt', function (receipt) { ... }) 는 트랜잭션 receipt를 사용할 수 있을 때 이벤트가 트리거되며 두번째 인자인 콜백 함수를 실행합니다.
다음 사항들만 추가 작성하면 이번 콘텐츠 완성입니다.
회고
이번 실습은 처음부터 애를 먹었습니다...
코드에 대한 이해가 덜 되어서 그런지 리액트로 어플리케이션 뼈대를 만들고 메타마스크 지갑과 연동시키는 과정에서 코드 에러가 자주 나서 쉽지 않았습니다ㅠㅠ
NFT 어플리케이션 만들기 있어서 개념이해는 됐지만, 아직까지도 코드에 대한 기본기 부족이 너무 힘들게 만드는 것 같습니다. 이 글을 마치며 코드에 대한 이해도를 더욱 높이려 시간 할애해서 공부하러 가야겠습니다...