NFT, Web3完全に理解した になるためのチュートリアル
こんにちは、「なりたい自分で、生きていく」ためのメタバース、 REALITYを運営しているDJ RIOです。
ここ数年、メタバースといえばKawaiiアバターになってキャッキャウフフするものだと信じてたのですが、どうもさいきんメタバースと言うと「NFT」とか「Web3」とか耳慣れない単語が飛び交うようになってきました。
みなさんはこれらを理解していますか? ぼくは何もわかりません><
なので今回は、暗号資産・NFT・Web3、その基盤となるブロックチェーンそのものを自分で作ったり独自コインを開発したりすることで、NFTやWeb3を完全に理解してしまおうと思います。
今回やること
自分専用のEthererumネットワークを構築し運用する
自分でコインを発行し、送金する
自分専用のNFTを開発・発行する
発行したNFTを表示させてみる
これらを実践することで、NFTやWeb3の根幹をなすブロックチェーンとはなんなのか?それがどう動いてコインやNFTの発行ややり取りを実現してるのかなんかを技術的に理解することができるはずです。
それでは早速やってみよ〜!
0.ブロックチェーンの概念図
ブロックチェーンはP2Pネットワークです。全体を管理するサーバは存在せず、多くのコンピューター(ノードと呼ばれます)が相互に接続して情報をやり取りしながら取引を処理して、あたかも1つのサーバーが存在するかのように振る舞っている、というものです。
今回はテストなので、ローカル環境に自分専用のEthereum完全互換ブロックチェーンを構築していきます。
1.Ethereumネットワークをつくる
以下の作業はすべて、Macbook Pro (13 inch M1, 2020) と macOS Monterey、gethバージョンは1.10.17-stable、ブラウザはChromeでMetamask拡張が入った環境で進めます。環境が違う人は適宜読み替えてね。
作業環境としては、以下のようなディレクトリ構造を作っていくことになります。
<プロジェクトディレクトリ>
┣ reality // ethereumノードを稼働させるためのデータディレクトリ
┃ ┣ genesis.json // ブロックチェーン初期化のための設定ファイル
┃ ┣ node01 // ノード1用作業ディレクトリ
┃ ┣ node02 // ノード2用作業ディレクトリ
┃ ┣ node03 // ノード3用作業ディレクトリ
┃ ┗ node04 // ノード4用作業ディレクトリ
┗ www // webサーバのルート
┣ metadata // NFTメタデータファイル置き場
┣ images // NFT画像置き場
┗ index.html // NFTビューア用スクリプト
1-1. gethのインストール
gethというのはGoで実装されたEthereumの公式な本体です。これをローカルマシン上で動作させることで、自分専用のEthereumを稼働させることができます。
公式のチュートリアルにしたがって、brewを使ってインストールしていきましょう(brewがない場合はここの手順を参考にまずはbrewをインストールしてね)
$ brew tap ethereum/ethereum
$ brew install ethereum
1-2. ノード用の準備
次に、ブロックチェーンのノード用作業ディレクトリを作ります。4つノードを立てる予定なので、作業ディレクトリも4つ作ります。
$ mkdir reality/node01 reality/node02 reality/node03 reality/node04
次に、Genesis blockを作ります。Blockchainというのはその名の通り取引履歴であるblockがchain上につながっているデータのことなんですが、その先頭のブロックがgenesis blockです。日本語で言えば 創世の書 です。厨二感あふれてかっこいい!
Genesis block生成用の設定ファイルがこちら。それぞれの設定の意味はこちら参照。最後のalloc のところにウォレットアドレスと金額を入れると、ブロックチェーン初期化時にそのアドレスに指定された残高がある状態でスタートします。
{
"config": {
"chainId": 15,
"homesteadBlock": 0,
"eip150Block": 0,
"eip155Block": 0,
"eip158Block": 0,
"byzantiumBlock": 0,
"constantinopleBlock": 0,
"petersburgBlock": 0,
"ethash": {}
},
"difficulty": "1",
"gasLimit": "8000000",
"alloc": {
"7df9a875a174b3bc565e6424a0050ebc1b2d1d82": { "balance": "300000" },
"f41c74c9ae680c1aa78f42e5647a62f353b7bdde": { "balance": "400000" }
}
}
ではこの設定ファイルを使って実際にgenesis blockを生成していきます。
$ geth init --datadir reality/node01 reality/genesis.json
これから全部で4つのノードを立てるので、残りの3つ分も同じ設定でgenesis blockを作っておきます。
$ geth init --datadir reality/node02/ reality/genesis.json
$ geth init --datadir reality/node03/ reality/genesis.json
$ geth init --datadir reality/node04/ reality/genesis.json
これでノードを起動する準備は整いました
1-3. ノードを起動する
では早速ノードを起動します。まずは1つめのノード。
$ geth --datadir reality/node01 --networkid 15 --http --http.addr [YOUR_LOCAL_IP] --http.port 8101 --http.vhosts=*
[YOUR_LOCAL_IP]のところには自分のマシンのIPアドレスを入れてください。127.0.0.1でも構いませんが、今後ノードのIPを指定するときには毎回同じものを使うのをお忘れなく。
特にエラーが出ずにログが流れ始めたら起動しています。別ターミナルから以下のコマンドで、今起動したノードに接続してみましょう。
$ geth attach reality/node01/geth.ipc
Welcome to the Geth JavaScript console!
instance: Geth/v1.10.17-stable/darwin-arm64/go1.18
coinbase: 0x139e1f9306904d04e29343c419c868b2d540b036
at block: 56682 (Thu Apr 28 2022 17:18:13 GMT+0900 (JST))
datadir: /Users/xxxx/dev/reality/node01
modules: admin:1.0 debug:1.0 eth:1.0 ethash:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0
To exit, press ctrl-d or type exit
>
と、コマンドコンソールが起動するので、ここで admin.nodeInfo というコマンドを入力するとこのノードの情報が表示されます。
> admin.nodeInfo
{
enode: "enode://9d693b9a9daf750ce6cbf17c7a00e4cb5c38c5255712e7de1c489052043b764d405422350510b6e9667df53a5eb54f3a8c46e60f3b0e3582b77683417b1b8116@127.0.0.1:30303",
enr: "enr:-KO4QPXiRIQoFrraupVuf_47DTx29ryyLjOw5khmFf2f9nNiGjFJasy5RCXKM_tszEH_3CuzTHiXcEUX_BEsr5RiVEOGAYBGlqfhg2V0aMfGhJ2pRlqAgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQKdaTuana91DObL8Xx6AOTLXDjFJVcS594cSJBSBDt2TYRzbmFwwIN0Y3CCdl-DdWRwgnZf",
id: "d9dab6894b0ed780e5de4aa641a3a780fe402d772e877853f46931290abba14d",
ip: "127.0.0.1",
listenAddr: "[::]:30303",
name: "Geth/v1.10.17-stable/darwin-arm64/go1.18",
ports: {
discovery: 30303,
listener: 30303
},
protocols: {
eth: {
config: {
byzantiumBlock: 0,
chainId: 15,
constantinopleBlock: 0,
eip150Block: 0,
eip150Hash: "0x0000000000000000000000000000000000000000000000000000000000000000",
eip155Block: 0,
eip158Block: 0,
ethash: {},
homesteadBlock: 0,
petersburgBlock: 0
},
difficulty: 46775862406,
genesis: "0xc3638c5057e516005c90f14b439798979abfe13b491a15af85b0bdc514f97051",
head: "0xf6fc338d54ec330d9c9783f171d2f850e523e08b15ea3f97ca52cb3e46fc3738",
network: 15
},
snap: {}
}
}
この出力の中の enr: から始まる文字列がこのノードの識別情報なのでメモっておいてください、この後使います。
続いて、このノードと連携する形で2つめと3つめのノードも起動します。動作させ続けないといけないので、それぞれ別のターミナルで起動するかバックグラウンドで起動するようにしましょう。
$ geth --datadir reality/node02 --networkid 15 --port 30305 --bootnodes "enr:-KO4QK3TrxbP-NRdnAPwR7qhV4xkMMCvRH2pxn-pz00YkQ6SB4mdZ8gjYIorpsuBy4cS46oaF4cUvfsFahRiopNiB3aGAYBGlqfcg2V0aMfGhJ2pRlqAgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQKdaTuana91DObL8Xx6AOTLXDjFJVcS594cSJBSBDt2TYRzbmFwwIN0Y3CCdl-DdWRwgnZf"
$ geth --datadir reality/node03 --networkid 15 --port 30306 --bootnodes "enr:-KO4QK3TrxbP-NRdnAPwR7qhV4xkMMCvRH2pxn-pz00YkQ6SB4mdZ8gjYIorpsuBy4cS46oaF4cUvfsFahRiopNiB3aGAYBGlqfcg2V0aMfGhJ2pRlqAgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQKdaTuana91DObL8Xx6AOTLXDjFJVcS594cSJBSBDt2TYRzbmFwwIN0Y3CCdl-DdWRwgnZf"
--bootnodes の後に指定する文字列は、1つめのノードのenrです。
これで、3ノードが接続された自分専用のブロックチェーンができました!
1-4. マイニングする
ただ現状ではマイナーがいないので、4つめのノードをマイナーとして起動します。マイナーとは、Bitcoinのマイニングと同じでブロックチェーンの取引内容(送金とか)を検証・記録していく係になるノードで、これがいないと取引が処理されません。
$ geth --datadir reality/node04 --networkid 15 --port 30307 --bootnodes "enr:-KO4QK3TrxbP-NRdnAPwR7qhV4xkMMCvRH2pxn-pz00YkQ6SB4mdZ8gjYIorpsuBy4cS46oaF4cUvfsFahRiopNiB3aGAYBGlqfcg2V0aMfGhJ2pRlqAgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQKdaTuana91DObL8Xx6AOTLXDjFJVcS594cSJBSBDt2TYRzbmFwwIN0Y3CCdl-DdWRwgnZf" --mine --miner.threads=1 --miner.etherbase=[YOUR_WALLET_ADDRESS]
--mine オプションでこのノードがマイナーになることを指定し、--miner.etherbase= 以降にはマイニング報酬を振り込む先のウォレットアドレスなので、お使いのMetamaskの自分のウォレットアドレスを指定してください。
これでマイナーノードも起動したので、取引が可能なブロックチェーンが動き始めたことになります。やったね!
2.Metamaskを接続する
さあここまで来たら普通のEthereumと同じようにMetamaskでウォレットの残高表示や送金処理が可能になります。
さっそくMetamaskを自作ブロックチェーンに接続します。
Metamaskの画面上部にあるネットワークタブプルダウンから、「ネットワーク追加」をクリックして、、
いま構築した自分のネットワークを追加します。今回のテストネットワークはrealityというネットワークで、発行されるコインはRLTと呼ぶことにしてみます。RPC URLのところはノード起動時に指定したIPアドレスとポート、チェーンIDにはGenesis blockの設定ファイルでchainIdとして指定したものと同じ数字を入れてください。
これで「保存」をすると、、
自分で作ったrealityネットワークに接続できました!! このウォレットのアドレスはマイナーノードを起動した時にマイニング報酬の振込先として指定したアドレスなので、マイニング報酬がすでに貯まってます。しかも、しばらく見てるとちょっとずつマイニング報酬でコインが増えていくんです。
なんかチャリンチャリンお金が増えてる感じがしてうれしいね!
3.送金してみる
ではこのブロックチェーン上で送金処理を実行してみましょう。
Metamaskで「送金」ボタンを押して、宛先(ここではぼくが使ってるもう一つのアドレス)を指定して、送金額を決めます。
1000RLTを送金することにして「次へ」を押し、取引の確認画面で承認してしばらく待つと、、
送金できた!!
この裏ではさっき起動した4つめのノードが送金処理を処理してブロックチェーンに取引内容を記録してるんですね。
4.NFTを発行する
めでたくブロックチェーンが稼働し始めたところで、ようやくみんな大好きNFTを発行してみます。NFTは売買するより作るほうがおもしろいよ!
4-1. そもそもNFTって(技術的に)なんなの?
NFTとは とか検索すると、複製不可能なデジタルデータ、とか、自分だけが持っていると証明できるデジタルアート、とか色んな説明がされてますね。
みなさんはその説明でわかりますか?ぼくは全然わかりません。
じゃあNFTってなんなのかというと、こういうものです。
NFTというのは、ERC-721と呼ばれるEthereumの規格の1つに基づき実装されたスマートコントラクト(プログラムのこと)、あるいはそのプログラムから生成されたトークンのことです。
今コレクターがやり取りしているNFTアートと呼ばれているものは、主に3つのデータによって構成されています。
1)スマートコントラクト(Solidityという言語で書かれたコード)
2)メタデータ(JSON形式のテキストデータ)
3)画像データ(PNG、SVGなど)
具体的には、スマートコントラクトのAPIの1つを叩くとNFTを定義するメタデータのURLが返ってきて、そのメタデータの中身を読むと画像を参照するURLが書かれてて、そのURLにアクセスすると実際のNFTアートの中身のPNG画像が返ってきます。
つまり自分でNFTアートを作る場合は、この3つを作っていくことになります。
では早速作っていきましょう!
4-2. NFTの元データを準備する
まずはNFTアートにするための画像とメタデータを作っていきます。
最近のNFTアートはパターン違いのキャラクターをたくさん作る形式が流行ってるみたいなので、そういう感じのを試しに作ってみます。
REALITYアバターを使えば無限の組み合わせのアバター画像が一瞬で作れて便利だね!
これで画像ができたので、次はこの画像を参照するメタデータを作成します。中身はこんな感じ↓
{
"title": "REALITY Avatar Collection",
"type": "object",
"name": "REALITY Avatar collection #4",
"description": "This is an avatar of REALITY Avatar NFT collection",
"image": "http://127.0.0.1:8080/ethereum-testnet/images/0004.png"
}
こんな感じで画像を参照するJSONファイルを画像の数と同じだけ作ります。このメタデータの中には上記のアバター画像をブラウザから参照可能なURLを記載します(画像にアクセスするためにWebサーバを立てておかないといけないのですがその説明は割愛します)
これでREALITY AvatarコレクションNFTを発行するための元ファイルが準備できました!
4-3. NFTのスマートコントラクトを書く
続いてブロックチェーン上でNFTを発行・流通させるために必要なスマートコントラクト(Ethereumブロックチェーン上で動作するプログラム)を書いていきます。Solidityという独自のプログラミング言語で書くことになるのですが、 Remixという非常に洗練されたWebベースの開発環境があるので、この後の作業はRemixでやっていきます。
ホームからNew Fileで新しいSolidityソース・ファイルを作成します。今回はRealityAvatar.solという名前のファイルを作りました。
そして以下のようなコードを書きます。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.2;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract NFT is ERC721, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIdCounter;
constructor() ERC721("REALITY NFT Avatar Collection", "RLTNFT"){}
function _baseURI() internal pure override returns (string memory) {
return "http://127.0.0.1:8080/ethereum-testnet/metadata/";
}
function safeMint(address to) public onlyOwner returns (uint256) {
require(_tokenIdCounter.current() < 100);
_tokenIdCounter.increment();
_safeMint(to, _tokenIdCounter.current());
return _tokenIdCounter.current();
}
}
ざっくり何をやってるかというと、
既存のテンプレートをインポートして
NFTコレクション名とかを指定するコンストラクタを書いて
メタデータを参照するURLを返すAPIを定義して
mintされた時に自動的にIDをインクリメントしながら発行する処理を書く
くらいです。NFTって簡単だね!
さらに詳しく理解するためには Solidity ERC721 あたりで検索してみてね。
次にこのコードをコンパイルします。Remixの画面左にあるタブの上から4つ目のところがコンパイルメニューなのでそこを開いて、「Complie RealityAvatar.sol」を押せば今表示されてるソースコードがコンパイルされます。
4-4. NFTのスマートコントラクトをデプロイする
コンパイルが終わったら、このスマートコントラクトをさっき作ったブロックチェーン上にデプロイして実際に動くようにします。
Remix の画面左のコンパイルのひとつ下がDeploy画面なのでそこを開いて、、、、
ENVIRONMENTで「Injected Web3」を選ぶと、Metamaskが連携していいかという確認が出るので承認してください(この時、Metamaskがさっき作ったrealityネットワークに接続されているか確認してください、じゃないとEthereumとか別ネットワークに繋がってしまう)。
連携が完了したら、ACCOUNTにはいまMetamaskで使っているウォレットアドレスが表示されてるはずなのでそれを確認して、CONTRACTのプルダウンから先ほどコンパイルしたRealityAvatar.solを選択します。
これでDeployボタンを押すとMetamask側でトランザクションの承認画面が出て、承認するとこのスマートコントラクトが自分のブロックチェーンにデプロイされて動作可能になりました!やったね!
4-5. NFTをmintしてウォレットに入れる
さあ、とうとうNFTをmint(定義されたNFTコレクション内のアイテムを実体化して個人のウォレットに入れること)をする番です。
Remixは、Deploy済みのスマートコントラクトのAPIを呼び出す機能があるのでそれを使うと早いです。
さきほどDeployしたのと同じ画面の下の方に、「Deployed Contracts」というセクションがあり、そこにさっきDeployしたスマートコントラクトのAPI一覧が並んでいます。
ここで「safeMint」というオレンジのボタンの右のボックスに、自分のウォレットアドレスを入力して「safeMint」ボタンを押すと、、、
Metamaskで取引の確認画面が出て、「確認」ボタンを押してしばらく待つと。。
取引が完了しました!これで自分のウォレットに今mintしたNFTが入ってます。
「ウォレットにNFTが入っている」ということを確認するためには、先ほどのAPIの別のコマンドを実行してみます。
ownerOfというボタンの右のTokenID(NFTコレクションの何番目のものかを指すID)のところに1と入れて、ownerOfボタンを押すと、その下にこのNFTを所有しているウォレットアドレスが表示されます。
これで自分のウォレットアドレスが出たら、自分のウォレット内にTokenID 1番のNFTが入っているということなんですね。
(なんで1番のNFTが入ってるかと言うと、さっきmintするのに使ったSafeMintという関数は1番から順番にNFTを発行していくので、最初に発行して自分のウォレットに入ったもののIDは1なのです)
次にこのNFTの画像がどう参照されているかを確認してみましょう。
同じくAPI一覧の下の方に tokenURIというボタンがあるので、その右に1 と入れてtokenURIボタンを押すと、その下にメタデータのURLが表示されます。
このURLにアクセスすると先ほど作成したJSONファイルが表示され、そのJSONファイルの中にある画像のURLを開くとアバター画像が表示されます。
この一連の流れが、NFTを発行し、それがユーザーのウォレットに入って、そのNFTの画像が表示される仕組みなのです。
ここまでのまとめ
NFTを構成する要素は、
(1)スマートコントラクト(プログラム)
(2)メタデータ(JSONファイル)
(3)メディアデータ(PNGや動画ファイル等)
の3つ
スマートコントラクトを書いてコンパイルしてブロックチェーン上にdeployすると、そのプログラムにアクセスできるようになる
特定のユーザーのウォレットにNFTを入れるのも、特定NFTアイテムを持っているウォレットを特定するのも、そのNFTのメタデータを取得するのも、スマートコントラクトで関数として定義されている
NFTアートが表示されるまでには、
(1)スマートコントラクトのAPIへアクセスしてメタデータURLを取得し
(2)メタデータURLにアクセスして画像URLを取得し
(3)画像URLにアクセスして画像を表示する
という手順が踏まれている
さてここまでの流れで、NFTを作って、それをウォレットに入れて、NFTにアクセスできるようになりました。
ここまでやればNFTもブロックチェーンも仕組みとしては完全に理解したと思うのですが、やっぱ自分が持ってるNFTの画像がバッと表示されないと、なんかNFTを持ってる感が感じられないんですよね。
ということで、次の章ではおまけとして、取得したNFTを表示するWebフロントエンドを実装してみます。
5.NFTを表示する
ここでは、自分が受け取ったNFTを表示するめちゃくちゃ原始的なWebフロントエンドをJavascriptで実装してみます。
おまけなので、コードだけ載せておきます。冒頭のディレクトリ一覧の、www/index.htmlとして配置してブラウザからアクセスすると動くと思います。
<html>
<script type="text/javascript" src="https://cdn.ethers.io/lib/ethers-5.0.umd.min.js"></script>
<script type='application/javascript'>
var provider;
var signer;
var myAddress;
// Metamaskに接続する
async function connectWallet() {
provider = new ethers.providers.Web3Provider(window.ethereum, "any");
await provider.send("eth_requestAccounts", []);
signer = provider.getSigner();
myAddress = await signer.getAddress();
document.getElementById('connect-button').remove();
document.getElementById('wallet-address').innerHTML = myAddress;
document.getElementById('wallet-info').style.visibility = 'visible';
}
// 指定されたコントラクトアドレスから、送付されたNFT一覧を取得し表示する
async function fetchNFTs() {
//今回作ったNFTのスマートコントラクトにアクセスするためのAPI定義を準備
const abi = [
'event Transfer(address indexed from, address indexed to, uint256 value)',
'function tokenURI(uint256 _tokenId) view returns (string memory)'
];
const contractAddress = document.getElementById('contract-address').value;
const contract = new ethers.Contract(contractAddress, abi, signer);
//NFT発行イベントを抽出するためのフィルタ
filter = contract.filters.Transfer(null, myAddress)
// 自分のアドレス宛に発行されたNFT一覧を取得
txs = await contract.queryFilter(filter);
// NFTの表示
txs.forEach(async function (item, index, ar){
tokenId = item.topics[3];
url = await contract.tokenURI(tokenId);
// スマートコントラクトのtokenURI()から取得したメタデータのURLを読み込み
fetch(url, {method: 'GET', cache: 'no-cache'})
.then((response) => {
return response.json();
})
.then((result) => {
displayNFT(result);
});
});
}
// 取得したNFTを表示するhtmlを生成
function displayNFT(metadata) {
div = Object.assign(document.createElement('div'), {className: 'nft-item'});
div.appendChild(Object.assign(document.createElement('img'), {src: metadata.image}));
div.appendChild(Object.assign(document.createElement('p'), {innerHTML: metadata.name} ));
console.log(div);
document.getElementById('items').appendChild(div);
}
</script>
<style>
* {
font-family: Roboto, Helvetica, Arial, sans-serif;
font-weight: 500;
color: #555555;
}
div#connect-wallet button{
margin-bottom: 20px;
padding: 20px;
font-weight: bold;
}
input#contract-address {
width: 300px;
}
div.nft-item {
width: 300px;
text-align:center;
float: left;
}
div.nft-item img {
height: 250px;
width: 250px;
}
div.nft-item p {
margin: 5px;
text-align:center;
}
</style>
<body>
<div id='connect-wallet'>
<div id='connect-button'>
<button onClick="connectWallet()" > Connect Metamask </button>
</div>
<div id='wallet-info' style="visibility:hidden">
Your wallet address is: <span id='wallet-address'></span>
</div>
</div>
<div>
<input type="text" id="contract-address" name="contract-address" placeholder="NFT contract address" />
<button onclick="fetchNFTs()">Get my NFTs</button>
</div>
<hr/>
<div id='items'>
</div>
</body>
</html>
やっていることは、
Connect Metamaskボタンを押したら、Metamaskと連携して自分のウォレットアドレスを取得する
テキストボックスに先ほど発行したNFTスマートコントラクトのコントラクトアドレスを入力して「Get my NFTs」ボタンを押したら、Metamaskを通じてブロックチェーンにアクセスし、自分がmintしたNFT一覧を取得してきて表示する
という感じです!
おわりに
これで、謎めいていたブロックチェーンもNFTも完全に理解できましたね。
(もし記載内容の通りやってもうまくいかなかったり、間違ってるところがあったらコメントでこっそり教えて下さいね。)
Web3はどこ行ったって? それは思想の話であって技術の話じゃないので、「ユーザーがデータの所有権を取り戻し、非中央集権的に世界を動かしていく時代のことだ」くらいの理解をしておけばOKです!
REALITYでは、来たるべきメタバース時代にむけて「なりたい自分で、生きていく」ことができるプラットフォームを開発しています。
興味がある人はぜひこちらのサイトを見てみてね!
また、この記事が参考になったよって人はぜひイイネ!とシェアをお願いします!
それではまたね!
参考文献
このチュートリアルを作るにあたって、以下の記事を参考にさせていただきました。
この記事が気に入ったらサポートをしてみませんか?