Wagmi 1.4.12 버전에서 겪었던 이슈 정리
리액트 Web3 프로젝트에 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));