[Blockchain Service] 쉽고 편리한 NFT 개발 가이드

NAVER CLOUD PLATFORM
NAVER CLOUD PLATFORM
23 min readDec 13, 2023

--

이번 포스팅에서는 NAVER Cloud Platform에서 제공하는 Blockchain Service를 이용하여 NFT 시스템을 구축하는 과정을 소개해 드리겠습니다.

네이버 클라우드 플랫폼 Blockchain Service (링크)

목차

  1. NFT (Non-Fungible Token)란?
  2. NFT 활용처
  3. ERC-721
  4. NFT 구현
  5. NAVER Cloud Platform를 활용한 배포/테스트

순서대로 살펴보며 간편하게 NFT를 테스트해 보세요.

참고 자료

이전에 발행한 Blockchain Service 활용 개발 사례도 함께 살펴 보세요.

1. NFT (Non-Fungible Token)란?

설명에 앞서 ‘대체 가능하다’ 라는 것의 정의를 생각해 볼게요. 동일한 것과 교환 또는 대체될 수 있음을 의미하죠. 동전을 예로 들면, 나의 100원 동전은 다른 사람의 100원 동전과 의미와 가치 모두 동일하게 서로 대체할 수 있습니다.

이에 반해 ‘대체 불가능하다'라는 것은 위치나 층수 등에 따라 다른 등기부등본 처럼 세상에 딱 하나밖에 없고, 서로 같은 가치를 가질 수 없는 개념입니다.

이처럼 온・오프라인에서 독특하거나 희소한 무언가를 대체 불가능한 블록체인 원장에 토큰으로 생성하고 저장하는 형태를 NFT(Non-fungible token)라고 말합니다.

2.NFT 활용처

NFT의 대체불가능한 특징을 활용하여 특별한 가치를 제공 할 수 있습니다. 몇 가지 NFT 활용 예시를 소개드립니다.

  • 예술품 : 작품의 원본과 소유권을 보장할 수 있어 복제품이나 표절을 방지하는 역할을 합니다. 하나의 예술품의 지분을 여러개로 나눠서 소유하는 형태도 구현할 수 있습니다.
  • 게임 : 고유한 아이템, 캐릭터 등을 토큰화 하여 거래할 수 있습니다.
  • 브랜드 : 나이키는 운동화나 의류등에 NFT를 활용하여 정품을 보장하고 타 플랫폼과 연계하여 커뮤니티와 팬덤을 형성하였습니다. 그 결과로 고객 로열티도 높아졌습니다. (관련 기사 : 링크)
  • 권리 : 저작권, 소유권, 지적재산권 등의 권리를 NFT로 만들고 판매 하는 모델로 사용될 수 있습니다.
  • 티켓 : 커뮤니티나 공연에 참여할 수 있는 입장권을 NFT로 만들어 활용할 수 있습니다.

이처럼 NFT 기술은 로열티 및 리워드 프로그램 구축에도 활용되어, 궁극적으로 브랜드와 소비자 관계를 더욱 돈독하게 데에도 값지게 쓰일 수 있습니다.

3. ERC-721

이더리움에 NFT를 구현하려면 ERC(Ethereum Request for Comments)-721 토큰 기술문서의 표준을 따라야 합니다.

네이버 클라우드 플랫폼 Blockchain Service에서 사용하는 Hyperledger Fabric도 ERC-721 토큰 표준을 따라 구현할 수 있습니다. (사용 가이드 : 링크)

ERC-721의 주요 함수를 보면 NFT가 어떤 방식으로 생성, 전달되는지 이해할 수 있습니다.

pragma solidity ^0.4.20;

