이더리움 디앱을 개발하려면 Solidty 언어에 대해서 무조건 알아야합니다.
솔리디티는
C++
,Python
그리고Javascript
의 영향을 받아,EVM (Ethereum Virtual Machine)
에서 구동되도록 설계된 언어입니다.
EVM
은 이더리움에서 사용되는 가상머신으로,Solidity
나EVM bytecode
로 작성된 스마트 컨트랙트를 실행하는 런타임 환경입니다.더 자세한 내용이 필요하신 분은 솔리디티의 공식 문서를 참고해주세요!
저는 크립토 좀비를 이용해보면서 솔리디티 문법을 익히려고 합니다.
저처럼 처음 하는 사람도 손쉽게 따라할 수 있고, 옆에 문법에 대한 설명도 자세히 나와있어서 입문자에게 좋은 것 같습니다.
Crypto Zombie로 들어가셔서 직접 해보실 수 있어요!
(한국어 버전도 있지만 영어 버전 설명이 더 자세하게 나와있어서, 웬만하면 영문으로 보시는걸 추천드려요!)
첫번째로 고유의 DNA를 가지는 좀비를 랜덤하게 생성하는 컨트랙트를 만들 것이다.
DNA는 16자리의 10진수로 이루어져있고 각 자리수에 따라 여러 특성을 부여한다.
예를 들어, DNA가8356281049284737
이라면 맨 앞 두자리83
은 머리, 다음 두자리56
은 눈 특성을 결정한다.
이제 본격적으로 문법을 배워가면서 좀비 생성 스마트 컨트랙트를 만들어보자!
솔리디티로 작성된 코드들을 보면 항상
pragma solidity
로 시작하는 것을 볼 수 있다.
솔리디티는 지속적으로 업데이트가 되고 있는데 그 버전에 따라 기능이 추가되거나 삭제되는 경우가 있다.버전에 따른 오류를 막기 위해서
pragma
를 사용해서 필요한 컴파일러의 버전을 반드시 명시해줘야 한다.
// ^ 로 특정 버전 명시
// 또는 부등호를 이용해서 여러 버전 명시 가능
pragma solidity ^0.4.19 // 0.4.19 버전 사용
pragma solidity >=0.5.0 <0.6.0 // 0.5.x 버전 사용
contract
란 이더리움 어플리케이션의 기본 구성 단위이며, 솔리디티에서는contract
단위로 코드를 작성한다.
따라서 모든 변수와 함수는contract
안에 속해야 한다.
(자바스크립의class
와 비슷해보인다.)
contract ZombieFactory {
}
State Vaiables
는contract storage
에 영원히 저장되는 변수이다. 즉, 이더리움 블록체인에 저장되는 것이다.
솔리디티에서 정수에 대한 타입은int8
~int256
,uint8
~uint256
이 있으며,int
와uint
는 각각int256
,uint256
의 별칭이다.
변수에 값을 지정할 때는타입 변수명 = 값
의 형태로 적어준다.
contract ZombieFactory {
uint dnaDigits = 16; // 문장 끝은 항상 세미콜론(;)
}
연산자 설명 + 더하기 - 빼기 * 곱하기 / 나누기 % 나머지 ** 제곱
contract ZombieFactory {
uint dnaDigits = 16;
uint dnaModulus = 10 ** 16; // 10의 16제곱 (10^16)
}
struct
는 복잡한 자료형을 만들때 사용한다.
contract ZombieFactory {
...
struct Zombie {
string name;
uint dna;
}
}
솔리디티에서 배열은 두가지 타입이 있다.
fixed
: 길이가 고정된 배열dynamic
: 길이의 제한이 없는 배열
uint[2] fixedArray; // 길이가 2인 uint 배열
string[5] stringArray; // 길이가 5인 string 배열
uint[] dynamicArray; // 길이 제한이 없는 uint 배열
배열 선언할 때 public을 적어주면, 솔리디티가 자동으로 이 배열에 대한
getter method
를 생성해준다.
contract ZombieFactory {
...
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
}
함수의 선언은 아래의 형태로 진행한다.
contract ZombieFactory { ... function createZombie (string memory _name, uint _dna) public { } }
필수는 아니지만 전역변수와 구분하기 위해 함수의 인자를 명시할 때, 앞에
_
를 붙여준다.
함수의 파라미터를 적어줄 때 타입을 지정하는데, 이때 변수의
reference type
의 변수에는memory
를 입력해야 한다.
이렇게 하면 해당 인자의 복사본을memory
에 임시 저장할 수 있고, 함수에서는 복사본을 사용하여 입력한 변수의 값을 변경하지 않고 보존할 수 있다.
따라서reference type
변수를 함수의 인자로 사용할 때는memory
를 같이 적어주어야 한다.
reference type
변수들 :string
,array
,struct
,mapping
call by value vs call by reference
call by value
: 값을 복사하여 그 복사본을 사용, 복사본을 변경해도 기존 변수의 값은 변경 되지 않는다.call by reference
: 값을 직접 호출하여 사용, 변수 값을 변경하면 참조한 기존 변수의 값도 변경된다.
함수의 인자를 명시한 뒤에는
visibility
를 정해주어야 한다.
(명시를 안하더라도 기본적으로public
함수가 된다.)
public
: 누구든 contract의 함수를 불러서 실행할 수 있다.private
: 자신 이외에 다른 사람들이 이 함수를 사용할 수 없다. private 함수명은 보통_
를 맨 앞에 붙여준다.
public
을 사용하면 해당 contract가 공격에 노출되기 때문에, 항상private
으로 함수를 만드는 습관을 들이는 것이 좋다.
그리고 그 함수를 공유할 필요가 있을 때public
으로 바꾸면 된다!위에서 만든 함수를
private
으로 바꾸고 안에 내용을 추가 해보자. (함수명도_
추가!)
contract ZombieFactory {
...
function _createZombie (string memory _name, uint _dna) private {
zombies.push(Zombie(_name, _dna));
}
}
솔리디티에서는 함수를 선언할 때, 함수의
returns (타입)
을 이용해서 return 값의 타입도 명시한다.string hello = "Hello!"; function sayHello() public returns (string) { return hello; }
view
:state
변수를 사용하지만 변경하지 않는 함수
방금 만든sayHello()
함수는state
를 변경하지 않는다. 이런 경우는view
함수로 선언하면 된다.function sayHello() public view returns (string) {
pure
: app에 있는 어떤 데이터도 사용하지 않고 변경하지도 않는 함수(오로지 사용하는 인자에 따라 반환값이 결정됨)function _multiply(uint a, uint b) private pure returns (uint) { return a *b; }
이더리움에서는
SHA3
의 한 버전인keccak256
라는 해쉬 함수가 내장되어있다.
해쉬 함수는 기본적으로 입력값을 랜덤에 가까운 256bit 16진수로 변환한다.
그리고 해시 함수는 입력값의 작은 변화에도 아예 다른 해쉬 값을 도출한다.
우리는 이를 용해서 유사 랜덤 숫자를 만들때 사용할 것이다. (보안 상으로는 좋은 방법이 아님)
keccack256
함수는byte
타입의 값을 입력해줘야 한다.
이를 위해서abi.encodedPacked()
를 사용하여 입력값을byte
타입으로 바꿔준다.keccak256(abi.encodePacked("aaab"));
변수의 타입에 따라 연산이 안되는 경우가 생긴다.
그럴땐 타입 변환을 해주면 된다.uint8 a = 5; uint b = 6; uint8 c = a * b; // 둘의 타입이 다르기 때문에 에러 발생 uint8 c = a * uint8(b); // 타입 변환으로 에러 해결
이제 위에서 배운 내용을 이용해서 좀비의 랜덤 DNA를 반환하는 함수를 만들어보자.
(여기서 사용한ABI
에 대해서 자세히 알고싶은 분은 여기를 참고해주세요.)
contract ZombieFactory {
...
function _generateRandomDna (string memory _name) private view returns (uint) {
uint rand = uint(keccak256(abi.encodePacked(_name)));
return rand % dnaModulus;
}
}
event
는 자바스크립트에서eventListener
와 비슷하다.
이를 이용해서 특정 동작이 발생하는지 기다렸다가, 그 동작이 발생하는 순간 원하는 이벤트를 발생시킬 수 있다.
event
: 이벤트를 정의한다.emit
: 이벤트 발생 시점을 지정한다.좀비가 발생했을 때
NewZombie
라는 이벤트를 발생시켜보자.
contract ZombieFactory {
event NewZombie(uint id, 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);
}
...
}
pragma solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
event NewZombie(uint id, string name, uint dna); // 새로운 좀비가 생성됐을 때의 이벤트 정의
uint dnaDigits = 16; // DNA는 16자리 수
uint dnaModulus = 10 ** dnaDigits; // 16자리 수보다 많은 경우, 16자리 수보다 큰 수는 제외할 때 사용
// Zombie : name, dnan 값을 가짐
struct Zombie {
string name;
uint dna;
}
// zombies : Zombie로 이루어진 배열
Zombie[] public zombies;
// zombie id => user's address : zombie를 소유한 사용자 매핑
mapping (uint => address) public zombieToOwner;
// user's address => number of owned zombies : 사용자가 소유한 좀비 수 매핑
mapping (address => uint) ownerZombieCount;
// 좀비의 name과 dna를 이용해서 좀비 생성하는 함수
function _createZombie(string memory _name, uint _dna) private {
uint id = zombies.push(Zombie(_name, _dna)); // 생성한 좀비는 zombis 배열에 추가, 배열의 인덱스 : 좀비의 id
zombieToOwner[id] = msg.sender; // 생성된 좀비와 현재 사용자를 매핑함
ownerZombieCount[msg.sender]++; // 현재 사용자의 좀비 보유 수를 1 증가 시킴
emit NewZombie(id, _name, _dna); // 새로운 좀비가 생성됐다는 이벤트 발생
}
// 좀비의 이름으로 랜덤한 dna 발생하는 함수
function _generateRandomDna(string memory _name) private view returns (uint) {
uint randomDna = uint(keccak256(abi.encodePacked(_name))); // keccak256으로 유사 난수를 발생시켜 dna 값으로 사용
return randomDna % dnaModulus; // 랜덤 DNA가 16자리가 되도록 dnaModulus를 나눈 나머지를 반환
}
// 랜덤한 새로운 좀비 생성하는 함수
function createRandomZombie(string memory _name) public {
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}