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

NAVER Cloud
NAVER Cloud
Published in
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
NAVER Cloud

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