본문으로 건너뛰기

NFT 발행기 튜토리얼

Solidity
NFT
Alchemy
스마트 컨트랙트
프론트엔드
Pinata
erc-721
중급
smudgil
2021년 10월 6일
49 분 소요

웹2 배경을 가진 개발자들이 겪는 가장 큰 어려움 중 하나는 스마트 컨트랙트를 프론트엔드 프로젝트에 연결하고 상호작용하는 방법을 알아내는 것입니다.

디지털 자산에 대한 링크, 제목, 설명을 입력할 수 있는 간단한 UI인 NFT 발행기를 구축함으로써 다음을 배우게 됩니다.

  • 프론트엔드 프로젝트를 통해 메타마스크에 연결하기
  • 프론트엔드에서 스마트 컨트랙트 메서드 호출하기
  • 메타마스크를 사용하여 트랜잭션 서명하기

이 튜토리얼에서는 프론트엔드 프레임워크로 React (opens in a new tab)를 사용할 것입니다. 이 튜토리얼은 주로 Web3 개발에 초점을 맞추고 있으므로 React의 기본 개념을 설명하는 데 많은 시간을 할애하지는 않겠습니다. 대신 프로젝트에 기능을 구현하는 데 집중할 것입니다.

사전 지식으로 React에 대한 초급 수준의 이해가 필요합니다. 컴포넌트, props, useState/useEffect 및 기본 함수 호출이 어떻게 작동하는지 알아야 합니다. 이러한 용어를 처음 들어본다면 이 React 소개 튜토리얼 (opens in a new tab)을 확인해 보세요. 시각적인 학습을 선호하는 분들께는 Net Ninja의 훌륭한 Full Modern React Tutorial (opens in a new tab) 비디오 시리즈를 강력히 추천합니다.

아직 계정이 없다면, 이 튜토리얼을 완료하고 블록체인에 무언가를 구축하기 위해 Alchemy 계정이 반드시 필요합니다. 여기 (opens in a new tab)에서 무료 계정에 가입하세요.

거두절미하고, 바로 시작하겠습니다!

NFT 만들기 101

코드를 살펴보기 전에 NFT를 만드는 과정이 어떻게 작동하는지 이해하는 것이 중요합니다. 이는 두 단계로 이루어집니다.

이더리움 블록체인에 NFT 스마트 컨트랙트 게시하기

두 NFT 스마트 컨트랙트 표준의 가장 큰 차이점은 ERC-1155는 다중 토큰 표준이며 일괄 처리 기능을 포함하는 반면, ERC-721은 단일 토큰 표준이므로 한 번에 하나의 토큰 전송만 지원한다는 것입니다.

발행 함수 호출하기

일반적으로 이 발행 함수는 두 개의 변수를 매개변수로 전달해야 합니다. 첫 번째는 새로 발행된 NFT를 받을 주소를 지정하는 recipient이고, 두 번째는 NFT의 메타데이터를 설명하는 JSON 문서로 확인되는 문자열인 NFT의 tokenURI입니다.

NFT의 메타데이터는 이름, 설명, 이미지(또는 다른 디지털 자산) 및 기타 속성과 같은 특성을 가질 수 있게 하여 NFT에 생명력을 불어넣습니다. 다음은 NFT의 메타데이터가 포함된 tokenURI의 예시 (opens in a new tab)입니다.

이 튜토리얼에서는 React UI를 사용하여 NFT 스마트 컨트랙트 발행 함수를 호출하는 두 번째 부분에 초점을 맞출 것입니다.

Sepolia와 같이 지원되는 테스트넷에 배포된 ERC-721 NFT 스마트 컨트랙트가 필요합니다. 직접 배포하고 싶다면 Alchemy의 Sepolia에 스마트 컨트랙트 배포하기 (opens in a new tab) 가이드를 추천합니다.

좋습니다. 이제 NFT를 만드는 방법을 이해했으니 시작 파일을 클론해 보겠습니다!

시작 파일 클론하기

먼저 nft-minter-tutorial GitHub 리포지토리 (opens in a new tab)로 이동하여 이 프로젝트의 시작 파일을 가져옵니다. 이 리포지토리를 로컬 환경에 클론하세요.

클론한 nft-minter-tutorial 리포지토리를 열면 minter-starter-filesnft-minter라는 두 개의 폴더가 있는 것을 확인할 수 있습니다.

  • minter-starter-files에는 이 프로젝트의 시작 파일(기본적으로 React UI)이 포함되어 있습니다. 이 튜토리얼에서는 이 UI를 이더리움 지갑 및 NFT 스마트 컨트랙트에 연결하여 생명력을 불어넣는 방법을 배우게 되므로 이 디렉토리에서 작업할 것입니다.
  • nft-minter에는 완료된 전체 튜토리얼이 포함되어 있으며, 막혔을 때 참고용으로 제공됩니다.

