회사에서 NFT 민팅 작업을 하는데, 민팅 로직을 개선한 결과가 너무 마음에 들었다. 결론적으로, 트랜잭션 해시값을 미리 가져와서 민팅을 비동기적으로 진행할 수 있게 만들었다.
배경
회사에서 이더리움 & 폴리곤 메인넷에 NFT 민팅하는 업무를 맡았다.
처음으로 진행하는 메인넷 배포 & 민팅에 설레이면서도 불안함을 가지면서 작업을 진행했다.
폴리곤의 경우, MATIC의 가격이 높지 않아서 민팅에 부담이 없었지만
이더리움의 경우, 가격이 높아서 그런지 아무리 회사 비용이라고는 해도 부담이 컸다. (이더리움은 오늘 일자(2/11)로 가격이 약 193 만원 정도이다.)
개인적으로 블록체인 개발자는 최대한 회사에서 블록체인으로 인한 지출을 줄여줄 수 있게끔 도와주는 역할이라 생각한다.
이 돈을 아끼고 싶어서 민팅할 때에 최대한 가스비를 적게 설정하고 트랜잭션을 진행하는데, 민팅 한번할 때마다 약 1분 정도를 소요했다.
다행히 기획했던 NFT에 대한 민팅은 잘 진행했지만, 개발자로서 개선해야할 부분이 너무 확연해서 좀 더 디벨롭하기로 한다.
이슈
이번 작업에서 개선해야 할 부분에 대한 리스트업을 해보았다.
1. 가스 최적화 - 어떻게 해야 최적의 가스비를 산출할까?
이번에 내가 맡은 NFT 민팅 임무는 두 가지 상황에서 이루어진다.
- (1) NFT 체험 행사 때 실시간으로 참여자들의 작품을 민팅
- 속도가 생명. 가스비 ↑
- 메인 체인 : Polygon
- (2) 이더리움 네트워크에 대표 작품 19작을 행사 전에 미리 민팅
- 미리 진행하는 것이다보니 가스비는 최소한으로 ↓
각 상황에 맞게 가스비를 산정하여 진행해야 한다.
2. 동기적인 트랜잭션 코드
문제는 내가 작성한 코드가 동기적이다보니 트랜잭션이 해시값을 리턴해줘야 민팅 과정이 끝난다는 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 동기적인 코드(해시 리턴 값을 가져오기까지 오래걸림)
await web3.eth.accounts
.signTransaction(
{
from: REACT_APP_ADDRESS,
to: to,
gasPrice: 5000000, // 이더리움으로 진행할 시 1000000
data: data,
},
privateKey
)
.then(async (Tx) => {
await web3.eth
.sendSignedTransaction(Tx.rawTransaction)
.then((hash, err) => {
if (err) {
console.log(err);
} else {
console.log("민팅이 완료되었습니다.");
return hash;
}
});
});
- 동기적 로직의 아쉬운 점 :
- 트랜잭션이 담긴 블록이 연결되기까지 기다려야 해서 서비스 단의 속도가 느리다.
블록이 블록체인 네트워크에 추가되는 과정
만약 이더리움 고얼리(goerli) 테스트넷으로 트랜잭션을 동기적으로 보내면 약 1분 정도 소요된다.
- 트랜잭션이 담긴 블록이 연결되기까지 기다려야 해서 서비스 단의 속도가 느리다.
3. (Optional) Upgradeable Contract
CTO님이 무심결에 지나가면서 하신 말씀이 있다. “민팅은 아무 주소나 할 수 있어요?”
내가 만든 컨트랙트는 onlyOwner
가 붙어있어서 최초에 컨트랙트를 배포한 주소가 아니면 민팅을 할 수가 없기 때문에 “가능하게 만들까요?” 라고 말씀드렸지만
“아니 이번에 진행할 때에는 크게 상관없을 것 같으니 넘어갑시다.” 라고 하셨다.
하지만, 개발자로서 필요함이 1%라도 있으면 준비해놔야 겠다는 생각이 들었다.
해결과정
이 세가지의 이슈들을 해결해보자.
1. 가스 최적화 - 어떻게 해야 최적의 가스비를 산출할까?
사실 이 부분은 “내가 해결했다.” 라기 보다는 “안썼던 web3.js의 기능을 갖다썼다.” 라고 하는 것이 더 맞는 말이다.
(ether.js
에도 있겠지만) web3.js
에는 estimateGas라는 기능이 있다.
이 기능은 사용자가 설정한 트랜잭션 옵션을 토대로 로컬 블록체인
에 돌려보고 나오는 가스비들을 Binary Search 알고리즘 통해 최적의 값으로 산출해내는 기능이다.
1
2
3
4
5
6
7
8
// 내 컨트랙트 기능을 미리 실행해보는 estimateGas
const estimate = await MyMetaGalleryContract.mintNFT(
REACT_APP_ADDRESS,
tokenURI
).estimateGas({
from: REACT_APP_ADDRESS,
gas: 5000000,
});
2. 동기적인 트랜잭션 코드
‘어떻게 하면 트랜잭션 플로우를 좀 더 효율적으로 사용할 수 있을까?’에 대한 생각을 하면서 이리저리 트랜잭션 테스트를 해보던 중
트랜잭션 해시값은 미리 리턴이 되고, Scan으로 확인해보면 <pending>
처리 되어있는 것을 확인했다.
‘어차피 트랜잭션은 블록체인에 올라가 있을 것이고, 노드가 처리를 하느냐 마느냐의 문제인가?’ 가 내가 내린 결론이다.(틀리면 말씀주세요 ㅎ)
그렇다면, 트랜잭션 해시값을 미리 받아올 수 있다면? 그 후 플로우는 블록체인에 넘기면 되는 것 아닌가!?
이렇게 결론이 마친 후 코드를 수정하였다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 사인한 트랙잭션에 대한 해시를 미리 넘겨서 빨리 끝낸다.
const signTx = await web3.eth.accounts
.signTransaction(
{
from: REACT_APP_ADDRESS,
to: to,
gas: estimateGas,
data: data,
},
privateKey
)
.then((res) => {
return res.rawTransaction;
});
const txHash = web3.utils.sha3(signTx); // 이 부분이 내 트랜잭션 해시값을 얻어낼 수 있는 바로 그곳.
const result = {
hash: txHash,
tx: signTx,
};
return result;
결과
회사 프로젝트라 메인넷 스크린샷을 찍을 수 없었습니다.
결과적으로 민팅은 최적화된(빠르고 값이 최소화) 가스비로, 민팅을 진행하게 되면 비동기적으로 주문을 올려놓는 형식이 되었다.
민팅을 하고 기다리면 오픈씨에 민팅된 결과물이 뜬다. 다음은 개선된 결과가 만족스러운 폭풍 민팅의 결과다.
- (+Optional) Upgradeable Contract
- 프록시 컨트랙트 성공했는데 포스트가 너무 길어져 다음으로 넘길게요.
- 레포
느낀점
나는 Developer(개선하는 사람) 이다!