이번 포스팅에서는 NFT의 개념에 대해 알아보고 Remix를 사용해 테스트넷에 배포한 NFT를 OpenSea 마켓에 올려보면서 전체적인 흐름을 파악해보고자 한다.
< 목차 >
- NFT ??
- NFT & 이미지 ??
- Remix로 컨트랙트 배포하기
- OpenSea에 NFT 올리기
1. NFT ??
NFT는 "대체불가능토큰" ( Non-Fungible Token ) 의 약자로 말 그대로 하나의 토큰을 다른 토큰으로 대체하는 것이 불가능한 토큰을 일컫는다. 기존에 우리가 만들었던 것이 ERC-20 토큰이라면 NFT는 ERC-721 을 표준으로 해서 만들어진 토큰이라고 생각하면 된다. ERC-20이 아닌 ERC-721로 발행되는 토큰은 대체 불가능하며 모두 제 각각의 가치(value)를 갖고 있다.
ERC-20 으로 발행된 토큰은 fungible Token , ERC-721로 발행되는 토큰은 Non-fungible Token 이라고 한다. 둘의 차이에 대해 좀 더 얘기하자면 fungible Token의 경우 발행되는 모든 토큰의 값어치가 일정하다. 예를 들어 ERC-20으로 JwToken을 1000개 발행할 경우, 1000개의 JwToken은 모두 동일한 값어치를 갖는다. fungible Token 에서는 토큰의 수량만이 의미를 갖는다고 한다면 ERC-721로 발행되는 Non-fungible Token의 경우에는 토큰 하나 하나가 의미를 갖게 된다. 즉, ERC-721로 JwToken 을 1000개 발행할 경우, 1000개의 JwToken 하나하나가 모두 다른 의미를 갖고 있는 셈이다.
NFT를 영화관 티켓에 비유해 이해해 볼 수도 있다. 1000개의 영화관 티켓이 있다고 했을 때 이 1000개의 티켓은 모두 동일한 값어치를 가지는 티켓이 아니다. 티켓마다 고유의 번호가 있고 각각의 티켓이 가리키고 있는 좌석이 존재한다. fungible Token의 경우 모든 토큰이 동일한 값어치 혹은 의미를 갖지만 Non-fungible Token은 마치 영화관 티켓처럼 토큰 하나하나가 각각의 고유한 의미를 가지고 있는 것이다.
개념적으로 NFT가 어떤 것인지에 대해 알았다면 다음 과제는 이러한 개념을 Code 상으로 어떻게 구현할 것인지에 대해 생각해봐야 한다. 하지만 그리 어렵지 않게 구현이 가능하다. 토큰을 하나 생성할 때마다 고유한 키값을 부여해주기만 하면 된다. 기존에 ERC-20 으로 토큰을 발행할 때는 mint( ) 함수를 이용해 다음과 같이 발행했었다.
// ERC20 토큰 발행
mint(msg.sender, 1000); // 인자값: 발행자, 총 발행량
ERC-721에서는 토큰을 발행할 때마다 고유한 값을 부여해주면서 발행해주면 된다. 마치 primary key와 같은 고유한 키 값을 인자값으로 전달하여 다음과 같은 방식으로 토큰을 발행하게 된다.
// ERC721 토큰 발행
mint(msg.sender, 1);
mint(msg.sender, 2);
mint(msg.sender, 3);
mint(msg.sender, 4);
// 인자값으로 고유한 키값 전달
인자값으로 전달된 1, 2, 3, 4는 ERC-20에서처럼 수량을 의미하는 것이 아닌 1번에 대한 토큰, 2번에 대한 토큰,, 이러한 의미를 가지고 있는 고유한 값이라고 생각하면 된다. 이렇게 토큰 하나하나에 고유한 키 값이 부여되면서 발행되기 때문에 특정 계정이 보유하고 있는 토큰을 표현할 때도 ERC-20 과는 다른 방식으로 해석된다.
mapping(address => uint) public balances;
// 0x1234 => 2000
위와 같은 mapping(address => uint) 데이터 타입으로 balances가 표현될 때 ERC20 토큰의 경우 특정 계정이 총 몇개의 토큰을 보유하고 있는지로 해석된다. 예를 들어 0x1234 => 2000 이라고 표현되어 있을 때 0x1234 라는 계정이 총 2000개의 토큰을 보유하고 있다는 의미로 해석된다. 하지만 ECR721 토큰이라면 단순히 총 2000개의 토큰이라는 의미가 아닌, 1~2000까지의 고유한 키값을 가진 토큰을 보유하고 있다는 의미로 해석된다. 그리고 이러한 이유로 ERC721 에는 다음과 같은 데이터 타입 역시 존재한다.
// ERC721
mapping(uint => address) public owned;
여기서 mapping(uint => address) 데이터 타입의 uint는 토큰의 고유 번호를 의미한다. 1 => 0x1234 와 같이 표현되어 있다면 1번 토큰의 주인은 0x1234 계정이라는 의미로 해석할 수 있다.
2. NFT & 이미지 ??
NFT가 어떠한 토큰을 의미하는지에 대해서 알아보았는데 우리가 익히 알고 있는 OpenSea와 같은 NFT 마켓플레이스에서는 다음과 같은 이미지들을 NFT라고 말하면서 판매하고 있다.
NFT는 고유한 키값을 가지고 있는 토큰인데,, 토큰 안에 어떻게 이미지를 넣는 것일까?? 과연 스마트 컨트랙트 내용 안에 이미지를 넣는 것이 가능할까?? 네트워크 상에서는 이미지도 하나의 텍스트이기 때문에 이미지를 인코딩한 내용을 바이트 단위로 잘라서 넣는다면 불가능 할 것 같지는 않다. 하지만 이미지를 이더리움 네트워크 상에 배포하는 미친 짓은 누구도 하지 않을 것이다. 이미지 하나가 소비하는 가스비를 생각한다면 굉장히 비효율적인 방식이기 때문이다.
그렇다면 NFT 안에 이미지를 저장하는 것은 어떠한 방식으로 이루어질까? 실제 NFT가 발행되어 tokenID가 1번인 토큰이 존재한다고 하자. 1번 토큰은 하나의 URL을 가지게 된다. 예를 들면 "http://localhost:3000/metadata/1.json" 과 같은 URL이 1번 토큰에 매칭되는 URL이 되는 것이다. 그리고 2번 토큰에는 "http://localhost:3000/metadata/2.json" 과 같은 URL이 매칭되며 각각의 토큰에 매칭되는 URL을 통해 다음과 같은 JSON 형태의 데이터를 가져올 수 있다.
{
"name": "원숭이 이미지",
"description": "이거 비싼거임",
"image": "http://localhost:3000/image/1.jpg",
"attributes": [...]
}
다시말해, NFT를 발행하게 되면 토큰마다 고유한 키 값이 부여되고 각각의 키 값에 매칭되는 URL을 통해 위와 같은 JSON 형태의 데이터를 가져올 수 있게 되는 것이다. 그리고 해당 JSON 파일 안에는 image 경로가 들어가 있다. 우리가 NFT에 이미지 혹은 영상, 음원 등을 넣을 수 있다는 것은 결국 JSON 데이터 안에 존재하는 이미지, 영상, 음원의 경로를 읽어서 화면 상에 표출해주는 것일 뿐이다.
// 2번 토큰 => "http://localhost:3000/metadata/1.json"
const response = axios.post("http://localhost:3000/metadata/1.json");
const image = response.data.image;
<img src="{image}" />
3. Remix로 컨트랙트 배포하기
스마트 컨트랙트의 작성에는 truffle을 사용하고 컨트랙트의 배포는 Remix를 사용해서 진행해보고자 한다. Remix는 스마트 컨트랙트의 작성 및 배포를 쉽게 할 수 있도록 도와주는 툴이다. 그냥, Ethereum IDE 라고 생각하면 된다.
$ mkdir truffle # truffle 디렉토리 생성
$ cd truffle # 디렉토리 이동
$ truffle init
$ npm install -g @remix-project/remixd
$ cd contracts # truffle/contracts 디렉토리 이동
$ remixd -s . --remix-ide https://remix.ethereum.org
우선 @remix-project/remixd 를 글로벌로 설치해주도록 하자. 그리고 truffle init 을 실행한 다음 truffle/contracts/ 디렉토리 안으로 이동하여 remixd -s . --remix-ide https://remix.ethereum.org 를 입력해 실행시켜주면 된다.
이제 https://remix.ethereum.org/ 에 접속한 다음 Workspaces를 localhost로 바꿔준다.
Workspaces가 localhost로 변경이 완료되었다면 다음과 같이 로컬 컴퓨터에 있는 디렉토리들이 보이는 것을 확인할 수 있다. 이것으로 Remix 세팅은 완료되었고 이제 OpenZeppelin을 사용해 ERC721 토큰을 만들어 보도록 하자.
VSCode 터미널에서 OpenZeppelin-solidity를 설치해준 다음 아래의 디렉토리 경로로 들어가보자.
$ npm install openzeppelin-solidity
$ # 디렉토리 경로
node_modules/openzeppelin-solidity/contracts/token/ERC721/extensions/ERC721Enumerable.sol
이 ERC721Enumerable.sol 파일을 이용해 contracts 디렉토리 안에 Minting 컨트랙트를 간략하게 작성해보고자 한다. Minting 컨트랙트의 전체 코드는 다음과 같다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import '../node_modules/openzeppelin-solidity/contracts/token/ERC721/extensions/ERC721Enumerable.sol';
contract Minting is ERC721 {
// ERC721 생성자 함수 실행 ERC721(_name, _symbol)
constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol) {
}
function _minting(uint _tokenId) public {
_mint(msg.sender, _tokenId);
// _tokenId : 토큰의 고유한 키값 , msg.sender : 토큰 받을 계정
}
function tokenURI(uint _tokenId) public override pure returns (string memory) {
return 'https://gateway.pinata.cloud/ipfs/QmPwjnvWYN4etA5eW4yAbWCTy2ukEC1Jj5417VLGyH5XpU/1/1.json';
}
// NFT에 대한 정보를 JSON 파일에 보관
}
import 문을 통해 openzeppelin-solidity에서 제공해주는 파일을 가져온 다음 Minting 컨트랙트가 ERC721을 상속 받도록 하였다. 그리고 constructor( ) 함수 옆에 ERC721( _name, _symbol ) 을 작성해 상속받은 ERC721의 생성자 함수를 같이 실행시켜주도록 하였다. 클래스 문법에서 자식 클래스가 부모 클래스를 상속 받을 때 constructor( ) 함수 안에서 super( )를 사용하는 것을 Solidity에서는 이와 같이 constructor( ) 함수 옆에 바로 작성한다고 생각하면 된다.
_minting( ) 함수는 인자값으로 _tokenId 값을 받도록 했으며 해당 함수 안에서 _mint( ) 함수를 호출해 함수를 실행한 msg.sender에게 _tokenId 값에 따라 생성된 토큰을 지급해주었다. 여기서 인자값으로 전달하는 _tokenId 값이 바로 토큰을 생성할 때 부여하게 되는 고유한 키 값이라고 보면 된다.
tokenURI( ) 함수는 인자값으로 전달받은 _tokenId 값과 매칭되는 URI를 return 해주는 함수이다. 여기서는 간단하게 구현하기 위해 고정된 URI를 반환하도록 하였다.
Minting 컨트랙트 작성을 완료한 다음 Remix를 살펴보면 다음과 같이 작성된 내용으로 최신화가 된 것을 확인할 수 있다.
참고로 Remix에서는 node_modules/ 디렉토리까지 접근을 해주기 때문에 import 구문에서 node_moduels 경로 부분을 제외해주도록 하자.
컨트랙트 작성을 완료했으므로 컴파일을 진행해줘야 하는데 왼쪽 세번째 탭에서 작성한 솔리디티 버전에 맞게 컴파일러를 선택한 다음 Auto compile을 체크해 주도록 하자.
컴파일이 완료되었다면 왼쪽 네번째 탭에서 배포를 진행해주면 된다. ENVIRONMENT에서 연결할 곳을 Injected Provider - Metamask 로 지정하면 메타마스크에서 다음과 같은 연결 문구가 나오게 된다.
여기서는 Rinkeby 테스트넷에 배포를 진행하고자 Rinkeby 테스트넷 계정과 연결을 진행하였다. 이후 ACCOUNT에서 계정을 선택한 다음 CONTRACT에서 우리가 작성한 Minting.sol 파일을 선택해준다. 마지막으로 DEPLOY 부분에서 Minting 컨트랙트의 constructor( ) 함수에서 작성한 인자값을 전달해 준 다음 transact를 누르면 배포가 완료된다.
컨트랙트 배포로 인해 트랜잭션이 발생하였으므로 메타마스크에서 전송 처리를 해주면 된다.
컨트랙트 배포가 완료되면 Deployed Contracts 부분에서 아래와 같이 배포된 컨트랙트 내용을 조회할 수 있다.
abi 파일을 기준으로 현재 우리가 배포한 컨트랙트가 어떠한 함수들을 가지고 있는지 볼 수 있으며 함수의 실행 역시 가능하다. call( ) 메소드의 경우 하늘색으로 표시되며 send( ) 메소드는 주황색으로 표시된다. call( )은 단순히 상태변수를 조회하는 용도이기에 트랜잭션이 발생하지 않지만 send( ) 메소드의 경우 트랜잭션이 발생하게 된다. 그리고 아직은 구현되지 않은 payable 함수의 경우에는 빨간색으로 표시된다.
그리고 위와 같이 balanceOf( ) 함수를 통해 특정 계정이 소유하고 있는 토큰 개수를 조회할 수 있으며 (여기서는 컨트랙트 배포자의 계정을 조회함) _tokenId 값을 23으로 하여 _minting( ) 함수를 실행시키는 트랜잭션을 일으킨 다음 ownerOf( ) 함수를 통해 tokenId 값이 23인 토큰을 소유하고 있는 계정을 조회할 수도 있다.
여기까지 Remix로 Rinkeby 테스트넷에 Minting 컨트랙트를 배포하는 작업을 완료하였다. 그리고 컨트랙트 배포자 계정으로 tokenId 값이 23인 토큰 역시 발행해 놓은 상태이다. 이제 OpenSea에서 배포한 NFT를 올리는 작업을 진행해보자.
4. OpenSea에 NFT 올리기
OpenSea NFT 마켓 플레이스에 NFT를 올리는 방법은 크게 두 가지이다.
- OpenSea 사이트를 통해 NFT를 등록하는 방법
- 직접 스마트 컨트랙트를 작성해서 NFT를 등록하는 방법
직접 스마트 컨트랙트를 작성해서 NFT를 등록할 경우 우리가 원하는 여러가지 기능들을 추가적으로 넣을 수 있다는 장점이 있다. 앞서 작성하고 배포한 Minting 컨트랙트를 통해 발행한 tokenId 값이 23인 토큰을 OpenSea NFT 마켓에 올려보도록 하자.
OpenSea 마켓 플레이스에도 테스트넷이 존재하기 때문에 테스트넷에 NFT를 올려보도록 하겠다. OpenSea 테스트넷 도메인 => https://testnets.opensea.io/
메타마스크 지갑 연결을 진행해주도록 하자. 지갑 연결 이후 Profile을 클릭하면 다음과 같이 Collected 부분에 우리가 발행한 NFT가 있는 것을 확인할 수 있다.
우리는 단지 스마트 컨트랙트를 테스트넷에 배포만 했을 뿐인데 OpenSea라는 사이트가 테스넷에 있는 우리의 Account 내용을 조회해서 화면상에 보여주고 있는 것이다. 즉, OpenSea와 연결된 계정이 가지고 있는 정보들을 화면 상에 보여주는 하나의 Dapp이라고 볼 수 있다.
Minting 컨트랙트 안의 tokenURI( ) 함수가 return 하고 있는 URI 는 다음과 같은 JSON 파일을 전달해준다.
실제 OpenSea 테스트넷에 올라온 우리의 NFT를 조회하면 JSON 파일 안에 있는 내용들로 NFT의 정보들이 구성되어 있는 것을 확인할 수 있다. 즉, 발행된 NFT와 매칭되는 URI를 통해 NFT의 정보를 JSON 파일 형태로 보관하고 있는 것이라고 생각하면 된다. 그리고 OpenSea에서는 그 JSON 파일 안에 있는 정보들을 화면 상에 보여주고 있을 뿐인 것이다.
sell 버튼을 누르게 되면 원하는 판매 가격을 입력하고 두번의 서명을 진행하게 된다.
첫번째 서명은 앞서 공부했던 Approve와 관련된 서명이다. 이는 NFT에 대한 권한을 OpenSea에게 위임하겠다는 서명이라고 보면된다. (OpenSea가 판매를 대행해주기 때문,,) 이후 두번째 서명은 판매와 관련된 서명이다. 이렇게 두번의 서명을 완료하고 나면 다음과 같이 판매 등록이 되어있는 것을 확인할 수 있다.
요약하면, NFT의 메커니즘은 고유한 키 값을 가지고 있는 각각의 토큰마다 JSON 파일 URI를 매칭시키고 JSON 파일 안의 image 속성에 이미지 URL을 집어 넣은 것이라고 볼 수 있다. NFT 이미지를 보여주고 싶다면 JSON 파일 안의 image 속성 안에 존재하는 URL을 가져와서 화면 상에 보여주기만 하면 되는 것이다.
'Ethereum' 카테고리의 다른 글
Ethereum/이더리움 - NFT / SaleToken 컨트랙트 (0) | 2022.08.02 |
---|---|
Ethereum/이더리움 - NFT / contract ERC721 (0) | 2022.07.27 |
Ethereum/이더리움 - OpenZeppelin / 토큰 컨트랙트 / 스왑 컨트랙트 (2) | 2022.07.23 |
Ethereum/이더리움 - 인터페이스 & ERC-20 / 토큰 발행하기 (0) | 2022.07.22 |
Ethereum/이더리움 - Solidity(솔리디티) function payable (0) | 2022.07.21 |