이번 포스팅에서는 DApp을 만들기 위한 기초 작업을 진행해보려 한다. 구글 Chrome 확장 프로그램으로 메타마스크를 설치한 다음 직접 만든 웹 사이트와 메타마스크를 연결하는 작업을 해보도록 하자.
< 목차 >
- [메타마스크] 네트워크 추가하기
- 프론트 & 메타마스크 연결
1. [메타마스크] 네트워크 추가하기
Chrome 브라우저에서 확장 프로그램으로 메타마스크를 설치해준 다음 로그인까지 진행해주도록 하자. ( 설치하고 계정을 생성하는 과정은 생략,, ) 메타마스크 계정에 로그인을 완료하면 다음과 같은 화면이 나오게 된다.
가나쉬(ganache)를 이용해 생성한 로컬 이더리움 네트워크를 메타마스크에 추가해주는 작업을 진행보면서 메타마스크에서 네트워크를 추가하는 방법에 대해 알아보자.
우측 상단의 "이더리움 메인넷" 부분을 클릭하면 메타마스크와 연결되어 있는 네트워크 목록을 확인할 수 있다. 하단에 있는 네트워크 추가 버튼을 눌러서 원하는 네트워크를 추가할 수 있다.
네트워크 추가 버튼을 누르면 아래와 같은 화면이 나오게 되는데 추가하고 싶은 네트워크의 네트워크 이름 , 새 RPC URL , 체인 ID , 통화 기호 를 채워주면 된다.
현재 우리는 가나쉬로 생성한 로컬 이더리움 네트워크를 추가할 것이기 때문에 다음과 같이 적어주었다.
체인 ID 부분을 살펴보면 1234로 작성되어 있는 것을 확인할 수 있는데 메인넷을 가지고 있는 블록체인 네트워크는 저마다 고유한 체인 ID를 가지고 있다. 가나쉬로 생성한 로컬 이더리움 네트워크의 경우 디폴트 체인 ID가 1337 이다. 하지만 가나쉬를 실행할 때 옵션 값을 입력해 원하는 체인 ID로 로컬 이더리움 네트워크를 생성할 수 있다.
$ npx ganache-cli --chainId 1234
저장 버튼을 누르면 다음과 같이 가나쉬 네트워크가 메타마스크에 연결된 것을 확인할 수 있다.
2. 프론트 & 메타마스크 연결
프론트 서버와 메타마스크를 연결하는 작업은 DApp을 만들기 위한 기초 작업 중의 하나로 필수적인 요소라고 해도 과언이 아니다. 트랜잭션을 발생시키기 위해서는 사용자의 개인키를 이용해 서명을 만드는 과정이 반드시 포함되어야 하는데 개인키를 자신이 만든 서버에 저장하는 것은 보안적으로 굉장히 취약하기 때문이다. 따라서 메타마스크와 같은 지갑 프로그램을 연결해서 사용하는 경우가 대부분이다.
메타마스크는 사용자의 개인키를 관리해주는 지갑 프로그램으로 우리가 만든 프론트에서는 메타마스크에 요청을 보내주기만 하면 된다. 요청을 받은 메타마스크가 사용자의 개인키를 이용해 서명을 만들고 메타마스크에서 사용자가 트랜잭션을 발생시킨 블록체인 네트워크로 요청을 보내게 된다. 우리가 해줘야 할 것은 프론트 서버와 메타마스크가 서로 소통할 수 있게끔 커넥션을 맺어주는 작업이다.
위의 그림은 DApp에서 트랜잭션을 발생시키게 되는 흐름도를 간략히 그려본 것이다. 하지만 위와 같은 방식으로만 서버를 구성하게 될 경우 이슈가 발생하는 부분이 있다. 가령 코인으로 물건을 거래할 수 있는 쇼핑몰을 만들게 될 경우 판매하는 모든 물품의 정보를 블록체인 네트워크 상에 저장한다는 것은 말이 되지 않는다. 블록체인 네트워크 상에 데이터를 저장한다는 것은 트랜잭션 발생시킨다는 의미이고 이더리움 네트워크의 경우 gas가 소비되기 때문이다. 위와 같은 구조에서는 상품을 등록할 때마다 가스비를 지불하면서 데이터를 저장해야만 하는 문제가 발생하게 된다.
단지, 쇼핑몰에 등록되어 있는 상품들을 사용자들이 코인 혹은 토큰으로 구매할 수 있게끔 하고 싶은 것 뿐인데 상품 데이터까지 블록체인 네트워크 상에 저장하는 것은 굉장히 비효율적이다. 그래서 프론트 뿐만 아니라 백엔드 쪽도 같이 구현해서 상품 데이터는 DB에 저장해 놓고 상품 목록을 가져오거나 특정 상품에 대한 정보들을 가져오고 싶을 때는 백엔드 서버에 요청해서 가져오는 방식을 취하게 된다.
그렇다면 이러한 프로세스 속에서 트랜잭션 객체(txObject)를 만드는 주체는 누가되어야 할 것인가에 대한 의문점이 생기게 된다. 이에 대해서는 대략적으로 세가지 정도의 케이스가 존재한다.
- 프론트에서 메타마스크에게 다이렉트로 요청을 보내 트랜잭션을 발생시키는 방법.
- 백엔드에서 서명을 제외한 트랜잭션 객체를 만든 다음 프론트에 전달하고 프론트에서는 백엔드로부터 전달받은 트랜잭션 객체를 메타마스크에게 전달해 메타마스크에서 서명과 함께 트랜잭션을 발생시키는 방법. ( 옵션에 따라 상품 가격이 변동되는 경우 이와 같은 로직을 주로 사용한다. )
- 백엔드에서 사용자의 개인키를 이용해 서명을 만들고 서명까지 포함된 완전한 트랜잭션 객체를 만들어서 블록체인 네트워크에게 다이렉트로 요청하는 방법.
하지만 3번과 같은 방식은 백엔드 서버 쪽에서 사용자의 개인키를 DB 같은 곳에 보관해 놓고 있다가 사용자로부터 요청이 들어왔을 때 개인키를 사용해 서명을 만들어주는 방식으로 굉장히 위험한 방법이다. 사용자 입장에서는 본인의 계정 비밀번호를 남에게 맡기는 것이 되버리기 때문이다. 주로 1, 2번과 같은 방식으로 DApp을 만들게 되며 중요한 포인트는 사용자의 개인키를 이용해 서명을 만드는 주체는 우리가 아닌 메타마스크가 된다는 점이다.
이제 본격적으로 CRA(create-react-app)를 이용해 프론트 쪽을 만들어 보도록 하자. 터미널에 npx create-react-app [디렉토리명] 을 입력해 React 프로젝트 디렉토리를 생성한 다음 src/ 디렉토리 아래에 hooks 디렉토리를 만들어 useWeb3.js 라는 파일을 만들어 주었다.
// useWeb3.js 파일
import { useEffect, useState } from 'react';
import Web3 from 'web3/dist/web3.min.js';
// web3 라이브러리 안에는 브라우저가 아닌 nodejs에서만 사용 가능한 라이브러리들이 존재
// webpack 설정을 잡아주거나 최소기능만을 가져오는 방법으로 해결
const useWeb3 = () => {
const [account, setAccount] = useState(null);
const [web3, setWeb3] = useState(null);
const getChainId = async () => {
const chainId = await window.ethereum.request({
// 메타마스크가 사용하고 있는 네트워크의 체인 아이디를 return
method: 'eth_chainId',
});
return chainId;
};
const getRequestAccounts = async () => {
const accounts = await window.ethereum.request({
// 연결이 안되어 있다면 메타마스크 내의 계정들과 연결 요청
// 연결이 되었다면 메타마스크가 갖고 있는 계정들 중 사용하고 있는 계정 가져오기
method: 'eth_requestAccounts',
});
console.log(accounts);
return accounts;
};
const addNetwork = async (_chainId) => {
// 메타마스크에서 네트워크 추가를 할 때 들어가는 속성들
const network = {
chainId: _chainId,
chainName: 'Ganache',
rpcUrls: ['http://127.0.0.1:8545'],
nativeCurrency: {
name: 'Ethereum',
symbol: 'ETH', // 통화 단위
decimals: 18, // 소수점 자리수
},
};
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [network],
});
};
// window 객체에 대한 접근은 모든 요소들이 랜더 완료되었을 때 하는 것이 효과적이다.
useEffect(() => {
// console.log(window.ethereum);
const init = async () => {
try {
const targetChainId = '0x4d2';
const chainId = await getChainId(); // 1234 , hex: 0x4d2
console.log('체인 아이디 : ', chainId);
if (targetChainId !== chainId) {
// 네트워크 추가하는 코드
addNetwork(targetChainId);
}
const [accounts] = await getRequestAccounts();
// web3 라이브러리를 메타마스크에 연결 (맵핑)
const web3 = new Web3(window.ethereum);
setAccount(accounts);
setWeb3(web3);
} catch (e) {
console.error(e.message);
}
};
if (window.ethereum) {
// 메타마스크 설치된 클라이언트
// window.ethereum.request() : 메타마스크에 요청 보내는 메소드
// RPC 사용
init();
}
}, []);
return [account, web3];
};
export default useWeb3;
useWeb3.js 파일 안에서 프론트와 메타마스크를 연결해 메타마스크와 소통할 수 있는 useWeb3 라는 커스텀 훅을 만들어 주었다. useWeb3 훅에서 중요한 포인트들만 짚고 넘어가고자 한다.
우선 React 프로젝트에서 web3 라이브러리를 import 해서 사용할 경우 에러가 발생하게 된다. web3 라이브러리 안에는 Node.js 환경에서만 사용 가능한 라이브러리들이 존재한다. 빌드한 파일을 해석하는 주체는 브라우저이기 때문에 브라우저 상에서 web3를 읽을 때 에러가 발생하게 되는 것이다. React 내의 webpack 설정을 잡아주거나 web3 라이브러리의 최소 기능만을 가져와서 사용하는 방법으로 해결해야만 한다. 여기에서는 후자의 방법으로 진행하였다.
// web3에서 최소 기능만을 가져오기
import Web3 from 'web3/dist/web3.min.js';
다음으로 useWeb3 훅 안에서 선언해준 함수들을 살펴보자.
const getChainId = async () => {
const chainId = await window.ethereum.request({
// 메타마스크가 사용하고 있는 네트워크의 체인 아이디를 return
method: 'eth_chainId',
});
return chainId;
};
getChainId 함수는 현재 메타마스크에 연결되어 있는 네트워크의 체인 ID를 반환하는 함수로 만들어주었다. 여기서 window.ethereum.request( ) 라는 낯설어보이는 메소드를 발견하게 될 것이다. 브라우저의 window 객체 안에는 ethereum이라는 속성이 존재하지 않는다. 이는 우리가 Chrome 확장 프로그램으로 메타마스크를 설치할 때 메타마스크에서 자체적으로 window 객체 안에 만들어 놓은 속성값이다. ( Chrome 개발자 도구의 콘솔 창에서 window.ethereum 을 입력하면 다음과 같은 내용이 나오는 것을 확인할 수 있다. )
window.ethereum.request( ) 메소드를 이용하면 메타마스크에게 요청을 보낼 수 있으며 이 때 RPC를 사용해서 요청을 보내게 된다. 따라서 window.ethereum.request( ) 메소드의 인자값으로는 method: 'eth_chainId' 라는 속성값을 갖는 객체를 전달해주게 된다.
const getRequestAccounts = async () => {
const accounts = await window.ethereum.request({
// 연결이 안되어 있다면 메타마스크 내의 계정들과 연결 요청
// 연결이 되었다면 메타마스크가 갖고 있는 계정들 중 사용하고 있는 계정 가져오기
method: 'eth_requestAccounts',
});
console.log(accounts);
return accounts;
};
getRequestAccounts 함수는 메타마스크에 존재하는 계정들 중 프론트와 연결된 계정들을 가져오는 함수로 만들어주었다. 메타마스크에서는 아래의 그림과 같이 현재 메타마스크 상에 존재하는 계정들이 프론트 서버와 연결이 되어 있는지 안되어있는지에 대한 조회가 가능하다. 메타마스크에서 이야기하는 계정의 연결이란 사이트 url (여기서는 localhost:3000) 에서 해당 계정을 토대로 메타마스크에게 어떠한 요청을 보냈었는지에 대한 여부를 의미하는 것 같다.
getRequestAccounts 함수 안에서 window.ethereum.request( ) 메소드의 인자값으로 들어간 객체의 method 속성값인 'eth_requestAccounts'의 경우 다음과 같은 처리를 해준다.
- 계정 연결이 안되어 있다면 메타마스크 내의 계정들과 연결 요청
- 계정 연결이 되어 있다면 메타마스크에서 현재 사용중인 계정 가져오기
const addNetwork = async (_chainId) => {
// 메타마스크에서 네트워크 추가를 할 때 들어가는 속성들
const network = {
chainId: _chainId,
chainName: 'Ganache',
rpcUrls: ['http://127.0.0.1:8545'],
nativeCurrency: {
name: 'Ethereum',
symbol: 'ETH', // 통화 단위
decimals: 18, // 소수점 자리수
},
};
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [network],
});
};
addNetwork 함수는 메타마스크에 연결할 네트워크를 추가해주는 함수로 만들어주었다. 메타마스크에서 직접 네트워크를 추가해도 되지만 사용자의 편의를 위해 우리가 제작한 웹사이트에 접속했을 때 자동적으로 메타마스크가 네트워크를 추가해 주도록 하기 위함이다. network 객체를 만들어서 네트워크를 추가할 때 들어가는 속성값들을 넣어주었다. 그리고 window.ethereum.request( ) 메소드의 인자값으로 { method: 'wallet_addEthereumChain' , params: [network] } 를 넣어주었다. 이제 _chainId를 인자값으로 전달하여 addNetwork( ) 함수를 호출했을 때 _chainId에 해당하는 블록체인 네트워크가 메타마스크에 추가될 것이다.
// window 객체에 대한 접근은 모든 요소들이 랜더 완료되었을 때 하는 것이 효과적이다.
useEffect(() => {
const init = async () => {
try {
const targetChainId = '0x4d2';
const chainId = await getChainId(); // 1234 , hex: 0x4d2
if (targetChainId !== chainId) {
// 네트워크 추가하는 코드
addNetwork(targetChainId);
}
const [accounts] = await getRequestAccounts();
// web3 라이브러리를 메타마스크에 연결 (맵핑)
const web3 = new Web3(window.ethereum);
setAccount(accounts);
setWeb3(web3);
} catch (e) {
console.error(e.message);
}
};
if (window.ethereum) {
// 메타마스크 설치된 클라이언트
// window.ethereum.request() : 메타마스크에 요청 보내는 메소드
// RPC 사용
init();
}
}, []);
window 객체에 대한 접근은 모든 컴포넌트들이 랜더 완료되었을 때 하는 것이 효과적이다. 따라서 useEffect( ) 메소드를 사용해 앞서 정의해주었던 함수들을 실행시켜 주었다. if (window.ethereum ) 조건문 안에서 init( ) 함수를 호출하는 방식으로 나머지 함수들이 호출되게끔 하였는데 이는 메타마스크가 설치되어 있는 클라이언트에 한해서 앞서 정의해 놓은 함수들을 실행시키기 위함이다.
현재 메타마스크와 연결되어 있는 네트워크의 chainId 가 우리가 원하는 체인 ID (targetChainId)가 아닐 경우 addNetwork( targetChainId ) 메소드를 호출하도록 하였고 getRequestAccounts( ) 메소드를 호출해 메타마스크에 연결된 계정을 가져올 수 있도록 했다. 한가지 짚고 넘어가야할 것은 const web3 = new Web3( window.ethereum ) 부분이다.
const web3 = new Web3(window.ethereum);
web3 인스턴스를 생성할 때 window.ethereum 을 new Web3( )의 인자값으로 전달하여 web3 인스턴스를 통해서도 메타마스크로 요청을 보낼 수 있도록 하였다. 그리고 이렇게 생성한 web3 인스턴스를 account와 함께 useWeb3( ) 함수의 return 값으로 만들었다. 이는 다른 컴포넌트에서 메타마스크로 요청을 보내고 싶을 때 useWeb3 를 import 하기만 하면 web3 인스턴스를 사용해 메타마스크로 요청을 보내는 코드를 쉽게 작성할 수 있도록 하기 위함이다.
아래는 App.js 파일이다.
// App.js 파일
import './App.css';
import useWeb3 from './hooks/useWeb3';
import { useEffect, useState } from 'react';
function App() {
const [account, web3] = useWeb3();
const [isLogin, setIsLogin] = useState(false);
const [balance, setBalance] = useState(0);
const handleSubmit = async (e) => {
e.preventDefault();
await web3.eth.sendTransaction({
from: account,
to: e.target.received.value,
value: web3.utils.toWei(e.target.amount.value, 'ether'),
});
};
useEffect(() => {
const init = async () => {
// web3? 문법
// web3가 null 값이라면 undefined를 반환해준다.
const balance = await web3?.eth.getBalance(account);
setBalance(balance / 10 ** 18);
};
if (account) setIsLogin(true);
init();
}, [account]);
if (!isLogin)
return (
<div>
<h1>메타마스크 로그인 이후 사용해주세요.</h1>
</div>
);
return (
<div className="App">
<div>
<h3>{account}님 환영합니다.</h3>
<div>Balance : {balance} ETH</div>
</div>
<div>
<form onSubmit={handleSubmit}>
<input type="text" id="received" placeholder="받을 계정" />
<input type="number" id="amount" placeholder="보낼 금액" />
<input type="submit" value="전송" />
</form>
</div>
</div>
);
}
export default App;
useWeb3 를 import 해서 web3.eth.sendTransaction( ) 과 같은 방식으로 메타마스크에게 쉽게 요청을 보내고 있는 것을 확인할 수 있다. web3.eth.sendTransaction( ) 및 web3 메소드들은 이전 포스팅에서 정리해두었으니 참고하길 바란다.
App.js 안에서 구현된 코드들은 다음의 실행순서를 따른다.
- 메타마스크에 로그인되어 있지 않다면 "로그인 이후 사용해주세요" 라는 문구를 보여준다.
- 메타마스크 로그인 이후 메타마스크와 연결된 네트워크를 가나쉬로 생성한 로컬 이더리움 네트워크로 변경해준다.
- 계정이 연결되어 있지 않다면 계정 연결 요청을 한다.
- 계정 연결을 완료하면 웹 페이지에서 해당 계정의 잔액을 조회할 수 있다.
- 웹 페이지에 있는 input 박스를 이용해 트랜잭션을 생성하기 위한 정보로써 "받을 계정"과 "보낼 금액"을 사용자로부터 입력받을 수 있다.
- 전송 버튼을 누르면 메타마스크로 요청을 보내게 되고 메타마스크에서 서명을 만들어 트랜잭션을 발생시킨다.
참고)
2022.06.29 - [Ethereum] - Ethereum/이더리움 - Web3
시연 영상)
'Ethereum' 카테고리의 다른 글
Ethereum/이더리움 - private 네트워크 RPC 설정하기 (1) | 2022.07.01 |
---|---|
Ethereum/이더리움 - private 네트워크 (1) | 2022.06.30 |
Ethereum/이더리움 - Web3 (0) | 2022.06.29 |
Ethereum/이더리움 - 비트코인 vs 이더리움 (0) | 2022.06.28 |
Ethereum/이더리움 - 개발 환경 세팅 (Go , Geth , Ganache) (0) | 2022.06.28 |