다음으로 코드 편집기에서 minter-starter-files 복사본을 열고 src 폴더로 이동합니다.

우리가 작성할 모든 코드는 src 폴더 아래에 위치합니다. Minter.js 컴포넌트를 편집하고 추가 JavaScript 파일을 작성하여 프로젝트에 Web3 기능을 부여할 것입니다.

2단계: 시작 파일 확인하기

코딩을 시작하기 전에 시작 파일에 이미 제공된 내용을 확인하는 것이 중요합니다.

React 프로젝트 실행하기

브라우저에서 React 프로젝트를 실행하는 것으로 시작하겠습니다. React의 장점은 프로젝트가 브라우저에서 실행되면 저장하는 모든 변경 사항이 브라우저에 실시간으로 업데이트된다는 것입니다.

프로젝트를 실행하려면 minter-starter-files 폴더의 루트 디렉토리로 이동한 다음 터미널에서 npm install를 실행하여 프로젝트의 종속성을 설치합니다.

cd minter-starter-files
npm install

설치가 완료되면 터미널에서 npm start를 실행합니다.

npm start

이렇게 하면 브라우저에서 http://localhost:3000/ (opens in a new tab) 이 열리고 프로젝트의 프론트엔드를 볼 수 있습니다. 이는 NFT 자산에 대한 링크를 입력하는 곳, NFT의 이름을 입력하는 곳, 설명을 제공하는 곳의 3가지 필드로 구성되어야 합니다.

"Connect Wallet" 또는 "Mint NFT" 버튼을 클릭해 보면 작동하지 않는 것을 알 수 있습니다. 아직 해당 기능을 프로그래밍해야 하기 때문입니다! :)

Minter.js 컴포넌트

참고: nft-minter 폴더가 아닌 minter-starter-files 폴더에 있는지 확인하세요!

편집기의 src 폴더로 돌아가서 Minter.js 파일을 엽니다. 이 파일은 우리가 작업할 주요 React 컴포넌트이므로 이 파일의 모든 내용을 이해하는 것이 매우 중요합니다.

이 파일의 맨 위에는 특정 이벤트 후에 업데이트할 상태 변수가 있습니다.

//상태 변수
const [walletAddress, setWallet] = useState("")
const [status, setStatus] = useState("")
const [name, setName] = useState("")
const [description, setDescription] = useState("")
const [url, setURL] = useState("")

React 상태 변수나 상태 훅에 대해 들어본 적이 없으신가요?  (opens in a new tab) 문서를 확인해 보세요.

각 변수가 나타내는 의미는 다음과 같습니다.

  • walletAddress - 사용자의 지갑 주소를 저장하는 문자열
  • status - UI 하단에 표시할 메시지를 포함하는 문자열
  • name - NFT의 이름을 저장하는 문자열
  • description - NFT의 설명을 저장하는 문자열
  • url - NFT의 디지털 자산에 대한 링크인 문자열

상태 변수 다음에는 구현되지 않은 세 가지 함수인 useEffect, connectWalletPressed, onMintPressed가 표시됩니다. 이 함수들은 모두 async인데, 그 안에서 비동기 API 호출을 할 것이기 때문입니다! 이들의 이름은 그 기능과 동일합니다.

  • useEffect (opens in a new tab) - 컴포넌트가 렌더링된 후 호출되는 React 훅입니다. 빈 배열 [] prop이 전달되기 때문에(3번째 줄 참조) 컴포넌트의 첫 번째 렌더링 시에만 호출됩니다. 여기에서 지갑 리스너와 다른 지갑 함수를 호출하여 지갑이 이미 연결되어 있는지 여부를 반영하도록 UI를 업데이트할 것입니다.
  • connectWalletPressed - 이 함수는 사용자의 메타마스크 지갑을 탈중앙화 애플리케이션(dapp)에 연결하기 위해 호출됩니다.
  • onMintPressed - 이 함수는 사용자의 NFT를 발행하기 위해 호출됩니다.

이 파일의 끝부분에는 컴포넌트의 UI가 있습니다. 이 코드를 주의 깊게 살펴보면 해당 텍스트 필드의 입력이 변경될 때 url, name, description 상태 변수를 업데이트한다는 것을 알 수 있습니다.

또한 ID가 mintButtonwalletButton인 버튼을 각각 클릭할 때 connectWalletPressedonMintPressed가 호출되는 것을 볼 수 있습니다.

마지막으로 이 Minter 컴포넌트가 어디에 추가되는지 살펴보겠습니다.

