Skip to main content

Command Palette

Search for a command to run...

Wagmi 1.4.12 버전에서 겪었던 이슈 정리

리액트 Web3 프로젝트에 Wagmi 1.4.12 버전을 적용하면서 프로젝트에서 발생했던 이슈, 그리고 어떻게 해결했는지 기록했습니다.

Updated
4 min readView as Markdown
Wagmi 1.4.12 버전에서 겪었던 이슈 정리

React & Web3

리액트 기반 Web3 어플리케이션 프론트엔드를 개발할 때 wagmi 라이브러리를 채택했다.

  • 타입스크립트 지원

  • 다양한 Web3 지갑과 빠르게 연동 가능

  • 자체적인 React Hooks 제공

프론트엔드를 개발하면서 겪었던 여러가지 이슈들을 정리했다.

useWalletClient

지갑에 로그인된 사용자 계정과 상호작용할 수 있는 인터페이스를 가져올 수 있는 React Hook이다.

  • 메시지 서명

  • 사용자의 Web3 주소를 가져오기

  • 트랜잭션 실행

export default function UserAccount() {
  const { data: walletClient, isError, isLoading } = useWalletClient();

  if (!walletClient) {
    return null;
  }
  return (
    <div>
      <h2>User Address: {walletClient.account.address}</h2>
    </div>
  );
}

페이지가 전부 렌더링되었음에도 불구하고 트랜잭션 실행이 불가능하다는 제보가 있었다. 브라우저 지갑과 로그인돼있는 상태에도 사용자의 주소를 가져오지 못하는 문제가 있다는 걸 확인했다.

깃허브 저장소 이슈 페이지에서 사람들이 동일한 이슈를 겪고 있다는 것을 알게 되었고, 버전을 올리는 것보다는 다른 방법이 있을지 대안을 생각해 봤다.

Config 객체가 제공하는 메소드로 Wallet Client를 반환하는 Hook을 직접 작성했다.

async function getWalletClient(address?: Address) {
  const config = getConfig();
  const walletClients = await Promise.all(
    config.connectors.map((connector) => {
      return connector.getWalletClient({ chainId: CHAIN_ID });
    })
  );
  return (
    walletClients.find((client) =>
      isAddressEqual(client.account.address, address)
    ) ?? null
  );
}

function useWalletClient() {
  const { address } = useAccount();
  const fetchKey = {
    key: "useWalletClient",
    address,
  };
  const { data: walletClient, mutate } = useSWR(
    fetchKey,
    async ({ address }) => {
      if (!address) {
        return null;
      }
      return getWalletClient(address);
    },
    { dedupingInterval: 0 }
  );

  return {
    walletClient,
    refresh: mutate,
  };
}

업데이트 후 지갑에서 사용자를 변경했을 때 변경된 사용자의 주소가 화면에 나타나지 않는다는 이슈가 있었다. 다음과 같은 이벤트가 발생했을 때 Wallet Client를 변경해주는 작업을 진행했다.

첫번째 useEffect는 지갑에서 사용자가 변경되었을 때 Wallet Client를 새로 가져와서 업데이트시킨다.

두번째 useEffect는 지갑 잠금이 풀리거나, 지갑과 연동된 체인이 변경되었을 때 업데이트시키는 역할을 한다.

이미 getWalletClient로 새로운 Wallet Client를 가져올 수 있기 때문에 추가적인 Revalidation은 방지하도록 했다.

const { address } = useAccount();
useEffect(() => {
  const onAccountChange = async (addresses: Address[]) => {
    const targetAddress = addresses.at(0);
    const nextWalletClient = await getWalletClient(targetAddress);

    mutate(nextWalletClient, { revalidate: false });
  };
  window.ethereum?.on("accountsChanged", onAccountChange);

  return () => {
    window.ethereum?.removeListener("accountsChanged", onAccountChange);
  };
}, [mutate]);

useEffect(() => {
  const onAccountChange = async () => {
    const nextWalletClient = await getWalletClient(address);

    mutate(nextWalletClient, { revalidate: false });
  };
  window.ethereum?.on("chainChanged", onAccountChange);
  window.ethereum?.on("connect", onAccountChange);
  return () => {
    window.ethereum?.removeListener("chainChanged", onAccountChange);
    window.ethereum?.removeListener("connect", onAccountChange);
  };
}, [address, mutate]);

데이터 가져오기

프로젝트에서는 백엔드 API 및 컨트랙트에서 네트워크 요청 및 데이터를 효율적으로 관리하기 위해 useSWR 라이브러리를 채택하고 있었다.

컴포넌트가 렌더링될 때 useSWR은 첫번째 인수가 같으면 네트워크 요청을 추가적으로 하지 않고 이미 캐시된 데이터를 반환해준다.

useSWR 첫번째 인자에는 네트워크 요청을 구분하기 위해 문자열, 배열, 객체 등을 지정할 수 있다.

프로젝트에는 queryKey라는 이름의 객체로 네트워크 요청을 구분한다.

const queryKey = {
  key: "SOME_DATA_COMPONENT_NEED",
  address: address,
};

클래스 인스턴스 등의 다소 복잡한 객체가 있으면 데이터를 저장하지 못하는 문제가 있었다.

