DApp 프런트엔드 실무: Web3.js·Ethers.js 선택부터 IPFS 스토리지 연동까지
DApp 아키텍처를 계층별로 나누고, Web3.js와 Ethers.js의 실질적 차이, IPFS 핀닝·지갑 서명·트랜잭션 오류 처리를 코드와 함께 정리한다.
2026-08-12 · 최초 발행 2025-12-11
지갑 연결 버튼 하나 붙이는 일이 간단해 보여도, DApp 프런트엔드는 일반 웹 개발과 다른 실패 지점을 여럿 갖고 있다. RPC 노드가 응답하지 않을 때, 사용자가 서명을 거부할 때, 가스가 부족해 트랜잭션이 되돌아갈 때 — 이런 경로마다 별도의 처리가 필요하다. 여기서는 DApp을 이루는 계층 구조와 라이브러리 선택, IPFS 연동, 그리고 실제로 부딪히는 오류 처리를 코드 중심으로 정리한다.
DApp은 계층으로 쪼개서 봐야 한다
DApp은 프런트엔드, 온체인, 오프체인 세 계층의 조합이다. 프런트엔드는 React·Vue 같은 SPA로 구현되며 지갑이 주입하는 provider와 SDK를 통해 컨트랙트를 호출한다. 상태 관리와 사용자 피드백의 일관성을 이 계층에서 확보해야 한다. 온체인 계층은 Solidity 같은 언어로 작성된 스마트 컨트랙트가 비즈니스 규칙과 자산 상태를 정의하고, 이벤트 로그를 통해 비동기 후처리를 트리거한다. 오프체인 계층은 IPFS와 핀닝 서비스, The Graph 같은 인덱서, 그리고 백엔드 캐시·큐 시스템으로 구성되어 성능과 확장성을 온체인 부담에서 분리한다.
계층을 분리하는 이유는 단순하다. 온체인에 모든 데이터를 올리면 가스 비용이 감당 안 되고, 프런트엔드에 모든 상태를 몰아넣으면 신뢰성이 지갑 세션에 종속된다. 세 계층이 각자의 책임을 지키면 장애 지점도 계층별로 국소화된다.
Web3.js와 Ethers.js, 무엇이 다른가
두 라이브러리 모두 JSON-RPC 바인딩, ABI 인코딩/디코딩, 지갑 서명과 트랜잭션 전송 기능을 제공하고 브라우저·Node 환경을 모두 지원한다는 공통점이 있다. 차이는 설계 철학에서 온다. Ethers.js는 모듈성과 정적 타입 친화적 설계, Provider와 Signer를 분리한 구조가 강점이다. Web3.js는 배터리 포함형 API와 오랜 웹 호환성 이력이 강점이다.
| 항목 | 성능 | 확장성 | 일관성 | 안정성 | 운영 편의 |
|---|---|---|---|---|---|
| Web3.js | 대형 번들, 기능 일체형 구성 | 플러그인 생태 다수, 레거시 호환 강점 | 콜백/프로미스 혼재 구간 주의 | 장기간 사용 이력, 사례 풍부 | 튜토리얼·예제 풍부, 진입 장벽 낮음 |
| Ethers.js | 경량 번들, 빠른 로드 타임 | 모듈화·멀티프로바이더 구성 용이 | Provider/Signer 분리로 API 일관 | 타입 친화적, 에러 체계 명료 | 테스트·시뮬레이션 도구와 궁합 우수 |
경량성과 타입 안정성이 우선이면 Ethers.js를, 레거시 예제나 기존 코드와의 호환을 우선하면 Web3.js를 유지하거나 점진적으로 전환하는 쪽이 현실적이다.
지갑·프로바이더·네트워크 구성
지갑은 비밀키 관리와 서명을 수행하며 MetaMask·WalletConnect 같은 표준 인터페이스를 쓴다. 권한 요청 흐름은 최소화해서 설계해야 한다. 프로바이더는 RPC 엔드포인트 연결자로, 퍼블릭 RPC를 쓸지 Infura·Alchemy 같은 전용 노드를 쓸지는 가용성·요금·속도의 트레이드오프 문제다. 네트워크는 메인넷과 테스트넷(Sepolia, Polygon Amoy 등)을 분리 운영하고, 체인 ID와 EIP-1559 가스 전략을 명시적으로 관리해야 한다.
IPFS와 핀닝: 저장은 됐는데 사라질 수 있다
IPFS는 CID(Content Identifier)로 파일의 무결성을 검증하고, 게이트웨이 접근이나 자체 노드 운영을 통해 대용량·정적 자산을 저장하기에 적합하다. 다만 IPFS 네트워크에 파일을 올리는 것과 그 파일이 계속 접근 가능한 상태로 남는 것은 별개 문제다. 지속 가용성을 확보하려면 별도의 핀닝이 필요하며, Infura·Pinata 같은 핀닝 서비스를 활용할 때는 SLA·지역 복제·요금을 함께 검토해야 한다.
보안·키 관리·트랜잭션 안정성
키 관리의 원칙은 명확하다. 브라우저 지갑에 위임하고 서버측 서명은 금지한다. 도메인 바인딩(EIP-4361, SIWE)으로 피싱을 저감할 수 있다. 트랜잭션 측면에서는 가스 추정에 여유분을 더한 최대 수수료 상한 설정, nonce 동시성 제어와 재전송 전략이 필요하고, revert나 out-of-gas 같은 실패 케이스에 대한 사용자 피드백을 규격화해야 한다.
실무 시나리오
NFT 민팅 DApp은 파일 업로드 → IPFS CID 획득 → 메타데이터 JSON 생성·업로드 → CID를 포함한 컨트랙트 민팅 호출 순서로 진행된다. 운영 시에는 핀닝 복제도, 게이트웨이 캐시, 메타데이터 스키마 버저닝을 관리해야 한다.
DeFi 스테이킹 프런트엔드는 컨트랙트 ABI 로드 → Allowance 확인·Approve → Stake 트랜잭션 전송 → 이벤트 기반 UI 업데이트 순서를 따른다. 가격 피드 오라클의 지연이나 체인 재구성(Reorg) 대응, 프런트 캐시 만료 정책이 운영 포인트다.
공급망 데이터 앵커링은 원장 외부 데이터의 해시를 생성해 IPFS에 저장하고, CID와 해시를 온체인에 기록한 뒤 검증 시점에 온·오프체인 값을 비교하는 구조다. 개인정보 비식별 처리와 규제 준수, 데이터 보존 전략을 함께 설계해야 한다.
표준 처리 흐름
사용자의 지갑 서명 요청이나 파일 업로드 요청이 입력이고, IPFS 업로드·CID 반환과 컨트랙트 함수 호출·가스 추정·트랜잭션 브로드캐스트가 처리 단계다. 출력은 트랜잭션 영수증·이벤트와 CID·게이트웨이 URL이며, 권한 거부·가스 부족·revert 원인은 표준화된 에러로 처리해야 한다.
코드로 보는 최소 구성
전제조건은 Node.js 18+와 브라우저 지갑(MetaMask), 그리고 ethers@6·web3@4·ipfs-http-client@59.x(최신 정보 확인 필요) 패키지다. 테스트넷은 Sepolia RPC를 사용한다고 가정한다.
파일 CID를 저장하는 최소한의 Solidity 컨트랙트부터 본다. 환경은 Solidity 0.8.20, Hardhat 2.21+다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract CidVault {
event AssetSaved(address indexed owner, string cid);
mapping(address => string[]) private _assets;
function save(string calldata cid) external {
require(bytes(cid).length > 0, "empty cid");
_assets[msg.sender].push(cid);
emit AssetSaved(msg.sender, cid);
}
function count(address owner) external view returns (uint256) {
return _assets[owner].length;
}
function get(address owner, uint256 index) external view returns (string memory) {
return _assets[owner][index];
}
}
배포는 Hardhat 기준으로 npx hardhat init으로 프로젝트를 만들고, contracts/CidVault.sol을 저장한 뒤 npx hardhat compile을 실행하고, 스크립트에서 ethers.getContractFactory("CidVault").deploy()를 호출하면 된다.
브라우저에서 Ethers.js로 이 컨트랙트를 호출하는 코드는 다음과 같다. 환경은 Vite·Next.js 같은 브라우저 번들, ethers@6이다.
<script type="module">
import { ethers } from "https://cdn.jsdelivr.net/npm/ethers@6.11.1/dist/ethers.min.js";
const abi = [
"function save(string cid) external",
"function count(address owner) view returns (uint256)"
];
const contractAddress = "0xYourDeployedAddress";
async function connect() {
if (!window.ethereum) throw new Error("No wallet");
const provider = new ethers.BrowserProvider(window.ethereum);
await provider.send("eth_requestAccounts", []);
return await provider.getSigner();
}
async function saveCid(cid) {
const signer = await connect();
const c = new ethers.Contract(contractAddress, abi, signer);
const gas = await c.save.estimateGas(cid);
const tx = await c.save(cid, { gasLimit: gas + 20000n });
const receipt = await tx.wait();
console.log("Saved, block:", receipt.blockNumber);
}
window.saveCid = saveCid;
</script>
같은 동작을 Web3.js(web3@4)로 구현하면 이렇다.
<script src="https://cdn.jsdelivr.net/npm/web3@4.5.0/dist/web3.min.js"></script>
<script>
const abi = [
{ "name":"save", "type":"function", "stateMutability":"nonpayable",
"inputs":[{"name":"cid","type":"string"}], "outputs":[] }
];
const contractAddress = "0xYourDeployedAddress";
async function saveCidWeb3(cid) {
if (!window.ethereum) throw new Error("No wallet");
const web3 = new Web3(window.ethereum);
const [from] = await web3.eth.requestAccounts();
const contract = new web3.eth.Contract(abi, contractAddress);
const gas = await contract.methods.save(cid).estimateGas({ from });
const receipt = await contract.methods.save(cid).send({ from, gas });
console.log("Saved, tx:", receipt.transactionHash);
}
window.saveCidWeb3 = saveCidWeb3;
</script>
파일을 IPFS에 올리는 부분은 ipfs-http-client로 처리한다. 브라우저·Node 공통이며 인증 토큰은 서비스별로 발급받는다.
// Node 또는 브라우저 번들 환경
import { create } from "ipfs-http-client";
// 예: Infura 엔드포인트 사용
const client = create({
url: "https://ipfs.infura.io:5001/api/v0",
headers: { Authorization: "Basic " + btoa("PROJECT_ID:PROJECT_SECRET") } // 필요 시
});
export async function uploadFile(fileOrBlob) {
const { cid } = await client.add(fileOrBlob, { pin: true });
// 게이트웨이 URL은 가용성 보장을 위해 다중 구성 권장
const url = `https://ipfs.io/ipfs/${cid.toString()}`;
return { cid: cid.toString(), url };
}
에러 처리는 세 갈래로 정리해두면 된다. 사용자가 서명을 거부하면 code 4001을 받는데, UI에서 재시도와 가이드를 제공한다. 가스가 부족할 수 있으니 추정치에 여유분을 더하고 EIP-1559 maxFeePerGas 상한을 설정한다. 네트워크 자체가 불가용할 때는 RPC 멀티엔드포인트 라우팅과 지수 백오프 재시도로 대응한다.
기대할 수 있는 개선폭
경량 SDK와 지연 로딩을 적용하면 초기 로드 타임을 20~40% 절감할 수 있다고 추정된다. 멀티엔드포인트와 재시도 정책을 적용하면 RPC 호출 실패율을 30% 이상 줄일 수 있고, 대용량 자산을 IPFS로 이전하고 핀닝 전략을 최적화하면 스토리지 비용을 50% 이상 절감할 수 있다. 정성적으로는 데이터 무결성과 검증 가능성이 높아지고, 특정 공급자에 대한 종속이 완화되며 멀티체인 확장의 유연성이 생긴다. 개발과 운영의 경계를 분리하면 릴리스 리스크도 줄어든다.
DApp 아키텍처의 계층화 설계와 Web3.js·Ethers.js의 적절한 선택은 안정적인 서비스 운영의 전제 조건이다. IPFS 기반 스토리지는 무결성과 비용 면에서 우수한 대안이지만, 핀닝과 게이트웨이 다중화 없이는 실사용 가용성을 보장하지 못한다는 점을 놓치지 말아야 한다.