interface ERC721 /* is ERC165 */ {
//Required methods
function balanceOf(address _owner) external view returns (uint256); // 주소가 보유하고 있는 NFT 갯수
function ownerOf(uint256 _tokenId) external view returns (address); // NFT 소유 주소 확인
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable; // 안전하게 NFT 소유 주소 변경
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable; // 안전하게 NFT 소유 주소 변경 (data항목 없음)
function transferFrom(address _from, address _to, uint256 _tokenId) external payable; // NFT 소유주 변경 (_to 확인 안함)
function approve(address _approved, uint256 _tokenId) external payable; // 주소에 NFT 전송권한 부여
function setApprovalForAll(address _operator, bool _approved) external; // NFT 소유자가 해당주소로 모든 NFT토큰에 대한 권한 부여
function getApproved(uint256 _tokenId) external view returns (address); //해당 NFT의 전송권한을 가지고 있는 주소를 조회
function isApprovedForAll(address _owner, address _operator) external view returns (bool); // 주소가 NFT의 전송권한을 가지고 있는지 조회

//Events
event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

// ERC-165 Compatibility (https://github.com/ethereum/EIPs/issues/165)
function supportsInterface(bytes4 _interfaceID) external view returns (bool);
}
  • NFT 소유권(_token_id, _owner)을 주요 파라미터로 하며
  • NFT 전송은 송신주소(_from) 에서 수신주소(_to)로 _tokenId를 변경하는 과정입니다.
  • NFT에 작품명과 이미지 등 세부정보를 담을 수도 있는데, 데이터 크기에 따라 오프체인(IPFS)등에 데이터를 저장할 수 있습니다.

위 표준으로 Hyperledger Fabric 체인 코드를 개발합니다.

4. NFT 구현

중요한 코드 일부만 설명 하였으며, 해당 소스코드는 hyperledger fabric에서 제공하는 샘플 코드를 다운 받으실 수 있습니다. (샘플 코드 다운로드 : 링크)

tokenERC721.js

  • Javascript로 체인코드를 개발하기 위하여 fabric-contract-api를 사용합니다.
  • 체인코드는 Contract 상속받아 구현하며, context(코드상 ctx)는 트랜잭션을 실행하기 위하여 사용합니다.
  • org1msp가 NFT 발행권한이 있다고 코드에 msp명이 하드 코딩 되어있습니다.
  • hyperledger fabric 체인코드에 대해 좀 더 자세한 컨셉과 개발모델을 확인하려면 hyperledger fabric페이지 에서 확인해 주세요.

소스코드

'use strict';

const { Contract } = require('fabric-contract-api');

const nameKey = 'name';
const symbolKey = 'symbol';