다른 모든 컴포넌트의 컨테이너 역할을 하는 React의 메인 컴포넌트인 App.js 파일로 이동하면 7번째 줄에 Minter 컴포넌트가 주입된 것을 볼 수 있습니다.

이 튜토리얼에서는 Minter.js file만 편집하고 src 폴더에 파일을 추가할 것입니다.

이제 우리가 작업할 내용을 이해했으니 이더리움 지갑을 설정해 보겠습니다!

이더리움 지갑 설정하기

사용자가 스마트 컨트랙트와 상호작용하려면 이더리움 지갑을 dapp에 연결해야 합니다.

메타마스크 다운로드

이 튜토리얼에서는 이더리움 계정 주소를 관리하는 데 사용되는 브라우저 기반 가상 지갑인 메타마스크를 사용할 것입니다. 이더리움의 트랜잭션이 어떻게 작동하는지 더 자세히 알고 싶다면 이 페이지를 확인해 보세요.

여기 (opens in a new tab)에서 무료로 메타마스크를 다운로드하고 계정을 생성할 수 있습니다. 계정을 생성할 때, 또는 이미 계정이 있는 경우, (실제 돈을 다루지 않도록) Sepolia와 같이 지원되는 테스트 네트워크로 전환해야 합니다.

퍼싯에서 이더 추가하기

NFT를 발행하려면(또는 이더리움 블록체인에서 트랜잭션에 서명하려면) 가짜 ETH가 필요합니다. 테스트넷 ETH를 얻으려면 Alchemy Sepolia 퍼싯 (opens in a new tab)과 같이 유지 관리되는 퍼싯을 사용하고 Sepolia 계정 주소를 입력하세요. 곧 메타마스크 계정에서 ETH를 확인할 수 있을 것입니다!

잔액 확인하기

잔액이 있는지 다시 확인하기 위해 Alchemy의 샌드박스 도구 (opens in a new tab)를 사용하여 eth_getBalance (opens in a new tab) 요청을 해보겠습니다. 이 요청은 지갑에 있는 ETH의 양을 반환합니다. 메타마스크 계정 주소를 입력하고 “Send Request”를 클릭하면 다음과 같은 응답을 볼 수 있습니다.

{"jsonrpc": "2.0", "id": 0, "result": "0xde0b6b3a7640000"}

참고: 이 결과는 ETH가 아닌 Wei 단위입니다. Wei는 이더의 가장 작은 단위로 사용됩니다. Wei에서 ETH로의 변환은 1 ETH = 10¹⁸ Wei입니다. 따라서 0xde0b6b3a7640000을 십진수로 변환하면 1*10¹⁸이 되며, 이는 1 ETH와 같습니다.

휴! 가짜 돈이 모두 잘 들어있네요!

UI에 메타마스크 연결하기

이제 메타마스크 지갑이 설정되었으니 dapp에 연결해 보겠습니다!

MVC (opens in a new tab) 패러다임을 따르기 위해 dapp의 로직, 데이터 및 규칙을 관리하는 함수를 포함하는 별도의 파일을 만든 다음, 해당 함수를 프론트엔드(Minter.js 컴포넌트)에 전달할 것입니다.

connectWallet 함수

이를 위해 src 디렉토리에 utils라는 새 폴더를 만들고 그 안에 모든 지갑 및 스마트 컨트랙트 상호작용 함수를 포함할 interact.js라는 파일을 추가해 보겠습니다.

interact.js 파일에 connectWallet 함수를 작성한 다음, 이를 Minter.js 컴포넌트에서 가져와 호출할 것입니다.

interact.js 파일에 다음을 추가합니다.

이 코드가 무엇을 하는지 분석해 보겠습니다.

먼저, 우리 함수는 브라우저에서 window.ethereum가 활성화되어 있는지 확인합니다.

window.ethereum는 메타마스크 및 기타 지갑 제공업체가 주입하는 전역 API로, 웹사이트가 사용자의 이더리움 계정을 요청할 수 있게 해줍니다. 승인되면 사용자가 연결된 블록체인에서 데이터를 읽고 사용자에게 메시지 및 트랜잭션 서명을 제안할 수 있습니다. 자세한 내용은 메타마스크 문서 (opens in a new tab)를 확인하세요!

window.ethereum존재하지 않는다면 메타마스크가 설치되지 않았음을 의미합니다. 이 경우 반환되는 address가 빈 문자열이고 status JSX 객체가 사용자가 메타마스크를 설치해야 함을 전달하는 JSON 객체가 반환됩니다.

