DApp 아키텍처와 Web3.js·Ethers.js 연동 설계
DApp의 지갑·RPC·스마트 컨트랙트·인덱서 구조와 Web3.js, Ethers.js를 활용한 트랜잭션 운영 설계를 정리합니다.
2026-08-14 · 최초 발행 2025-10-31
지갑 서명에서 체인 상태 반영까지
DApp은 스마트 컨트랙트를 백엔드로 삼아 블록체인 네트워크와 상호작용하는 애플리케이션이다. 사용자는 지갑에서 서명하고, 애플리케이션은 서명된 트랜잭션을 RPC Provider를 통해 체인으로 제출한다. 이후 Receipt와 이벤트를 받아 화면 상태를 갱신하는 흐름이 기본이 된다.
이 구조에는 블록체인 노드, 지갑, 스마트 컨트랙트, RPC Provider, 인덱싱 레이어가 함께 들어간다. Web3.js와 Ethers.js 같은 클라이언트 라이브러리는 RPC 호출, 계정 및 서명 처리, ABI 인코딩, 이벤트 구독을 연결한다. 지갑은 브라우저 확장, 모바일, 하드웨어 방식으로 연동할 수 있고, 서버 측 키를 사용하는 경우에도 트랜잭션 생성·서명·전송 흐름이 필요하다.
체인과의 통신은 Infura, Alchemy, 자체 Geth 또는 Nethermind 같은 RPC Provider의 JSON-RPC API를 통해 이뤄진다. 이벤트와 상태 조회는 The Graph나 자체 인덱서를 활용해 최적화할 수 있다. Web3.js v4와 Ethers.js v6은 API가 다르므로 사용하는 버전의 문서를 확인해야 한다.
각 레이어가 맡는 역할
프런트엔드는 React·Vue 기반 SPA 또는 모바일 하이브리드 앱으로 구성할 수 있다. window.ethereum 주입 여부를 감지하고, 체인과 계정 변경 이벤트를 처리하며, 트랜잭션의 펜딩·성공·실패 상태를 사용자에게 보여준다.
키는 MetaMask, WalletConnect, hardware wallet 등 사용자 지갑에 두는 방식이 기본이다. 서버 측에서는 최소 권한 원칙에 따라 핫월렛 사용을 제한하거나 서명 전담 서비스를 분리한다. EIP-3074, 세션키, AA도 함께 고려 대상이 된다.
RPC 계층은 퍼블릭과 프라이빗 엔드포인트를 이중화하고, 지역별 라우팅과 레이트 리밋 대응을 맡는다. 중요한 요청 경로에는 재시도, 백오프, 체인 리오그에 대비한 최소 N 컨펌 정책이 필요하다.
스마트 컨트랙트는 Solidity로 구현하며, Proxy 기반 업그레이더블 구성을 택했다면 스토리지 호환성을 관리해야 한다. 주요 상태 변화는 이벤트로 남기고, Role-based 또는 Ownable 접근 제어를 명확히 둔다.
Web3.js와 Ethers.js는 ABI 인코딩·디코딩, BigNumber 처리, EIP-1559 가스 매개변수 처리를 제공한다. Ethers.js는 모듈화, 정확성, 문서화 측면이 강점이고, Web3.js는 친숙한 API와 넓게 배포된 생태계가 강점이다.
트랜잭션이 전달되고 확인되는 경로
사용자 의도는 폼 입력이나 버튼 트리거로 시작한다. 지갑 연결 상태와 체인·계정을 확인한 뒤 수수료를 추정하고, 트랜잭션을 생성해 사용자 서명을 받는다. 이후 RPC로 전송하고 Receipt 폴링 또는 이벤트 구독을 통해 N 컨펌까지 상태를 관리한다.
화면에는 성공 또는 실패 결과와 갱신된 잔액·상태를 반영한다. 사용자 거절, 가스 부족, Nonce 경합처럼 실패 원인을 구분하고 재시도 방법을 제시해야 한다.
Web3.js와 Ethers.js를 고르는 기준
| 지표 | Web3.js | Ethers.js |
|---|---|---|
| 성능 | 표준적 호출 성능, 이벤트 처리시 메모리 사용량 주의 | 경량 모듈, 빠른 ABI 처리 및 정밀 BigInt 지원 |
| 확장성 | 기존 생태계 예제/튜토리얼 풍부 | 트리 셰이킹·모듈성으로 번들 최적화 유리 |
| 일관성 | v1→v4 간 API 변화, 마이그레이션 가이드 확인 필요 | v5→v6 대규모 변경, 타입 일관성·정확성 강화 |
| 안정성 | 오랜 사용 이력, 노드/프로바이더 호환성 광범위 | 엄격한 타입/에러 모델, 테스트 커버리지 우수 |
| 운영 편의 | web3.eth.* 친숙한 API, 빠른 온보딩 | Provider/Signer/Contract 분리로 테스트·주입 용이 |
최신 버전과 브라우저·번들러 호환성은 공식 문서에서 확인한다.
ERC-20 연결 코드
실행 환경은 Node.js 18+와 ethers, web3, dotenv 패키지를 전제로 한다. .env에는 Provider RPC 엔드포인트인 RPC_URL, 테스트넷 전용 개인키인 PRIVATE_KEY, 테스트넷 ERC-20 컨트랙트 주소인 ERC20_ADDRESS를 설정한다.
사용할 간단한 ERC-20 ABI는 다음과 같다.
balanceOf(address) view returns (uint256)transfer(address,uint256) returns (bool)event Transfer(address,address,uint256)
Ethers.js v6로 잔액 조회와 이벤트 구독
// package.json: { "type": "module" }
import "dotenv/config";
import { ethers } from "ethers";
const { RPC_URL, PRIVATE_KEY, ERC20_ADDRESS } = process.env;
if (!RPC_URL || !PRIVATE_KEY || !ERC20_ADDRESS) {
throw new Error("환경변수 설정 필요: RPC_URL, PRIVATE_KEY, ERC20_ADDRESS");
}
const ERC20_ABI = [
"function balanceOf(address) view returns (uint256)",
"function transfer(address to, uint256 amount) returns (bool)",
"event Transfer(address indexed from, address indexed to, uint256 value)",
];
const provider = new ethers.JsonRpcProvider(RPC_URL);
const wallet = new ethers.Wallet(PRIVATE_KEY, provider);
const token = new ethers.Contract(ERC20_ADDRESS, ERC20_ABI, wallet);
async function main() {
const me = await wallet.getAddress();
const bal = await token.balanceOf(me);
console.log("내 잔액:", bal.toString());
// 전송 예시: 받는 주소와 금액 설정 필요
// const tx = await token.transfer('0x받는주소', 1n); // 소수점 없는 최소 단위
// console.log('Tx hash:', tx.hash);
// const receipt = await tx.wait(1); // 1 컨펌 대기
// console.log('확인 블록:', receipt.blockNumber);
// 이벤트 구독
token.on("Transfer", (from, to, value, ev) => {
console.log("Transfer 이벤트:", {
from,
to,
value: value.toString(),
block: ev.blockNumber,
});
});
}
main().catch(console.error);
v6에서는 BigInt를 사용하므로 amount는 1n 형태로 지정한다. EIP-1559 가스 전략에서는 maxFeePerGas, maxPriorityFeePerGas를 지정할 수 있다.
Web3.js v4로 계약 호출하기
// package.json: { "type": "module" }
import "dotenv/config";
import Web3 from "web3";
const { RPC_URL, PRIVATE_KEY, ERC20_ADDRESS } = process.env;
if (!RPC_URL || !PRIVATE_KEY || !ERC20_ADDRESS) {
throw new Error("환경변수 설정 필요: RPC_URL, PRIVATE_KEY, ERC20_ADDRESS");
}
const web3 = new Web3(RPC_URL);
const account = web3.eth.accounts.wallet.add(PRIVATE_KEY);
const ERC20_ABI = [
{
constant: true,
inputs: [{ name: "owner", type: "address" }],
name: "balanceOf",
outputs: [{ name: "", type: "uint256" }],
type: "function",
},
{
constant: false,
inputs: [
{ name: "to", type: "address" },
{ name: "amount", type: "uint256" },
],
name: "transfer",
outputs: [{ name: "", type: "bool" }],
type: "function",
},
{
anonymous: false,
inputs: [
{ indexed: true, name: "from", type: "address" },
{ indexed: true, name: "to", type: "address" },
{ indexed: false, name: "value", type: "uint256" },
],
name: "Transfer",
type: "event",
},
];
const token = new web3.eth.Contract(ERC20_ABI, ERC20_ADDRESS);
async function main() {
const me = account.address;
const bal = await token.methods.balanceOf(me).call();
console.log("내 잔액:", bal.toString());
// 전송 예시
// const gas = await token.methods.transfer('0x받는주소', '1').estimateGas({ from: me });
// const tx = await token.methods.transfer('0x받는주소', '1').send({ from: me, gas });
// console.log('Tx hash:', tx.transactionHash);
// 이벤트 구독(웹소켓 엔드포인트 권장)
token.events
.Transfer({ fromBlock: "latest" })
.on("data", (ev) => console.log("Transfer 이벤트:", ev.returnValues))
.on("error", console.error);
}
main().catch(console.error);
실시간 이벤트 구독에는 WebSocket RPC를 권장한다. 가스 추정이 실패하면 여유분 가스 설정과 재시도 전략이 필요하다.
DApp에서 만나는 적용 패턴
DeFi 스왑 UI에서는 DEX Router 컨트랙트 라우팅, 다중 호출 멀티콜, 슬리피지·데드라인 파라미터 관리가 필요하다. 가격과 유동성 데이터는 인덱서 또는 서드파티 API 캐시에서 가져올 수 있다.
NFT 민팅과 세일 흐름에서는 메르클 트리 기반 화이트리스트 검증, 서명 기반 오프체인 권한증명, 온체인 검증을 결합한다. 민팅 큐, 레이트 리밋, 동시성 제어는 가스 워를 줄이는 데 사용된다.
DAO 거버넌스는 스냅샷 기반 투표와 오프체인 집계, 온체인 최종화로 구성할 수 있다. 멀티시그 금고인 Gnosis Safe 실행 자동화와 배치 트랜잭션도 연결 대상이다.
엔터프라이즈 토큰 게이팅에서는 Siwe 기반 지갑 소유 증명으로 프리미엄 컨텐츠 접근을 제어한다. 역할 토큰과 소울바운드 토큰은 권한관리 수단으로 활용할 수 있다.
운영 안정성을 위한 선택
사용자 키는 지갑에 위임하고, 서버의 서명은 최소화하거나 MPC·HSM을 사용한다. 원클릭 결제 UX와 하드웨어 지갑 의존에 따른 보안 사이에는 트레이드오프가 있다.
다중 RPC 엔드포인트, 헬스체크, 페일오버, 요청 샘플링 검증은 Provider 장애에 대비하는 수단이다. 비용은 증가하지만 가용성과 지연시간을 개선할 수 있다.
트랜잭션 신뢰성은 Nonce 매니저, replacement 시 maxPriorityFeePerGas 상향, idempotency 키로 다룬다. 리오그를 고려한 N 컨펌 정책과 이벤트 재처리·중복 방지도 함께 설계한다.
비용과 성능 측면에서는 Arbitrum, Optimism, zk 같은 L2와 EIP-4337 번들러를 통한 가스 대납을 고려할 수 있다. 브리지 복잡성이 늘어나는 대신 가스 절감과 지연 개선을 기대할 수 있다.
운영 지표로는 성공율, 펜딩 시간, 컨펌 분포, RPC 오류코드별 비중을 본다. 로그에는 tx hash, nonce, chainId, sessionId를 포함해 요청 간 상관관계를 추적한다.
RPC 이중화와 재시도를 적용하면 전송 성공률 99.9% 수준 달성이 가능하다. L2 전환 시 평균 가스 비용은 80% 이상 절감되고, TTF(finality)는 30~70% 개선될 수 있다. 명시적인 상태관리는 실패 원인을 드러내 사용자 신뢰도를 높이며, Ethers.js의 모듈형 구조는 테스트 용이성과 개발 속도 개선에 기여한다.