(BlockChain) NFT-Market (1)
              
          2023. 3. 16. 17:11ㆍBlockChain
반응형
    
    
    
  NFT를 거래할 수 있는 조그마한 Market을 만들어 보려고 한다
일단, 시작에 앞서 파일 및 폴더 환경설정부터 하면서 천천히 진행하려 한다
0. TS 설치
npm list -g
npm i -g typescript ts-node- 이전에 TS를 사용한 적이 없거나 컴퓨터를 교체하여서 window를 재설치 혹은 새로 깔았다면 전역에 ts를 설치해 주자
1. React (with.TS)
yarn create react-app front --template typescript
cd front
yarn add web3 axios @metamask/providers- @metamask/providers : TS환경에서 React내 window.ethereum의 자료형을 TS Compiler가 인지하지 못하기에 위와 같은 라이브러리를 설치
- 이후 설치된 후 front(react) 폴더 내에 있는 react-app-env.d.ts 파일내부에 아래와 같이 코드를 작성해 준다
 
// react-app-env.d.ts
import { MetaMaskInpageProvider } from "@metamask/providers";
declare global {
  interface Window {
    ethereum?: MetaMaskInpageProvider;
  }
}
2. Express(back)
mkdir back
cd back
npm init -y
npm i express dotenv @openzeppelin/contracts @remix-project/remixd cors multer @pinata/sdk
npm i -D @types/node nodemon @types/express @types/multer prettier-plugin-solidity tsconfig-paths2-1. back폴더 내 package.json 부분 코드 수정
"start": "node ./build/index.js",
"start:dev": "nodemon --watch \"src/**/*.ts\" --exec \"ts-node\" src/index.ts"- 위처럼 JSON파일 설정 후 back폴더 위치에서 아래 명령어로 express를 실행할 예정
- npm run start:dev
 
3. Custom Hook 생성
// front/src/modules/useWeb3.ts
import { useCallback, useState } from "react";
import Web3 from "web3";
export const useWeb3 = (): {
  web3?: Web3;
  account: string;
  chainId: string | null | undefined;
  logIn: () => void;
} => {
  const [web3, setWeb3] = useState<Web3 | undefined>();
  const [account, setAccount] = useState<string>("");
  const [chainId, setChainId] = useState<string | null | undefined>("");
  const logIn = useCallback(async () => {
    try {
      if (!window.ethereum) console.log("MetaMask is not exist");
      const _web3 = new Web3((window as any).ethereum);
      setWeb3(_web3);
      const [_account] = (await window.ethereum?.request({
        method: "eth_requestAccounts",
      })) as Array<string>;
      if (_account) setAccount(_account);
      setChainId(window.ethereum?.networkVersion);
    } catch (err) {
      console.log(err);
    }
  }, []);
  return { web3, account, chainId, logIn };
};- 작성 중 react-app-env.d.ts 파일에 메타마스크 자료형 설정
- useCallback Hook을 사용함으로써 랜더링이 이루어질 때마다 함수가 선언되지 않게 설정
- 두 번째 매개변수로 빈 배열을 넣어줌으로써 최초 마운팅 될 때만 함수 선언이 되도록 설정
 
4. 최상위 App.tsx 컴포넌트에서 useWeb3 Hook을 가져와서 정보를 출력
// front/src/App.tsx
import { useWeb3 } from "./modules/useWeb3";
import {} from "react";
import Mint from "./components/Mint";
function App() {
  const { account, chainId, logIn } = useWeb3();
  return (
    <div>
      <div>
        {account ? (
          <div>
            <div>ChainId : {chainId}</div>
            <div>Account : {account}</div>
            <Mint />
          </div>
        ) : (
          <div>
            <button
              onClick={() => {
                logIn();
              }}
            >
              LogIn MetaMask
            </button>
          </div>
        )}
      </div>
    </div>
  );
}
export default App;- 메타마스크가 연결되어 있지 않으면 연결할 수 있는 버튼을 출력
- 해당 버튼을 누르면 현재 메타마스크의 계정으로 연결
 
