보다 더 생생한 기록

[코드리뷰] Project 1 - Opensea.io 클론코딩 본문

블록체인

[코드리뷰] Project 1 - Opensea.io 클론코딩

viviviviviid 2022. 10. 27. 03:22

코드스테이츠 블록체인 과정 中 프로젝트 첫번째. 오픈씨 클론코딩

 

팀명 : 체크셔츠 ( ∵ 공대생 네명 )
팀원 : 임형대(팀장), 김준섭, 최진영, 서민석
내 포지션 : 프론트엔드, 스마트컨트랙트 (사실 FE2BE를 구성하기위해 백엔드까지 했다.)
팀 구성

 

여러 페이지 중 민트페이지

 

소감을 정리하고 내용을 글로 정리한다 한들, 나중에 다시 찾아봤을 때 코드가 뭔지, 어떤 생각으로 썼는지 확인하는 것을 중요하게 생각하기에 주요 내용을 제외한 잡설은 생략하겠습니다. 

 

Client/src/component/

MetamaskConnect.js : 메타마스크 연결과 관련된 내용들이 포함되어 있음. 부모 컴포넌트에서 호출할 경우 연결된 지갑주소를 props로 전달해줌.

MetamaskConnect.js

const GetJson = () => {
  axios.post('http://localhost:4000/senddata', { account: `${userData[0].account}` })
  .then(function(res){
    NFTData.push(res.data);
  }).catch(function (error) {
    console.log(error);
  });
  return NFTData;
};

const MetamaskConnect = () => { 

...

  const sendAccountValue = (props) => {
    props.getTextValue({ account });
  };
  
...

}


MintPage.js

  const getTextValue = async () => {
    const data = await getUserData();
    setTextValue(data[0]);
    console.log("textvalue:",textValue)
  };

 

우선 위 코드는 ProfilePage를 위한 /senddata 쪽에 연결된 지갑주소를 보내는 코드이다. 이를 통해 현재 연결된 지갑주소의 NFT가 화면에 뿌려지도록 만들기 위한 초석이다. 

 

하지만 실수를 범했다. 메타마스크 내에서 web3.eth.getAccount를 쓰려고 노력하면 될걸, state와 props를 가지고 낑낑대면서 부모 컴포넌트에 주소 해쉬값을 보내려했으니 말이다. 그러다보니 후반에 지갑연결 한번으로는 값이 추출안되는 버그들이 발생했고, 이는 기한내에 해결하지 못했다. (2번 누르면 그제서야 값이 온다 LOL) 민트페이지에서도 여러 과정을 거쳐야 했다. 애초에 하나보내려고 코드라인을 몇배로 늘린셈이였다.

 

왜 web3.eth 메소드를 포기했냐하면, 아래와 같이 평소에 사용하던 infura에 대한 내용을 파라미터로 안넣었다. (팀원에게 받은 일부코드를 발췌하여 그대로 적용했다.) 이 과정에서 web3.eth.method 사용시 여러 에러가 발생했고, 당연히 못쓰는줄 알았다. 즉 고칠생각도 안하고 의심조차 안한것이다.

  // window.ethereum 연결 후 여러 세팅 준비
  const web3 = new Web3(Web3.givenProvider || []); // 고정

 


 

Abi.js : NFT 민팅을 위해 솔리디티 언어로 배포된 코드의 abi 파일. 이를 이용하여 js를 통해 컨트랙트의 함수를 호출할 수 있음.

const Abi = [
  {
    inputs: [],
    stateMutability: 'nonpayable',
    type: 'constructor',
  },

...

]

 

remix내에서 스마트컨트랙트 코드를 솔리디티 언어로 작성한 후 컴파일 하면, compile details에서 얻을 수 있는 abi 형태의 값을 가져온다. 그 내용을 저장하여 

  const contract = new web3.eth.Contract(Abi, contractHx); // abi : 복사해서 그대로 // 고정
  
  contract.methods.mintNFT(addr, token)

 

이와 같이 contract로 만들어 배포된 컨트랙트의 함수를 이용할 수 있게 한다.

 


 

HeadCarousel.js : 웹페이지 홈의 정중앙에 캐러셀형태의 이미지를 삽입하여 사용자가 여러 정보들을 쉽게 볼 수 있게 함.

