2023년 5월, 이더리움 지갑 개발자 그룹이 모여 dapp과의 연결을 위한 새로운 업계 표준을 수립했습니다. 그 이전까지는 지갑이 웹 브라우저의 window.ethereum 전역 객체를 통해 EIP-1193 provider를 노출하는 방식이 일반적이었습니다. 이 방식은 사용자가 주로 하나의 지갑만 소유하고 연결하던 초기에는 잘 작동했습니다.
생태계가 성장하고 사용자들이 여러 지갑을 동시에 사용하기 시작하면서, dapp 개발자들이 사용자가 연결된 지갑을 감지하는 프로세스를 적절히 관리하거나, 여러 지갑이 설치된 경우 사용자에게 사용할 지갑을 선택할 수 있는 옵션을 제공하기가 어려워졌습니다.
2023년 10월, 현재 EIP-6963 또는 Multi-Wallet Injected Provider Discovery로 알려진 이 표준이 승인되었습니다. 이후 지갑 개발자들은 EIP-6963을 wallet provider 검색의 기본 방식으로 도입하기 시작했습니다. MetaMask도 예외가 아니며, MetaMask 지갑 API와 MetaMask SDK에서 이 표준을 지원하도록 보장했습니다.
또한 window.ethereum에 의존하는 dapp은 지속적인 지원이 보장됩니다. 그러나 선호하는 provider 검색 메커니즘으로서 모든 dapp에 EIP-6963 지원을 구현할 것을 권장합니다. 가장 쉬운 방법은 EIP-6963을 지원하는 서드파티 라이브러리와 통합하는 것입니다. 사용 가능한 라이브러리의 목록은 MetaMask 문서를 방문하여 확인하세요.
이 튜토리얼은 사용 가능한 서드파티 라이브러리를 사용하지 않기로 결정한 경우, dapp에 EIP-6963 지원을 구현하는 기본 단계를 안내합니다. React와 TypeScript를 사용하지만, JavaScript 개발자라면 누구든 자신이 선택한 프레임워크에서 이를 따라 구현할 수 있습니다.
Web3 지갑을 위한 지원 인터페이스 및 타입 정의
dapp에서 멀티 지갑 검색을 지원하는 첫 번째 단계는 개선 제안서에 명시된 모든 지원 인터페이스와 타입을 정의하는 것입니다. 이러한 인터페이스와 타입은 이더리움 wallet provider를 처리하는 구조화되고 표준화된 방법을 제공하여, 여러 이더리움 지갑의 통합 및 검색을 용이하게 합니다.
interface EIP6963ProviderInfo {
walletId: string;
uuid: string;
name: string;
icon: string;
}
interface EIP1193Provider {
isStatus?: boolean;
host?: string;
path?: string;
sendAsync?: (request: { method: string, params?: Array<unknown> }, callback: (error: Error | null, response: unknown) => void) => void;
send?: (request: { method: string, params?: Array<unknown> }, callback: (error: Error | null, response: unknown) => void) => void;
request: (request: { method: string, params?: Array<unknown> }) => Promise<unknown>;
}
interface EIP6963ProviderDetail {
info: EIP6963ProviderInfo;
provider: EIP1193Provider;
}
type EIP6963AnnounceProviderEvent = {
detail: {
info: EIP6963ProviderInfo;
provider: EIP1193Provider;
}
}
각 인터페이스와 타입이 무엇을 나타내는지 이해할 수 있도록 주석을 추가했습니다. 더 자세한 설명은 개선 제안서를 참조하세요.
이벤트 구독 및 새로운 연결 알림 받기
다음 단계는 새로운 지갑 연결이 감지될 때 dapp에 알림을 보내는 방법을 구축하는 것입니다. EIP-6963의 목적은 여러 지갑 간의 원활한 연결 및 전환을 가능하게 하는 것임을 기억하세요. React에서는 아래 샘플 코드로 이를 구현할 수 있습니다.
declare global {
interface WindowEventMap {
"eip6963:announceProvider": CustomEvent<EIP6963AnnounceProviderEvent>;
}
}
let providers: EIP6963ProviderDetail[] = [];
export const store = {
value: () => providers,
subscribe: (callback: () => void) => {
function onAnnouncement(event: EIP6963AnnounceProviderEvent) {
if (providers.some(p => p.info.uuid === event.detail.info.uuid)) return;
providers = [...providers, event.detail];
callback();
}
window.addEventListener("eip6963:announceProvider", onAnnouncement as EventListener);
window.dispatchEvent(new Event("eip6963:requestProvider"));
return () => window.removeEventListener("eip6963:announceProvider", onAnnouncement as EventListener);
}
}
위 코드 샘플에 대한 설명은 다음과 같습니다:
WindowEventMap을 "eip6963:announceProvider"라는 커스텀 이벤트로 확장하여, 새로운 이더리움 wallet provider의 가용성을 알리기 위한 전역 이벤트 타입을 설정합니다.
감지된 모든 wallet provider를 추적하는 데 사용되는 외부 store를 named provider 배열로 초기화합니다. 이 배열은 wallet provider 및 해당 이더리움 provider에 대한 정보를 포함하는 EIP6963ProviderDetail 인터페이스를 준수하는 객체를 저장합니다.
store 객체 내의 subscribe 함수를 통해 구독 메커니즘을 도입합니다. 이 함수는 "eip6963:announceProvider" 이벤트를 수신하고 새로 발표된 provider의 세부 정보로 외부 store를 업데이트합니다. 중복 항목을 방지하기 위해 store에 없는 provider만 추가되도록 합니다.
store 객체는 두 가지 핵심 기능을 제공합니다: 1. value 함수는 외부 store의 현재 상태, 즉 감지된 wallet provider 목록을 검색할 수 있게 합니다.
subscribe 함수는 컴포넌트 또는 애플리케이션의 다른 부분이 store의 변경 사항을 구독할 수 있게 합니다. 새 provider가 발표되어 store에 추가되면, 구독된 엔티티는 콜백 메커니즘을 통해 알림을 받아 업데이트된 provider 목록에 반응할 수 있습니다.
다음으로, 위의 store.tsx에 정의된 외부 store와 로컬 상태를 동기화하기 위한 useSyncExternalStore hook을 정의해 보겠습니다:
import { useSyncExternalStore } from "react";
import { store } from "./store";
export const useSyncProviders = ()=> useSyncExternalStore(store.subscribe, store.value, store.value)
감지된 각 Web3 Wallet Provider에 대한 버튼 동적 표시
useSyncProviders hook을 사용하여 감지된 각 wallet provider에 대한 버튼을 동적으로 렌더링하는 React 컴포넌트를 정의해 보겠습니다:
import { useState } from 'react';
import { useSyncProviders } from '../hooks/useSyncProviders';
import { formatAddress } from '~/utils';
export const DiscoverWalletProviders = () => {
const [selectedWallet, setSelectedWallet] = useState<EIP6963ProviderDetail | undefined>();
const [userAccount, setUserAccount] = useState<string>('');
const providers = useSyncProviders();
const handleConnect = async (providerWithInfo: EIP6963ProviderDetail) => {
const accounts = await providerWithInfo.provider.request({ method: 'eth_requestAccounts' }).catch(console.error);
if (accounts && accounts[0]) {
setSelectedWallet(providerWithInfo);
setUserAccount(accounts[0]);
}
};
return (
<>
<h2>Wallets Detected:</h2>
<div>
{providers.length > 0 ? (
providers.map((provider) => (
<button key={provider.info.uuid} onClick={() => handleConnect(provider)}>
<img src={provider.info.icon} alt={provider.info.name} />
<div>{provider.info.name}</div>
</button>
))
) : (
<div>There are no announced providers.</div>
)}
</div>
<hr />
<h2>{userAccount ? 'Wallet Selected' : 'No Wallet Selected'}</h2>
{userAccount && (
<div>
<img src={selectedWallet!.info.icon} alt={selectedWallet!.info.name} />
<div>{selectedWallet!.info.name}</div>
<div>({formatAddress(userAccount)})</div>
</div>
)}
</>
);
};
위 React 컴포넌트에 대한 설명은 다음과 같습니다:
두 개의 상태 hook인 selectedWallet과 userAccount를 사용하여 현재 선택된 이더리움 wallet provider와 사용자의 계정 주소를 각각 추적합니다.
useSyncProviders hook을 사용하여 사용 가능한 각 이더리움 wallet provider를 동적으로 감지합니다. 이 hook은 provider 배열을 반환하며, 각 provider는 provider의 정보와 이더리움 provider 객체를 포함하는 EIP6963ProviderDetail 인터페이스를 준수합니다.
wallet provider 버튼이 클릭될 때 호출되는 handleConnect 함수를 정의합니다. 이 함수는 eth_requestAccounts 메서드와 함께 provider의 request 메서드를 사용하여 사용자에게 계정 접근을 요청합니다. 접근이 허용되면 첫 번째 계정 주소가 userAccount에 저장되고, provider의 세부 정보가 selectedWallet에 저장됩니다.
마지막으로, 감지된 각 wallet provider에 대한 버튼 목록을 렌더링하여 provider의 이름과 아이콘을 표시합니다.
컴포넌트가 정의되었으므로, 마지막 단계는 이 컴포넌트를 렌더링하는 것입니다.
import './App.css'
import { DiscoverWalletProviders } from './components/DiscoverWalletProviders'
function App() {
return (
<>
<DiscoverWalletProviders/>
</>
)
}
export default App
이 몇 가지 단계를 통해 기본 수준에서 dapp에 EIP-6963 지원을 성공적으로 구현했습니다. 방금 구축한 기능의 시각적 표현은 아래를 참조하세요. 시각 자료에 캡처된 dapp의 전체 코드는 GitHub에서 확인할 수 있습니다.
EIP-6963과 Web3 지갑의 미래
지갑 개발자와 서드파티 연결 라이브러리에 의한 이 표준의 광범위한 채택이 계속되면서, 특히 신규 사용자 온보딩 UX 개선 측면에서 이더리움 커뮤니티에 열리는 다양한 사용 사례에 대해 기대가 큽니다.
MetaMask는 EIP-6963의 더욱 광범위한 채택을 보장하기 위해 다른 지갑 빌더, dapp 개발자, 서드파티 연결 라이브러리와 협력하기를 열망합니다. 이 표준을 지원하기 위한 저희의 작업에 대한 자세한 내용은 MetaMask 문서를 참조하세요.