블록체인: Smart Contract
이론적 논의가 길어졌습니다. 이론을 먼저 공부하고 코드를 작성하면 학습 수준은 깊어지지만 휘발성이 높습니다. 공부는 많이 했는데 기억나는 건 딱히 없는 대참사가 펼쳐지죠. 코드를 먼저 작성하고 후에 이론으로 보강하는 전략은 휘발되는 지식의 양은 확실히 줄지만 이론에 대
1. Smart Contract 🎯
이론적 논의가 길어졌습니다.
이론을 먼저 공부하고 코드를 작성하면 학습 수준은 깊어지지만 휘발성이 높습니다. 공부는 많이 했는데 기억나는 건 딱히 없는 대참사가 펼쳐지죠.
코드를 먼저 작성하고 후에 이론으로 보강하는 전략은 휘발되는 지식의 양은 확실히 줄지만 이론에 대한 깊이는 다소 얕아질 수밖에 없겠죠.

트레이드오프가 있지만, 저는 보통 후자의 방식으로 공부를 합니다.
만들지 않은 부분에 대한 이론적 학습은 휘발성이 높다고 생각합니다. 성격도 급한 편이고요. 명확히 이해하지 못한 채 진행했지만 후에 깨달음을 얻을 때 기억이 장기화되는 것 같습니다.
각자의 성향과 상황에 맞게 공부하시고요, 저는 오늘 바로 간단한 스마트 컨트랙트를 작성하고 배포하는 과정을 공유드리고자 합니다. 참고로 이번 실습은 매우 쉽습니다.
1-1. OpenZeppelin Wizard + Remix ✅

OpenZeppelin Wizard는 사용자가 UI 조작을 통해 Smart Contract 코드를 작성할 수 있도록 해주는 도구입니다. '딸깍'하면 간단한 Smart Contract 코드가 나옵니다. OpenZeppelin Wizard에서 Open in Remix 버튼을 클릭할 예정입니다.

Remix는 웹 브라우저 기반의 Ethereum IDE입니다. 별도의 설치 과정이 필요하지 않아서 가벼운 프로젝트, 작은 규모의 컨트랙트를 개발에 유용합니다. 다양한 버전의 Solidity Compiler도 지원합니다.
OpenZeppelin Wizard에서 버튼 딸깍으로 Smart Contract 코드를 만들고, Remix에서 버튼 딸깍으로 컴파일과 배포를 진행한 뒤 이것저것 살펴보면서 Smart Contract와 친해지는 것이 오늘의 목표입니다.

Name은 사람이 읽을 수 있는 형태의 토큰 전체 이름입니다. 저는 Minkwan 토큰을 만들었습니다. Symbol은 토큰에 대한 축약 표시로, MK로 설정했습니다. 거래소나 지갑에서 토큰을 간결하게 표시하는 식별자 역할을 합니다.
Mintable은 토큰 발행 권한입니다. 중앙은행이 필요에 따라 화폐를 추가로 발행하는 것처럼, 컨트랙트 관리자가 배포 후에도 새로운 토큰을 발행할 수 있는 기능입니다.
Permit은 오프 체인 서명을 통해 사용자가 가스비 없이 토큰 사용을 승인할 수 있게 하는 기능입니다. 조금 복잡한 개념이니 차근차근 설명하겠습니다. 먼저 온 체인과 오프 체인을 이해해야 합니다. 온 체인은 블록체인 네트워크에 직접 기록되는 것으로, 트랜잭션을 보내면 블록에 기록되며 가스비가 발생합니다. 반면 오프 체인은 블록체인 밖에서 일어나는 동작으로, 네트워크에 기록되지 않아 가스비가 발생하지 않습니다.
Permit의 작동 방식을 예시로 설명하면, 앨리스가 밥에게 자신의 토큰 100개 사용을 승인한다고 가정해 봅시다. 앨리스는 자신의 지갑(개인키)으로 승인 메시지에 서명만 합니다. 이 서명된 메시지를 밥에게 전달하면, 밥이 나중에 블록체인에 트랜잭션을 보낼 때 앨리스의 서명을 첨부합니다. 이 경우 가스비는 밥이 부담하게 됩니다. 앨리스는 서명만 했기 때문에 비용이 들지 않는 것이죠.
마지막으로 화면에는 보이지 않지만 Ownable이라는 기능도 있습니다. Ownable은 컨트랙트를 배포한 주소만이 Mint와 같은 특정 관리 함수를 실행할 수 있도록 제한하는 기능입니다. 즉, Owner만이 토큰 발행과 같은 민감한 작업을 수행할 수 있도록 보장하는 것입니다.