캐러셀 : 회전목마라는 뜻이며, 화면의 사진이 시간에 따라 또는 버튼을 눌러 다음사진으로 넘어 가게하는 개발용어.

 

→ 나중에 프론트엔드에서 써먹어볼만 한 내용.

 


Client/src/pages/

Home.js : 상단에는 /profile, /mintpage로 라우팅 되어있는 버튼들이 있으며, 중앙부에는 캐러셀형태의 이미지가있고, 하단에는 OpenSea 카테고리 형식의 내용들이 열거되어 있음

 

      <div className="row text-center">
        <div className="m-1 col card">

 

이 부분은 DOM의 className 내부에 주목해야한다. "text-center" 를 집어 넣으면 그 컴포넌트 내부에 있는 내용들이 중앙정렬되고 row와 col을 설정하여 행렬형태로 화면에 구성시킬 수 있다. m-1은 margin-1이라고 주변 여백을 설정해준다. 이와 같이 className을 이용하여 레이아웃을 구성할 수 있게하는 명령어에 대해 알아보자.

 

render() {
    return (
      <Container fluid>
        <Row>
          <Col className="item">col</Col>
          <Col className="item">col</Col>
          <Col className="item">col</Col>
        </Row>
      </Container>
    );
  }

 

row로 행을 선택하고 col로 열을 만들어준다. 결과는 아래와 같다. 이는 className에 넣어도 동일하게 여겨진다.

 

row, col 로 구성한 레이아웃

 

추가설명으로 리액트는 주어진 공간을 12개로 나눈다. 즉 col 1칸짜리라면 총 12개가 들어갈 수 있는것이다. 이를 응용하면 

 

render() {
    return (
      <Container>
        <Row>
          <Col className="item" md="6">
            col
          </Col>
          <Col className="item" md="3">
            col
          </Col>
          <Col className="item" md="3">
            col
          </Col>
        </Row>
      </Container>
    );
  }

 

화면의 크기는 xs, sm, md, lg, xl 순으로 구성되는데 거기에 차지할 공간만큼 숫자를 넣어주면된다. 위 코드는 총 6+3+3이 되며 총 세개의 col 이 허용된 가로축 한줄을 채우는 것이다. 

 

MintPage.js

<div className="col-12 mb-3">

 

이렇게 하면 12번째 열이되며 차지하는 공간은 mb-3이 되는것이다. 이 코드는 다음 줄로 넘어가서 컴포넌트가 구성되게 하려 했다.

 



MintPage.js : 메타마스크 지갑을 연결하면 account 해쉬값을, 이미지를 선택하면 File 값을 /tokenUri 로 POST하여 버퍼 형태로 처리 후 URI로 치환한 문자열 형태를, 텍스트필드에서 작성된 NFT의 아티스트, 콜렉션이름, NFT 이름을 JSON 형태로 만들어 DB에 전송할 수 있게 함.

 

필자가 가장 신경을 많이 썼고, 가장 애먹은 파트이다. NFT를 민트해야하는데 이를 위해 스마트 컨트랙트와 버튼을 연결해야하고, 그 버튼을 누르면 백엔드쪽으로 이미지 파일을 form형태로 보낸다. 그 이미지 파일은 버퍼형식이 되고 ipfs를 거쳐 tokenUri가 되어 돌아온다. 되돌려 받았다면 그 내용을 NFT의 세부사항이 담긴 JSON에 담아 다시 보내어 DB에 저장하게 한다.

 

이 과정이 버튼 하나로 처리되어야 하다보니 많은 문제가 생겼고 그에 따른 여러 트러블슈팅도 생겼다. (아직까지도 해결못한게 있긴하다.) 가장 잘못했던점은 나를 신뢰한 것이다. 무슨이유냐 하면, 처음 코드를 짤때 한곳에 짜놓고 여러 파일로 분리시키려 했는데, 시간도 모잘랐고 그거 처리하면서 에러 더 만들바엔 다음 단계진행하겠다는 마인드가 내 초기 목표를 무너뜨렸다. 다음 프로젝트 부터는 애초에 구상을 하고 파일을 만든뒤 그곳에서 작업을 진행해야겠다. 나중에 분리할생각말고. (진짜 분리 안하면 코드보기 더럽다.)

 