우리가 작성하는 대부분의 함수는 상태 변수와 UI를 업데이트하는 데 사용할 수 있는 JSON 객체를 반환할 것입니다.

이제 window.ethereum존재한다면 흥미로운 일이 벌어집니다.

try/catch 루프를 사용하여 window.ethereum.request({ method: "eth_requestAccounts" }); (opens in a new tab)를 호출하여 메타마스크에 연결을 시도합니다. 이 함수를 호출하면 브라우저에서 메타마스크가 열리고 사용자에게 지갑을 dapp에 연결하라는 메시지가 표시됩니다.

  • 사용자가 연결을 선택하면 method: "eth_requestAccounts"는 dapp에 연결된 사용자의 모든 계정 주소가 포함된 배열을 반환합니다. 결과적으로 connectWallet 함수는 이 배열의 첫 번째 address(9번째 줄 참조)와 사용자에게 스마트 컨트랙트에 메시지를 작성하라는 메시지를 표시하는 status 메시지가 포함된 JSON 객체를 반환합니다.
  • 사용자가 연결을 거부하면 JSON 객체는 반환된 address에 대한 빈 문자열과 사용자가 연결을 거부했음을 반영하는 status 메시지를 포함하게 됩니다.

Minter.js UI 컴포넌트에 connectWallet 함수 추가하기

이제 이 connectWallet 함수를 작성했으니 Minter.js. 컴포넌트에 연결해 보겠습니다.

먼저 Minter.js 파일의 맨 위에 import { connectWallet } from "./utils/interact.js";를 추가하여 함수를 Minter.js 파일로 가져와야 합니다. 이제 Minter.js의 처음 11줄은 다음과 같아야 합니다.

그런 다음 connectWalletPressed 함수 내부에서 가져온 connectWallet 함수를 다음과 같이 호출합니다.

const connectWalletPressed = async () => {
  const walletResponse = await connectWallet()
  setStatus(walletResponse.status)
  setWallet(walletResponse.address)
}

대부분의 기능이 interact.js 파일에서 Minter.js 컴포넌트로부터 어떻게 추상화되었는지 확인하셨나요? 이는 M-V-C 패러다임을 준수하기 위함입니다!

connectWalletPressed에서는 가져온 connectWallet 함수에 대해 단순히 await 호출을 수행하고, 그 응답을 사용하여 상태 훅을 통해 statuswalletAddress 변수를 업데이트합니다.

이제 Minter.jsinteract.js 두 파일을 모두 저장하고 지금까지의 UI를 테스트해 보겠습니다.

브라우저에서 localhost:3000을 열고 페이지 오른쪽 상단의 "Connect Wallet" 버튼을 누릅니다.

메타마스크가 설치되어 있다면 지갑을 dapp에 연결하라는 메시지가 표시될 것입니다. 연결 초대를 수락하세요.

이제 지갑 버튼에 주소가 연결되었음이 반영되는 것을 볼 수 있습니다.

다음으로 페이지를 새로고침해 보세요... 이상합니다. 지갑이 이미 연결되어 있는데도 지갑 버튼이 메타마스크를 연결하라는 메시지를 표시하고 있습니다...

하지만 걱정하지 마세요! 주소가 이미 dapp에 연결되어 있는지 확인하고 그에 따라 UI를 업데이트하는 getCurrentWalletConnected라는 함수를 구현하여 이 문제를 쉽게 해결할 수 있습니다!

getCurrentWalletConnected 함수

interact.js 파일에 다음 getCurrentWalletConnected 함수를 추가합니다.

이 코드는 앞서 작성한 connectWallet 함수와 매우 유사합니다.

주요 차이점은 사용자가 지갑을 연결할 수 있도록 메타마스크를 여는 eth_requestAccounts 메서드를 호출하는 대신, 여기서는 현재 dapp에 연결된 메타마스크 주소가 포함된 배열을 단순히 반환하는 eth_accounts 메서드를 호출한다는 것입니다.

이 함수가 작동하는 것을 보기 위해 Minter.js 컴포넌트의 useEffect 함수에서 호출해 보겠습니다.

connectWallet에서 했던 것처럼 다음과 같이 interact.js 파일에서 Minter.js 파일로 이 함수를 가져와야 합니다.

import { useEffect, useState } from "react"
import {
  connectWallet,
  getCurrentWalletConnected, //여기에 import
} from "./utils/interact.js"

이제 useEffect 함수에서 간단히 호출합니다.

useEffect(async () => {
  const { address, status } = await getCurrentWalletConnected()
  setWallet(address)
  setStatus(status)
}, [])

getCurrentWalletConnected 호출의 응답을 사용하여 walletAddressstatus 상태 변수를 업데이트한다는 점에 유의하세요.

