
NFT는 특정한 데이터와 연결지을 수 있는 하나의 토큰이다.토큰이 유일무이하다는 것이다.NFT에 대해 좀 더 자세히 이해해보도록 한다.
(그림 출처)
스케치 그림 100장을 그릴 것이다.유일무이하다.갤러리를 통해 스케치 그림들을 판매할 수 있다.스케치 그림을 피카소가 그렸다고 확신하는데 그 이유는 다음과 같다.스케치 그림을 그렸다고 주장하기 때문이다.서명이 그림에 있기 때문이다.스케치 그림을 판매한 갤러리에서 ""피카소 오리지널""임을 증명하는 증명서를 주었기 때문이다.증명서가 장기적으로 유효할 것이라 믿어야 하지만, 이는 매우 간단하다. 그냥 믿으면 되기 때문이다.NFT 영역에서 생각해보자.JPEG 파일같은 디지털 그림을 100장 그린 뒤 판매한다고 가정한다.스마트 컨트랙트를 작성하여 자신의 NFT Collection을 만들 것이다.유일무이한 구별자를 부여할 것이다. (Sketch#1, Sketch#2, Sketch#3, 등등)NFT Collection을 출시 할 준비가 되었을 때, 그는 자신의 이더리움 지갑을 이용해서 NFT Collection에 서명한다.NFT를 판매하는 것은 단순히 NFT를 구매자의 지갑 주소로 transfer하는 과정이다.피카소 오리지널을 피카소 본인으로부터 받게 된다.피카소 오리지널임을 증명할 수 있다.
- 피카소가
서명하고작성한스마트 컨트랙트로부터NFT를 전송 받았기 때문에 이는피카소 오리지널이다.- 피카소가 작품을 만들 때 부여했던
유일무이한 구별자를 통해 이것이오리지널임을 확인 할 수 있다.
유일무이하고 가치 있게만들 수 있다.NFT는 예술 작품, 게임 등이 연관된 프로젝트가 아주 많이 진행되고 있다.로컬 이더리움 네트워크를 통해 실습을 진행한다.스마트 컨트랙트가 컴파일되고 호출되는 것을 로컬 상에서 실습할 수 있다.
스마트 컨트랙트를 작성한다.- 작성한
스마트 컨트랙트를블록체인상에배포(deploy)한다.웹사이트를 만들어, 누구나 쉽게 우리의NFT Collection을Mint할 수 있도록 한다.
이더리움이 어떻게 동작하는지를 이해하는데 좋은 자료 ( 링크 )hardhat 이라는 툴을 이용하여 실습을 진행한다.Node와 npm을 이용해 설치할 수 있다.mkdir epic-nfts cd epic-nfts npm init -y npm install --save-dev hardhat
hardhat에 대한 보다 자세한 설명은 ( [0x01] ) 또는 ( Hardhat Tutorial ) 두 곳을 참조하면 좋을 것 같다.hardhat의 설치를 마쳤다면, 샘플 프로젝트를 만들 수 있다.npx hardhat

Create a basic sample project를 선택한 후 전부 Enter키로 넘기면 아래와 같이 설치되는 과정이 나온다.
hardhat-waffle과 hardhat-ether를 추가로 설치해주어야 한다.npm install --save-dev @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers

OpenZeppelin이라는 라이브러리를 이용할 것이다. 아래와 같이 입력하여 추가로 설치한다.npm install @openzeppelin/contracts

npx hardhat run scripts/sample-script.js

Hardhat으로스마트 컨트랙트를컴파일하여바이트코드를 생성한다.Hardhat은 일회성의로컬 블록체인을 돌린다. 일회성이란,스마트 컨트랙트의 실행이 끝나면 종료됨을 의미한다.Hardhat이스마트 컨트랙트를로컬 블록체인 네트워크로배포(deploy)한다.
스마트 컨트랙트를 작성한다.컨트랙트는 아래와 같다.// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.4; import "hardhat/console.sol"; contract MyEpicNFT { constructor() { console.log("This is my NFT contract. Whoa!"); } }
컨트랙트를 작성한 뒤, 다음 세 작업을 행해야 한다.
Hardhat으로스마트 컨트랙트를컴파일하여바이트코드를 생성한다.Hardhat은 일회성의로컬 블록체인을 돌린다. 일회성이란,스마트 컨트랙트의 실행이 끝나면 종료됨을 의미한다.Hardhat이스마트 컨트랙트를로컬 블록체인 네트워크로배포(deploy)한다.
JavaScript를 이용하여 스크립트를 작성하면 보다 간단히 작업을 할 수 있다.run.js파일을 scripts/디렉토리 밑에 작성한다.const main = async () => { const nftContractFactory = await hre.ethers.getContractFactory('MyEpicNFT'); const nftContract = await nftContractFactory.deploy(); await nftContract.deployed(); console.log("Contract deployed to:", nftContract.address); }; const runMain = async () => { try { await main(); process.exit(0); } catch (error) { console.log(error); process.exit(1); } }; runMain();
const nftContractFactory = await hre.ethers.getContractFactory('MyEpicNFT');
MyEpicNft라는 이름의컨트랙트를 참조한다.
const nftContract = await nftContractFactory.deploy();
스크립트실행 후 일회성로컬 블록체인 네트워크를 만들었다가스크립트가 끝나면 같이네트워크를 파괴한다.
await nftContract.deployed();
컨트랙트가로컬 블록체인 네트워크에 실제로배포되기 전까지 기다린다.
console.log("Contract deployed to:", nftContract.address);
- 한 번
배포되고 나면 해당컨트랙트의 주소를console log를 이용해서 출력한다.
스크립트를 실행한다.npx hardhat ./scripts/run.js

코드 내에서 자주 보이는 hre 에 대한 이해를 위해 ( 이 곳 ) 을 참조해도 좋다.MyEpicNFT.sol을 아래와 같이 수정한다.pragma solidity ^0.8.4; // We first import some OpenZeppelin Contracts. import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; import "@openzeppelin/contracts/utils/Counters.sol"; import "hardhat/console.sol"; // We inherit the contract we imported. This means we'll have access // to the inherited contract's methods. contract MyEpicNFT is ERC721URIStorage { // Magic given to us by OpenZeppelin to help us keep track of tokenIds. using Counters for Counters.Counter; Counters.Counter private _tokenIds; // We need to pass the name of our NFTs token and its symbol. constructor() ERC721 ("SquareNFT", "SQUARE") { console.log("This is my NFT contract. Woah!"); } // A function our user will hit to get their NFT. function makeAnEpicNFT() public { // Get the current tokenId, this starts at 0. uint256 newItemId = _tokenIds.current(); // Actually mint the NFT to the sender using msg.sender. _safeMint(msg.sender, newItemId); // Set the NFTs data. _setTokenURI(newItemId, "blah"); // Increment the counter for when the next NFT is minted. _tokenIds.increment(); } }
코드에서는 상속(inheretance)이라는 개념을 사용하고 있다. ( 참고 )NFT는 ERC721을 스탠다드로 한다. ( 참고 )OpenZeppelin은 이 NFT의 스탠다드를 구현해 놓은 라이브러리이며, 이를 이용해 우리 마음껏 NFT를 커스터마이즈 하는 것이 가능하다.ERC721에 관한 OpenZeppelin의 상세는 ( 이 곳 )에서 확인 할 수 있다.코드를 하나 하나 살펴보자.import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; import "@openzeppelin/contracts/utils/Counters.sol"; import "hardhat/console.sol";
- 우선
OpenZeppelin의컨트랙트들을 불러온다.
contract MyEpicNFT is ERC721URIStorage { ... }
ERC721URIStorage라는컨트랙트를 상속 받고 있다.- 이제
MyEpicNFT는ERC721URIStorage의 함수 등을 쓸 수 있다.
using Counters for Counters.Counter; Counters.Counter private _tokenIds;
OpenZeppelin에서 가져온Counters를 통해tokenID를 보다 쉽게 트래킹 할 수 있다.
constructor() ERC721 ("SquareNFT", "SQUARE") { console.log("This is my NFT contract. Woah!"); }
ERC721의 두 인자로는이름(name)과심볼(symbol)을 전달해야 한다.
function makeAnEpicNFT() public { uint256 newItemId = _tokenIds.current; _safeMint(msg.sender, newItemId); _setTokenURI(newItemId, "blah"); _tokenIds.increment(); } }
makeAnEpicNFT()함수를유저들이 호출하면NFT를 받을 수 있다.newItemId에 현재토큰 아이디를 넣는다. 초기값은0이다._safeMint()에서는newItemId를id로 하여msg.sender에게NFT를Mint한다.
msg.sender를 사용하면 조금 더안전한 코딩을 할 수 있다._setTokenURI()에서는newItemId를id로 하는NFT에 두 번쨰 인자를데이터로 설정한다._tokenIds.increment()를 통해카운트값을 증가시킨다.
tokenURI와 로컬에서 실행해보기tokenURI란 NFT의 실제 데이터가 있는 곳을 말한다.metadata라고 불리는 JSON파일의 링크를 의미한다.metadata의 예시이다.{ "name": "Spongebob Cowboy Pants", "description": "A silent hero. A watchful protector.", "image": "https://i.imgur.com/v7U019j.png" }
structure를 맞춰주는 것이 좋다.structure를 무시하고 작업할 경우, OpenSea와 같은 NFT관련 서비스를 이용하는 데 문제가 발생할 수 있다.ERC721의 metatdata 스탠다드는 이 세 가지 이며, 이를 따름으로써 보다 효과적으로 NFT를 활용할 수 있다.ERC721 metadata에 대한 JSON 링크를 생성할 수 있다.NFT 화 해보도록 한다.{ "name": "Epic Smile", "description": "Look at this big smile.", "image": "https://images.velog.io/images/c0np4nn4/post/39bc50e1-674e-4472-a7a7-1a40c3f59eb0/1.PNG" }

