CryptoZombie[Course 1] - Solidity: Beginner to Intermediate Smart Contracts, Making the Zombie Factory

Yeonu-Kim·2026년 5월 2일

CryptoZombie

목록 보기
1/5

세계관의 설정을 파악해보자.

모든 좀비들은 16자리의 DNA를 가지고 있다. 이때 각 자리수에 따라 좀비의 특성이 정해지기도 한다. (앞 두자리는 눈 색깔, 그 다음 두 자리는 키 등등)

Chapter 2: Contracts

솔리디티의 코드는 contracts의 캡슐화 버전이다.

contract: 이더리움 앱에서 블록을 구성하는 내용

모든 함수나 변수들이 contract에 포함될 수 있음.

이때 solidity는 버전에 따라서 문법이 달라질 수 있으므로 version pragma를 통해 컴파일 버전을 고정해두어야 한다.

pragma solidity >=0.5.0 < 0.6.0;

contract ZombieFactory {
}

Chapter 3: State Variables & Integers

contract를 만들기 위해서는 변수가 필요하다.

이때 contract 내부에서 사용하는 것을 state variable이라고 한다. 마치 DB에 저장하는 것처럼 변수값이 contract 안에서 영구적으로 저장된다.

지원하는 자료형은 아래와 같다.

Value Typeint/uint부호 있는/없는 정수, 8비트 단위로 int8 ~ int256 지정 가능 (기본값: 256비트)
booltrue / false
address20바이트 이더리움 주소
address payableETH 송금이 가능한 주소 (transfer, send 메서드 보유)
bytes1~bytes32고정 크기 바이트 배열
fixed/ufixed선언은 가능하지만 아직 완전히 구현되지 않아 실제로는 거의 사용 불가
enum사용자 정의 열거형
Referencearray고정 크기 uint[5], 동적 크기 uint[]
struct사용자 정의 타입
mappingmapping(KeyType => ValueType), 해시맵처럼 동작 (storage에만 저장 가능)
bytes동적 바이트 배열
stringUTF-8 문자열

핵심 포인트 몇 가지:

  1. string은 인덱스 접근이나 length 조회가 안 됨 → 바이트 단위 조작이 필요하면 bytes를 써야 해
  2. mapping은 iterable하지 않아서 키 목록을 별도로 관리해야 함
  3. Solidity에는 float이 사실상 없어서 보통 wei 단위 정수로 금액을 다룸

Value vs Reference

  • Value: 값을 전달함.
  • Reference: 원본 변수의 주소를 넘겨줌.
pragma solidity >=0.5.0 <0.6.0;

contract ZombieFactory {
	uint dnaDigits = 16;
}

Chapter 4: Math Operations

일반적으로 사용하는 연산자는 다 사용할 수 있음.

+, -, *, /, %, **

pragma solidity >=0.5.0 <0.6.0;

contract ZombieFactory {
	uint dnaDigits = 16;
	uint dnaModulus = 10 ** dnaDigits;
}

Chapter 5: Structs

여러 타입들을 묶은 자료형이 필요할 수 있는데, Struct를 사용하면 쉽게 만들 수 있음.

struct Person {
	uint age;
	string name;
}
pragma solidity >=0.5.0 <0.6.0;

contract ZombieFactory {
	uint dnaDigits = 16;
	uint dnaModulus = 10 ** dnaDigits;
	struct Zombie {
		string name;
		uint dna;
	}
}

Chapter 6: Arrays

마찬가지로 배열도 사용할 수 있음.

배열 종류는 fixed array와 dynamic array 모두 지원함.

uint[2] fixedArray;
uint[] dynamicArray;

struct나 string 등의 복합 자료형으로도 array를 만들 수 있다.

이때 dynamic array를 사용하면 블록체인에 영구적으로 기재가 되므로 데이터베이스처럼 사용할 수 있음.

string[5] stringArray;
Person[] people;

이때 public 키워드를 사용하면 다른 contract에서 해당 변수를 읽을 수 있음.