이 코드를 추가한 후 브라우저 창을 새로고침해 보세요. 버튼에 연결되었다고 표시되고 연결된 지갑 주소의 미리보기가 표시되어야 합니다. 새로고침한 후에도 말이죠!

addWalletListener 구현하기

dapp 지갑 설정의 마지막 단계는 사용자가 연결을 끊거나 계정을 전환할 때와 같이 지갑의 상태가 변경될 때 UI가 업데이트되도록 지갑 리스너를 구현하는 것입니다.

Minter.js 파일에 다음과 같은 addWalletListener 함수를 추가합니다.

여기서 무슨 일이 일어나고 있는지 간단히 분석해 보겠습니다.

  • 먼저, 우리 함수는 window.ethereum가 활성화되어 있는지(즉, 메타마스크가 설치되어 있는지) 확인합니다.
    • 그렇지 않다면 status 상태 변수를 사용자에게 메타마스크를 설치하라는 메시지를 표시하는 JSX 문자열로 설정합니다.
    • 활성화되어 있다면 3번째 줄에 메타마스크 지갑의 상태 변경을 수신하는 리스너 window.ethereum.on("accountsChanged")를 설정합니다. 여기에는 사용자가 dapp에 추가 계정을 연결하거나, 계정을 전환하거나, 계정 연결을 끊는 경우가 포함됩니다. 연결된 계정이 하나 이상 있는 경우 walletAddress 상태 변수는 리스너가 반환한 accounts 배열의 첫 번째 계정으로 업데이트됩니다. 그렇지 않으면 walletAddress는 빈 문자열로 설정됩니다.

마지막으로 useEffect 함수에서 이를 호출해야 합니다.

useEffect(async () => {
  const { address, status } = await getCurrentWalletConnected()
  setWallet(address)
  setStatus(status)

  addWalletListener()
}, [])

짜잔! 모든 지갑 기능 프로그래밍을 완료했습니다! 이제 지갑이 설정되었으니 NFT를 발행하는 방법을 알아보겠습니다!

NFT 메타데이터 101

이 튜토리얼의 0단계에서 방금 이야기했던 NFT 메타데이터를 기억하시나요? 이는 디지털 자산, 이름, 설명 및 기타 속성과 같은 특성을 가질 수 있게 하여 NFT에 생명력을 불어넣습니다.

이 메타데이터를 JSON 객체로 구성하고 저장해야 스마트 컨트랙트의 mintNFT 함수를 호출할 때 tokenURI 매개변수로 전달할 수 있습니다.

"Link to Asset", "Name", "Description" 필드의 텍스트는 NFT 메타데이터의 다양한 속성을 구성합니다. 이 메타데이터를 JSON 객체로 포맷할 것이지만, 이 JSON 객체를 저장할 수 있는 몇 가지 옵션이 있습니다.

  • 이더리움 블록체인에 저장할 수 있습니다. 하지만 그렇게 하면 비용이 매우 많이 듭니다.
  • AWS나 Firebase와 같은 중앙화된 서버에 저장할 수 있습니다. 하지만 이는 우리의 탈중앙화 이념에 어긋납니다.
  • 분산 파일 시스템에서 데이터를 저장하고 공유하기 위한 탈중앙화된 프로토콜이자 피어 투 피어 네트워크인 IPFS를 사용할 수 있습니다. 이 프로토콜은 탈중앙화되어 있고 무료이므로 가장 좋은 옵션입니다!

메타데이터를 IPFS에 저장하기 위해 편리한 IPFS API 및 툴킷인 Pinata (opens in a new tab)를 사용할 것입니다. 다음 단계에서 이를 수행하는 방법을 정확히 설명하겠습니다!

Pinata를 사용하여 IPFS에 메타데이터 고정하기

Pinata (opens in a new tab) 계정이 없다면 여기 (opens in a new tab)에서 무료 계정에 가입하고 이메일 및 계정 확인 단계를 완료하세요.

Pinata API 키 생성하기

https://pinata.cloud/keys (opens in a new tab) 페이지로 이동한 다음 상단의 "New Key" 버튼을 선택하고 Admin 위젯을 활성화로 설정한 후 키의 이름을 지정합니다.

그러면 API 정보가 포함된 팝업이 표시됩니다. 이 정보를 안전한 곳에 보관하세요.

이제 키가 설정되었으니 프로젝트에 추가하여 사용할 수 있도록 해보겠습니다.

.env 파일 생성하기

환경 파일에 Pinata 키와 시크릿을 안전하게 저장할 수 있습니다. 프로젝트 디렉토리에 dotenv 패키지 (opens in a new tab)를 설치해 보겠습니다.