- 연결이 되어 있으면 현재 연결된 계정의 체인정보와 계정을 브라우저에 출력
5. mint 함수를 사용하는 Mint 컴포넌트 생성
import axios from "axios";
import { useCallback, useState, FormEvent, ChangeEvent } from "react";
const Mint = () => {
  const [NftName, setName] = useState<string>("");
  const [NftDescription, setDescription] = useState<string>("");
  const [file, setFile] = useState<File | undefined>();
  const [img, setImg] = useState<string | ArrayBuffer>("");
  const settingName = useCallback((e: FormEvent<HTMLInputElement>) => {
    setName(e.currentTarget.value);
  }, []);
  const settingDescription = useCallback((e: FormEvent<HTMLInputElement>) => {
    setDescription(e.currentTarget.value);
  }, []);
  const fileChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
    if (e.currentTarget.files && e.currentTarget.files.length > 0) {
      setFile(e.currentTarget.files[0]);
      const reader = new FileReader();
      reader.readAsDataURL(e.currentTarget.files[0]);
      reader.onload = () => {
        if (reader.result) setImg(reader.result);
      };
    }
  }, []);
  const mint = async () => {
    if (!NftName || !NftDescription || !file) return;
    const formData = new FormData();
    formData.append("file", file);
    formData.append("name", NftName);
    formData.append("description", NftDescription);
    const result = (
      await axios.post("http://localhost:8080/api/mint", formData)
    ).data;
    console.log(result);
  };
  return (
    <div>
      <input type={"text"} placeholder={"NftName"} onInput={settingName} />
      <input
        type={"text"}
        placeholder={"NftDescription"}
        onInput={settingDescription}
      />
      <input type={"file"} onChange={fileChange} />
      {img && (
        <div>
          <img src={img.toString()} alt="" />
        </div>
      )}
      <button onClick={mint}>Start Minting</button>
    </div>
  );
};
export default Mint;- 컴포넌트 내에 input onInput으로 입력에 대한 값들을 state변수에 저장
- 입력할 때마다 현재 해당 값들을 state의 set함수로 재정의
- 이렇게 state값을 설정하는 함수는 랜더링 될 때마다 불필요하게 선언될 필요가 없기에 useCallback Hook으로 막아준다
 
- 업로드할 이미지를 미리 보기를 위해서 업로드 input에 대한 change함수 생성
- onChange 이벤트를 통해서 값이 변화가 있을 때마다 현재 해당 값이 있으면서 해당 값의 길이가 0보다 클 때 set함수를 통하여 이미지의 값을 재정의한다
- 이후 내장되어 있는 파일을 읽는 객체를 만들어서 내용을 가지고 element, 즉 브라우저에 출력해 주기 위해 준비
- 로딩이 되면 그 결과값을 setImg함수로 state img 변수를 재정의 한다
- 그 결과값을 img태그의 src에 toString() 메서드로 이미지주소 값으로 변환하여 접근한다
 
 
- NFT이름과 설명 그리고 이미지까지 state변수에 저장이 되었다면 axios요청 시 전달 데이터로 넣어준다
- 파일 업로드를 위해서 express 환경에서 multer라이브러리를 사용하기 때문에 formData라는 내장 객체에 append 하여 axios.post 요청할 때 같이 보내준다
 
6. axios에 요청에 대한 함수를 만들었기에 express 환경 설정
tsconfig.json생성
{
  "exclude": ["node_modules"],
  "compilerOptions": {
    "outDir": "./build/",
    "target": "ES6",
    "lib": ["ES6", "DOM"],
    "moduleResolution": "node",
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "removeComments": true,
    "allowJs": true,
    "baseUrl": ".",
    "typeRoots": ["./node_modules/@types", "./@types"],
    "paths": {
      "@core/*": ["src/core/*"],
      "*": ["@types/*"]
    }
  },
  "ts-node": {
    "files": true,
    "require": ["tsconfig-paths/register"]
  }
}src/index.ts
import { Readable } from "stream";
// stream은 nodejs가 가지고 있는 내장라이브러리다
// 데이터를 stream화 해주는 라이브러리
const pinata = new pinataSDK(process.env.API_Key, process.env.API_Secret);
// pinata 페이지 로그인 후 API Keys를 눌러서 새로운 키를 발급받고 그 후에 나오는 Key, secret를 env파일에 넣어준다back/.env
API_Key= pinata웹페이지에서 API_KEY값
API_Secret= pinata웹페이지에서 API_SECRET값
7. axios 응답 라우터 코드 작성
app.post(
  "/api/mint",
  upload.single("file"),
  async (req: Request, res: Response) => {
    const { name, description }: { name: string; description: string } =
      req.body;
    const imgResult: {
      IpfsHash: string;
      PinSize: number;
      Timestamp: string;
      isDuplicate?: boolean;
    } = await pinata.pinFileToIPFS(Readable.from(req.file.buffer), {
      pinataMetadata: { name: Date.now().toString() },
      pinataOptions: { cidVersion: 0 },
    });
    if (imgResult.isDuplicate) console.log("img Duplicated");
    const jsonResult = await pinata.pinJSONToIPFS(
      {
        name,
        description,
        image: `https://gateway.pinata.cloud/ipfs/${imgResult.IpfsHash}`,
      },
      {
        pinataMetadata: { name: Date.now().toString() + ".json" },
        pinataOptions: { cidVersion: 0 },
      }
    );
    res.send("mint Complete");
  }
);- upload.single : multer를 사용하여 formData에 전달된 input type file이 한 개이기에 single로 axios 응답 쪽에 설정
- const {name, description} : {name:string, description:string} = req.body;
- 구조분해할당을 통해 NFT이름과 설명의 변수명과 값을 그대로 가져와서 사용한다
 