다음과 같은 방법은 지양해야 했다.

const queryKey = {
  key: "SOME_DATA",
  publicClient,
  walletClient,
};

Wallet Client는 지갑에 현재 로그인된 사용자의 주소가 변경될 때만 업데이트된다. 네트워크 요청을 보내기 전에 우선 Client 종류의 객체들은 null인지 아닌지만 검사하고, queryKey에는 사용자의 지갑 주소를 작성했다.

const isNotNil = <T>(arg: T): arg is NonNullable<T> =>
  arg !== undefined && arg !== null;
const isFetched = isNotNil(publicClient) && isNotNil(walletClient);
const queryKey = {
  key: "SOME_DATA",
  address,
};

useSWR(isFetched ? queryKey : null, (queryKey) => {
  // Request to Backend API or Contracts
});

주소 비교

컨트랙트 및 백엔드 API에서 데이터를 받아올 때 사용자 주소의 실제 값은 같은데 모양이 다른 경우가 있었다.

사용자 주소는 20 바이트로 이루어진 16진수 문자열이다. 알파벳이 대소문자가 다른 경우는 JS 비교 연산자로는 서로 주소가 다르다고 인식되는 것이었다.

다음과 같은 상황에는 콘솔에 false로 출력된다.

const addressFromWallet = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const addressFromAPI = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaA";

console.log(addressFromWallet === addressFromAPI);

주소를 제대로 비교하기 위해서는 viem 패키지에서 제공하는 함수가 필요했다. 이 패키지는 wagmi가 의존하는 패키지라 직접적인 설치는 필요가 없었지만 개발 편의성을 위해 프로젝트 내 유틸 함수를 따로 작성했다.

두 주소 중에 하나라도 null 또는 undefined일 경우 무조건 false를 반환한다.

다음과 같은 상황에서는 JS 연산자와 다르게 true를 반환한다.

import {
  Address,
  isAddressEqual as isAddressEqualViem,
  isAddress as isAddressViem,
} from "viem";

export const isAddressEqual = (previous?: Address, next?: Address) => {
  if (isNil(previous) || isNil(next)) {
    return false;
  }
  if (isAddressViem(previous) && isAddressViem(next)) {
    return isAddressEqualViem(previous, next);
  }
  return false;
};

console.log(isAddressEqual(addressFromWallet, addressFromAPI));

More from this blog

AI 에이전트에서 작업 맥락을 관리하기 위해 사용한 방법

AI 에이전트의 불편함 Codex CLI와 같은 AI 코딩 에이전트를 통해서 프로젝트 개발 속도를 높일 수 있다. 그리고 혼자 작업했을 때는 놓칠 수 있는 부분도 발생할 수 있는데 AI 에이전트를 통해 보완할 수 있었다. 컴포넌트에 대해서 접근성 태그 작성 API 호출하는 비동기 코드 작성할 때 에러 처리 그리고 AI 에이전트를 잘 활용하면 불가능하

Mar 8, 202610 min read

지시사항을 분할하여 Codex로 효율적인 작업하기

이슈 Codex와 같은 AI 기반 코드 에이전트는 출력 토큰의 수가 제한되어 있다. 그래서 다양한 작업을 하나의 프롬프트에 전부 몰아서 작성하면 원하는 결과가 나오지 않을 수 있다. Codex와 같은 LLM 기반 서비스는 출력 토큰 수를 초과하게 되면 일부 단계가 누락되거나 특정 요소를 과하게 요약할 수 있고 이는 전체적인 답변 퀄리티를 낮출 수 있기 때문이다. 그래서 다양한 AI 공식문서는 특정 작업을 더 작은 단위로 분할하는 방식을 강조한다...

Nov 3, 20255 min read

Key를 활용하여 컴포넌트 상태를 초기화하기

개요 컴포넌트의 상태가 업데이트되어도 컴포넌트 내부에 배치된 컴포넌트의 상태는 그대로 유지되는 경우가 있었다. 예전에는 Props 전달 및 내부에서 useEffect를 통해서 상태를 초기 상태로 업데이트하도록 했는데 useEffect를 남용하게 되면 상태 변화 추적이 어려워지는 문제가 있었다. 그런데 useEffect 외에도 Key를 통해서 컴포넌트 상태를 초기화할 수 있다는 사실을 알게 되었다. 리스트 렌더링 React 프론트엔드에서 목록 형...

Sep 21, 20253 min read

내가 AGENTS.md를 작성하는 방법

Codex CLI 요즘 프로젝트 개발에 Codex CLI를 활용하는 이유는 다음과 같다. 특정 프로젝트를 작업할 때 Claude Code는 종종 사용량을 초과한 것과 다르게 Codex CLI는 ChatGPT Plus 요금제만으로도 토큰 사용량 초과 걱정 없이 충분하게 활용할 수 있었다. 타 CLI 기반 AI 개발 도구에 비해서 요구사항을 더 정확하게 구현하고 꼭 필요한 작업만 진행하기 때문에 코드를 검토하는 시간을 줄일 수 있다. 반복적인 코드...

Sep 21, 20254 min read
N

Nowon Lee

22 posts