
처음으로 진행해본 프로젝트였다! 화려한 기능을 가진 화려한 사이트를 구현한 것은 아니었지만, 처음으로 협업하며 개발을 해보았다는 점에서 나에겐 큰 의미가 있는 프로젝트였다.
✏️ 역할 분담
- Front-end (내가 담당한 🙋🏻♀️)
클라이언트 웹페이지 및 web3 일부 로직 구현- Back-end
모랄리스 사용을 위한 api 개발 및 web3 로직 구현- Smart Contract
ERC-721 발행 및 조회, 전송을 위한 컨트랙트 개발 및 배포

우리 팀은 필수적으로 필요한 최소 기능만을 빠르게 구현한 뒤, 배포까지의 전 과정을 경험해 보는 것을 목표로 하였다. 우리 사이트는 검색어를 통해 rinkeby 테스트넷 상에 발행된 NFT를 조회할 수 있고, 메타마스크 지갑을 연결하여 직접 NFT를 발행 하고 특정 주소로 전송할 수도 있다.
이 라이브러리를 사용하면리액트 클라이언트 개발, 스마트 컨트랙트 개발 및 배포, web3 연동까지 통합된 하나의 프로젝트 안에서 개발할 수 있어 매우 편리하다! 간단한 토이 프로젝트 개발에는 무조건 사용하는 것이 좋을 것 같다. (truffle-react-box tutoral page)
truffle-react-box 깃헙 페이지에서는 아래와 같이 소개하고 있다.

'dog' 라는 키워드로 검색하면, Token 이름에 'dog'가 포함되는 NFT들이 리스팅된다.

NFT name, description을 입력하고 원하는 파일을 선택하면 민팅을 할 수 있다. 메타마스크를 통해 지갑에 서명까지 하면 NFT가 성공적으로 발행되었다는 메시지와 함께 해당 발행 트랜잭션에 대한 정보를 확인할 수 있는 이더스캔 링크를 제공한다.

현재 로그인된 지갑 계정이 소유하고 있는 NFT들을 확인할 수 있다. 각 NFT 하단에 수신인의 지갑 주소를 입력하고 transfer 버튼을 누르면 NFT를 전송할 수 있다. 이 때도 역시 메타마스크를 통한 서명이 필요하다. NFT가 성공적으로 전송되면 해당 트랜잭션에 대한 이더스캔 링크가 제공된다.

리액트로 개발되었으며, 매우 간단한 사이트이기 때문에 truffle-react-box 외에 특별한 라이브러리나 기술스택은 사용하지 않았다.