터미널에서 새 탭을 열고(로컬 호스트를 실행 중인 탭과 별개로) minter-starter-files 폴더에 있는지 확인한 다음 터미널에서 다음 명령을 실행합니다.

npm install dotenv --save

다음으로 명령줄에 다음을 입력하여 minter-starter-files의 루트 디렉토리에 .env 파일을 만듭니다.

vim.env

이렇게 하면 vim(텍스트 편집기)에서 .env 파일이 열립니다. 저장하려면 키보드에서 "esc" + ":" + "q"를 순서대로 누르세요.

다음으로 VSCode에서 .env 파일로 이동하여 다음과 같이 Pinata API 키와 API 시크릿을 추가합니다.

REACT_APP_PINATA_KEY = <pinata-api-key>
REACT_APP_PINATA_SECRET = <pinata-api-secret>

파일을 저장하면 JSON 메타데이터를 IPFS에 업로드하는 함수를 작성할 준비가 된 것입니다!

pinJSONToIPFS 구현하기

다행히도 Pinata에는 JSON 데이터를 IPFS에 업로드하기 위한 전용 API (opens in a new tab)와 약간의 수정만으로 사용할 수 있는 편리한 axios를 사용한 JavaScript 예제가 있습니다.

utils 폴더에 pinata.js라는 또 다른 파일을 만들고 다음과 같이 .env 파일에서 Pinata 시크릿과 키를 가져오겠습니다.

require("dotenv").config()
const key = process.env.REACT_APP_PINATA_KEY
const secret = process.env.REACT_APP_PINATA_SECRET

다음으로 아래의 추가 코드를 pinata.js 파일에 붙여넣습니다. 걱정하지 마세요. 모든 것이 무엇을 의미하는지 분석해 드릴 것입니다!

그렇다면 이 코드는 정확히 무엇을 할까요?

먼저 브라우저 및 Node.js용 프로미스 기반 HTTP 클라이언트인 axios (opens in a new tab)를 가져옵니다. 이를 사용하여 Pinata에 요청을 보낼 것입니다.

그런 다음 JSONBody를 입력으로 받고 헤더에 Pinata API 키와 시크릿을 사용하여 pinJSONToIPFS API에 POST 요청을 보내는 비동기 함수 pinJSONToIPFS가 있습니다.

  • 이 POST 요청이 성공하면 우리 함수는 success 부울이 true이고 메타데이터가 고정된 pinataUrl가 포함된 JSON 객체를 반환합니다. 반환된 이 pinataUrl를 스마트 컨트랙트의 발행 함수에 대한 tokenURI 입력으로 사용할 것입니다.
  • 이 POST 요청이 실패하면 우리 함수는 success 부울이 false이고 오류를 전달하는 message 문자열이 포함된 JSON 객체를 반환합니다.

connectWallet 함수의 반환 유형과 마찬가지로, 매개변수를 사용하여 상태 변수와 UI를 업데이트할 수 있도록 JSON 객체를 반환하고 있습니다.

스마트 컨트랙트 로드하기

이제 pinJSONToIPFS 함수를 통해 NFT 메타데이터를 IPFS에 업로드하는 방법을 알았으니, 스마트 컨트랙트의 인스턴스를 로드하여 mintNFT 함수를 호출할 방법이 필요합니다.

앞서 언급했듯이 이 튜토리얼에서는 이 기존 NFT 스마트 컨트랙트 (opens in a new tab)를 사용할 것입니다. 하지만 이를 어떻게 만들었는지 알고 싶거나 직접 만들어보고 싶다면 다른 튜토리얼인 "NFT를 만드는 방법" (opens in a new tab)을 확인해 보시기를 강력히 추천합니다.

컨트랙트 ABI

파일을 자세히 살펴보았다면 src 디렉토리에 contract-abi.json 파일이 있다는 것을 눈치채셨을 것입니다. ABI는 컨트랙트가 호출할 함수를 지정하고 함수가 예상하는 형식으로 데이터를 반환하도록 보장하는 데 필요합니다.

또한 이더리움 블록체인에 연결하고 스마트 컨트랙트를 로드하려면 Alchemy API 키와 Alchemy Web3 API가 필요합니다.

아직 Alchemy 계정이 없다면 여기에서 무료로 가입하세요. (opens in a new tab)

Alchemy 계정을 생성한 후에는 앱을 생성하여 API 키를 생성할 수 있습니다. 이를 통해 Sepolia 테스트 네트워크에 요청을 보낼 수 있습니다.

내비게이션 바에서 "Apps"에 마우스를 올리고 "Create App"을 클릭하여 Alchemy 대시보드의 "Create App" 페이지로 이동합니다.

