이번 프로젝트에서 마이페이지 화면 구현, 이더리움 네트워크에서 NFT 목록, 상세정보 조회 기능을 개발하였다. 사실 프론트엔드 보다는 백엔드에 더 관심이 많은 편인데, 메타마스크와 연동하는 등 프론트에서 해야 할 기능이 많아 팀원 4명이 모드 프론트엔드 작업을 하게 되었다.
월요일부터 목요일까지 Ethers.js 라이브러리 사용법을 리서치하고, 기능을 개발하고, 에러를 해결하느라 허둥지둥 했던 탓에 후련하기도 하지만 아쉽기도 하다. 마무리하는 단계에서 프로젝트 전체 코드를 찬찬히 읽어보니 이 부분을 이렇게 분리했으면, 이 부분은 이렇게 처리했으면 더 나은 기능을 개발할 수 있었을 것 같단 생각이 끊이질 않아서 그런 듯 하다. 그래서 개발을 할 때는 우선 기능을 다 개발하고 그 다음에 리팩토링을 하라고 하는 걸까?
그리고 첫 프로젝트가 이렇게 힘들었다면 다음 주부터 시작하는 두 번째 프로젝트는 대체 얼마나 힘들지 모르겠다 싶어 부담감이 느껴진다. 그래도 뭐 하다보면 되겠지.
사실 이번 프로젝트도 부담감이 컸고 다른 동기들이 하도 잘한다 잘한다 하니 잘 하는 모습을 보여야 할 것 같아 더 긴장했었다. 하지만 팀원들이 모두 서로를 배려하고 감정 상하는 일 없이 프로젝트를 진행해서 사이가 많이 가까워졌다. 특히 팀장님이 모두의 의견을 존중하면서 팀장 역할을 잘 하셨고, 프로젝트 구현, 배포 등 다양한 분야에서 해박해 의지할 수 있었다. 프로젝트를 마무리하는 단계에서 혼자 공부한 내용을 보여주셨는데 그 수가 적지 않고 퀄리티도 높아 반성했다.
프로젝트를 진행하면서 아쉬운 부분을 정리하였다. 이후에 다른 프로젝트를 할 때 다시 참고하려고 한다.
현재 NFT 리스트 조회, NFT 상세 정보 조회, 지갑 연동, NFT 발행 코드가 들어간 wallet.js
모듈의 구현은 다음과 같다.
import { ethers } from 'ethers';
import seanapseNftAbi from './SeanapseNFT.json'
const SEANAPSE_NFT_CONTRACT_ADDRESS = '0xe18585AE18ea624E361f71BE760DFA1050baaA99'
async function getNftList() {
const provider = new ethers.providers.Web3Provider(window.ethereum);
let contract = new ethers.Contract(SEANAPSE_NFT_CONTRACT_ADDRESS, seanapseNftAbi, provider);
let totalSupply = await contract.totalSupply()
if(totalSupply === 0) {
return null
}
let arr = []
for (let i = 1; i <= totalSupply; i++) {
arr.push(i)
}
let nftList = []
for(let tokenId of arr) {
let owner = await contract.ownerOf(tokenId)
let tokenURI = await contract.tokenURI(tokenId)
fetch("https://ipfs.io/ipfs/" + tokenURI.substr(7))
.then(res=> res.json())
.then(out => {
let name = out.name
let image = out.image
nftList.push({name, image, tokenId, owner, out})
})
if(tokenId === arr.length) {
return nftList
}
}
}
async function getNft(tokenId) {
const provider = new ethers.providers.Web3Provider(window.ethereum);
let contract = new ethers.Contract(SEANAPSE_NFT_CONTRACT_ADDRESS, seanapseNftAbi, provider);
let owner = await contract.ownerOf(tokenId)
let tokenURI = await contract.tokenURI(tokenId)
let network = await provider.getNetwork(1);
return fetch("https://ipfs.io/ipfs/" + tokenURI.substr(7))
.then(res=> res.json())
.then(out => {
const imagePath = "https://ipfs.io/ipfs/" + (out.image).substr(7)
const nftInfo = {
"owner": owner
, "name": out.name
, "image": imagePath
, "description": out.description
, "attributes": out.attributes
, "contract": SEANAPSE_NFT_CONTRACT_ADDRESS
, "standard": "ERC-721"
, "network": network.name
, "tokenId": tokenId
}
return nftInfo
})
}
const connectWallet = async () => {
if(window.ethereum){
try{
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts'})
alert('지갑 연결 성공!');
return accounts[0];
}catch(err){
alert(err.code);
}
}
else{
alert("Please install metamask!");
}
}
const createNFT = async (recipient, tokenURI) => {
try{
const provider = await new ethers.providers.Web3Provider(window.ethereum);
const signer = await provider.getSigner();
let contract = await new ethers.Contract(SEANAPSE_NFT_CONTRACT_ADDRESS, seanapseNftAbi, signer, provider);
// let sContract = await contract.connect(signer);
return contract.mintNFT(recipient, tokenURI);
}
catch(err){
alert(err);
}
}
export {getNftList, connectWallet, createNFT, getNft};
여기서 contract
, provider
등의 데이터는 변동이 없는 항목이고 여러 함수에서 같이 사용하고 있으니 각 함수마다 만들어서 사용할 것이 아니라 싱글톤 패턴을 사용했다면 더 효율적으로 관리할 수 있었을 것 같다. 한 명이 개발을 한 게 아니라 각자 필요한 기능을 만들고 합쳤기 때문에 고려하지 못한 부분이라 생각된다. 다음에는 모듈마다 담당자를 정해 리팩토링을 하도록 하면 좋을 것 같다.
초기에 프로젝트 환경을 구성할 때 팀장님이 컴포넌트를 세분화해 조립하는 식으로 구현하자고 제안하셨다. 하지만 이에 대한 이해도가 부족한 팀원들은(나!) components
, layouts
, routes
디렉토리를 각자 자기가 이해한대로 분리해서 결합했다.
하지만 나중에 팀장님이 분리한 화면 파일 구조를 보니 훨씬 명확하고 깔끔했다. 그 구조는 다음과 같다.
└── src
├── App.js
├── components
│ ├── create
│ │ ├── index.js (이름이 겹치지 않도록 export로 컴포넌트 이름 명시)
│ │ ├── input
│ │ │ ├── description.js
│ │ │ ├── index.js
│ │ │ ├── input.js
│ │ │ ├── title.js
│ │ │ └── wrapper.js
│ │ ├── loading.js
│ │ ├── submit.js
│ │ ├── title.js
│ │ ├── upload
│ │ │ ├── index.js
│ │ │ ├── inputFile.js
│ │ │ └── wrapper.js
│ │ └── wrapper.js
├── index.js
├── layouts
│ └── createLayout (이하 각 컴포넌트 조립하여 관리)
│ ├── createInput.js
│ ├── createInputProperties.js
│ ├── createUpload.js
│ └── index.js
├── logo.svg
├── reportWebVitals.js
├── routes
│ └── Create.js (컴포넌트 조립한 layout 조립)
└── setupTests.js
나는 layout
과 routes
의 구분이 불분명하게 작성해 아쉬움이 많다. 다음 프로젝트를 할 때는 이 부분을 개선하고 싶다.
현재 씨냅스에서는 Home에서 NFT List를 조회하고 이를 App.js
에 선언된 state
로 관리한다. 이렇게 하면 MyPage에서 같은 내용의 목록을 한 번 더 조회할 필요가 없으니 더 효율적일거라 생각했는데, state
관리가 여간 까다로운게 아니었다. 업데이트 할 때마다 리랜더링 되는 이슈도 관리해야 하고, 하위 컴포넌트로 전달할 때도 여러 번 빼먹어 에러가 발생했다. 프로젝트 초기에 Redux를 사용했다면 좋았을텐데, 문제점이 발견되기 시작한게 프로젝트 구현이 반쯤 완료됐을 때라 바꾸기 어려웠다. 다음 프로젝트에서는 Redux를 사용해 State를 관리해야겠다.
NFT 리스트를 조회할 때 타이밍 이슈가 발생했다.
async function getNft(tokenId) {
const provider = new ethers.providers.Web3Provider(window.ethereum);
let contract = new ethers.Contract(SEANAPSE_NFT_CONTRACT_ADDRESS, seanapseNftAbi, provider);
let owner = await contract.ownerOf(tokenId)
let tokenURI = await contract.tokenURI(tokenId)
let network = await provider.getNetwork(1);
// 리턴 누락으로 발생
return fetch("https://ipfs.io/ipfs/" + tokenURI.substr(7))
.then(res=> res.json())
.then(out => {
const imagePath = "https://ipfs.io/ipfs/" + (out.image).substr(7)
const nftInfo = {
"owner": owner
, "name": out.name
, "image": imagePath
, "description": out.description
, "attributes": out.attributes
, "contract": SEANAPSE_NFT_CONTRACT_ADDRESS
, "standard": "ERC-721"
, "network": network.name
, "tokenId": tokenId
}
return nftInfo
})
}
owner
, tokenURI
, network
를 조회하고 fetch
로 metadata 파일을 받아오려는데, 메타데이터를 다 받아오기도 전에 getNft
에서 빈 데이터를 리턴하는 것이었다.
원인을 파악하던 중에 fetch
앞에 return
이 누락된 것이 원인임을 알게 되었다.
async
,await
, fetch
와 비동기 구현의 이해 부족이었다.
관련된 개념을 더 제대로 이해하고 다음 프로젝트에 들어가야겠다.
팀원들과 역할을 나눠 개발을 진행하면서 같은 오류가 시간차를 두고 발생하는 경우가 있었다. 이 때 간단하게라도 에러 로그와 발생 원인, 해결 방법을 정리해놨다면 편했을텐데, 기능을 개발하기에 바빠 따로 정리해놓지 않았더니 같은 에러를 여러 번 잡아야 하는 불편함이 있었다. 그리고 블로그에 기록해두면 나중에 내가 찾아보기도 좋았을텐데 아쉽다. 다음에는 임시 저장으로라도 기록해둬야겠다.
파일 이름을 정하는데 다들 생각하는게 비슷비슷하다보니 다른 기능이면서 같은 이름을 한 파일이 여럿 있었다. 이 때문에 코드 리뷰를 하면서 헷갈려 실수하는 경우도 있어 아쉬웠다. 다음부터는 맡은 파트-파일 기능 등으로 이름을 붙이는 규칙을 만들어야겠다.
현재 NFT 목록을 조회할 때 약 30초 가량을 기다려야 목록이 조회된다.
async function getNftList() {
const provider = new ethers.providers.Web3Provider(window.ethereum);
let contract = new ethers.Contract(SEANAPSE_NFT_CONTRACT_ADDRESS, seanapseNftAbi, provider);
let totalSupply = await contract.totalSupply()
if(totalSupply === 0) {
return null
}
let arr = []
for (let i = 1; i <= totalSupply; i++) {
arr.push(i)
}
let nftList = []
for(let tokenId of arr) {
let owner = await contract.ownerOf(tokenId)
let tokenURI = await contract.tokenURI(tokenId)
fetch("https://ipfs.io/ipfs/" + tokenURI.substr(7))
.then(res=> res.json())
.then(out => {
let name = out.name
let image = out.image
nftList.push({name, image, tokenId, owner, out})
})
if(tokenId === arr.length) {
return nftList
}
}
}
프로젝트를 마치고 생각해보니 컨트랙트 함수를 실행하는데 시간이 걸리기 때문인 것 같은데, 이를 클라이언트에서 여러 번 통신을 연결하지 않고 컨트랙트를 발행할 때 함수를 만들어서 사용했다면 더 시간이 단축됐을 거란 아쉬움이 있다. 다음에는 컨트랙트에 함수를 만드는 걸 어려워하지 말고 일단 해봐야겠다.
1차 끝!
다음 주 프로젝트를 들어가기 전에 오래 써 반응 속도가 예전같지 않은 마우스를 바꾸고 청소를 더 해야겠다. 다음 프로젝트 주제는 인센티브 SNS라던데 긴장된다. 하지만 하다보면 될 거라 생각된다.