마찬가지로 데이터베이스처럼 쓰려면 매우 유용함.

Person[] people; 
Person[] public people;
pragma solidity >=0.5.0 <0.6.0;

contract ZombieFactory {
	uint dnaDigits = 16;
	uint dnaModulus = 10 ** dnaDigits;
	struct Zombie {
		string name;
		uint dna;
	}
	Zombie[] public zombies;
}

Chapter 7: Function Declarations

contract 내부에서 function도 쓸 수 있음.

변수에도 자료형을 적어둬야 하고, public으로 두면 다른 contract에서도 사용할 수 있다.

memory 키워드는 reference type을 사용하면 반드시 사용해야 한다. (arrays, mappings, strings, bytes 등등)

그럼 value type에 memory를 쓰면 원본 값이 변경이 될까?

→ 안됨. value type은 항상 복사만 가능하기 때문에 컴파일 에러가 발생함.

function eatHam(string memory _name, uint _amount) public {

}
pragma solidity >=0.5.0 <0.6.0;

contract ZombieFactory {

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    struct Zombie {
        string name;
        uint dna;
    }

    Zombie[] public zombies;

    function createZombie (string memory _name, uint _dna) public {
    
    }

}

Chapter 8: Working With Structs and Arrays

struct로 선언한 값은 변수로 생성할 수도 있음.

push를 사용하면 배열 안에 원소를 넣을 수도 있음.

struct Person {
	uint age;
	string name;
}

Person[] public people;

Person satoshi = Person(80, "Satoshi");
people.push(satoshi);
people.push(Person(16, "Vitalik"));
pragma solidity >=0.5.0 <0.6.0;

contract ZombieFactory {

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    struct Zombie {
        string name;
        uint dna;
    }

    Zombie[] public zombies;

    function createZombie (string memory _name, uint _dna) public {
	    zombies.push(Zombie(_name, _dna));
    }

}

Chapter 9: Private/Public Functions

기본적으로는 모든 함수가 public으로 선언됨.

public하다: 현재 contract뿐만 아니라 다른 contract에서도 선언한 함수를 사용할 수 있다.

그런데 이게 항상 좋은 것은 아닐 수 있다. 다른contract에서 공격을 할 수도 있기 때문이다.

별도로 private 키워드를 사용해주면 선언된 contract 안에서만 사용할 수 있다.

uint[] numbers;

functon _addToArray(uint _number) private {
	numbers.push(_number);
}
pragma solidity >=0.5.0 <0.6.0;

contract ZombieFactory {

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    struct Zombie {
        string name;
        uint dna;
    }

    Zombie[] public zombies;

    function _createZombie (string memory _name, uint _dna) private {
	    zombies.push(Zombie(_name, _dna));
    }

}

Chapter 10: More on Functions

function은 return할 수도 있음. 이때 return하는 값에 대해서 type을 알려줘야 함.

이때 reference type이므로 memory도 같이 붙음.

string greeting = "Hello World";

function sayHello() public returns (string memory) {
	return greeting
}

이때 함수 중에서 주어진 변수를 전혀 변경하지 않는 것들도 있음.

이렇듯 단순히 읽기만 하는 경우에는 view 키워드를 사용해서 별도로 선언할 수도 있음.

아예 변수 자체에 접근하지도 않는 경우도 있는데, 이때에는 pure 키워드를 사용함.

function sayHello() public view returns (string memory) {
	return greeting; // greeting 변수가 변화하지 않음.
}

function _multiply(uint a, uint b) private pure returns (uint) {
	return a * b; // contract 안에서 선언된 변수에 접근하지 않음.
}

pure이나 view 키워드를 까먹어도 솔리디티 컴파일러 안에서 warning을 남겨주므로 참고하여 키워드를 추가해주면 됨.

pragma solidity >=0.5.0 <0.6.0;