Open in Remix 버튼을 클릭하면 Remix로 넘어오게 됩니다.
이 상태에서 command+s(macOS 기준)를 누르면 자동으로 해당 코드가 컴파일됩니다. 좌측에 현재 클릭되어 있는 아이콘을 클릭하고 해당 필드에서 deploy 버튼을 클릭하면 자동으로 배포가 됩니다. 이제 밑에서 여러 가지 버튼을 눌러보시면 됩니다. 코드만 조금 더 살펴보겠습니다.
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.5.0
pragma solidity ^0.8.27;
import {ERC20} from "@openzeppelin/contracts@5.5.0/token/ERC20/ERC20.sol";
import {ERC20Permit} from "@openzeppelin/contracts@5.5.0/token/ERC20/extensions/ERC20Permit.sol";
import {Ownable} from "@openzeppelin/contracts@5.5.0/access/Ownable.sol";
contract Minkwan is ERC20, Ownable, ERC20Permit {
constructor(address initialOwner)
ERC20("Minkwan", "MK")
Ownable(initialOwner)
ERC20Permit("Minkwan")
{}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
}코드가 Solidity 0.8.27 버전 이상에서 컴파일되어야 함을 지정했습니다. OpenZeppelin 라이브러리에서 ERC20, ERC20Permit, Ownable 등의 함수를 가져왔습니다.
Minkwan이라는 Contract의 생성자를 보죠. Contract가 배포될 때 관리자로 지정할 주소를 외부로부터 입력받습니다. 좌측 필드에서 우리가 넣어줄 값에 해당되고, 보통은 Contract를 배포하는 자신의 주소를 입력합니다.
ERC20 함수에 Minkwan과 MK를 전달하여 토큰의 이름과 심볼에 대한 초기화를 진행합니다. 그리고 Ownable 함수에 initialOwner 주소값을 전달하여, 해당 주소를 Contract의 공식적인 소유자로 지정합니다. 마지막으로 ERC20Permit에 토큰의 이름(Minkwan)을 전달하여 Permit과 관련된 저장소 등을 초기화합니다.
이어서 mint 함수를 정의합니다. address to는 새로 발행된 토큰을 받을 지갑 주소, amount는 토큰의 수량을 의미합니다. 이때, public onlyOwner는 이 함수를 누구나 호출할 수 있지만 오직 Contract의 소유자만이 실행할 수 있도록 제한하는 역할을 합니다. 토큰을 발행해 달라는 요청은 받아주겠지만, 실제 토큰 발생은 내가 결정할 테니 선 넘지 말라는 것이죠.
실제로 _mint라는 ERC-20 표준에 정의된 내부 함수를 호출하여 실제로 주소의 잔액을 amount 만큼 증가시키고 총 발행량을 업데이트합니다. 재밌죠? 재밌습니다.
1-2. Hardhat + Ethers ✅

Hardhat은 단단한 뚝배기입니다. Ethereum Smart Contract 개발의 워크플로우(컴파일, 테스트, 배포, 디버깅 등)를 지원하는 프레임워크입니다.

Ethers.js는 EVM 블록체인과 상호작용 가능한 자바스크립트 라이브러리입니다.
Hardhat은 Smart Contract를 개발, 컴파일, 테스트, 배포하는 전체 환경을 제공하며, Ethers.js는 그 환경 내에서 또는 외부 애플리케이션에서 실제로 블록체인과 데이터를 주고받고 트랜잭션을 처리하는 통신 역할을 합니다.
기본 설정부터 시작하죠.
1. npm init
2. npm install --save-dev hardhat
3. npx hardhat --init
4. npm install --save-dev @nomicfoundation/hardhat-toolbox
5. npm install dotenv
6. npm install ethers
7. npm install @openzeppelin/contracts1. npm init으로 빈 파일에서 package.json을 생성 2. hardhat을 설치 3. hardhat을 실행(Hardhat 2(older version)로 실행) - Hardhat 3.x.x와 Toolbox 간의 버전 충돌 이슈 4. hardhat-toolbox 설치 5. dotenv 설치 6. ethers 설치 7. openzeppelin/contracts 설치