const MintPage = () => {
  //@ 하위컴포넌트인 MetamaskConnect.js에서 지갑 주소를 가져올 state
  const [textValue, setTextValue] = useState('');
  //@ NFT 콜렉션
  const [nftCollectionName, setNftCollectionName] = useState();
  //@ NFT 작가
  const [nftArtist, setNftArtist] = useState();
  //@ NFT 이름
  const [nftName, setNftName] = useState();
  //@ 로딩
  const [loading, setLoading] = useState(false);
  //@ 민팅 완료확인
  const [mintDone, setMintDone] = useState(false);
  //@ 이미지 등록 및 프리뷰 테스트
  const [imageSrc, setImageSrc] = useState('');
  
  ... }

 

벌써 토나오지 않은가. 진짜 이거보고 개선해야겠다는 생각이 계속 들었다. 상태변수가 많으니 찾기도 번거롭고, 다른사람한테 보여줄때도 직관적이지 않다.

 

  const contract = new web3.eth.Contract(Abi, contractHx); // abi : 복사해서 그대로 // 고정

 

이건 진짜 나중에도 무조건 사용해야하는 내용인데, 위에서 언급했다시피 contract의 method 함수를 이용하기 위해선 코드와 같이 구성해야한다. 근데 이해가 안되는점은 결국 contractHx에 연결되어 있는 함수들이 abi를 대표하는건데, 왜 굳이 파라미터에 둘다 들어가야될까라는 의문이 든다. 

 

  const getTextValue = async () => {
    const data = await getUserData();
    setTextValue(data[0]);
    console.log("textvalue:",textValue)
  };

 

초반에 설명했던 메타마스크 지갑 연결시 MintPage.js에서 처리해줘야하는 내용. (특이한 사항은 없다)

 

  const saveTextToJson = () => {
    const details = {
      account: textValue.account,
      artist: `${nftArtist}`,
      collection: `${nftCollectionName}`,
      name: `${nftName}`,
    };
    return details;
  };

 

텍스트 박스에 발행할 NFT의 상세사항과 서버를 거쳐 만들어진 tokenUri 값을 JSON 형태로 구성시켜 보낼 준비를 한다. 

 

  const postJsonData = () => {
    saveTextToJson(); // 보내기 직전 내용 전체 저장

    const data = imageSrc; // 이미지 파일
    const formData = new FormData();
    formData.append('img', data);

    axios
      .post('http://localhost:4000/tokenUri', formData)
      .then(function (res) {
        // console.log(res.data)
        mintNFT(res.data); // tokenUri 형태로 받고
        const tokenJson = { tokenUri: `${res.data}` }; // 받은걸 DB에 올릴 형식으로 만들어주고
        const finalJson = Object.assign(saveTextToJson(), tokenJson); // textField에 적었던 내용과 합침
        axios.post('http://localhost:4000/getthedata', finalJson); // DB를 위해서 POST
      })
      .catch(function (error) {
        console.log(error);
      });  
  
  const mintNFT = (token) => {
    contract.methods
      .mintNFT(addr, token)
      .send({ from: addr })
      .on('receipt', function (receipt) {
        console.log(receipt); //메소드내를 변경하므로 .send() 사용 vs 계약상태를 변경하지않는다면 .call()
        if(receipt){
          setLoading(false);
        }
      });
    setLoading(true);
  };


  };

 

민트 기능의 근간이 되는 부분이다. 쪼개서 보자.

 

const postJsonData = () => {
    saveTextToJson(); // 보내기 직전 내용 전체 저장

    const data = imageSrc; // 이미지 파일
    const formData = new FormData();
    formData.append('img', data);

    axios
      .post('http://localhost:4000/tokenUri', formData)
      .then(function (res) {
        // console.log(res.data)
        mintNFT(res.data); // tokenUri 형태로 받고
        const tokenJson = { tokenUri: `${res.data}` }; // 받은걸 DB에 올릴 형식으로 만들어주고
        const finalJson = Object.assign(saveTextToJson(), tokenJson); // textField에 적었던 내용과 합침
        axios.post('http://localhost:4000/getthedata', finalJson); // DB를 위해서 POST
      })
      .catch(function (error) {
        console.log(error);
      });
  };

 

