문제 상황
배경지식 설명
[Web3의 지갑 연결 = Web2의 로그인]
Web3에서 지갑을 연결하는 행위는 Web2에서 로그인하는 것과 거의 같다고 보시면 됩니다. 마치, 구글이나 카카오로 로그인하여 Web2 서비스를 이용하는 것처럼 블록체인 플랫폼에서는 지갑을 연결함으로써 Web3 서비스를 이용하게 됩니다.
[Keplr Wallet = Cosmos 블록체인의 대표 지갑]
현재, 여러 블록체인 플랫폼이 있습니다. 가장 유명한 이더리움부터 솔라나, 코스모스, 앱토스 등이 있죠.
[클라이언트 역할]
클라이언트가 있어야 트랜잭션을 쏘거나 쿼리를 날릴 수 있습니다.
Supernova에서 모든 기능은 트랜잭션과 쿼리로 이루어져 있기에 클라이언트는 거의 모든 페이지에서 필요한 객체입니다.
문제 개요
대부분 페이지에서 트랜잭션을 쏘거나 쿼리를 날리게 됩니다. 그때마다 클라이언트를 매번 생성해야 되는데, 이런 구조는 불필요한 연산을 하게 되며 코드 재사용성이 떨어집니다. 또한, 사용자 입장에서 딜레이를 경험하게 되어 사용자 경험이 저하되는 문제가 발생합니다.
해결 과정
트랜잭션을 쏘거나 쿼리를 날리게 될 때마다 클라이언트를 매번 생성하는 것은 비효율적이며 사용자 경험이 저하됩니다.
이를 해결하고자 지갑 연결할 때, 트랜잭션과 쿼리 로직에 필요한 클라이언트를 바로 생성하여 Recoil의 Atom에 저장합니다. 그리고 useRecoilValue 메소드를 사용하여 저장된 클라이언트를 불러와 트랜잭션과 쿼리 로직에서 사용합니다.
어떻게 구현할까?
[클라이언트 구조 분석]
// useChains.tsx
const wasmClient = async (체인_정보) => {
const offlineSigner = window.keplr.getOfflineSigner(
체인_id,
); // 트랜잭션에 서명하는 객체
const options: SigningCosmWasmClientOptions = {
gasPrice: {
amount: // 트랜잭션 수수료 수량 설정
denom: // 트랜잭션 수수료 자산 종류 설정
},
};
const client = SigningCosmWasmClient.connectWithSigner(
rpc_주소, // 클라이언트가 블록체인 노드와 통신할 수 있는 RPC 주소
offlineSigner,
);
return client;
};
TypeScript
복사
SigningCosmWasmClient 객체의 connectWithSigner 메소드를 사용해서 Cosmos 기반 블록체인과 상호작용하는 클라이언트를 비동기적으로 생성합니다. 이를 위해서 offlineSigner와 RPC 주소가 필요합니다.
offlineSigner는 사용자의 지갑에서 오프라인 서명자를 반환하는 Keplr의 getOfflineSigner 함수를 이용합니다. 이를 통해 사용자의 서명 권한을 안전하게 관리하고, 트랜잭션 수행 시 필요한 가스 가격을 설정합니다. (이 함수는 Amino와 Protobuf 모두에 대응할 수 있는 서명을 반환하기에, Launchpad 체인과 Stargate 체인 모두에서 사용할 수 있습니다)
RPC(Remote Procedure Call) 주소는 클라이언트가 블록체인 노드와 통신할 수 있는 경로를 제공합니다. 이를 통해 클라이언트는 네트워크의 상태를 조회하거나 트랜잭션을 전송하는 등의 작업을 수행할 수 있습니다. RPC 주소 없이는 클라이언트가 블록체인 네트워크에 접근하거나 그 안에서 작업을 실행할 방법이 없기 때문에, 블록체인과 상호작용하기 위해 필수적입니다.
[Recoil 활용]
// useChains.tsx
const [wasmClient, setWasmClient] = useRecoilState(getWasmClient);
setWasmClient(newWasmClient]);
TypeScript
복사
Recoil의 useRecoilState 훅을 사용함으로써, getWasmClient atom에서 정의된 상태를 사용하는 wasmClient 상태와 그 상태를 업데이트하는 함수 setWasmClient를 선언할 수 있게 됩니다.
이 훅을 통해서 컴포넌트 내에서 간단하고 선언적인 방식으로 상태를 읽고 쓸 수 있게 되어 React 애플리케이션의 상태 관리를 용이하게 만듭니다.
// coreState.ts
export const getWasmClient = atom<SigningCosmWasmClient | {}>({
key: `wasmClient/${v1()}`,
default: {},
});
TypeScript
복사
atom 은 Recoil의 상태 단위를 의미하며, 이 atom은 애플리케이션 전반에서 사용할 수 있는 전역 상태를 제공합니다.
Recoil을 사용하여 SigningCosmWasmClient 타입 또는 빈 객체를 저장할 수 있는 atom을 정의합니다. 여기서 key는 atom의 고유 식별자로 사용되며, default는 상태의 초기값을 설정합니다.
이를 통해 상태 관리를 통한 애플리케이션의 유지 보수성이 향상됩니다.
해결 결과
Keplr로 지갑 연결을 할 때, 트랜잭션과 쿼리에 필요한 클라이언트를 생성하여 Recoil의 atom에 저장하도록 구현했습니다. 이를 통해 불필요한 클라이언트 생성은 최소화하고 사용자 경험을 개선되는 효과를 가져왔습니다.
아쉬웠던 점
현재는 Nova Client와 Wasm 클라이언트 두 개를 따로 생성하여 저장합니다. Nova 클라이언트는 Stake/Unstake 기능에 사용되며, Wasm 클라이언트는 Swap, Liquidity 기능에서 사용됩니다.
이 두 클라이언트를 하나의 클라이언트로 추상화하여 이 클라이언트로 트랜잭션과 쿼리 로직에 활용했다면, 코드가 더 간결해지지 않았을까 하는 아쉬움이 있습니다.