- imgResult : NFT이미지로 사용할 이미지를 pinata를 이용해서 올린다
- jsonResult NFT JSON파일을 pinata를 이용해서 올린다
8. List 컴포넌트를 생성
import axios from "axios";
import { useCallback, useState, useEffect } from "react";
interface nftData {
  name: string;
  description: string;
  image: string;
}
const List = () => {
  const [list, setList] = useState<Array<nftData>>([]);
  const listUp = useCallback(async () => {
    const result = (await axios.get("http://localhost:8080/api/list")).data;
    setList(result.tempArr);
  }, []);
  useEffect(() => {
    listUp();
  }, []);
  return (
    <ul>
      {list.map((item, index) => (
        <Item item={item} key={`item-${index}`} />
      ))}
    </ul>
  );
};
export default List;
const Item = ({ item: { name, description, image } }: { item: nftData }) => {
  return (
    <li>
      <div>{name}</div>
      <div>{description}</div>
      <div>
        <img src={image} alt="" />
      </div>
    </li>
  );
};- pinata. IPFS에 업로드한 전체 JSON파일의 목록들을 출력
- interface를 따로 선언해서 코드의 중복과 가독성을 높여준다
- interface nftData 선언 없이 한다면 다음과 같다
 
const [list, setList] = useState<
  Array<{
    name: string;
    description: string;
    image: string;
  }>
>([]);
const Item = ({
  item: { name, description, image },
}: {
  item: {
    name: string;
    description: string;
    image: string;
  };
}) => {
  return (
    <li>
      <div>{name}</div>
      <div>{description}</div>
      <div>
        <img src={image} alt="" />
      </div>
    </li>
  );
};
9. List 컴포넌트를 완성 후 임시 배열들을 express 환경에 선언해 주고 axios.get 요청에 대한 응답으로 가져와서 list.map 메서드를 실행한다
app.get("/api/list", (req: Request, res: Response) => {
  const tempArr = [
    {
      name: "test NFT",
      description: "testing NFT with Pinata",
      image:"https://gateway.pinata.cloud/ipfs/Qmcc65wxFtcJyEtEU1riHMUP8a7Vr4gJTxezoyQZCWyazu",
    },
    {
      name: "test NFT",
      description: "testing NFT with Pinata",
      image:"https://gateway.pinata.cloud/ipfs/Qmcc65wxFtcJyEtEU1riHMUP8a7Vr4gJTxezoyQZCWyazu",
    },
    {
      name: "test NFT",
      description: "testing NFT with Pinata",
      image:"https://gateway.pinata.cloud/ipfs/Qmcc65wxFtcJyEtEU1riHMUP8a7Vr4gJTxezoyQZCWyazu",
    },
  ];
  res.send({ tempArr });
});
'BlockChain' 카테고리의 다른 글
| (BlockChain) NFT-Market (2) (0) | 2023.03.17 | 
|---|---|
| (BlockChain) NFT 거래 컨트랙트 (0) | 2023.03.14 | 
| (BlockChain) NFT 토큰 컨트랙트 (0) | 2023.03.14 | 
| (BlockChain) Remix 활용 (0) | 2023.03.13 | 
| (BlockChain) Token Swap (0) | 2023.03.13 |