위에서 saveTextToJson을 통해 텍스트박스 내용들을 한번 저장한다. 이때는 tokenUri 객체가 포함되지 않은 상태이다. 그 뒤 이미지 파일 업로드 진행 후 받은 파일을 FormData()를 이용해 formData 상수에 담아준다. 이때 FormData()란 HTML5DML <form>태그를 이용해 input 값을 서버에 전송하는 것과 같은 진행을 하기 위해 필요한 것이다. 즉, FormData란 HTML이 아닌 자바스크립트쪽에서 폼데이터를 다루는 객체라고 보면된다. 이제 이걸 axios로 보내면 되는 것이다. 

 

하지만 실제로 js에서 폼 전송을 할 이유는 특정상황을 제외하곤 거의 없다고 한다. 특정상황으로는 이미지같은 멀티미디어 파일을 페이지 전환없이 비동기로 제출하고 싶을때나, 자바스크립트로 좀더 타이트하게 폼데이터를 관리하고 싶을때 사용한다고 하니, 첫번째 경우가 우리에 해당하지 않나 싶다. 

 

자 이제 폼데이터를 보냈다. 그러면 백엔드쪽에서 폼데이터를 받고 버퍼 형태로 바꾼뒤 ipfs를 거쳐 tokenUri에 담아 데이터를 보낸다. 그걸 res로 받은 뒤, 민팅을 하기위해 mintNFT에 토큰URI를 인자로 보내 실행하면 트랜잭션 창이 열리게 된다. 트잭이 완료되면 블록체인 블록위에 올라가게 되는것이다. 

 

그 뒤 백엔드를 거쳐 DB에 저장할 tokenUri 객체가 들어간 JSON 파일을 구성한뒤 보내면 끝이다.

 

  const mintNFT = (token) => {
    contract.methods
      .mintNFT(addr, token)
      .send({ from: addr }) //메소드내를 변경하므로 .send() 사용 vs 계약상태를 변경하지않는다면 .call()
      .on('receipt', function (receipt) {
        console.log(receipt); 
        if(receipt){
          setLoading(false);
        }
      });
    setLoading(true);
  };

 

contract.method를 이용해 mintNFT 함수를 이용하는데 파라미터로 지갑주소와 토큰 URI를 넣어줘야한다. 토큰 URI는 상위 함수에서 전달된다. 
여기서 .send를 붙여줘야한다. 주석과 마찬가지로 .send()와 .call()의 차이점을 알아야한다. 여기서 중요한 점은 .send()안에 파라미터가 들어가야하는데, 다른곳에선 안넣어도 되지만 컨트랙트를 사용할때는 누가 호출했는지를 알려줘야하는듯 하다. (도움주신분의 답변을 까먹어버렸다...)

 

그 밑의 on('receipt', function (receipt) { console.log(receipt) } 이 부분은 의외로 중요한 내용이 있는 부분이다.
민트 함수를 호출하여 트랜잭션을 보낸 뒤 .on()을 이용하여 이벤트가 끝난뒤 response 값을 가져온다. 여기서는 트랜잭션이 처리되고 그 트랜잭션과 관련된 내용이 넘어오게 된다. 이 내용이 넘어오는 때가 곧 민팅이 완료된 시점이기에 나는 이때 로딩창을 사라지게 만들었다. 이 부분은 추후 많이 사용할 것 같다.

 




Profile.js : 메타마스크 지갑을 연결하면 account 해쉬값을 전달받아, /senddata에 전달하고 그 값을 DB에서 필터링하여 지갑과 관련된 객체만을 추출한뒤 받아옴. 그 객체 내용들에는 account, artist, collection, name, tokenUri 값이 있으며 이를 화면에 나타나게해주어 지갑에서 민팅한 NFT를 열거시켜줌.

 

 


Server/apps/

tokenUri.js : 이 코드를 호출하게 되면 이미지 버퍼값을 ipfs를 통해 tokenUri 값으로 치환시켜줌.

 


Server/

index.js : Client 에서 받아온 이미지 파일은 버퍼 형식으로 바꿔주고, 민팅한 NFT의 내용들을 담은 JSON 파일들을 DB에 저장시켜주며, Profile에서 호출시 필요한 내용들만 필터링하여 JSON형태로 보내줌

 

 

 

 

 

 

 

 

'블록체인' 카테고리의 다른 글

CORS / OPTIONS / preflight  (0) 2022.11.10
[코드리뷰] Project 2 - 인센티브 기반 커뮤니티  (1) 2022.11.10
웹 개발  (0) 2022.08.31
HTTP, SSR/CSR  (0) 2022.08.08
React - React로 사고하기  (0) 2022.08.03