はじめてのWeb3自分表現

既存Webスキルで始める!ReactとEthers.jsによるWeb3フロントエンド開発実践ガイド

Tags: Web3, React, Ethers.js, フロントエンド, dApp開発

Web2の世界で培ったフロントエンド開発のスキルは、Web3の分散型アプリケーション(dApp)開発においても非常に強力な武器となります。特に、モダンなWeb開発のデファクトスタンダードであるReactと、Ethereumブロックチェーンとのインタラクションを担う軽量なライブラリEthers.jsの組み合わせは、Web3フロントエンド開発の主流の一つです。

この記事では、Webエンジニアの皆さんが自身の既存スキルを活かし、ReactとEthers.jsを用いてWeb3フロントエンド開発を始めるための実践的なガイドを提供します。基本的な概念から、具体的なコード例を交えた開発手法、そして開発におけるベストプラクティスまでを網羅的に解説いたします。

1. Web2とWeb3フロントエンド開発のパラダイムシフト

従来のWeb2アプリケーションは、ユーザーのデータやビジネスロジックが中央集権型のサーバーに集中していました。これに対し、Web3アプリケーションは、スマートコントラクトやブロックチェーン、IPFSといった分散型技術を基盤とします。フロントエンド開発の観点から見ると、この変化は主にデータの取得とトランザクションの実行方法に現れます。

この根本的な違いを理解することが、Web3フロントエンド開発の第一歩です。既存のReact開発スキルは、UI/UXの構築や状態管理の面でそのまま活かせますが、ブロックチェーンとの連携部分に新たな知識が必要になります。

2. Ethers.jsの基礎:ブロックチェーンとの対話

Ethers.jsは、JavaScript/TypeScriptからEthereumブロックチェーンとインタラクションするための強力なライブラリです。Web3.jsと比較してより軽量で、TypeScriptフレンドリーな設計が特徴です。Ethers.jsを使いこなす上で重要な3つの概念を解説します。

2.1. Provider(プロバイダー)

Providerは、ブロックチェーンネットワークへの読み取り専用接続を提供します。これにより、アカウントの残高取得、トランザクションの取得、スマートコントラクトのView関数(状態を変更しない読み取り専用関数)の呼び出しなどが可能になります。

import { ethers } from "ethers";

// MetaMaskが注入するwindow.ethereumを使用する例
if (window.ethereum) {
  const provider = new ethers.BrowserProvider(window.ethereum);
  provider.getBalance("0xYourWalletAddress").then((balance) => {
    console.log(`Balance: ${ethers.formatEther(balance)} ETH`);
  });
}

ethers.BrowserProvider は、MetaMaskのようなブラウザ拡張ウォレットが提供する window.ethereum オブジェクトをラップする際に便利です。

2.2. Signer(サイナー)

Signerは、ウォレットのアカウントを表現し、トランザクションの署名やスマートコントラクトのState変更関数(状態を変更する書き込み関数)の実行を可能にします。MetaMaskなどのウォレットを通じてユーザーの承認を得る必要があります。

import { ethers } from "ethers";

async function connectWalletAndGetSigner() {
  if (window.ethereum) {
    const provider = new ethers.BrowserProvider(window.ethereum);
    // ユーザーにMetaMaskへの接続を要求
    await provider.send("eth_requestAccounts", []); 
    const signer = await provider.getSigner();
    console.log(`Connected account: ${await signer.getAddress()}`);
    return signer;
  }
  return null;
}

2.3. Contract(コントラクト)

Contractクラスは、特定のスマートコントラクトとインタラクトするための抽象化を提供します。コントラクトのアドレスとABI(Application Binary Interface)を基にインスタンス化し、定義された関数を呼び出すことができます。

import { ethers } from "ethers";

// サンプルコントラクトのABIとアドレス
const contractAddress = "0xYourContractAddress";
const contractABI = [
  // ... コントラクトのABI配列 ...
  "function name() view returns (string)",
  "function setValue(string _newValue) public",
  "event ValueChanged(string oldValue, string newValue)"
];

async function interactWithContract() {
  const signer = await connectWalletAndGetSigner();
  if (!signer) return;

  const contract = new ethers.Contract(contractAddress, contractABI, signer);

  // View関数(読み取り専用)の呼び出し
  const name = await contract.name();
  console.log(`Contract Name: ${name}`);

  // State変更関数(書き込み)の呼び出し
  try {
    const tx = await contract.setValue("Hello Web3!");
    await tx.wait(); // トランザクションがマイニングされるまで待機
    console.log("Value updated successfully!");
  } catch (error) {
    console.error("Error setting value:", error);
  }
}

3. ReactとEthers.jsによるWeb3フロントエンドの構築

Reactアプリケーション内でEthers.jsを効果的に統合するためには、非同期処理と状態管理が鍵となります。

3.1. ウォレット接続コンポーネントの作成

ユーザーがウォレットを接続し、アカウント情報を表示する基本的なコンポーネントを実装します。

import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';

