BlockchainDevGuide0004
개발 환경과 배포 완전 정복 A-Z
Hardhat · Foundry · 테스트넷 배포 · IPFS · CI/CD — 현업 블록체인 개발자의 필수 워크플로우
🗺️ BlockchainDevGuide 시리즈 진행 현황
블록체인 개념 이해 ✅ Guide0002
JS 프로그래밍 기초 ✅ Guide0003
이더리움·스마트 컨트랙트 🔥 Guide0004
개발환경·배포 ⏳ Guide0005
NFT·DeFi 심화 ⏳ Guide0006
보안·감사 ⏳ Guide0007
실전 프로젝트
📋 학습 목차 (10 Chapters)
| 챕터 | 주제 | 핵심 기술 |
|---|---|---|
| 01 | 개발 환경 완전 세팅 | Node.js, nvm, VSCode 확장, Git 설정 |
| 02 | Hardhat 프로젝트 완전 정복 | 컴파일·테스트·디버깅·Gas 리포트 |
| 03 | Foundry — 차세대 개발 프레임워크 | forge·cast·anvil·chisel, Fuzz 테스트 |
| 04 | 테스트넷 완전 가이드 | Sepolia·Mumbai·Faucet·RPC·환경변수 |
| 05 | Hardhat Ignition 배포 시스템 | 모듈·배포 계획·재현 가능 배포 |
| 06 | Etherscan & 컨트랙트 검증 | 소스 공개·ABI·읽기·쓰기 인터페이스 |
| 07 | IPFS & 분산 스토리지 | Pinata·NFT.storage·Filecoin·메타데이터 |
| 08 | 업그레이더블 컨트랙트 배포 | Proxy 패턴·UUPS·Transparent·Beacon |
| 09 | CI/CD 파이프라인 구축 | GitHub Actions·자동 테스트·자동 배포 |
| 10 | 현업 면접 Q&A TOP 15 | 배포·환경·보안 실전 면접 완전 대비 |
🖥️ Chapter 01. 개발 환경 완전 세팅
블록체인 개발을 시작하기 전 — 완벽한 환경 구성 A-Z
🛠️ 1.1 필수 도구 설치 순서
| 순서 | 도구 | 버전 | 용도 | 설치 확인 |
|---|---|---|---|---|
| 1 | nvm | 최신 | Node.js 버전 관리자 | nvm --version |
| 2 | Node.js | 18.x LTS | JS 런타임 | node --version |
| 3 | Git | 최신 | 소스 버전 관리 | git --version |
| 4 | VSCode | 최신 | 코드 에디터 | 앱 실행 확인 |
| 5 | Foundry | 최신 | Solidity 개발 툴킷 | forge --version |
| 6 | MetaMask | 최신 | 브라우저 지갑 | 크롬 확장 설치 |
| 7 | Hardhat | 최신 | 이더리움 개발 환경 | npx hardhat --version |
# ===== macOS / Linux 설치 스크립트 =====
# 1. nvm 설치
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc # 또는 source ~/.zshrc
# 2. Node.js 18 LTS 설치
nvm install 18
nvm use 18
nvm alias default 18
node --version # v18.x.x 확인
# 3. Foundry 설치 (forge, cast, anvil, chisel)
curl -L https://foundry.paradigm.xyz | bash
foundryup
forge --version # forge 0.2.x 확인
# 4. Hardhat 글로벌 설치 (선택)
npm install -g hardhat
# ===== Windows (PowerShell) =====
# nvm-windows 다운로드: https://github.com/coreybutler/nvm-windows
nvm install 18.17.0
nvm use 18.17.0
🔧 1.2 VSCode 필수 확장 프로그램
// .vscode/settings.json — 권장 설정
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[solidity]": {
"editor.defaultFormatter": "NomicFoundation.hardhat-solidity"
},
"solidity.compileUsingRemoteVersion": "0.8.20",
"solidity.formatter": "prettier",
"files.exclude": {
"**/node_modules": true,
"**/.git": true
}
}
🔐 1.3 환경변수(.env) 보안 관리
개인키와 API 키는 절대 코드에 직접 작성하면 안 됩니다. .env 파일로 분리하고 .gitignore에 추가하세요.
# .env 파일 (절대 깃허브에 올리지 마세요!)
PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY
MAINNET_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY
ETHERSCAN_API_KEY=YOUR_ETHERSCAN_API_KEY
POLYGONSCAN_API_KEY=YOUR_POLYGONSCAN_API_KEY
COINMARKETCAP_API_KEY=YOUR_CMC_API_KEY # Gas Reporter USD 환산
# .gitignore — 반드시 추가
.env
.env.local
node_modules/
artifacts/
cache/
typechain-types/
coverage/
⛔ 절대 하지 말아야 할 것
• .env를 .gitignore에 추가 안 함 → 수백만 원 피해 사례 다수
• 테스트넷 키와 메인넷 키 동일 사용 → 실수로 메인넷 배포 위험
• API 키 공유 → 요금 폭탄 및 레이트 리밋
⚒️ Chapter 02. Hardhat 프로젝트 완전 정복
컴파일 · 테스트 · 디버깅 · Gas 리포트 — 실무 Hardhat 워크플로우
📁 2.1 Hardhat 프로젝트 구조 완전 이해
# Hardhat 프로젝트 초기화
mkdir blockchain-project && cd blockchain-project
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox dotenv
npm install @openzeppelin/contracts
npx hardhat init
# → "Create a JavaScript project" 선택
# 최종 프로젝트 구조
blockchain-project/
├── contracts/ # Solidity 소스
│ ├── Token.sol
│ ├── NFT.sol
│ └── interfaces/
│ └── IToken.sol
├── scripts/ # 배포/상호작용 스크립트
│ ├── deploy.js
│ └── interact.js
├── test/ # 테스트 파일
│ ├── Token.test.js
│ └── NFT.test.js
├── ignition/ # Hardhat Ignition 모듈
│ └── modules/
│ └── Token.js
├── hardhat.config.js # 핵심 설정 파일
├── .env # 환경변수 (gitignore)
├── .gitignore
└── package.json
⚙️ 2.2 hardhat.config.js 완전 가이드
require("@nomicfoundation/hardhat-toolbox");
require("@openzeppelin/hardhat-upgrades"); // 업그레이더블
require("hardhat-gas-reporter"); // Gas 리포트
require("solidity-coverage"); // 코드 커버리지
require("dotenv").config();
const { PRIVATE_KEY, SEPOLIA_RPC_URL, MAINNET_RPC_URL,
ETHERSCAN_API_KEY, COINMARKETCAP_API_KEY } = process.env;
module.exports = {
solidity: {
compilers: [
{
version: "0.8.20",
settings: {
optimizer: { enabled: true, runs: 200 },
viaIR: true // 대형 컨트랙트 컴파일 최적화
}
}
]
},
networks: {
hardhat: {
chainId: 31337,
forking: { // 메인넷 포크 (선택)
url: MAINNET_RPC_URL,
blockNumber: 19000000
}
},
localhost: { url: "http://127.0.0.1:8545" },
sepolia: {
url: SEPOLIA_RPC_URL || "",
accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [],
chainId: 11155111,
gasPrice: "auto"
},
polygon: {
url: "https://polygon-rpc.com",
accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [],
chainId: 137
},
arbitrum: {
url: "https://arb1.arbitrum.io/rpc",
accounts: PRIVATE_KEY ? [PRIVATE_KEY] : [],
chainId: 42161
}
},
etherscan: {
apiKey: {
mainnet: ETHERSCAN_API_KEY,
sepolia: ETHERSCAN_API_KEY,
polygon: process.env.POLYGONSCAN_API_KEY,
arbitrumOne: process.env.ARBISCAN_API_KEY
}
},
gasReporter: {
enabled: process.env.REPORT_GAS === "true",
currency: "USD",
coinmarketcap: COINMARKETCAP_API_KEY,
outputFile: "gas-report.txt",
noColors: true
},
paths: {
sources: "./contracts",
tests: "./test",
cache: "./cache",
artifacts: "./artifacts"
},
mocha: { timeout: 60000 } // 느린 테스트 타임아웃
};
🧪 2.3 Hardhat 테스트 심화 — Fixtures & Snapshot
// test/Token.test.js — 실전 테스트 패턴
const { expect } = require("chai");
const { ethers, network } = require("hardhat");
const { loadFixture, time } = require("@nomicfoundation/hardhat-network-helpers");
// Fixture: 재사용 가능한 초기 상태 스냅샷
async function deployTokenFixture() {
const [owner, alice, bob, carol] = await ethers.getSigners();
const Token = await ethers.getContractFactory("BlockToken");
const token = await Token.deploy(owner.address);
await token.waitForDeployment();
// 초기 세팅
await token.transfer(alice.address, ethers.parseEther("1000"));
await token.transfer(bob.address, ethers.parseEther("500"));
return { token, owner, alice, bob, carol };
}
describe("BlockToken — 완전한 테스트 스위트", function() {
describe("Deployment", function() {
it("초기 공급량 확인", async function() {
const { token, owner } = await loadFixture(deployTokenFixture);
const total = await token.totalSupply();
expect(total).to.equal(ethers.parseEther("10000000"));
});
});
describe("Transfers", function() {
it("정상 전송", async function() {
const { token, alice, bob } = await loadFixture(deployTokenFixture);
await token.connect(alice).transfer(bob.address, ethers.parseEther("100"));
expect(await token.balanceOf(bob.address)).to.equal(ethers.parseEther("600"));
});
it("잔액 부족 revert", async function() {
const { token, carol } = await loadFixture(deployTokenFixture);
await expect(
token.connect(carol).transfer(carol.address, 1)
).to.be.reverted;
});
});
describe("Time Travel — 시간 조작 테스트", function() {
it("1년 후 스테이킹 보상 확인", async function() {
const { token, alice } = await loadFixture(deployTokenFixture);
// 1년 뒤로 시간 이동
await time.increase(365 * 24 * 60 * 60);
// 보상 계산 로직 테스트...
});
});
describe("Impersonation — 임의 주소 사칭 테스트", function() {
it("고래 지갑 행동 시뮬레이션", async function() {
const whaleAddress = "0x28C6c06298d514Db089934071355E5743bf21d60";
await network.provider.request({
method: "hardhat_impersonateAccount",
params: [whaleAddress]
});
const whale = await ethers.getSigner(whaleAddress);
// whale 계정으로 트랜잭션 실행...
});
});
});
📊 2.4 Gas 리포트 분석
Gas Reporter 출력 예시
REPORT_GAS=true npx hardhat test
·-------------------------------------|----------------------------|-------------|-----------------------------·
| Solc version: 0.8.20 · Optimizer enabled: true · Runs: 200 · Block limit: 30000000 gas │
······································|····························|·············|······························
| Methods │
·············|·······················|············|············|·············|·············|···················
| Contract · Method · Min · Max · Avg · # calls · usd (avg) │
·············|·······················|············|············|·············|·············|···················
| BlockToken· approve · 46,213 · 46,225 · 46,221 · 8 · $0.14 │
| BlockToken· transfer · 51,452 · 68,652 · 60,052 · 15 · $0.18 │
| BlockToken· mint · 68,924 · 68,924 · 68,924 · 3 · $0.21 │
·············|·······················|············|············|·············|·············|···················
| Deployments · · % of limit · │
······································|············|············|·············|·············|···················
| BlockToken · - · - · 1,234,567· 4.1 % · $3.71 │
·-------------------------------------|------------|------------|-------------|-------------|-------------------·
🔨 Chapter 03. Foundry — 차세대 개발 프레임워크
Rust로 만든 초고속 도구 — forge · cast · anvil · chisel
📚 3.1 Foundry vs Hardhat 비교
| 항목 | Hardhat | Foundry |
|---|---|---|
| 언어 | JavaScript | Solidity (테스트도!) |
| 속도 | 보통 | ⚡ 10~100배 빠름 |
| Fuzz 테스트 | 별도 도구 필요 | ✅ 내장 |
| Invariant 테스트 | 없음 | ✅ 내장 |
| 생태계 | 더 넓음 (플러그인) | 성장 중 |
| 추천 사용 | DApp 개발, 스크립트 | 보안 감사, 프로토콜 |
# ===== Foundry 프로젝트 세팅 =====
forge init my-foundry-project
cd my-foundry-project
# OpenZeppelin 라이브러리 추가 (forge install)
forge install OpenZeppelin/openzeppelin-contracts
forge install foundry-rs/forge-std
# foundry.toml 설정
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc = "0.8.20"
optimizer = true
optimizer-runs = 200
fuzz.runs = 1000 # Fuzz 테스트 횟수
invariant.runs = 256
# 빌드 / 테스트 / 가스 스냅샷
forge build
forge test -vvvv # -v 많을수록 상세 로그
forge snapshot # gas snapshot 저장
forge coverage # 커버리지 리포트
💻 3.2 Foundry 테스트 작성 (Solidity로!)
// test/Token.t.sol — Foundry 테스트는 Solidity로 작성!
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/BlockToken.sol";
contract BlockTokenTest is Test {
BlockToken public token;
address public owner = address(1);
address public alice = address(2);
address public bob = address(3);
function setUp() public {
vm.startPrank(owner); // owner로 tx 발생
token = new BlockToken(owner);
token.transfer(alice, 1000 ether);
vm.stopPrank();
}
// === 기본 테스트 ===
function test_Transfer() public {
vm.prank(alice);
token.transfer(bob, 100 ether);
assertEq(token.balanceOf(bob), 100 ether);
}
function test_RevertWhen_InsufficientBalance() public {
vm.prank(bob);
vm.expectRevert(); // revert 예상
token.transfer(alice, 1);
}
// === Fuzz 테스트 — 자동으로 랜덤 입력값 생성! ===
function testFuzz_Transfer(uint256 amount) public {
amount = bound(amount, 1, 1000 ether); // 범위 제한
vm.prank(alice);
token.transfer(bob, amount);
assertEq(token.balanceOf(bob), amount);
assertEq(token.balanceOf(alice), 1000 ether - amount);
}
// === Invariant 테스트 — 불변식 검증 ===
function invariant_TotalSupplyConstant() public view {
// 어떤 트랜잭션 후에도 totalSupply는 변하지 않아야 함
assertEq(token.totalSupply(), 10_000_000 ether);
}
// === 시간 조작 ===
function test_TimeWarp() public {
uint256 future = block.timestamp + 365 days;
vm.warp(future); // 블록 타임스탬프 변경
vm.roll(block.number + 1000); // 블록 번호 변경
}
// === ETH 잔액 설정 ===
function test_EthBalance() public {
vm.deal(alice, 10 ether); // alice에게 ETH 10 부여
assertEq(alice.balance, 10 ether);
}
}
🔧 3.3 cast & anvil — 터미널 블록체인 도구
# ===== anvil: 로컬 EVM 노드 (Hardhat Node 대체) =====
anvil # 기본 실행 (20개 테스트 계정)
anvil --fork-url $MAINNET_RPC_URL --fork-block-number 19000000
anvil --chain-id 1337 --block-time 2 # 2초마다 블록 생성
# ===== cast: 터미널에서 RPC 호출 =====
# 잔액 조회
cast balance 0x742d35Cc6634C0532925a3b8D4C9b5c8b4a6F9F4 --rpc-url $SEPOLIA_RPC_URL
# 컨트랙트 함수 호출 (읽기)
cast call 0xContractAddress "balanceOf(address)(uint256)" 0xAddress --rpc-url $SEPOLIA_RPC_URL
# 컨트랙트 함수 호출 (쓰기 — 트랜잭션)
cast send 0xContractAddress "transfer(address,uint256)" 0xToAddress 1000000000000000000 --private-key $PRIVATE_KEY --rpc-url $SEPOLIA_RPC_URL
# ABI 인코딩/디코딩
cast abi-encode "transfer(address,uint256)" 0xAddress 1000
cast decode-calldata "transfer(address,uint256)" 0xa9059cbb...
# 트랜잭션 정보 조회
cast tx 0xTxHash --rpc-url $MAINNET_RPC_URL
# ETH 단위 변환
cast to-unit 1000000000000000000 ether # → 1.000000000000000000
cast from-wei 1000000000 gwei # → 1.000000000
# 블록 정보
cast block latest --rpc-url $MAINNET_RPC_URL
# ===== chisel: Solidity REPL (실시간 Solidity 실행) =====
chisel
# > uint256 x = 100 * 1e18;
# > keccak256(abi.encodePacked("hello"))
🌐 Chapter 04. 테스트넷 완전 가이드
Sepolia · Mumbai · Faucet · RPC 엔드포인트 · 환경 설정
📡 4.1 주요 테스트넷 현황
| 네트워크 | Chain ID | 통화 | Faucet | 추천도 |
|---|---|---|---|---|
| Sepolia | 11155111 | ETH | sepoliafaucet.com | ⭐⭐⭐ 이더리움 메인 |
| Mumbai | 80001 | MATIC | faucet.polygon.technology | ⭐⭐ Polygon 개발용 |
| Arbitrum Sepolia | 421614 | ETH | faucet.quicknode.com | ⭐⭐ L2 개발용 |
| Base Sepolia | 84532 | ETH | base-faucet.vercel.app | ⭐⭐ Base L2 |
| Hardhat Local | 31337 | ETH | 내장 계정 자동 지급 | ⭐⭐⭐ 로컬 개발 |
🔌 4.2 RPC 엔드포인트 제공업체 비교
• Enhanced API (NFT, 트랜잭션 히스토리)
• WebSocket 지원
• 최고의 신뢰성
• 가장 오래된 서비스
• IPFS 게이트웨이
• MetaMask 기반
• 멀티체인 지원
• 가장 빠른 응답
• 스트림 기능
• 50+ 체인 지원
• 레이트 리밋 있음
• 테스트용 적합
# ===== MetaMask에 테스트넷 추가 방법 =====
# Sepolia 설정 (가장 많이 사용)
Network Name: Sepolia Testnet
RPC URL: https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY
Chain ID: 11155111
Symbol: ETH
Block Explorer: https://sepolia.etherscan.io
# Hardhat 로컬 네트워크 설정 (개발용)
Network Name: Hardhat Local
RPC URL: http://127.0.0.1:8545
Chain ID: 31337
Symbol: ETH
Block Explorer: (없음)
# ===== Faucet으로 테스트 ETH 받기 =====
# 1. Alchemy Sepolia Faucet: https://sepoliafaucet.com
# 2. Chainlink Faucet: https://faucets.chain.link/sepolia
# 3. QuickNode Faucet: https://faucet.quicknode.com/ethereum/sepolia
# → 하루 0.1~0.5 SEP ETH 지급 (소셜 인증 필요할 수 있음)
🚀 Chapter 05. Hardhat Ignition 배포 시스템
재현 가능한 선언적 배포 — 현업 표준 배포 방식
📚 5.1 Ignition이란?
Hardhat Ignition은 기존 scripts/deploy.js 방식을 대체하는 선언적 배포 시스템입니다. 배포 상태를 추적하고, 실패 시 중단 지점부터 재시도하며, 완전히 재현 가능합니다.
// ignition/modules/Token.js — Ignition 배포 모듈
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules");
const TokenModule = buildModule("TokenModule", (m) => {
// 배포 파라미터 (--parameters로 주입 가능)
const initialOwner = m.getParameter("initialOwner");
// 컨트랙트 배포
const token = m.contract("BlockToken", [initialOwner]);
// 배포 후 초기화 (선택)
m.call(token, "mint", [initialOwner, ethers.parseEther("1000000")]);
return { token };
});
module.exports = TokenModule;
# ===== Ignition 배포 명령어 =====
# 로컬 배포
npx hardhat ignition deploy ignition/modules/Token.js --network localhost
# Sepolia 배포 (파라미터 지정)
npx hardhat ignition deploy ignition/modules/Token.js --network sepolia --parameters '{"TokenModule":{"initialOwner":"0xYourAddress"}}'
# 배포 상태 확인
npx hardhat ignition status ignition/deployments/chain-11155111
# 검증 포함 배포
npx hardhat ignition deploy ignition/modules/Token.js --network sepolia --verify
# 배포 결과 (자동 생성)
# ignition/deployments/chain-11155111/deployed_addresses.json
{
"TokenModule#BlockToken": "0xDeployedContractAddress..."
}
🔗 5.2 멀티 컨트랙트 의존성 배포
// ignition/modules/DeFiProtocol.js
const DeFiModule = buildModule("DeFiModule", (m) => {
const owner = m.getParameter("owner");
// 1. TokenA 배포
const tokenA = m.contract("BlockToken", [owner], { id: "TokenA" });
// 2. TokenB 배포
const tokenB = m.contract("BlockToken", [owner], { id: "TokenB" });
// 3. AMM 풀 배포 (TokenA, TokenB 주소 주입)
const amm = m.contract("SimpleAMM", [tokenA, tokenB]);
// 4. 초기 유동성 공급 (AMM 배포 후 실행)
const approveA = m.call(tokenA, "approve", [amm, ethers.parseEther("100000")], {
from: owner,
after: [amm]
});
m.call(amm, "addLiquidity", [
ethers.parseEther("10000"),
ethers.parseEther("10000")
], { after: [approveA] });
return { tokenA, tokenB, amm };
});
🔍 Chapter 06. Etherscan & 컨트랙트 검증
소스코드 공개 · ABI 인터페이스 · 읽기/쓰기 UI
📚 6.1 Etherscan 검증이 중요한 이유
배포된 컨트랙트의 소스코드를 Etherscan에 공개하면: ①사용자 신뢰도 향상 ②Read/Write 탭으로 직접 상호작용 ③버그 신고 및 감사 편의성 ④보안 커뮤니티 검토. 미검증 컨트랙트는 "불투명한 블랙박스"로 인식됩니다.
# ===== 방법 1: Hardhat verify 플러그인 (권장) =====
# 배포 후 자동 검증
npx hardhat verify --network sepolia 0xDeployedAddress "0xConstructorArg1" "arg2"
# 생성자 인자가 복잡한 경우 (배열, 구조체)
# scripts/verify-args.js 파일 생성
module.exports = [
"BlockToken",
"BTKN",
ethers.parseEther("10000000")
];
npx hardhat verify --network sepolia 0xAddress --constructor-args scripts/verify-args.js
# ===== 방법 2: Etherscan 웹사이트 직접 업로드 =====
# 1. etherscan.io/address/0xAddress → Contract 탭
# 2. "Verify and Publish" 클릭
# 3. Compiler Type: Solidity (Single File / Multi-Part)
# 4. Compiler Version: 0.8.20
# 5. Optimization: Yes, 200 runs
# 6. 소스코드 붙여넣기 → Verify
# ===== 방법 3: Foundry verify =====
forge verify-contract 0xDeployedAddress src/BlockToken.sol:BlockToken --chain sepolia --etherscan-api-key $ETHERSCAN_API_KEY --constructor-args $(cast abi-encode "constructor(address)" 0xOwner)
🔍 6.2 Etherscan에서 컨트랙트 직접 사용하기
| 탭 | 기능 | MetaMask 필요 |
|---|---|---|
| Code | 검증된 소스코드 · ABI · 바이트코드 확인 | ❌ |
| Read Contract | view/pure 함수 무료 호출 (잔액, 이름 등) | ❌ |
| Write Contract | 상태 변경 함수 트랜잭션 실행 | ✅ 필요 |
| Events | 발생한 이벤트 로그 확인 | ❌ |
| Internal Txns | 내부 ETH 전송 추적 | ❌ |
📦 Chapter 07. IPFS & 분산 스토리지
NFT 메타데이터 · 이미지 · 파일을 탈중앙화 저장소에
📚 7.1 IPFS란? 왜 필요한가?
IPFS(InterPlanetary File System)는 콘텐츠 기반 주소 지정을 사용하는 분산 파일 시스템입니다. NFT 이미지를 중앙 서버에 저장하면 서버가 다운될 때 "이미지를 찾을 수 없음"이 되지만, IPFS는 파일 내용의 해시가 주소이므로 변조 불가능합니다.
| 서비스 | 무료 한도 | 특징 | 추천 |
|---|---|---|---|
| Pinata | 1GB / 100파일 | 가장 유명, SDK 완비 | ⭐⭐⭐ |
| NFT.storage | 무제한 (NFT 한정) | Filecoin 백업, NFT 전용 | ⭐⭐⭐ |
| web3.storage | 5GB | Protocol Labs 운영 | ⭐⭐ |
| Infura IPFS | 5GB | Infura 생태계 통합 | ⭐⭐ |
💻 7.2 Pinata + NFT 메타데이터 완전 구현
// scripts/upload-to-ipfs.js
const { PinataSDK } = require("pinata");
const fs = require("fs");
const path = require("path");
require("dotenv").config();
const pinata = new PinataSDK({
pinataJwt: process.env.PINATA_JWT,
pinataGateway: process.env.PINATA_GATEWAY_URL
});
async function uploadNFTCollection(imageDir, count) {
const imageHashes = [];
const metadataHashes = [];
// 1단계: 이미지 일괄 업로드
console.log("📸 이미지 업로드 중...");
for (let i = 1; i <= count; i++) {
const imgPath = path.join(imageDir, `${i}.png`);
const file = new File(
[fs.readFileSync(imgPath)],
`${i}.png`,
{ type: "image/png" }
);
const result = await pinata.upload.file(file);
imageHashes.push(result.IpfsHash);
console.log(` ✅ ${i}.png → ipfs://${result.IpfsHash}`);
}
// 2단계: 메타데이터 JSON 생성 및 업로드
console.log("📝 메타데이터 업로드 중...");
for (let i = 1; i <= count; i++) {
const metadata = {
name: `BlockchainNFT #${i}`,
description: "A unique blockchain developer collectible",
image: `ipfs://${imageHashes[i-1]}`,
external_url: `https://myproject.io/nft/${i}`,
attributes: [
{ trait_type: "Rarity", value: getRarity(i) },
{ trait_type: "Level", value: getLevel(i) },
{ trait_type: "Token ID", value: i }
]
};
const result = await pinata.upload.json(metadata);
metadataHashes.push(result.IpfsHash);
console.log(` ✅ Metadata #${i} → ipfs://${result.IpfsHash}`);
}
// 3단계: 결과 저장
fs.writeFileSync("./ipfs-hashes.json", JSON.stringify({ imageHashes, metadataHashes }, null, 2));
console.log("✨ 업로드 완료! ipfs-hashes.json 저장됨");
return metadataHashes;
}
function getRarity(id) {
if (id <= 10) return "Legendary";
if (id <= 50) return "Epic";
if (id <= 200) return "Rare";
return "Common";
}
function getLevel(id) { return Math.floor(Math.random() * 100) + 1; }
uploadNFTCollection("./images", 1000).catch(console.error);
⬆️ Chapter 08. 업그레이더블 컨트랙트 배포
배포 후 수정 불가? — Proxy 패턴으로 해결
📚 8.1 Proxy 패턴 완전 이해
// npm install @openzeppelin/hardhat-upgrades @openzeppelin/contracts-upgradeable
// contracts/TokenV1.sol — UUPS 업그레이더블 토큰
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract TokenV1 is Initializable, ERC20Upgradeable, OwnableUpgradeable, UUPSUpgradeable {
// ⚠️ constructor 대신 initialize 사용!
function initialize(address initialOwner) public initializer {
__ERC20_init("BlockToken", "BTKN");
__Ownable_init(initialOwner);
__UUPSUpgradeable_init();
_mint(initialOwner, 10_000_000 ether);
}
function _authorizeUpgrade(address) internal override onlyOwner {}
}
// contracts/TokenV2.sol — 새 기능 추가 버전
contract TokenV2 is TokenV1 {
uint256 public version;
function initializeV2() public reinitializer(2) {
version = 2;
}
// 새로운 기능 추가
function newFeature() external pure returns (string memory) {
return "V2 Feature!";
}
}
// scripts/deploy-upgradeable.js
const { ethers, upgrades } = require("hardhat");
async function main() {
const [deployer] = await ethers.getSigners();
// V1 배포 (UUPS 프록시와 함께)
const TokenV1 = await ethers.getContractFactory("TokenV1");
const proxy = await upgrades.deployProxy(TokenV1, [deployer.address], {
initializer: "initialize",
kind: "uups"
});
await proxy.waitForDeployment();
console.log("Proxy 주소 (변하지 않음):", await proxy.getAddress());
// V2로 업그레이드
const TokenV2 = await ethers.getContractFactory("TokenV2");
const upgraded = await upgrades.upgradeProxy(proxy, TokenV2);
await upgraded.initializeV2();
console.log("✅ V2로 업그레이드 완료! 주소 동일", await upgraded.getAddress());
}
main().catch(console.error);
🔄 Chapter 09. CI/CD 파이프라인 구축
GitHub Actions로 자동 테스트 · 린팅 · 배포 파이프라인
📚 9.1 블록체인 CI/CD 파이프라인 구조
스마트 컨트랙트는 배포 후 수정이 어렵습니다. GitHub Actions로 PR마다 자동 테스트 → 보안 스캔 → 테스트넷 배포 파이프라인을 구축하면 배포 전 버그를 99% 차단합니다.
| 단계 | 도구 | 내용 | 트리거 |
|---|---|---|---|
| 1. Lint | solhint, prettier | 코드 스타일 검사 | 모든 push/PR |
| 2. Compile | hardhat compile | 컴파일 오류 확인 | 모든 push/PR |
| 3. Test | hardhat test / forge | 유닛·통합·Fuzz 테스트 | 모든 push/PR |
| 4. Coverage | solidity-coverage | 커버리지 80%+ 강제 | 모든 push/PR |
| 5. Security | Slither, Mythril | 취약점 정적 분석 | main 브랜치 PR |
| 6. Deploy | hardhat ignition | 테스트넷 자동 배포 | develop 머지 |
🔒 9.2 GitHub Secrets 설정 방법
Settings → Secrets and variables → Actions → New repository secret 에서 아래 값을 등록하세요:
| Secret 이름 | 값 | 용도 |
|---|---|---|
| PRIVATE_KEY | 0x로 시작하는 개인키 | 배포 서명 |
| SEPOLIA_RPC_URL | Alchemy/Infura URL | 테스트넷 연결 |
| ETHERSCAN_API_KEY | Etherscan API Key | 소스코드 검증 |
| COINMARKETCAP_API_KEY | CMC API Key | Gas USD 환산 |
// .solhint.json — 린팅 규칙
{
"extends": "solhint:recommended",
"rules": {
"compiler-version": ["error", "^0.8.20"],
"reentrancy": "error",
"avoid-call-value": "error",
"max-line-length": ["warn", 120]
}
}
// package.json scripts
{
"scripts": {
"test": "hardhat test",
"coverage": "hardhat coverage",
"lint": "solhint contracts/**/*.sol",
"gas": "REPORT_GAS=true hardhat test",
"deploy:sepolia": "hardhat ignition deploy ignition/modules/Token.js --network sepolia --verify"
}
}
🎤 Chapter 10. 현업 면접 Q&A TOP 15
개발환경·배포·인프라 면접 실전 완전 대비
A: Hardhat은 JavaScript 기반으로 생태계가 넓고 DApp 개발·스크립트 작성에 적합합니다. Foundry는 Rust 기반으로 10~100배 빠르고, 테스트를 Solidity로 작성하며 Fuzz/Invariant 테스트가 내장되어 보안 감사와 프로토콜 개발에 적합합니다. 실무에서는 둘을 함께 사용하는 경우도 많습니다.
A: 기존 deploy.js는 명령형(imperative) — 매번 재실행하면 컨트랙트가 중복 배포될 수 있습니다. Ignition은 선언형(declarative) — 배포 상태를 JSON으로 추적하고, 중간에 실패해도 완료된 부분은 건너뛰고 재시도합니다. 또한 배포 결과가 자동으로 저장되어 재현 가능하며 팀 공유도 쉽습니다.
A: ①.env 파일 + dotenv 라이브러리 + .gitignore 등록 ②GitHub Actions에서 Secrets 사용 (절대 코드에 하드코딩 금지) ③테스트넷 배포용 별도 지갑 사용 ④메인넷은 Ledger/Trezor 하드웨어 지갑 또는 Safe(멀티시그)로 배포 ⑤AWS Secrets Manager, HashiCorp Vault 같은 비밀 관리 서비스 활용.
A: ①컴파일러 버전 불일치 (hardhat.config.js와 Etherscan 설정이 다른 경우) ②Optimizer 설정 불일치 (runs 수가 다를 때) ③라이브러리 링크 누락 (library 주소 불일치) ④생성자 인자 ABI 인코딩 오류 ⑤멀티파트 컨트랙트에서 import 경로 불일치. --verify 플래그 사용 시 자동으로 올바른 설정이 전달됩니다.
A: HTTP URL은 위치 기반(서버:포트/경로). IPFS는 콘텐츠 기반 — 파일 내용의 SHA-256 해시가 주소(CID). 동일한 내용 → 동일한 CID → 변조 불가. 단점: 파일을 아무도 핀(pin)하지 않으면 가비지 컬렉션될 수 있어 Pinata 같은 핀닝 서비스가 필요합니다. NFT tokenURI에 ipfs://CID 형식을 사용합니다.
A: Transparent Proxy는 ProxyAdmin 컨트랙트가 별도로 필요하고 모든 호출에서 admin 체크 (Gas 비쌈). UUPS는 업그레이드 로직이 구현 컨트랙트에 있어 Gas 효율적이고 구조가 단순합니다. 단, UUPS는 업그레이드 함수를 구현 컨트랙트에서 제거하면 영구 동결 위험. 현재 OpenZeppelin은 UUPS를 권장합니다.
A: 단위 테스트는 미리 정한 입력값으로만 검증. Fuzz 테스트는 무작위 입력값 수천 개를 자동 생성하여 예상치 못한 엣지 케이스를 발견합니다. Foundry는 testFuzz_ 접두사 함수에 자동으로 무작위 파라미터를 주입합니다. bound()로 유효 범위를 제한하는 것이 핵심. 오버플로우, 언더플로우, 특수값(0, max) 버그를 효과적으로 탐지합니다.
A: 메인넷의 특정 블록 상태를 로컬에 복제하여 테스트하는 방법. hardhat.config.js의 forking 설정 또는 anvil --fork-url로 사용. 실제 Uniswap, Aave 등 프로토콜과 상호작용 테스트가 가능합니다. ①기존 DeFi 프로토콜과 통합 테스트 ②거래소 유동성이 있는 환경에서 스왑 테스트 ③실제 고래 주소를 impersonate하여 테스트할 때 필수입니다.
A: 커버리지 100%는 모든 코드 라인이 테스트에서 실행되었다는 의미이지만, "모든 시나리오가 검증되었다"는 의미는 아닙니다. 한계: ①특수 입력값(0, max, type(uint256).max)이 빠질 수 있음 ②경쟁 조건(reentrancy) 미탐지 ③Gas 경계값 미테스트. 커버리지 + Fuzz 테스트 + Invariant 테스트 + 정적 분석을 조합해야 합니다.
A: ①chain ID 별 config 분리 ②각 체인별 block.timestamp, block.number 동작 차이 ③L2 체인은 Gas 모델이 다름 (Arbitrum: L1 data fee + L2 execution fee) ④주소 재사용 (CREATE2로 동일 주소 배포 가능) ⑤브릿지 컨트랙트와의 연동 ⑥체인별 Etherscan 등가 서비스 (Polygonscan, Arbiscan) 검증. ignition/deployments에 체인별 폴더가 자동 생성됩니다.
A: Slither는 Trail of Bits가 만든 Python 정적 분석 도구입니다. ①Reentrancy (재진입 공격) ②Uninitialized variables ③Incorrect ERC20/721 구현 ④Arbitrary send (임의 ETH 전송) ⑤Weak PRNG (예측 가능한 랜덤) ⑥Missing zero-address check ⑦tx.origin 사용 ⑧Divide before multiply (정밀도 손실). 슬리더는 80여 개 검사기를 가지고 있습니다.
A: ①즉시 Pause (일시정지 기능이 있다면) ②버그의 심각도 파악 (자금 탈취 가능 여부) ③Upgradeable이면 새 로직 컨트랙트 준비 ④커뮤니티/사용자에게 투명하게 공지 ⑤Safe(멀티시그) 티임사이너 모두에게 업그레이드 승인 요청 ⑥업그레이드 후 검증. 업그레이드 불가능한 컨트랙트라면 새 버전 배포 + 마이그레이션 가이드 제공이 유일한 방법입니다.
A: V1에서 V2로 업그레이드 시 새 변수를 기존 변수 중간에 삽입하면 스토리지 슬롯 재배열로 데이터 손상. 방어법: ①기존 변수 순서를 절대 바꾸지 않음 ②새 변수는 반드시 끝에 추가 ③__gap[50] 예약 슬롯 패턴 (상속 체인 중간 컨트랙트에 예비 슬롯 확보) ④@openzeppelin/hardhat-upgrades의 validateUpgrade()로 업그레이드 전 자동 검증.
A: 프로젝트마다 호환되는 Node.js 버전이 다를 수 있습니다. nvm을 쓰면 디렉토리별 .nvmrc 파일로 버전 자동 전환 가능. 예: Hardhat 최신 버전은 Node 18+ 요구, 오래된 프로젝트는 Node 16 요구. CI/CD에서도 actions/setup-node의 node-version을 .nvmrc와 동기화하면 "내 컴퓨터에서는 되는데..." 문제를 방지합니다.
A: ①optimizer: true, runs: 200 (200회 호출 기준 최적화. 라이브러리처럼 많이 호출되면 runs: 1000 권장) ②viaIR: true (대형 컨트랙트 IR 기반 최적화, 컴파일 느림) ③REPORT_GAS=true npx hardhat test로 함수별 Gas 측정 ④forge snapshot으로 PR 전후 Gas 비교 자동화 ⑤storage 접근 최소화, mapping 활용, calldata 파라미터 사용이 핵심.
✅ 학습 체크리스트
🛠️ 개발 환경
☑ nvm + Node.js 18 LTS 설치
☑ Foundry (forge/cast/anvil) 설치
☑ VSCode Solidity 확장 설정
☑ .env + .gitignore 보안 설정
⚒️ 빌드 & 테스트
☑ Hardhat Fixture + 시간 조작 테스트
☑ Foundry Fuzz + Invariant 테스트
☑ Gas 리포트 분석 및 최적화
☑ cast/anvil 터미널 도구 활용
🚀 배포 & 인프라
☑ Ignition 모듈로 재현 가능한 배포
☑ Sepolia 테스트넷 배포 + 검증
☑ IPFS Pinata NFT 메타데이터 업로드
☑ UUPS 업그레이더블 컨트랙트 배포
🔄 CI/CD
☑ GitHub Actions 자동 테스트 구축
☑ Slither 보안 스캔 파이프라인
☑ solhint 린팅 규칙 설정
☑ GitHub Secrets 안전 관리
BlockchainDevGuide0004 완료!
개발 환경 세팅 · Hardhat · Foundry · 테스트넷 배포
Etherscan 검증 · IPFS · 업그레이더블 컨트랙트 · CI/CD까지 완전 정복!
📌 다음 편 예고: BlockchainDevGuide0005
NFT · DeFi 심화 — 실전 프로젝트 완성
NFT 마켓플레이스 · ERC-1155 게임아이템 · Uniswap V3 통합 · Aave 대출 프로토콜 · DAO 거버넌스