블록체인 학습
0 XP

Chapter 10: 이벤트

저장 방식별 가스 비교하기

저장 방식별 가스 비교하기

"이벤트는 상태 변수보다 훨씬 저렴하다." 이 챕터의 event vs 상태 변수 비교 에서 읽은 문장입니다. 그런데 얼마나 저렴한 걸까요? 두 배일까요, 열 배일까요?

말로 외우는 대신 직접 재 봅시다. 같은 데이터를 두 가지 방식으로 저장하는 함수를 만들고, 각각의 receipt.gasUsed 를 출력해 비교합니다. 이번 랩이 끝나면 여러분은 이 문장을 자기가 측정한 숫자로 말할 수 있게 됩니다.

1단계 — 비교용 컨트랙트 작성

contracts/GasCompare.sol 을 만듭니다. 두 함수는 받는 데이터가 완전히 같고, 그것을 어디에 두느냐만 다릅니다.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
 
contract GasCompare {
    struct Record {
        address writer;
        uint256 value;
        uint256 savedAt;
    }
 
    // 방식 A 가 쓰는 저장소
    Record[] public records;
 
    // 방식 B 가 쓰는 로그
    event Recorded(address indexed writer, uint256 value, uint256 savedAt);
 
    // 방식 A: 상태 변수 배열에 저장한다
    // 체인 상태에 영구히 남고, 컨트랙트가 나중에 읽을 수 있다
    function saveToStorage(uint256 _value) public {
        records.push(Record({ writer: msg.sender, value: _value, savedAt: block.timestamp }));
    }
 
    // 방식 B: 이벤트로만 발행한다
    // 로그에만 남고, 컨트랙트는 이 값을 다시 읽을 수 없다
    function saveToEvent(uint256 _value) public {
        emit Recorded(msg.sender, _value, block.timestamp);
    }
 
    function totalRecords() public view returns (uint256) {
        return records.length;
    }
}

두 함수의 차이는 records.push(...) 한 줄과 emit Recorded(...) 한 줄뿐입니다. 이 한 줄의 값이 얼마인지가 이번 실험의 주제입니다.

npx hardhat compile

2단계 — 배포와 측정 스크립트

노드를 켜 둔 채로 scripts/gasTest.js 를 만듭니다. 배포와 측정을 한 스크립트에서 처리하면 주소를 옮겨 적을 필요가 없습니다.

// scripts/gasTest.js
import { network } from 'hardhat'
 
const { ethers } = await network.getOrCreate()
 
const c = await ethers.deployContract('GasCompare')
await c.waitForDeployment()
console.log('배포:', await c.getAddress())
 
// 가스를 재서 돌려주는 작은 도우미
async function measure(label, txPromise) {
  const tx = await txPromise
  const receipt = await tx.wait()
  console.log(`${label}: ${receipt.gasUsed}`)
  return receipt.gasUsed
}

tx.wait() 가 돌려주는 receipt 안에 gasUsed 가 들어 있습니다. 이것이 그 트랜잭션이 실제로 소모한 가스입니다.

gasUsed 는 bigint 입니다. 다른 bigint 와는 그냥 빼고 나눌 수 있지만, 일반 number 와는 섞을 수 없습니다. 비율을 구할 때는 Number(a) / Number(b) 처럼 먼저 변환하세요.

3단계 — 두 방식 측정하기

스크립트에 이어서 작성합니다.

const gasStorage = await measure('상태 변수 저장', c.saveToStorage(100n))
const gasEvent = await measure('이벤트 발행  ', c.saveToEvent(100n))

실행합니다.

npx hardhat run scripts/gasTest.js --network localhost

두 줄의 숫자가 나옵니다. 어느 쪽이 더 큰가요? 지금 화면에 나온 두 값을 직접 적어 두세요.

4단계 — 차이 계산하기

스크립트 끝에 이어 붙입니다.

const diff = gasStorage - gasEvent
const ratio = (Number(gasStorage) / Number(gasEvent)).toFixed(1)
 
console.log('---')
console.log('차이:', diff, '가스')
console.log('배수:', ratio, '배')

이벤트 쪽이 수천에서 수만 가스만큼 적게 나올 것입니다. 정확한 값은 컴파일러 버전과 옵티마이저 설정에 따라 달라지므로, 이 교재의 숫자를 외우지 말고 여러분 화면에 찍힌 값을 기준으로 삼으세요.

차이가 나는 이유는 저장 위치가 다르기 때문입니다. 상태 변수는 EVM 의 스토리지에 들어갑니다. 모든 노드가 영원히 들고 있어야 하고 컨트랙트가 언제든 읽을 수 있는 공간이라, EVM 에서 가장 비싼 자원입니다.

반면 이벤트는 로그에 기록됩니다. 트랜잭션 영수증에 붙어 외부에서 읽을 수 있지만, 컨트랙트 자신은 절대 읽을 수 없는 공간이라 훨씬 쌉니다.

5단계 — 두 번째 호출은 같은 값일까?

마지막으로 흥미로운 관찰을 하나 더 합니다. 스크립트 끝에 붙입니다.

console.log('--- 두 번째 호출 ---')
const gasStorage2 = await measure('상태 변수 저장', c.saveToStorage(200n))
const gasEvent2 = await measure('이벤트 발행  ', c.saveToEvent(200n))
 
console.log('상태 변수 1회차 대비 차이:', gasStorage2 - gasStorage)
console.log('이벤트    1회차 대비 차이:', gasEvent2 - gasEvent)

이벤트 쪽은 두 번 다 사실상 같은 값이 나옵니다. 하지만 상태 변수 쪽은 첫 호출과 다른 값이 나옵니다.

배열에 처음 원소를 넣을 때는 길이를 담는 슬롯이 0 에서 다른 값으로 바뀝니다. EVM 은 이렇게 비어 있던 슬롯을 처음 채우는 일에 훨씬 높은 요금을 매깁니다. 두 번째부터는 이미 값이 있는 슬롯을 고치는 것이므로 더 쌉니다.

이것이 실무에서 "조회에 쓰지 않을 데이터는 상태 변수 말고 이벤트로 남겨라" 는 규칙이 나온 근거입니다. 거래 내역, 활동 기록, 알림처럼 컨트랙트 로직이 다시 읽을 필요가 없는 데이터는 이벤트로 충분하고, 그 선택만으로 사용자가 내는 수수료가 눈에 띄게 줄어듭니다.

시작 전 준비물

  • Hardhat 3 프로젝트와 실행 중인 로컬 노드
  • `receipt.gasUsed` 를 출력해 본 경험(Chapter 5 랩)
  • event 선언과 emit 문법

진행 확인

0 / 5

각 단계를 직접 수행하고, 아래 ‘확인 기준’이 실제로 보이면 체크하세요. 모두 체크하면 완료할 수 있습니다.