처음으로 진행해본 프로젝트였다! 화려한 기능을 가진 화려한 사이트를 구현한 것은 아니었지만, 처음으로 협업하며 개발을 해보았다는 점에서 나에겐 큰 의미가 있는 프로젝트였다.
✏️ 역할 분담
- 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
등을 통한 성능 최적화를 하지 않음