블록체인 학습
0 XP

Chapter 10: 이벤트

데이터 구조 컨트랙트 배포하기

시작 전 준비물

  • Chapter 5 랩에서 만든 Hardhat 3 프로젝트
  • `npx hardhat node` 로 로컬 노드를 띄울 수 있는 상태
  • 배열·구조체·매핑 문법(Chapter 7~9)

진행 확인

0 / 5

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

데이터 구조 컨트랙트 배포하기

Chapter 7 부터 9 까지 배열, 구조체, 매핑을 배웠습니다. 브라우저에서는 문법이 맞는지까지만 확인했습니다. 하지만 이 자료구조들은 일반 프로그래밍 언어의 자료구조와 결정적으로 다른 성질이 있고, 그 차이는 실제 체인 위에 올려 봐야 드러납니다.

이번 랩에서는 셋을 한 컨트랙트에 모아 로컬 노드에 배포하고, 스크립트로 데이터를 넣어 가며 그 성질을 확인합니다. Chapter 5 의 랩에서 만든 Hardhat 프로젝트를 그대로 사용합니다. 새 프로젝트를 만들었다면 npx hardhat --init --template mocha-ethers 부터 다시 진행하세요.

1단계 — 사용자 등록부 컨트랙트 작성

contracts/UserRegistry.sol 을 만듭니다. 구조체로 사용자 정보를 묶고, 매핑으로 주소마다 찾아가고, 배열로 전체 목록을 관리하는 전형적인 조합입니다.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
 
contract UserRegistry {
    // 구조체: 관련 있는 값을 하나로 묶는다
    struct User {
        string name;
        uint256 age;
        uint256 joinedAt;
    }
 
    // 매핑: 주소로 바로 찾아간다 (조회가 빠르다)
    mapping(address => User) public users;
 
    // 배열: 전체 목록을 순회할 수 있게 한다
    address[] public userList;
 
    event UserRegistered(address indexed account, string name);
 
    function register(string calldata _name, uint256 _age) public {
        users[msg.sender] = User({ name: _name, age: _age, joinedAt: block.timestamp });
        userList.push(msg.sender);
        emit UserRegistered(msg.sender, _name);
    }
 
    function totalUsers() public view returns (uint256) {
        return userList.length;
    }
}

joinedAt 에 넣은 block.timestamp 는 이 트랜잭션이 담긴 블록의 시각을 초 단위 정수로 알려 주는 전역 변수입니다. 지금은 "등록한 시각을 기록한다" 정도로만 이해하고 넘어가세요. block 전역 변수는 Chapter 11 에서 자세히 다룹니다.

매핑과 배열을 함께 둔 이유가 중요합니다. 매핑은 주소 하나로 즉시 찾아갈 수 있지만, "등록된 사용자 전체" 를 훑을 방법이 없습니다. 매핑에는 길이도, 키 목록도 없기 때문입니다. 그래서 순회가 필요하면 배열을 따로 유지해야 합니다.

npx hardhat compile

2단계 — 노드 실행과 배포

한 터미널에서 노드를 켭니다.

npx hardhat node

다른 터미널에서 scripts/deployRegistry.js 를 만들고 배포합니다.

// scripts/deployRegistry.js
import { network } from 'hardhat'
 
const { ethers } = await network.getOrCreate()
 
const registry = await ethers.deployContract('UserRegistry')
await registry.waitForDeployment()
 
console.log('UserRegistry 주소:', await registry.getAddress())
npx hardhat run scripts/deployRegistry.js --network localhost

출력된 주소를 메모해 둡니다.

3단계 — 서로 다른 계정으로 사용자 등록

register 는 msg.sender 를 키로 쓰므로, 같은 계정으로 여러 번 부르면 덮어써질 뿐입니다. 사용자를 여럿 만들려면 계정을 바꿔 가며 호출해야 합니다.

scripts/fillRegistry.js 를 만듭니다.

// scripts/fillRegistry.js
import { network } from 'hardhat'
 
const { ethers } = await network.getOrCreate()
 
const ADDRESS = '여기에_배포된_주소'
const registry = await ethers.getContractAt('UserRegistry', ADDRESS)
 
// 노드가 출력했던 테스트 계정 20개를 그대로 가져온다
const signers = await ethers.getSigners()
const people










registry.connect(signer) 는 컨트랙트에 연결하는 계정을 바꾼 새 객체를 돌려줍니다. 이 계정이 트랜잭션에 서명하고, 그 주소가 컨트랙트 안에서 msg.sender 가 됩니다.

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

세 줄이 차례로 출력되며, 각각 다른 블록 번호를 갖습니다. 트랜잭션 하나가 블록 하나라는 사실이 여기서도 확인됩니다.

4단계 — 없는 키를 조회해 보기

scripts/readRegistry.js 를 만듭니다.

// scripts/readRegistry.js
import { network } from 'hardhat'
 
const { ethers } = await network.getOrCreate()
 
const ADDRESS = '여기에_배포된_주소'
const registry = await ethers.getContractAt('UserRegistry', ADDRESS)
const signers = await ethers.getSigners()
 
// 등록한 계정
const found =




두 번째 출력이 이 랩에서 가장 중요한 장면입니다. 등록한 적 없는 주소를 조회했는데도 오류가 나지 않습니다. 대신 빈 문자열과 0n 이 돌아옵니다.

매핑은 "모든 가능한 키가 이미 기본값으로 채워져 있는 무한한 표" 처럼 동작합니다. 그래서 존재하지 않는 키란 없고, 없는 키를 물어보면 그 타입의 기본값을 돌려줍니다. 자바스크립트 객체가 없는 속성에 undefined 를 돌려주는 것과는 다릅니다. 여기서는 값이 없는 것과 값이 0 인 것을 구분할 수 없습니다.

이 성질 때문에 실제 컨트랙트에는 보통 bool exists 같은 필드를 따로 둡니다. 그것이 없으면 "가입한 적 없는 사람" 과 "나이를 0 으로 등록한 사람" 을 영영 구별할 수 없습니다.

5단계 — 배열로 전체 순회

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

const total = await registry.totalUsers()
console.log('총 사용자 수:', total)
 
for (let i = 0n; i < total; i++) {
  const addr = await registry.userList(i)
  const user = await registry.users(addr)
  console.log(`${i

총 사용자 수: 3n 과 함께 세 명이 등록한 순서 그대로 출력됩니다. 매핑만으로는 절대 만들 수 없었던 목록입니다. 배열은 순서를 갖는 대신 조회가 느리고, 매핑은 조회가 빠른 대신 순서를 모릅니다. 그 트레이드오프를 이제 코드 두 줄의 차이로 확인한 셈입니다.

=
[
{ signer: signers[0], name: '민준', age: 28n },
{ signer: signers[1], name: '서연', age: 34n },
{ signer: signers[2], name: '도윤', age: 41n },
]
for (const person of people) {
// connect() 로 "누가 호출하는지" 를 바꾼다 — 이것이 msg.sender 가 된다
const tx = await registry.connect(person.signer).register(person.name, person.age)
const receipt = await tx.wait()
console.log(`${person.name} 등록 — 가스 ${receipt.gasUsed}, 블록 ${receipt.blockNumber}`)
}
await
registry.
users
(signers[
0
].address)
console.log('등록된 사용자:', found.name, found.age, found.joinedAt)
// 한 번도 등록한 적 없는 계정
const missing = await registry.users(signers[9].address)
console.log('없는 사용자:', JSON.stringify(missing.name), missing.age, missing.joinedAt)
}: ${
user
.
name
} (${
addr
})`
)
}