contract ZombieFactory {

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    struct Zombie {
        string name;
        uint dna;
    }

    Zombie[] public zombies;

    function _createZombie (string memory _name, uint _dna) private {
	    zombies.push(Zombie(_name, _dna));
    }
    
    function _generateRandomDna (string memory _str) private view returns (uint) {
	    // fill the function body later
    }

}

Chapter 11: Keccak256 and Typecasting

이더리움은 내부적으로 keccak256이라는 해시를 사용하고 있음.

이걸 사용하면 input 값을 256비트의 숫자로 변경할 수 있음.

수도-랜덤 숫자를 만들 때도 활용하기 좋음.

그래서 _generateRandomDna 함수에서도 임의의 문자열을 넣고 이걸 keccak256을 사용해서 숫자로 변경하는 작엄을 할것임.

keccak256(abi.encodePacked("aaaab"))
//6e91ec6b618bb462a4a6ee5aa2cb0e9cf30f7a052bb467b0ba58b8748c00d2e5

물론 수도-랜덤이 안전한 방법이 아니기는 하지만(동일한 해시 결과물이 나올 수 있으므로) 현 상황에서는 zomebie dna의 안정성이 그렇게 중요하지는 않기 때문에 우리는 사용해도 됨.

솔리디티 안에서도 데이터 타입 변환을 수행할 수 있음.

솔리디티 연산을 수행할 때 서로 다른 자료형으로 연산을 하려고 하면 자동으로 변환되지 않고 에러가 발생함.

반드시 typecast를 사용하여 변환하는 작업을 거쳐야 함.

uint8 a = 5;
uint b = 6;

uint8 c = a * b; // error
uint8 c = a * uint8(b);
pragma solidity >=0.5.0 <0.6.0;

contract ZombieFactory {

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    struct Zombie {
        string name;
        uint dna;
    }

    Zombie[] public zombies;

    function _createZombie (string memory _name, uint _dna) private {
	    zombies.push(Zombie(_name, _dna));
    }
    
    function _generateRandomDna (string memory _str) private view returns (uint) {
	    uint rand = uint(keccak256(abi.encodePacked(_str)));
	    return rand % dnaModulus;
    }

}

Chapter 13: Events

event: contract가 블록체인 프론트엔드 단에서 어떤 일이 일어났을 때 감지할 수 있도록 listening하는 것

// 1. 이벤트를 정의한다.
event IntegersAdded(uint x, uint y, uint result);

function add(uint _x, uint _y) public returns (uint) {
	uint result = _x + _y;
	// 2. 함수가 실행되었을 때 이벤트를 발생시킨다.
	emit IntegersAdded(_x, _y, result);
	return result;
}

이벤트는 트랜잭션이 실행될 때 EVM log라는 것을 통해 블록체인에 기록됨.

이때 프론트 단에서 이걸 읽어올 수 있음. 자바스크립트 예제는 아래와 같다.

// 3. 앱 프론트엔드 단에서 자바스크립트에서 아래와 같이 구현할 수 있다.
contract.events.NewZombie()
  .on('data', (event) => {
    const { zombieId, name, dna } = event.returnValues;
    console.log('새 좀비 생성!', zombieId, name, dna);
  })
  .on('error', console.error);
pragma solidity >=0.5.0 <0.6.0;

contract ZombieFactory {
		event NewZombie(uint zombieId, string name, uint dna);

    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;

    struct Zombie {
        string name;
        uint dna;
    }

    Zombie[] public zombies;

    function _createZombie (string memory _name, uint _dna) private {
	    uint id = zombies.push(Zombie(_name, _dna)) - 1;
	    emit NewZombie(id, _name, _dna);
    }
    
    function _generateRandomDna (string memory _str) private view returns (uint) {
	    uint rand = uint(keccak256(abi.encodePacked(_str)));
	    return rand % dnaModulus;
    }
}

이때 이벤트 파라미터에서는 string을 사용하더라도 memory 키워드가 필요하지 않음.

이벤트는 일종의 로그 같은거여서 저장소의 위치가 아니라 저장된 값 자체를 내려줘야 함.