const WalletConnector: React.FC = () => {
  const [signer, setSigner] = useState<ethers.Signer | null>(null);
  const [address, setAddress] = useState<string | null>(null);
  const [balance, setBalance] = useState<string | null>(null);

  useEffect(() => {
    // ページロード時にウォレット接続状態を確認(任意)
    if (window.ethereum) {
      const provider = new ethers.BrowserProvider(window.ethereum);
      provider.listAccounts().then(accounts => {
        if (accounts.length > 0) {
          provider.getSigner().then(s => {
            setSigner(s);
            s.getAddress().then(a => setAddress(a));
            s.getBalance().then(b => setBalance(ethers.formatEther(b)));
          });
        }
      });
    }
  }, []);

  const connectWallet = async () => {
    if (!window.ethereum) {
      alert("MetaMaskなどのEthereumウォレットをインストールしてください。");
      return;
    }

    try {
      const provider = new ethers.BrowserProvider(window.ethereum);
      await provider.send("eth_requestAccounts", []); // 接続要求
      const newSigner = await provider.getSigner();
      setSigner(newSigner);

      const newAddress = await newSigner.getAddress();
      setAddress(newAddress);

      const newBalance = await newSigner.getBalance();
      setBalance(ethers.formatEther(newBalance));

    } catch (error) {
      console.error("ウォレット接続エラー:", error);
    }
  };

  return (
    <div>
      {address ? (
        <div>
          <p>接続中のアドレス: {address}</p>
          <p>残高: {balance} ETH</p>
        </div>
      ) : (
        <button onClick={connectWallet}>ウォレットを接続</button>
      )}
    </div>
  );
};

export default WalletConnector;

3.2. スマートコントラクトとのインタラクション

ウォレットが接続されたら、そのSignerを使ってスマートコントラクトの関数を呼び出すことができます。

import React, { useState, useEffect } from 'react';
import { ethers } from 'ethers';

// App.tsxなど、上位コンポーネントからsignerをpropsで渡す想定
interface ContractInteractionProps {
  signer: ethers.Signer | null;
}

const contractAddress = "0xYourContractAddress"; // 実際にデプロイされたコントラクトアドレス
const contractABI = [
  "function currentMessage() view returns (string)",
  "function setMessage(string _newMessage) public",
  "event MessageUpdated(string oldMessage, string newMessage)"
];

const MessageBoard: React.FC<ContractInteractionProps> = ({ signer }) => {
  const [message, setMessage] = useState<string>("");
  const [newMessageInput, setNewMessageInput] = useState<string>("");
  const [loading, setLoading] = useState<boolean>(false);

  // メッセージを読み込む関数
  const fetchMessage = async () => {
    if (!signer) return;
    try {
      const provider = signer.provider;
      if (!provider) return;
      const contract = new ethers.Contract(contractAddress, contractABI, provider);
      const currentMsg = await contract.currentMessage();
      setMessage(currentMsg);
    } catch (error) {
      console.error("メッセージの読み込みエラー:", error);
    }
  };

  // メッセージを更新する関数
  const updateMessage = async () => {
    if (!signer) {
      alert("ウォレットを接続してください。");
      return;
    }
    if (!newMessageInput.trim()) {
      alert("新しいメッセージを入力してください。");
      return;
    }

    setLoading(true);
    try {
      const contract = new ethers.Contract(contractAddress, contractABI, signer);
      const tx = await contract.setMessage(newMessageInput);
      await tx.wait(); // トランザクションの完了を待つ
      alert("メッセージが更新されました!");
      setNewMessageInput("");
      await fetchMessage(); // 更新後にメッセージを再読み込み
    } catch (error) {
      console.error("メッセージの更新エラー:", error);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchMessage(); // コンポーネントロード時、またはsignerが変更された時にメッセージを読み込む
  }, [signer]);

  return (
    <div>
      <h3>メッセージボード</h3>
      <p>現在のメッセージ: {message || "メッセージなし"}</p>
      {signer ? (
        <div>
          <input
            type="text"
            value={newMessageInput}
            onChange={(e) => setNewMessageInput(e.target.value)}
            placeholder="新しいメッセージを入力"
            disabled={loading}
          />
          <button onClick={updateMessage} disabled={loading}>
            {loading ? "送信中..." : "メッセージを更新"}
          </button>
        </div>
      ) : (
        <p>ウォレットを接続してメッセージを更新できます。</p>
      )}
    </div>
  );
};

export default MessageBoard;

上記は非常に基本的な例ですが、ethers.Contract を使ってスマートコントラクトの関数を呼び出し、トランザクションの送信と待機を行うプロセスを示しています。

4. 開発におけるベストプラクティスと考慮事項

Web3フロントエンド開発には、従来のWeb開発とは異なる特有の考慮事項があります。

5. 次のステップと学習リソース

既存のWeb技術スキルを持つ皆さんは、Web3の世界への適応において非常に有利なスタート地点にいます。ReactとEthers.jsの基礎を習得したら、さらに深く学ぶためのリソースを活用してください。

Web3の世界は急速に進化しています。新しい技術やツールが常に登場するため、最新の情報にアンテナを張り、積極的にコミュニティに参加することも重要です。

結論

ReactとEthers.jsの組み合わせは、既存のWeb開発スキルをWeb3の分散型アプリケーション開発に橋渡しするための強力なパスを提供します。この記事で紹介した基本的な開発手法とベストプラクティスを参考に、ぜひあなた自身のWeb3表現を形にしてみてください。技術的な挑戦は多いかもしれませんが、その分、新しい創造の可能性が広がっています。