metadata 링크는 https://jsonkeeper.com/b/UZ1G 이다.이제 컨트랙트로 돌아가서 _setTokenURI() 함수를 수정한다.
_setTokenURI(newItemId, "https://jsonkeeper.com/b/UZ1G");
또한 바로 아래 라인에 다음과 같은 코드를 추가하여 Mint가 되었는지 체크한다.
console.log("An NFT w/ ID %s has been minted to %s", newItemId, msg.sender);
컨트랙트코드를 다시 한 번 정리하면 아래와 같다.pragma solidity ^0.8.4; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; import "@openzeppelin/contracts/utils/Counters.sol"; import "hardhat/console.sol"; contract MyEpicNFT is ERC721URIStorage { using Counters for Counters.Counter; Counters.Counter private _tokenIds; constructor() ERC721 ("SquareNFT", "SQUARE") { console.log("This is my NFT contract. Woah!"); } function makeAnEpicNFT() public { uint256 newItemId = _tokenIds.current(); _safeMint(msg.sender, newItemId); // Updated! _setTokenURI(newItemId, "https://jsonkeeper.com/b/UZ1G"); console.log("An NFT w/ ID %s has been minted to %s", newItemId, msg.sender); _tokenIds.increment(); } }
Mint하기run.js파일을 수정해서 makeAnEpicNFT()함수를 호출하기만 하면 된다.run.js파일은 아래와 같다.const main = async () => { const nftContractFactory = await hre.ethers.getContractFactory('MyEpicNFT'); const nftContract = await nftContractFactory.deploy(); await nftContract.deployed(); console.log("Contract deployed to:", nftContract.address); // Call the function. let txn = await nftContract.makeAnEpicNFT() // Wait for it to be mined. await txn.wait() // Mint another NFT for fun. txn = await nftContract.makeAnEpicNFT() // Wait for it to be mined. await txn.wait() }; const runMain = async () => { try { await main(); process.exit(0); } catch (error) { console.log(error); process.exit(1); } }; runMain();
NFT를 Mint하여, 아래 명령어를 입력한 뒤 결과를 확인하면 아래와 같다.npx hardhat run ./scripts/run.js

metadata로 _setTokenURI()를 호출하고 있으므로, newItemId는 둘 다 동일한 값임을 확인할 수 있다.
- 사실은 실행 과정에서
에러가 좀 났었는데, 디버깅 한 과정을 정리해보려고 한다.- 처음 난
에러는 아래와 같다.
current()가 없을리는 만무하지만 한 번 직접 보고 싶었다.- 함수는 진짜 있는데... 그럼 어디가 문제일까? 하고 튜토리얼의
코드와 내코드를 비교해봤다.
- 그 결과, 아래의 한 줄을 빼먹은 것이 발견되었다.
using Counters for Counters.Counter;Solidity에서using A for B구문은A라이브러리의함수를B에 붙이는 구문이다.- 마치
python에서의함수가 첫 번째 인자를self로 두는 것처럼,A라이브러리의함수는B를 첫 번째 인자로 생각할 수 있다.- ( 참고 사이트 )
- 우리의 코드를 다시 보며 조금 쉽게 설명해보고자 한다.
contract MyEpicNFT is ERC721URIStorage { using Counters for Counters.Counter; Counters.Counter private _tokenIds; /* ... */ /* ... */ _tokenIds.increment(); } ```- 우선
Counters.Counter를 자료형으로 하는 변수들이Counters의 함수를 사용할 수 있도록 선언한다.Counters.Counter를 자료형으로 하는private변수_tokenIds를 선언한다.- 일련의 작업 후,
_tokenIds는 자료형이Counters.Counter이므로,Counters의 함수increment(Counter storage param)을 사용할 수 있고, 이 때 함수의인자(param)는 변수인_tokenIds자신이 된다.
로컬 블록체인 네트워크가 아니라 실제 블록체인 네트워크에 컨트랙트를 배포하고 이를 이용해보도록 한다.테스트넷의 하나인 Rinkeby를 활용한다.블록체인 네트워크상에서 블록체인에 변화를 주는 모든 행위는 트랜잭션이다.블록체인 네트워크상에 우리의 컨트랙트를 배포한다는 것은 대략 아래와 같다.블록체인 네트워크에 참여하는 모든 채굴자들에게 우리의 컨트랙트를 보낸다.채굴자들이 새로운 블록을 생성할 때, 우리의 컨트랙트를 포함한 블록을 생성한다.새로운 블록이 모든 채굴자들에게 브로드캐스트되면, 누구나 우리의 컨트랙트를 확인할 수 있다.채굴자들에게 우리의 컨트랙트 정보를 전달하는 효과적인 방법으로 Alchemy( 링크 )를 이용하는 것이 있다.테스트넷을 위한 API Key를 확보하면 된다.이더리움 메인넷이 아니라 테스트넷을 이용하는 이유는 다음과 같다.메인넷과 유사한 환경에서 프로젝트를 테스트 하는 것이 가능하다.가짜 돈을 사용하기 때문에 테스트에 들어가는 비용이 없다.테스트넷에서 해 볼 것들은 아래와 같다.트랜잭션을 브로드캐스트하기채굴자들에 의해 선택되기를 기다리기블록으로 만들어지기를 기다리기채굴자들에게 우리의 트랜잭션이 포함된 새로운 블록이 브로드캐스트되기를 기다리기테스트를 위한 가짜 돈을 확보할 수 있는 방법 같다.0.1 ETH를 받을 수 있지만, 이 정도면 테스트에는 충분하다.배포를 위한 스크립트인 deploy.js를 run.js와 따로 구분해서 작성하도록 한다.const main = async () => { const nftContractFactory = await hre.ethers.getContractFactory('MyEpicNFT'); const nftContract = await nftContractFactory.deploy(); await nftContract.deployed(); console.log("Contract deployed to:", nftContract.address); // Call the function. let txn = await nftContract.makeAnEpicNFT() // Wait for it to be mined. await txn.wait() console.log("Minted NFT #1") txn = await nftContract.makeAnEpicNFT() // Wait for it to be mined. await txn.wait() console.log("Minted NFT #2") }; const runMain = async () => { try { await main(); process.exit(0); } catch (error) { console.log(error); process.exit(1); } }; runMain();
배포하기 전, hardhat.config.js를 아래와 같이 수정한다.require('@nomiclabs/hardhat-waffle'); module.exports = { solidity: '0.8.0', networks: { rinkeby: { url: 'YOUR_ALCHEMY_API_URL', accounts: ['YOUR_PRIVATE_RINKEBY_ACCOUNT_KEY'], }, }, };
Alchemy에서 가져온 API주소와 지갑의 개인 키를 입력한다.컨트랙트가 Rinkeby 네트워크상에 배포되는 것을 확인한다.npx hardhat run ./scripts/deploy.js --network rinkeby

코드내부에서 Mint를 했었으므로, opensea에 가보면 내 지갑에 NFT가 있는 것을 확인할 수 있다.
NFT는 더 이상 가치가 없을 것이다블록체인 상에 이 데이터들을 저장하는 방법이 있다.On-Chain (온체인)이라 부르고, 이를 통해 가치가 지속되리라는 신뢰를 얻을 수 있어 매우 중요하다.NFT의 이미지 데이터를 저장하는 방법으로 SVG를 이용한다.SVG는 이미지를 코드를 통해 렌더링 할 수 있는 방법으로 활용할 수 있다.SVG로 작성한 코드를 Base64를 이용해서 Encoding하여 활용할 수 있다.SVG 코드를 encoding한 뒤, 아래 형식에 맞춰 URL로 사용할 수 있다.data:image/svg+xml;base64,INSERT_YOUR_BASE64_ENCODED_SVG_HERE
코드로 이미지를 저장할 수 있으므로, 블록체인상에 쉽게 기록할 수 있음을 알 수 있다.JSON Metadata도 SVG와 마찬가지로 Base64를 이용해 On-Chain에 데이터를 기록할 수 있다.Base64로 인코딩된 SVG를 이용해 JSON Metadata를 다시 작성하면 아래와 같다.{ "name": "EpicLordHamburger", "description": "An NFT from the highly acclaimed square collection", "image": "data:image/svg+xml;base64,INSERT_YOUR_BASE64_ENCODED_SVG_HERE" }
JSON Metadata도 전부 Base64로 인코딩한다.JSON Metadata는 아래의 형식에 맞게 완성한 다음, 우리가 앞서 컨트랙트에서 _setTokenURI()함수의 두 번째 인자로 전달한 JSON Metadata URI값으로 사용할 수 있다.data:application/json;base64,INSERT_YOUR_BASE64_ENCODED_JSON_HERE
앞선 내용들을 토대로 컨트랙트를 수정한 뒤 배포하면 새로운 컨트랙트의 주소를 얻는다.

OpenSea에서 새로운 NFT가 Mint된 것을 확인할 수 있다.
[ 링크 ]

NFT를 생성해보도록 한다.NFT를 생성한다.블록체인 에서는 모든 정보가 공개되어 있고, 이는 곧 유사 난수를 만들더라도 모든 값들이 예측 가능함을 의미한다.랜덤을 구현하는 것이 거의 불가능하다.유사 난수를 사용하도록 한다.uint256(keccak256(string(abi.encodePacked("FIRST_WORD", Strings.toString(tokenId)))));
NFT에서 사용할 단어와 tokenId를 abi.encodePacked() 로 연결한다.
Solidity에서Concatenation을 하는 방법으로abi.encodePacked()를 사용하는 것이 가능하다.
string으로 바꾼다.random()을 통해 난수를 생성한다.
랜덤을 구현하는 방법으로 ( 링크 ) 에서 소개된 방법을 이용할 수도 있다.
컨트랙트에서 NFT를 생성하는 함수는 아래와 같다.
tokenURI값이 하드코딩된 부분을 포함해 컨트랙트 전체를 수정한다.SVG데이터에 대한 베이스 값으로 아래와 같이 변수를 하나 선언한다.string baseSvg = "<svg xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='xMinYMin meet' viewBox='0 0 350 350'><style>.base { fill: white; font-family: serif; font-size: 24px; }</style><rect width='100%' height='100%' fill='black' /><text x='50%' y='50%' class='base' dominant-baseline='middle' text-anchor='middle'>";
주어 + 동사 + 목적어 형태로 NFT를 생성해보려 한다.string[] firstWords = ["Davaron", "Alice", "Bryan", "Spencer", "Stephani", "Danny"]; string[] secondWords = ["eats", "takes", "hits", "makes", "throws", "has"]; string[] thirdWords = ["chicken", "baseball", "computer", "cookies", "battery", "time"];
랜덤값을 반환하는 함수 random()을 선언한다.랜덤값을 만드는 방법으로, keccak256()함수를 활용한다.function random(string memory input) internal pure returns (uint256) { return uint256(keccak256(abi.encodePacked(input))); }
랜덤으로 단어를 뽑아내는 함수를 만든다.function pickRandomWords(uint256 tokenId) public view returns (string memory) { uint256 rand; string sentence; rand = random(string(abi.encodePacked("FIRST_WORD", Strings.toString(tokenId)))); rand = rand % firstWords.length; sentence += firstWords[rand]; rand = random(string(abi.encodePacked("SECOND_WORD", Strings.toString(tokenId)))); rand = rand % secondWords.length; sentence += secondWords[rand]; rand = random(string(abi.encodePacked("THIRD_WORD", Strings.toString(tokenId)))); rand = rand % thirdWords.length; sentence += thirdWords[rand]; return sentence; }
NFT를 생성하는 함수, makeAnEpicNFT()를 위에 추가한 부분들에 맞춰 아래와 같이 수정한다.function makeAnEpicNFT() public { uint256 newItemId = _tokenIds.current(); string memory sentence = pickRandomWords(newItemId); string memory finalSvg = string(abi.encodePacked(baseSvg, sentence, "</text></svg>")); console.log("\n=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-="); console.log("SVG data : " + finalSvg); console.log("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n"); string memory json = Base64.encode( bytes( string( abi.encodePacked( '{"name": "', sentence, '", "description": "A highly acclaimed collection of squares.", "image": "data:image/svg+xml;base64,', Base64.encode(bytes(finalSvg)) , '"}' ) ) ) ); string memory finalTokenUri = string(abi.encodePacked("data:application/json;base64,", json)); _safeMint(msg.sender, newItemId); _setTokenURI(newItemId, finlaTokenUri); _tokenIds.increment(); console.log("An NFT w/ ID %s has been minted to %s", newItemId, msg.sender);
컨트랙트의 실행 결과는 아래와 같다.
deploy.js 코드를 아래와 같이 조금 수정한다.NFT를 Mint해보도록 한다.
NFT가 하나 하나 Minting되는 것을 확인할 수 있다.
컨트랙트 주소로 향해서 NFT를 확인해본다.
이미지가 안보이는 문제가 발생했다..
- 천천히 디버깅해보니,
SVG를 입력하는 과정에서 오타가 있었다...
- 하지만.. 위 문제를 해결했는데도 이미지 랜더링이 되지 않았다.
- 가능성은 아래와 같다.
Base64코드의 결함컨트랙트내 코드의 결함- 우선
Base64의 경우 아래와 같이 정상적으로 작동함을 알 수 있다.
- 따라서, 문제는
컨트랙트코드에 있음을 생각할 수 있다.- 튜토리얼에서 제공하는 예제 코드와 내가 작성한 코드를 비교해본다.
- 그 결과,
SVG에 대한 구문에서한 글자가 다른 것을 찾아냈다.http로 적어야 할 것을https로 적어서 생긴 문제였던 것이다.- 해당 코드를 수정한 후에는 정상적으로
tokenURI를 이용할 수 있었다.- ( NFT Preview Site )를 통해 정상 작동 함을 확인했다.
디버깅을 끝내고 다시 시도해보았다.컨트랙트 주소가 출력되고, NFT가 Mint되는 것을 확인할 수 있다.
OpenSea를 통해 Minting한 NFT를 확인할 수 있다.
이전 튜토리얼과 마찬가지로 이번에도 Replit을 이용한다.
구축할 웹사이트에서 Metamask를 연결하고 앞서 만든 NFT Minting 컨트랙트를 이용해보도록 한다.
튜토리얼에서 제공하는 템플릿을 포크하여 아래와 같이 간단히 사이트를 만들 수 있다.

지갑을 사이트에 연결한다는 것은 로그인하는 것과 같다고 생각하면 된다.window.ethereum()을 이용해서 쉽게 구현할 수 있다.App.jsx에서 window.ethereum() 을 이용해 지갑 연결 여부를 체크하는 코드는 아래와 같다.import React, { useEffect } from "react"; /* ... */ /* ... */ const App = () => { const checkIfWalletIsConnected = () => { /* * First make sure we have access to window.ethereum */ const { ethereum } = window; if (!ethereum) { console.log("Make sure you have metamask!"); return; } else { console.log("We have the ethereum object", ethereum); } } /* ... */ /* ... */ useEffect(() => { checkIfWalletIsConnected(); }, []) /* ... */ /* ... */ };
window.ethereum()으로 지갑 연결 여부를 확인한 다음은 접근 권한을 확인해야 한다.스마트 컨트랙트를 호출할 수 있다.App.jsx의 코드 내용은 아래와 같다.import React, { useEffect, useState } from "react"; /* ... */ /* ... */ const App = () => { /* * Just a state variable we use to store our user's public wallet. Don't forget to import useState. */ const [currentAccount, setCurrentAccount] = useState(""); /* * Gotta make sure this is async. */ const checkIfWalletIsConnected = async () => { const { ethereum } = window; if (!ethereum) { console.log("Make sure you have metamask!"); return; } else { console.log("We have the ethereum object", ethereum); } /* * Check if we're authorized to access the user's wallet */ const accounts = await ethereum.request({ method: 'eth_accounts' }); /* * User can have multiple authorized accounts, we grab the first one if its there! */ if (accounts.length !== 0) { const account = accounts[0]; console.log("Found an authorized account:", account); setCurrentAccount(account) } else { console.log("No authorized account found") } } /* ... */ /* ... */ useEffect(() => { checkIfWalletIsConnected(); }, []) /* ... */ /* ... */ };
useState() 를 사용하여 계정 정보를 저장async키워드로 연결 체크를 비동기로 처리계정 접근 권한 유무를 확인계정들 중 맨 첫 번째 계정을 사용버튼컴포넌트와 구현한 기능을 연결한다.App() 내부에 코딩하면 된다./* * Implement your connectWallet method here */ const connectWallet = async () => { try { const { ethereum } = window; if (!ethereum) { alert("Get MetaMask!"); return; } /* * Fancy method to request access to account. */ const accounts = await ethereum.request({ method: "eth_requestAccounts" }); /* * Boom! This should print out public address once we authorize Metamask. */ console.log("Connected", accounts[0]); setCurrentAccount(accounts[0]); } catch (error) { console.log(error) } } // Render Methods (onClick={connectWallet}) const renderNotConnectedContainer = () => ( <button onClick={connectWallet} className="cta-button connect-wallet-button"> Connect to Wallet </button> );
계정이 연결된 후에는 아래의 코드와 같이 버튼을 다르게 보여준다든지 하는 추가적인 기능을 구현할 수 있다.{currentAccount === "" ? ( renderNotConnectedContainer() ) : ( <button onClick={null} className="cta-button connect-wallet-button"> Mint NFT </button> )}
Mint NFT 버튼에 대해 onClick을 null로 정해둔다.NFT Mint웹 사이트를 통해서 NFT를 Mint하는 기능을 구현해본다.코드를 App.jsx에서 connectWallet()함수 안에 추가한다.const askContractToMintNft = async () => { const CONTRACT_ADDRESS = "INSERT_YOUR_DEPLOYED_RINKEBY_CONTRACT_ADDRESS"; try { const { ethereum } = window; if (ethereum) { const provider = new ethers.providers.Web3Provider(ethereum); const signer = provider.getSigner(); const connectedContract = new ethers.Contract(CONTRACT_ADDRESS, myEpicNft.abi, signer); console.log("Going to pop wallet now to pay gas...") let nftTxn = await connectedContract.makeAnEpicNFT(); console.log("Mining...please wait.") await nftTxn.wait(); console.log(`Mined, see transaction: https://rinkeby.etherscan.io/tx/${nftTxn.hash}`); } else { console.log("Ethereum object doesn't exist!"); } } catch (error) { console.log(error) } }
코드를 차근 차근 살펴보자.const provider = new ethers.providers.Web3Provider(ethereum); const signer = provider.getSigner();
ehters는프론트엔드와컨트랙트가 상호작용할 수 있도록 돕는라이브러리이다.코드상단에import { ethers } from "ethers";를 추가하여 사용할 수 있다.Provider는이더리움 노드와 상호작용 하기 위해 사용한다.Alchemy를 통해컨트랙트를배포했던 것과 마찬가지로, 여기서는Metamask를 이용해이더리움 노드들과 통신한다.Signer에 관한 내용은 ( 링크 )를 참조하면 된다.
const connectedContract = new ethers.Contract(CONTRACT_ADDRESS, myEpicNft.abi, signer);
컨트랙트와 실제로연결(connection)하도록 하는코드이다.컨트랙트 주소,ABI파일, 그리고signer가 인자로 필요하다.블록체인상의컨트랙트와 상호작용 하기 위해서는 항상 이 세 가지가 필요하다.
console.log("Going to pop wallet now to pay gas...") let nftTxn = await connectedContract.makeAnEpicNFT(); console.log("Mining...please wait.") await nftTxn.wait(); console.log(`Mined, see transaction: https://rinkeby.etherscan.io/tx/${nftTxn.hash}`);
- 나머지
코드는 앞서deploy.js에서도 작성했던 코드이다.
Null로 설정했던 onClick 부분을 수정한다.return ( {currentAccount === "" ? renderNotConnectedContainer() : ( /** Add askContractToMintNft Action for the onClick event **/ <button onClick={askContractToMintNft} className="cta-button connect-wallet-button"> Mint NFT </button> ) } );
ABI는 컨트랙트를 컴파일 할 때 생성되는 파일이다.hardhat은 컴파일 작업 후, artifacts/디렉토리에 ABI파일을 저장해둔다.ABI는 웹 앱이 컨트랙트와 상호작용 할 때 필요한 파일이다. ( 링크 )Replit을 이용해서 웹 앱을 만들 때는, artifacts/contracts/MyEpicNFT.sol/MyEpicNFT.json 에 존재하는 ABI파일의 내용을 복사한 뒤 Replit으로 돌아와 src/utils/MyEpicNFT.json에 저장하여 활용할 수 있다.App.jsx에 추가하면 된다.import myEpicNft from './utils/MyEpicNFT.json';
웹 앱에서 NFT를 Minting하는 것도 모두 구현해 보았다.App.jsx 파일의 내용은 ( 깃허브 ) 를 참조하면 된다.OpenSea에서 확인할 수 있는 NFT는 아래 형식의 URL을 따라 확인할 수 있다.https://testnets.opensea.io/assets/INSERT_CONTRACT_ADDRESS_HERE/INSERT_TOKEN_ID_HERE
https://testnets.opensea.io/assets/0x40553d47327d0ffcfc155480f4cad52a086c505d/16/sell
rarible 사이트를 이용해서도 확인할 수 있다.https://rinkeby.rarible.com/token/INSERT_CONTRACT_ADDRESS_HERE:INSERT_TOKEN_ID_HERE
https://rinkeby.rarible.com/token/0x40553d47327d0ffcfc155480f4cad52a086c505d:16?tab=details
웹 사이트(웹 앱)은 tokenId를 알지 못하므로, 컨트랙트를 수정해서 이를 추가해주도록 한다.Solidity의 Event를 사용할 것이다.Event는 webhook같은 역할로 활용할 수 있다.event 키워드로 이벤트를 선언하고, emit 키워드로 해당 이벤트를 호출할 수 있다.event NewEvent(address sender, uint256 data); /* ... */ emit NewEvent(msg.sender, data);
트랜잭션이 블록으로 채굴될 때, NFT가 반드시 Mint되는 것은 아니기 때문에 Event를 통해 컨트랙트는 웹 앱에게 NFT가 정상적으로 Mint되었음을 알릴 수 있게 된다.컨트랙트를 수정하였으므로, 아래 세 단계를 거쳐 전체 파일을 수정한다.
Rinkeby로 다시배포함App.jsx의컨트랙트 주소를 업데이트함.ABI파일을 업데이트 함.
마지막으로, 프론트엔드 코드에 아래 코드 등을 추가하면 끝난다. ( 전체 코드 )
connectedContract.on("NewEpicNFTMinted", (from, tokenId) => { console.log(from, tokenId.toNumber()) alert(`Hey there! We've minted your NFT. It may be blank right now. It can take a max of 10 min to show up on OpenSea. Here's the link: <https://testnets.opensea.io/assets/${CONTRACT_ADDRESS}/${tokenId.toNumber()}>`) });