class TokenERC721Contract extends Contract {


// 초기화 함수
async Initialize(ctx, name, symbol) {

// org1msp가 NFT발행 권한이 있다고 가정하고 진행하며 org1msp가 아닌경우 권한없음 에러를 발생시킵니다
const clientMSPID = ctx.clientIdentity.getMSPID();
if (clientMSPID !== 'org1msp') {
throw new Error('client is not authorized to set the name and symbol of the token');
}

// initialize가 이미 실행되었는지 확인합니다
const nameBytes = await ctx.stub.getState(nameKey);
if (nameBytes && nameBytes.length > 0) {
throw new Error('contract options are already set, client is not authorized to change them');
}

// contract option(name,symbol) 설정
await ctx.stub.putState(nameKey, Buffer.from(name));
await ctx.stub.putState(symbolKey, Buffer.from(symbol));
return true;
}

// NFT 발행
async MintWithTokenURI(ctx, tokenId, tokenURI) {

await this.CheckInitialized(ctx);

const clientMSPID = ctx.clientIdentity.getMSPID();
if (clientMSPID !== 'org1msp') {
throw new Error('client is not authorized to mint new tokens');
}

const minter = ctx.clientIdentity.getID();

const exists = await this._nftExists(ctx, tokenId);
if (exists) {
throw new Error(`The token ${tokenId} is already minted.`);
}

const tokenIdInt = parseInt(tokenId);
if (isNaN(tokenIdInt)) {
throw new Error(`The tokenId ${tokenId} is invalid. tokenId must be an integer`);
}
const nft = {
tokenId: tokenIdInt,
owner: minter,
tokenURI: tokenURI
};
const nftKey = ctx.stub.createCompositeKey(nftPrefix, [tokenId]);
// nft 저장
await ctx.stub.putState(nftKey, Buffer.from(JSON.stringify(nft)));
// balancePrefix.owner.tokenId 형태 key
const balanceKey = ctx.stub.createCompositeKey(balancePrefix, [minter, tokenId]);
// balance 저장
await ctx.stub.putState(balanceKey, Buffer.from('\u0000'));

const transferEvent = { from: '0x0', to: minter, tokenId: tokenIdInt };
// NFT Transfer 이벤트 발생
ctx.stub.setEvent('Transfer', Buffer.from(JSON.stringify(transferEvent)));

return nft;
}

// owner에게 할당된 NFT 갯수 확인
async BalanceOf(ctx, owner) {
await this.CheckInitialized(ctx);

// balancePrefix.owner.* 에 매칭되는 balanceKey 조회
const iterator = await ctx.stub.getStateByPartialCompositeKey(balancePrefix, [owner]);

let balance = 0;
let result = await iterator.next();
while (!result.done) {
balance++;
result = await iterator.next();
}
return balance;
}

// NFT owner 조회
async OwnerOf(ctx, tokenId) {
await this.CheckInitialized(ctx);

const nft = await this._readNFT(ctx, tokenId);
const owner = nft.owner;
if (!owner) {
throw new Error('No owner is assigned to this token');
}

return owner;
}

async _readNFT(ctx, tokenId) {
const nftKey = ctx.stub.createCompositeKey(nftPrefix, [tokenId]);
const nftBytes = await ctx.stub.getState(nftKey);
if (!nftBytes || nftBytes.length === 0) {
throw new Error(`The tokenId ${tokenId} is invalid. It does not exist`);
}
const nft = JSON.parse(nftBytes.toString());
return nft;
}

// identity 조회
async ClientAccountID(ctx) {
await this.CheckInitialized(ctx);

// Get ID of submitting client identity
const clientAccountID = ctx.clientIdentity.getID();
return clientAccountID;
}

// from이 소유한 NFT를 to로 이전
async TransferFrom(ctx, from, to, tokenId) {
await this.CheckInitialized(ctx);

const sender = ctx.clientIdentity.getID();
const nft = await this._readNFT(ctx, tokenId);

// sender가 소유자인지, 권한이 있는지 확인
const owner = nft.owner;
const tokenApproval = nft.approved;
const operatorApproval = await this.IsApprovedForAll(ctx, owner, sender);
if (owner !== sender && tokenApproval !== sender && !operatorApproval) {
throw new Error('The sender is not allowed to transfer the non-fungible token');
}

if (owner !== from) {
throw new Error('The from is not the current owner.');
}

nft.approved = '';
nft.owner = to;
const nftKey = ctx.stub.createCompositeKey(nftPrefix, [tokenId]);
// 소유자 이전
await ctx.stub.putState(nftKey, Buffer.from(JSON.stringify(nft)));

// from의 balance 삭제
const balanceKeyFrom = ctx.stub.createCompositeKey(balancePrefix, [from, tokenId]);
await ctx.stub.deleteState(balanceKeyFrom);

// to의 balance 저장
const balanceKeyTo = ctx.stub.createCompositeKey(balancePrefix, [to, tokenId]);
await ctx.stub.putState(balanceKeyTo, Buffer.from('\u0000'));

// NFT Transfer 이벤트 발생
const tokenIdInt = parseInt(tokenId);
const transferEvent = { from: from, to: to, tokenId: tokenIdInt };
ctx.stub.setEvent('Transfer', Buffer.from(JSON.stringify(transferEvent)));

return true;
}

async IsApprovedForAll(ctx, owner, operator) {
await this.CheckInitialized(ctx);

const approvalKey = ctx.stub.createCompositeKey(approvalPrefix, [owner, operator]);
const approvalBytes = await ctx.stub.getState(approvalKey);
let approved;
if (approvalBytes && approvalBytes.length > 0) {
const approval = JSON.parse(approvalBytes.toString());
approved = approval.approved;
} else {
approved = false;
}

return approved;
}

async CheckInitialized(ctx){
const nameBytes = await ctx.stub.getState(nameKey);
if (!nameBytes || nameBytes.length === 0) {
throw new Error('contract options need to be set before calling any function, call Initialize() to initialize contract');
}
}

... 일부 생략

}
module.exports = TokenERC721Contract;

주요 함수 역할

  • BalanceOf(ctx, owner) : owner가 가진 NFT의 갯수 조회
  • OwnerOf(ctx, tokenId) : NFT의 owner 조회
  • TransferFrom(ctx, from, to, tokenId) : NFT 이전
  • Approve(ctx, approved, tokenId) : NFT전송 권한 부여
  • MintWithTokenURI(ctx, tokenId, tokenURI) : NFT 생성

*블록체인에 큰 용량의 데이터를 넣기 어려우므로 IPFS등에 저장하고 해당 링크를 tokenURI에 저장하는 형태입니다.

5. NAVER Cloud Platform를 활용한 배포/테스트

체인코드 패키징

  • fabric-tools 이미지를 이용하여 패키징 합니다.