앱 이름을 지정하고(여기서는 "My First NFT!"로 정했습니다), 짧은 설명을 작성합니다. 앱 관리를 위한 환경(Environment)으로 "Staging"을 선택하고, 네트워크로 "Sepolia"를 선택합니다.

"Create app"을 클릭하면 끝입니다! 아래 표에 앱이 표시될 것입니다.

좋습니다. 이제 HTTP Alchemy API URL을 생성했으니 클립보드에 복사합니다...

…그런 다음 .env 파일에 추가해 보겠습니다. 최종적으로 .env 파일은 다음과 같은 모습이어야 합니다.

REACT_APP_PINATA_KEY = <pinata-key>
REACT_APP_PINATA_SECRET = <pinata-secret>
REACT_APP_ALCHEMY_KEY = https://eth-sepolia.g.alchemy.com/v2/<alchemy-key>

이제 컨트랙트 ABI와 Alchemy API 키가 준비되었으므로 Alchemy Web3 (opens in a new tab)를 사용하여 스마트 컨트랙트를 로드할 준비가 되었습니다.

Alchemy Web3 엔드포인트 및 컨트랙트 설정하기

먼저, 아직 설치하지 않았다면 터미널에서 홈 디렉토리인 nft-minter-tutorial로 이동하여 Alchemy Web3 (opens in a new tab)를 설치해야 합니다.

cd ..
npm install @alch/alchemy-web3

다음으로 interact.js 파일로 돌아가 보겠습니다. 파일의 맨 위에 다음 코드를 추가하여 .env 파일에서 Alchemy 키를 가져오고 Alchemy Web3 엔드포인트를 설정합니다.

require("dotenv").config()
const alchemyKey = process.env.REACT_APP_ALCHEMY_KEY
const { createAlchemyWeb3 } = require("@alch/alchemy-web3")
const web3 = createAlchemyWeb3(alchemyKey)

Alchemy Web3 (opens in a new tab)Web3.js (opens in a new tab)를 감싸는 래퍼(wrapper)로, 향상된 API 메서드와 기타 중요한 이점을 제공하여 Web3 개발자로서의 삶을 더 쉽게 만들어줍니다. 최소한의 구성만 필요하도록 설계되어 앱에서 바로 사용할 수 있습니다!

다음으로 컨트랙트 ABI와 컨트랙트 주소를 파일에 추가해 보겠습니다.

require("dotenv").config()
const alchemyKey = process.env.REACT_APP_ALCHEMY_KEY
const { createAlchemyWeb3 } = require("@alch/alchemy-web3")
const web3 = createAlchemyWeb3(alchemyKey)

const contractABI = require("../contract-abi.json")
const contractAddress = "0x4C4a07F737Bf57F6632B6CAB089B78f62385aCaE"

이 두 가지가 모두 준비되면 발행 함수 코딩을 시작할 준비가 된 것입니다!

mintNFT 함수 구현하기

interact.js 파일 내부에 이름 그대로 NFT를 발행할 mintNFT 함수를 정의해 보겠습니다.

수많은 비동기 호출(메타데이터를 IPFS에 고정하기 위한 Pinata, 스마트 컨트랙트를 로드하기 위한 Alchemy Web3, 트랜잭션에 서명하기 위한 메타마스크)을 수행할 것이므로 우리 함수도 비동기식이어야 합니다.

우리 함수에 대한 세 가지 입력은 디지털 자산의 url, name, description가 될 것입니다. connectWallet 함수 아래에 다음 함수 서명을 추가합니다.

export const mintNFT = async (url, name, description) => {}

입력 오류 처리

당연히 함수의 시작 부분에 일종의 입력 오류 처리를 두어 입력 매개변수가 올바르지 않은 경우 이 함수를 종료하는 것이 합리적입니다. 함수 내부에 다음 코드를 추가해 보겠습니다.

기본적으로 입력 매개변수 중 하나라도 빈 문자열인 경우 success 부울이 false이고 status 문자열이 UI의 모든 필드를 완료해야 함을 전달하는 JSON 객체를 반환합니다.

IPFS에 메타데이터 업로드하기

메타데이터가 제대로 포맷되었음을 확인한 후 다음 단계는 이를 JSON 객체로 래핑하고 우리가 작성한 pinJSONToIPFS를 통해 IPFS에 업로드하는 것입니다!

이를 위해 먼저 pinJSONToIPFS 함수를 interact.js 파일로 가져와야 합니다. interact.js의 맨 위에 다음을 추가해 보겠습니다.

import { pinJSONToIPFS } from "./pinata.js"

