Web3는 단순한 기술 트렌드가 아니에요. 인터넷의 새로운 패러다임이자 탈중앙화된 미래를 향한 첫걸음이에요. 이 가이드에서는 DApp(탈중앙 애플리케이션)이 블록체인과 어떻게 소통하는지, 실제 배포 가능한 코드와 함께 알아볼게요.
전통적인 Web2 애플리케이션과 달리 DApp은 중앙 서버 대신 블록체인 네트워크와 직접 통신해요. 이 과정에서 Web3.js나 Ethers.js 같은 라이브러리가 브릿지 역할을 담당하죠.
개발 환경 세팅 (5분 완성)
먼저 필요한 도구들을 설치해볼게요. Node.js 18 이상 버전이 필요해요.
# 프로젝트 생성 및 의존성 설치
mkdir web3-dapp-tutorial
cd web3-dapp-tutorial
npm init -y
npm install hardhat @nomicfoundation/hardhat-toolbox
npm install ethers@6 dotenv
npx hardhat init
환경 변수 설정을 위한 .env
파일을 생성하세요:
PRIVATE_KEY=your_wallet_private_key
ALCHEMY_API_KEY=your_alchemy_api_key
ETHERSCAN_API_KEY=your_etherscan_api_key
경고: 절대로 실제 자금이 있는 지갑의 프라이빗 키를 사용하지 마세요. 테스트용 지갑을 새로 만들어 사용해요.
핵심 스마트 컨트랙트 구현
Web3의 핵심은 스마트 컨트랙트에요. 간단한 메시지 저장 컨트랙트를 만들어볼게요. 이 컨트랙트는 가스 최적화와 보안을 모두 고려한 설계예요.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract MessageBoard {
struct Message {
address author;
string content;
uint256 timestamp;
uint256 likes;
}
Message[] public messages;
mapping(uint256 => mapping(address => bool)) public hasLiked;
mapping(address => uint256) public userMessageCount;
event MessagePosted(uint256 indexed id, address indexed author, string content);
event MessageLiked(uint256 indexed id, address indexed liker);
modifier validMessageId(uint256 _id) {
require(_id < messages.length, "Invalid message ID");
_;
}
function postMessage(string calldata _content) external {
require(bytes(_content).length > 0, "Empty message");
require(bytes(_content).length <= 280, "Message too long");
messages.push(Message({
author: msg.sender,
content: _content,
timestamp: block.timestamp,
likes: 0
}));
userMessageCount[msg.sender]++;
emit MessagePosted(messages.length - 1, msg.sender, _content);
}
}
위 컨트랙트의 가스 최적화 포인트를 살펴볼게요:
calldata
사용으로 메모리 복사 비용 절감- 이벤트 인덱싱으로 효율적인 로그 검색
- 구조체 패킹으로 스토리지 슬롯 최적화
추가로 좋아요 기능을 구현해볼게요:
function likeMessage(uint256 _id) external validMessageId(_id) {
require(!hasLiked[_id][msg.sender], "Already liked");
messages[_id].likes++;
hasLiked[_id][msg.sender] = true;
emit MessageLiked(_id, msg.sender);
}
function getMessages(uint256 _from, uint256 _count)
external
view
returns (Message[] memory)
{
require(_from < messages.length, "Invalid start index");
uint256 end = _from + _count;
if (end > messages.length) {
end = messages.length;
}
uint256 length = end - _from;
Message[] memory result = new Message[](length);
for (uint256 i = 0; i < length; i++) {
result[i] = messages[_from + i];
}
return result;
}
프론트엔드와 블록체인 연결
이제 React를 사용해 DApp 프론트엔드를 만들어볼게요. Web3 연결의 핵심은 Provider와 Signer 개념을 이해하는 거예요.
// utils/web3Connection.js
import { ethers } from 'ethers';
class Web3Connection {
constructor() {
this.provider = null;
this.signer = null;
this.contract = null;
}
async connect() {
if (!window.ethereum) {
throw new Error("MetaMask를 설치해주세요");
}
try {
// 지갑 연결 요청
await window.ethereum.request({
method: 'eth_requestAccounts'
});
// Provider와 Signer 설정
this.provider = new ethers.BrowserProvider(window.ethereum);
this.signer = await this.provider.getSigner();
// 네트워크 확인
const network = await this.provider.getNetwork();
console.log("Connected to network:", network.chainId);
return await this.signer.getAddress();
} catch (error) {
console.error("Connection failed:", error);
throw error;
}
}
}
React 컴포넌트에서 블록체인과 상호작용하는 방법을 보여드릴게요:
// components/MessageBoard.jsx
import { useState, useEffect } from 'react';
import { ethers } from 'ethers';
import MessageBoardABI from '../artifacts/contracts/MessageBoard.sol/MessageBoard.json';
const CONTRACT_ADDRESS = "0x..."; // 배포된 컨트랙트 주소
function MessageBoard() {
const [messages, setMessages] = useState([]);
const [contract, setContract] = useState(null);
const [account, setAccount] = useState('');
useEffect(() => {
initializeContract();
}, []);
async function initializeContract() {
if (!window.ethereum) return;
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const address = await signer.getAddress();
setAccount(address);
const contractInstance = new ethers.Contract(
CONTRACT_ADDRESS,
MessageBoardABI.abi,
signer
);
setContract(contractInstance);
await loadMessages(contractInstance);
// 이벤트 리스너 설정
contractInstance.on("MessagePosted", handleNewMessage);
return () => {
contractInstance.removeAllListeners();
};
}
}
메시지 전송 기능 구현:
async function postMessage(content) {
if (!contract) return;
try {
const tx = await contract.postMessage(content);
console.log("Transaction sent:", tx.hash);
// 트랜잭션 대기
const receipt = await tx.wait();
console.log("Transaction confirmed:", receipt);
// 가스 사용량 표시
const gasUsed = receipt.gasUsed;
const gasPrice = receipt.gasPrice;
const cost = ethers.formatEther(gasUsed * gasPrice);
console.log(`Gas used: ${gasUsed}, Cost: ${cost} ETH`);
} catch (error) {
if (error.code === 'ACTION_REJECTED') {
console.log("User rejected transaction");
} else {
console.error("Transaction failed:", error);
}
}
}
테스트넷 배포 실습
Sepolia 테스트넷에 배포하는 과정을 설명할게요. Hardhat 설정 파일을 수정해요:
// hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();
module.exports = {
solidity: {
version: "0.8.19",
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
},
networks: {
sepolia: {
url: `https://eth-sepolia.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}`,
accounts: [process.env.PRIVATE_KEY]
}
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY
}
};
배포 스크립트 작성:
// scripts/deploy.js
const hre = require("hardhat");
async function main() {
console.log("Deploying MessageBoard...");
const MessageBoard = await hre.ethers.getContractFactory("MessageBoard");
const messageBoard = await MessageBoard.deploy();
await messageBoard.waitForDeployment();
const address = await messageBoard.getAddress();
console.log(`MessageBoard deployed to: ${address}`);
// Etherscan 검증
if (hre.network.name === "sepolia") {
console.log("Waiting for block confirmations...");
await messageBoard.deploymentTransaction().wait(6);
await hre.run("verify:verify", {
address: address,
constructorArguments: []
});
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
배포 명령어:
npx hardhat run scripts/deploy.js --network sepolia
보안 고려사항
DApp 개발 시 반드시 체크해야 할 보안 포인트들이에요:
1. Reentrancy 방지
contract SecureContract {
mapping(address => uint256) private balances;
mapping(address => bool) private locked;
modifier noReentrant() {
require(!locked[msg.sender], "Reentrant call");
locked[msg.sender] = true;
_;
locked[msg.sender] = false;
}
function withdraw() external noReentrant {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
2. 프론트엔드 보안
// 입력값 검증
function validateInput(input) {
// XSS 방지
const sanitized = input.replace(/[<>]/g, '');
// 길이 제한
if (sanitized.length > 280) {
throw new Error("Input too long");
}
return sanitized;
}
// 네트워크 체크
async function checkNetwork() {
const chainId = await window.ethereum.request({
method: 'eth_chainId'
});
if (chainId !== '0xaa36a7') { // Sepolia chainId
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: '0xaa36a7' }]
});
}
}
메인넷 배포 체크리스트
메인넷 배포 전 반드시 확인해야 할 사항들이에요:
- 스마트 컨트랙트 감사: 전문 감사 업체의 검토 필수
- 가스 최적화 검증: 각 함수의 가스 소비량 측정
- 권한 관리: Owner 권한과 멀티시그 지갑 설정
- 업그레이드 전략: 프록시 패턴 사용 여부 결정
- 모니터링 설정: 이벤트 로그와 알림 시스템 구축
흔한 에러와 해결법
1. "Nonce too high" 에러
MetaMask 설정에서 계정 리셋이 필요해요: 설정 > 고급 > 계정 초기화
2. "Insufficient funds" 에러
테스트넷 Faucet에서 테스트 ETH를 받아야 해요:
- Sepolia Faucet: https://sepoliafaucet.com
3. "Gas estimation failed" 에러
컨트랙트 함수가 revert되는 경우예요. require 조건을 확인하세요.
4. CORS 에러
로컬 개발 시 발생할 수 있어요. 프록시 설정이나 CORS 확장 프로그램을 사용하세요.
마무리
Web3 DApp 개발은 전통적인 웹 개발과는 다른 패러다임을 요구해요. 블록체인의 불변성, 가스비, 트랜잭션 시간 등을 고려한 설계가 필요하죠. 이 가이드의 코드를 기반으로 자신만의 DApp을 만들어보세요.
중요 경고:
- 메인넷 배포 전 반드시 전문가의 감사를 받으세요
- 테스트넷에서 충분히 테스트하세요
- 실제 자금 사용 시 모든 책임은 본인에게 있어요