# /home/user/fabric-samples에 소스 다운로드, home/user 경로에서 실행
# fabric-tools 이미지 run
$ home/user> docker run -ti --rm -v ./fabric-samples/token-erc-721:/root hyperledger/fabric-tools:1.4.12 /bin/bash

# 컨테이너의 /root 경로로 이동
root@06bff2dd03d7:/# cd /root
root@06bff2dd03d7:~# ls
chaincode-go chaincode-java chaincode-javascript README.md

root@06bff2dd03d7:~# peer chaincode package erc721@v1.cds -n erc721 -v 1.0 -p /root/chaincode-javascript -l node
2023-12-05 12:08:46.776 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 001 Using default escc
2023-12-05 12:08:46.776 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 002 Using default vscc
  • 생성한 CDS 파일을 이미지 밖으로 복사합니다
# image id 확인
$ home/user> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
06bff2dd03d7 hyperledger/fabric-tools:1.4.12 "/bin/bash" 8 minutes ago Up 8 minutes intelligent_ritchie
# 파일 복사
$ home/user> docker cp 06bff2dd03d7:/root/erc721@v1.cds ./

블록체인 네트워크 생성

  1. Network 생성 (Blockchain Service > Networks > 네트워크 생성)
  • 필수연계상품 : Ncloud Kubernetes Service (4core 16GB Mem 이상의 worker 노드 최소 1개 이상 필요)
  • Orderer 생성
  • Peer 생성 : msp명은 org1msp 로 설정 필요
  • channel 생성

1–1) 생성할 네트워크 최종 정보

1–2) 네트워크 생성 완료 후 정보 조회

2. org2msp 추가 생성

2–1) Blockchain Service > Nodes > CAs > CA생성

  • CA이름 : org2-ca

2–2) Blockchain Service > Organizations > MSP생성 (2–1 완료 후 진행)

  • MSP 이름 : org2msp
  • CA 이름 : org2-ca

2–3) 컨소시엄/채널구성원 추가

  • Blockchain Service > Nodes > Orderers탭 > 컨소시엄관리 > org2msp를 구성원에 추가
  • Blockchain Service > Smart Contract > Channels > Channel설정 > Channel 구성원(MSP)관리 > org2msp 추가

인스톨/인스턴스화

1. 체인코드 인스톨을 진행합니다. (약 10~20 초 소요될 수 있습니다)

  • Blockchain Service > Smart Contracts > Chaincodes탭 > Chaincode 설치 버튼 클릭

2. erc721 체인코드 인스턴스화를 진행합니다.

  • Blockchain Service > Smart Contracts > Chaincodes탭 > erc621 체인코드 선택 후 ‘인스턴스화’ 버튼 클릭

3. 완료 후 상태를 확인합니다.

NFT 생성 테스트

네이버 클라우드 플랫폼 Blockchain Service에서는 Dapp을 만들지 않고 콘솔에서 직접 체인코드 호출 테스트를 할 수 있습니다. (인스턴스된 chaincode 선택 후 ‘Chaincode실행’ 버튼 클릭)

  1. 초기화 (Initialize) : name과 symbol을 초기화 합니다.

2. NFT 생성 (MintWithTokenURI) : NFT ID, URL을 입력하여 NFT를 생성합니다.

3. NFT owner 확인 (OwnerOf) : 위에서 생성한 NFT의 소유자를 조회합니다.

4. NFT 이전 (TransferFrom) : 소유자를 org1msp -> org2msp로 변경합니다.

5. NFT owner 확인 (OwnerOf) : org2msp로 이전한 NFT의 소유자를 조회합니다.

마무리하며

Hyperledger Fabric을 직접 CLI로 구축하고 설정한 후 테스트 하려면 난이도도 높고 시간도 무척 오래 걸립니다. 반면 네이버 클라우드 플랫폼 Blockchain Service를 활용하면 NFT 생성부터 이전 기능 까지 빠르고 쉽게 테스트해볼 수 있습니다.

앞서 설명드린 기능 외에도 Blockchain Service에서는 다양한 기능을 제공하고 있으니 사용해 보시며 블록체인 기술과 한걸음 더 가까워지시기 바랍니다.

  • Blockchain Service 사용자 가이드 : 링크

긴 글 읽어주셔서 감사합니다.

참고자료

문의하기

--

--

NAVER CLOUD PLATFORM
NAVER CLOUD PLATFORM

We provide cloud-based information technology services for industry leaders from startups to enterprises.