블록체인과 통신하는 것은 비동기이며, 일반적인 클라이언트와 서버의 통신보다 훨씬 오래 걸린다. 사용자 경험을 위해 loading indicator의 구현은 필수적이었다.
또한 message라는 state를 정의하고, 통신 결과에 따라 달라지는 message state에 따라서 적절한 결과 메시지가 노출되도록 하였다. 이러한 내용이 구현됨으로써 사용자 경험이 크게 개선될 수 있었다. 매우 간단하고 또 당연한 로직이지만 앞으로 또 다른 클라이언트를 개발할 때에도 세심하게 신경써야할 부분이라고 생각한다.
NFT를 민팅하는 컴포넌트에서 해당 내용을 구현한 전체적인 흐름은 다음과 같다.
const MintForm = () => {
// message state 정의
const [message, setMessage] = useState('');
// NFT 발행을 위한 정보를 메타데이터 생성 서버에 제출하는 함수
const handleSubmit = async (event) => {
// 입력된 정보가 올바르지 않으면 📌message = 'formError'
if (nameRef === '' || descRef === '' || imgFile === undefined) {
setMessage('formError');
return;
}
// 서버에 정보를 제출하고 응답을 기다리는 동안 📌message = 'loading'
setMessage('loading');
...
}
// 생성된 메타데이터로 NFT를 민팅하는 함수
const mintNFT = async (metaData) => {
...
try {
...
setTxAddress(txHash);
// 거래가 성공하면 📌message = 'success'
setMessage('success');
} catch (error) {
...
// 거래가 실패하면 📌message = 'failure'
setMessage('failure');
}
}
return (
...
// message의 상태에 따라, 아래 div에 표시되는 내용이 달라진다.
<div className="mintfomr-alert-message-wrapper">
{
message === 'formError' &&
<div className="mintform-filling-error-message">
All fields must be filled in.
</div>
}
{
message === 'success' &&
<div className="mintform-success-message">
NFT has been successfully issued!<br />
Check your Transaction
<a target="_blank" href={'https://rinkeby.etherscan.io/tx/' + txAddress}>HERE!</a>
</div>
}
{
message === 'failure' &&
<div className="mintform-failure-message">
Something wrong! Try again
</div>
}
{
message === 'loading' &&
<div className="mintform-loading-message">
Please wait...
<img className="loading-indicator2" alt="now loading..." src="loading2.gif" />
</div>
}
</div>
...
)
}
이번 프로젝트 이전까지는 거의 사용해보지 않았던 hook인데, 사용법이 너무 간단해서 이걸 왜 이제야 활용하게 되었나 싶다. 리액트를 한참 배울 때에는 useRef를 사용하지 않고, 각 input에 해당하는 state를 새로 생성하여 input 태그의 event.target.value를 새로운 state의 값으로 setState 하는 방법을 이용했었는데, useRef를 이용하는 것이 비교도 안 될 정도로 간단하다.
컴포넌트 안에서 아래처럼 정의하면 input에 입력되는 값을 nameRef로 손쉽게 접근할 수 있다.
const nameRef = useRef();
return (
<input ref={nameRef} type="text" />
)
postCSS나 styled-components를 이용할 수도 있었지만, 간단한 프로젝트이기 때문에 app.css에 모든 css를 몰아 넣었다. 정리되지 않은 무식한 방법일 수 있지만 주석만 잘 달아놓으면 단 하나의 css 파일만 이용하기 때문에 이 방법도 나름 괜찮기는 했다. 하지만 개선이 필요하다.
Opensea는 api 키 발급에 오랜 시간이 걸린다. 우리팀은 오픈씨와 비슷한 Moralis라는 플랫폼의 api를 이용하여 rinkeby 테스트넷에 접근하기로 했다.

NFT를 민팅하거나 전송할 때, 사용자가 메타마스크를 통해 서명할 수 있도록 구현해야 했다. 이를 구현하는 과정에서 어려움이 있었고, 구글링을 통해 솔루션을 찾아 아래와 같이 적용하여 해결하였다.
우리 컨트랙트에 eth_sendTransaction이라는 method로 request를 날리는 방식이다. MetaMask Docs에서 이 내용이 안내되고 있는 걸 보니 아마 메타마스크에서 제공하는 기능인 것 같다. 이곳에서 관련 내용을 참고할 수 있다.

docs 내용을 참고하여 우리 사이트에서는 아래와 같이 구현하였다.
const mintNFT = async (metaData) => {
window.contract = await new web3.eth.Contract(ABI, deploy_address);
const transactionParameters = {
to: deploy_address, // Required except during contract publications.
from: window.ethereum.selectedAddress, // must match user's active address.
'data': window.contract.methods.mintNFT(window.ethereum.selectedAddress, metaData).encodeABI() //make call to NFT smart contract
};
//sign transaction via Metamask
try {
const txHash = await window.ethereum
.request({
method: 'eth_sendTransaction',
params: [transactionParameters],
})
setTxAddress(txHash);
setMessage('success');
} catch (error) {
console.log(error);
setMessage('failure');
}
}

NFT searcing, minting, tranfering 기능만을 간단하게 구현해본 사이트이기 때문에 실제로 서비스하기에는 부족한 부분이 많다. 다음 프로젝트에서는 더욱 완성도 높은 서비스를 개발하기 위해 이 내용들을 잘 기억해둬야겠다.
Minform 컴포넌트의 경우 150줄이 넘어가고 있다. 서비스 로직은 따로 모듈화하여 구현하는 연습을 해야 하는데 아직은 한 파일에 몰아서 작성하는 것이 편하다..😭useState Hook으로도 충분할 듯 하다. 다음 프로젝트에서는 redux나 useReducer를 활용해볼 예정이다.App.css 파일에 모든 CSS 코드를 다 담았는데, 사이트의 규모가 좀 더 크거나 유지보수가 필요한 상황에서는 아마 크게 고생할 것이다. syled-Component나 postCSS를 적용해 보는 것도 좋았을 것 같다.memo, useCallback 등을 통한 성능 최적화를 하지 않음