pinJSONToIPFS가 JSON 본문을 받는다는 것을 기억하세요. 따라서 이를 호출하기 전에 url, name, description 매개변수를 JSON 객체로 포맷해야 합니다.

코드를 업데이트하여 metadata라는 JSON 객체를 만든 다음 이 metadata 매개변수를 사용하여 pinJSONToIPFS를 호출해 보겠습니다.

pinJSONToIPFS(metadata) 호출의 응답을 pinataResponse 객체에 저장한다는 점에 유의하세요. 그런 다음 이 객체를 파싱하여 오류가 있는지 확인합니다.

오류가 있는 경우 success 부울이 false이고 status 문자열이 호출에 실패했음을 전달하는 JSON 객체를 반환합니다. 그렇지 않으면 pinataResponse에서 pinataURL를 추출하여 tokenURI 변수로 저장합니다.

이제 파일 맨 위에서 초기화한 Alchemy Web3 API를 사용하여 스마트 컨트랙트를 로드할 차례입니다. mintNFT 함수의 맨 아래에 다음 코드 줄을 추가하여 window.contract 전역 변수에 컨트랙트를 설정합니다.

window.contract = await new web3.eth.Contract(contractABI, contractAddress)

mintNFT 함수에 추가할 마지막 항목은 이더리움 트랜잭션입니다.

이더리움 트랜잭션에 이미 익숙하다면 구조가 이전에 본 것과 매우 유사하다는 것을 알 수 있을 것입니다.

  • 먼저 트랜잭션 매개변수를 설정합니다.
    • to는 수신자 주소(우리의 스마트 컨트랙트)를 지정합니다.
    • from는 트랜잭션의 서명자(메타마스크에 연결된 사용자의 주소: window.ethereum.selectedAddress)를 지정합니다.
    • data에는 tokenURI와 사용자의 지갑 주소인 window.ethereum.selectedAddress를 입력으로 받는 스마트 컨트랙트 mintNFT 메서드에 대한 호출이 포함되어 있습니다.
  • 그런 다음 메타마스크에 트랜잭션 서명을 요청하는 await 호출인 window.ethereum.request,를 수행합니다. 이 요청에서 eth 메서드(eth_SentTransaction)를 지정하고 transactionParameters를 전달한다는 점에 유의하세요. 이 시점에서 브라우저에 메타마스크가 열리고 사용자에게 트랜잭션에 서명하거나 거부하라는 메시지가 표시됩니다.
    • 트랜잭션이 성공하면 함수는 부울 success가 true로 설정되고 status 문자열이 사용자에게 트랜잭션에 대한 자세한 정보를 위해 Etherscan을 확인하라는 메시지를 표시하는 JSON 객체를 반환합니다.
    • 트랜잭션이 실패하면 함수는 success 부울이 false로 설정되고 status 문자열이 오류 메시지를 전달하는 JSON 객체를 반환합니다.

전체적으로 mintNFT 함수는 다음과 같아야 합니다.

정말 거대한 함수네요! 이제 mintNFT 함수를 Minter.js 컴포넌트에 연결하기만 하면 됩니다...

Minter.js 프론트엔드에 mintNFT 연결하기

Minter.js 파일을 열고 맨 위의 import { connectWallet, getCurrentWalletConnected } from "./utils/interact.js"; 줄을 다음과 같이 업데이트합니다.

import {
  connectWallet,
  getCurrentWalletConnected,
  mintNFT,
} from "./utils/interact.js"

마지막으로 onMintPressed 함수를 구현하여 가져온 mintNFT 함수에 대해 await 호출을 수행하고 트랜잭션의 성공 또는 실패 여부를 반영하도록 status 상태 변수를 업데이트합니다.

const onMintPressed = async () => {
  const { status } = await mintNFT(url, name, description)
  setStatus(status)
}

블록체인 세계를 휩쓸어 보세요

농담입니다. 튜토리얼의 끝까지 오셨군요!

요약하자면, NFT 발행기를 구축함으로써 다음 방법을 성공적으로 배웠습니다.

  • 프론트엔드 프로젝트를 통해 메타마스크에 연결하기
  • 프론트엔드에서 스마트 컨트랙트 메서드 호출하기
  • 메타마스크를 사용하여 트랜잭션 서명하기

아마도 dapp을 통해 발행된 NFT를 지갑에서 자랑하고 싶으실 것입니다. 그렇다면 간단한 튜토리얼인 지갑에서 NFT를 보는 방법을 꼭 확인해 보세요!

그리고 언제나 그렇듯 질문이 있으시면 Alchemy 디스코드 (opens in a new tab)에서 도와드리겠습니다. 이 튜토리얼의 개념을 향후 프로젝트에 어떻게 적용할지 무척 기대됩니다!