번외: contract는 프론트에서 어떻게 받는가

solidity 파일을 컴파일하면 ABI와 bytecode가 생성됨.

ABI는 컨트랙트 인터페이스 명세 JSON이다. 프론트에서는 이 정보를 기반으로 어떤 함수와 이벤트가 있는지, 타입이 어떻게 되는지 알 수 있음.

bytecode는 메타마스크 등으로 배포

    import Web3 from 'web3';
    
    // 1. 블록체인 노드에 연결 (MetaMask or Infura 등)
    const web3 = new Web3(window.ethereum); // MetaMask 사용 시
    
    // 2. 컴파일 결과물
    const ABI = [
      {
        "anonymous": false,
        "inputs": [
          { "name": "zombieId", "type": "uint256" },
          { "name": "name",     "type": "string"  },
          { "name": "dna",      "type": "uint256" }
        ],
        "name": "NewZombie",
        "type": "event"
      },
      // ... 함수들도 여기 포함
    ];
    const CONTRACT_ADDRESS = "0x1234...abcd"; // 배포 후 받은 주소
    
    // 3. contract 객체 생성 ← 바로 이게 예제의 contract
    const contract = new web3.eth.Contract(ABI, CONTRACT_ADDRESS);
    
    // 4. 이제 이벤트 구독 가능
    contract.events.NewZombie()
      .on('data', (event) => {
        console.log(event.returnValues);
      });

Chapter 14: Web3.js

이제 자바스크립트에서 contract와 상호작용한다.

이더리움은 Web3.js를 사용하여 소통할 수 있다.

import Web3 from 'web3';

const web3 = new Web3(window.ethereum);
const ABI = [
  {
    "anonymous": false,
    "inputs": [
      { "name": "zombieId", "type": "uint256" },
      { "name": "name",     "type": "string"  },
      { "name": "dna",      "type": "uint256" }
    ],
    "name": "NewZombie",
    "type": "event"
  },
  // ... 함수들도 여기 포함
];
const CONTRACT_ADDRESS = "0x1234...abcd";
const zomebieFactoryContract new web3.eth.Contract(ABI, CONTRACT_ADDRESS);

// 버튼 불러오기
const zombieButton = document.getElementById("ourButton");
const zombieInput = document.getElementById("nameInput");
zombieButton.addEventListener("click", async () => {
	const name = zombieInput.value;
	const accounts = await web3.eth.getAccounts();
	zomebieFactoryContract.methods
		.createRandomZombie(name)
		.send({ from: accounts[0] });
});

// NewZombie 이벤트가 발생되면 이미지를 업데이트함.
zomebieFactoryContract.events.NewZombie()
  .on('data', (event) => {
    const { zombieId, name, dna } = event.returnValues;
    generateZombie(zombieId, name, dna);
  });

const generateZombie = (id, name, dna) => {
	const dnaStr = String(dna).padStart(16, "0");
	return {
		headChoice: dnaStr.substring(0, 2) % 7 + 1,
		eyeChoice: dnaStr.substring(2, 4) % 11 + 1,
		shirtChoice: dnaStr.substring(4, 6) % 6 + 1,
		skinColorChoice: parseInt(dnaStr.substring(6, 8) / 100 * 360),
		eyeColorChoice: parseInt(dnaStr.substring(8, 10) / 100 * 360),
		clothesColorChoice: parseInt(dnaStr.substring(10, 12) / 100 * 360),
		zombieName: name,
	}
}

send vs call

contract 안에서의 메서드를 호출하는 방식으로는 send와 call이 있다.

send: 트랜잭션을 실제로 발생시켜서 블록체인의 상태를 변경하는 것

call: 단순하게 contract 안에 있는 내용물을 조회하는 것. 트랜잭션이 발생하지 않음.

```jsx
zombieFactoryContract.methods
	.createRandomZombie(name)
	.send({ from: accounts[0] });

const zombies = await zombieFactoryContract.methods
												.getZombies().call();
```

0개의 댓글