위 구조에 맞게 파일을 만들어 주세요. 그다음 이전에 OpenZeppelin에서 실습했던 코드를 파일에 맞게 붙여 넣으면 됩니다. 아래 코드를 minkwan.sol 파일에 작성하라는 뜻입니다.
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.5.0
pragma solidity ^0.8.27;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract Minkwan is ERC20, Ownable, ERC20Permit {
constructor(address initialOwner)
ERC20("Minkwan", "MK")
Ownable(initialOwner)
ERC20Permit("Minkwan")
{}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
}npx hardhat compile위 명령어를 통해 minkwan.sol 파일을 컴파일합니다. 아래와 같이 컴파일이 잘 된 모습을 확인할 수 있습니다.
Downloading compiler 0.8.28
Compiled 21 Solidity files successfully (evm target: paris).Solidity가 paris 하드 포크 규칙에 맞춰 컴파일 되었습니다. 하드 포크는 블록체인 프로토콜이 이전 버전과 호환되지 않게 업그레이드하는 것을 의미합니다. paris 하드 포크는 PoW에서 PoS로 전환된 시점의 버전을 의미하는데요, paris 자체에는 특별한 의미가 없습니다. 이더리움 코어 개발자들은 오래전부터 하드 포크 버전에 세계 주요 도시 이름을 붙여왔습니다. 네이밍 규칙에 불과하죠.
이어서 hardhat을 실행하면 생성되는 hardhat.config.js 파일을 아래와 같이 설정해 줍니다.
// 필요한 Hardhat 플러그인 툴박스(Ethers, Chai 등)를 불러옵니다.
require("@nomicfoundation/hardhat-toolbox");
// .env 파일에 저장된 환경 변수(RPC URL, PRIVATE_KEY 등)를 불러와 사용할 수 있게 합니다.
require("dotenv").config();
// 기본 컴파일러 설정 객체를 정의합니다.
const DEFAULT_COMPILER_SETTINGS = {
// 사용할 Solidity 컴파일러 버전을 지정합니다.
// 참고: 이 값은 실제 계약 파일의 pragma와 일치해야 합니다.
version: "0.8.28",
settings: {
// 코드를 최적화하여 가스비를 절약하는 옵션입니다.
optimizer: {
// 최적화 사용 여부
enabled: true,
// 최적화 실행 횟수 (보통 200회 설정)
runs: 200,
},
// 상속 오류 방지를 위한 설정 (기본값 사용)
// viaIR: false,
},
};
/**
* @type import('hardhat/config').HardhatUserConfig
* Hardhat 설정 파일의 타입을 지정합니다.
*/
module.exports = {
// Solidity 컴파일러 설정 (수정된 올바른 문법)
solidity: DEFAULT_COMPILER_SETTINGS, // 'compilers' 배열을 사용하지 않고 직접 객체를 할당합니다.
// 네트워크 설정: 계약을 배포하거나 상호작용할 블록체인 네트워크를 정의합니다.
networks: {
// Hardhat 로컬 개발 네트워크 설정
hardhat: {
// 로컬 네트워크의 Chain ID를 31337로 설정합니다.
chainId: 31337,
},
// 'Monad' 네트워크 설정
monad: {
// 블록체인 노드에 접근할 RPC URL을 환경 변수에서 가져옵니다.
url: process.env.MONAD_RPC_URL,
// 트랜잭션 서명에 사용할 개인 키를 환경 변수에서 가져옵니다.
// 이 키는 배포 주소의 계정입니다.
accounts: [process.env.PRIVATE_KEY],
},
},
};작업을 마치면 .env 파일에서 PRIVATE_KEY와 MONAD_RPC_URL을 설정합니다. PRIVATE_KEY는 임의의 값으로 설정하면 되고, MONAD_RPC_URL은 https://testnet-rpc.monad.xyz로 설정하면 됩니다.
이제 아래와 같이 배포 스크립트를 작성합니다.
// Ethers.js 라이브러리에서 필요한 모듈을 가져옵니다. (블록체인 상호작용용)
const { ethers } = require("ethers");
// Hardhat 런타임 환경 객체를 가져옵니다. (아티팩트 읽기 등 Hardhat 기능 사용)
const hre = require("hardhat");
// 메인 비동기 함수를 정의합니다.
async function main() {
// 1. 계약 아티팩트(Artifacts) 읽기
// 컴파일된 'Minkwan' 계약의 아티팩트(ABI, Bytecode 포함)를 읽어옵니다.
const artifacts = await hre.artifacts.readArtifact("Minkwan");
// 아티팩트에서 ABI(함수 인터페이스)와 바이트코드(실제 코드)를 추출합니다.
const abi = artifacts.abi;
const bytecode = artifacts.bytecode;
// 2. Provider 및 서명자(Wallet) 설정
// 모나드 네트워크에 연결할 Provider 객체를 환경 변수 URL로 생성합니다.
const provider = new ethers.JsonRpcProvider(process.env.MONAD_RPC_URL);
// 트랜잭션 서명 및 전송을 담당할 Wallet 객체를 개인 키와 Provider로 생성합니다.
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
// 배포자 지갑의 현재 잔액을 조회합니다. (가스비 확인 목적)
const balance = await provider.getBalance(wallet.address);
// 배포자 주소를 변수에 저장합니다.
const deployer = wallet.address;
// 3. 정보 출력
// 현재 잔액을 콘솔에 출력합니다. (단위: Wei)
console.log("Balance: ", balance);
// 배포를 시도할 주소(Owner)를 출력합니다.
console.log("Deploying: ", deployer);
// 4. 계약 배포 (Deploy)
// ContractFactory 객체를 생성합니다. (계약을 배포하는 역할을 함)
const factory = new ethers.ContractFactory(abi, bytecode, wallet);
// 스마트 계약을 블록체인에 배포하고, 생성자 인자로 deployer 주소(initialOwner)를 전달합니다.
const minkwan = await factory.deploy(deployer);
// 트랜잭션이 블록에 포함되어 배포가 완료될 때까지 대기합니다.
await minkwan.waitForDeployment();
// 최종 배포된 계약의 주소(target)를 출력합니다.
console.log("Deployed to address: ", minkwan.target);
}
// main 함수를 호출하여 스크립트 실행을 시작합니다.
main()
// 성공 시 Node.js 프로세스를 종료합니다.
.then(() => process.exit(0))
// 오류 발생 시 오류를 출력하고 프로세스를 종료합니다.
.catch((err) => {
console.error(err);
process.exit(1);
});npx hardhat run scripts/deploy.js --network monad그리고 위 명령어로 실행해 보면 됩니다. 참고로 https://faucet.monad.xyz/ 에서 Gas Fee를 지불하기 위한 테스트용 ETH를 미리 받아야 정상적으로 동작합니다.
mint에 대한 스크립트도 필요하겠죠? 아래와 같이 mint 스크립트를 작성합니다.
const { ethers } = require("ethers");
const hre = require("hardhat");
async function main() {
const provider = new ethers.JsonRpcProvider(process.env.MONAD_RPC_URL);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
const artifacts = await hre.artifacts.readArtifact("Minkwan");
const abi = artifacts.abi;
const bytecode = artifacts.bytecode;
const minkwan = new ethers.Contract("0x로 시작하는 계약 주소", abi, wallet);
const mint = await minkwan.mint("토큰을 받을 주소", 1);
const receipt = await mint.wait();
console.log(receipt);
console.log("minted");
}
main()
.then(() => process.exit(0))
.catch((err) => {
console.error(err);
process.exit(1);
});npx hardhat run scripts/mint.js --network monadSmart Contract 개발 워크플로우와 친해져 봤습니다. 재밌죠? 재밌습니다.
More to read
Amazon VPC Architecture 이해하기
새로운 프로젝트를 기획하며, 개발에서 무엇을 가장 먼저 고민해야 하는지 다시 돌아보게 되었습니다.한때는 프론트엔드가 모든 설계의 출발점이라고 믿었습니다. 유저가 무엇을 보고, 어떤 흐름에서 머무르고 이탈하는지에 대한 이해 없이 서비스를 만든다는 건 불가능하다고 생각했기
'원사이트'프론트엔드 관점으로 알고리즘 이해하기
오랜만에 방법론에 관한 글을 쓰게 되었습니다. 최근 상황은 이렇습니다. SSAFY에서는 하루에 엄청난 양의 알고리즘 문제들을 과제로 수행하게 됩니다. 그 과정에서, '구현력'이 매우 떨어진다는 생각이 들었습니다. 완전히 어려운 문제라면 '아쉬움'이라는 감정조차 느끼지
SubnetVPC 설계의 시작: IP와 Subnet
반복되는 루틴 속에서 얻은 안정감을 발판 삼아, 이제는 기술적 스펙트럼을 넓히기 위한 개인 프로젝트에 착수하고자 합니다.이번 프로젝트의 목표는 단순한 포트폴리오 구축을 넘어, 실제 서비스 수준의 블로그 시스템 구현과 다국어 처리 적용 등 실무에 가까운 역량을 한 단계