Solidity

Ch1

Sila 2022. 7. 19. 22:14

이 카테고리는 기본적인 솔리디티 문법과 사용법을 알려주는 cryptozombie를 직접 해보고

 

이를 내가 이해한대로 정리해보기 위해 만들었다.

 

처음 컨트랙트와 솔리디티 버전을 적는 등의 기본적인 작업부터 차근차근 해보자.

 

그 쪽에서 제시해준 솔리디티 코드의 기초적인 활용을 내 방식대로 적당히 패러프레이징 하면서 익혀가는 것을 목적으로 한다.

1. set up

 

1. 솔리디티 파일을 작성할때 가장 먼저 작성해줄 것은 가장 첫 줄에 주석을 달고 spdx~ 하는 것과 현재 솔리디티의 버전이다.

 

2. 그리고 컨트랙트를 선언해준다. 컨트랙트 이름은 ZombieFactory이다.

 

// SPDX-License-Identifier: MIT
pragma solidity >=0.5.0 < 0.6.0;

contract ZombieFactory {

}

 

solidity는 반드시 세미콜론(;)을 붙여 코드를 끝맺어줘야 한다.

 

2. 변수 선언

 

다음으로 컨트랙트안에 상태 변수를 선언할 것이다.

 

상태 변수는 블록 체인에 올라간 컨트랙트 상에 계속해서 존재할 변수로,

 

돈(가스비)를 내고 이 변수들의 상태(값)을 바꿔주는게 스마트 컨트랙트의 기본적인 원리이다.

 

상태 변수를 선언할 때는 데이터 타입을 함께 선언해줘야한다.

 

지금은 음이 아닌 정수라는 의미를 가진 데이터타입 uint의 변수를 선언하고 그 값을 16으로 준다.

 

그 후, 이 변수를 파라미터로 가지는 변수 dnaModulus도 선언해준다.

 

// SPDX-License-Identifier: MIT
pragma solidity >=0.5.0 < 0.6.0;

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

 

js에서 class객체에 여러 key-value쌍을 넣은 적이 있는데 solidity에서는 struct(구조체)가 이를 대신한다.

 

 사용법도 js와 크게 다르지 않다. 다음과 같이 선언해주면 된다.

 

// SPDX-License-Identifier: MIT
pragma solidity >=0.5.0 <0.6.0;

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

 

다음으로 배열을 선언한다.

 

스마트 컨트랙트를 만들어 네트워크에 올리고, 이를 수정하는데는 데이터의 양에 따라 가스비가 든다.

 

따라서 최대한 데이터 사용량을 압축하는 것이 중요해지는데, 그렇기 때문에 솔리디티에서는 배열을 선언할 때,

 

배열의 index수를 미리 지정하기도 한다.

 

예를 들어 Zombie struct들을 배열로 가지는 zombies 배열을 선언하는데, 그 배열의 길이를 3으로 제한하고 싶다면

 

다음과 같이 꺽쇠 괄호 안에 3을 넣고 zombies 배열을 선언한다.

 

// SPDX-License-Identifier: MIT
pragma solidity >=0.5.0 <0.6.0;

contract ZombieFactory {
    uint dnaDigits = 16;
    uint dnaModulus = 10 ** dnaDigits;
    
    struct Zombie {
        string name;
        uint dna;
    }
    
    Zombie[3] public zombies;
    // Zombie struct를 원소로 가지고 길이는 3인 zombies 배열 선언
}

 

다만 지금은 데이터 압축 등의 경제적 측면에서 코드를 찌보는 것은 아니므로 이 제한을 두지는 않는다.

 

숫자를 지우고 괄호만 쓰면 그 길이에 제한이 없는 배열을 선언할 수 있다.

 

Zombie[] public zombies;

 

3. 함수 선언

다음으로 함수를 선언해보자.

 

solidity에서 함수는 선언해줘야 하는 속성값이 좀 있다. public으로 만들어서 외부에서 이 함수에 접근, 호출할 수 있도록 할것인지,

 

매개변수의 소스는 어디인지 (memory인지, calldata 인지 등)에 대해 전부 지정을 해주어야 에러가 나지 않는다.

 

(memory는 우리가 데이터를 일시적으로 저장하고 싶을 때 사용하며, calldata, storage는 데이터를 영구적으로

 

저장하고 싶을 때 사용한다.)

 

 

새로운 Zombie 구조체를 만드는 함수 createZombie를 선언하는데, 이를 아무나 호출할 수 없도록 private 속성을 부여할 것이다.

 

이 때, 구조체의 속성인 name, dna의 값을 매개변수로 주는데, 이 매개변수는 memory 속성을 준다.

 

private 함수는 함수명 앞에 언더바(_)를 붙이는게 관례라고 한다.

 

// SPDX-License-Identifier: MIT
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));
    }
}

 

이 이외에도 리턴값, 함수의 컨트랙트 데이터 접근 여부에 따라 속성을 추가적으로 붙여주어야 할 때가 있다.

 

예를 들어, 좀비의 이름을 매개변수로 받아, 이를 해시화해 숫자로 만든 후,

 

16 이하의 자릿수를 가지는 난수를 생성하는 함수를 작성하고 싶다고 하자.

 

이 함수는 string 을 매개 변수로 받고, 외부에서는 접근할 수 없도록 설정하고 싶으므로 private 속성을 붙인다. (이게 지금까지 한 거)

 

거기에 더해 이 함수는 return으로 uint타입의 변수를 준다.

 

또, '좀비의 이름' (name) 데이터에 접근해야 하되,

 

이를 수정하진 않고 그저 읽어 오기만 하므로 view 속성을 가진다.

 

데이터를 읽지조차 않는 함수의 경우 pureview대신 쓰면 된다.

 

이를 솔리디티 코드로 나타내면 다음과 같다. (함수 이름은 원본과 마찬가지로 _generateRandomDna로 한다.

 

// SPDX-License-Identifier: MIT
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) {
        // string 매개 변수, private 속성(private), view 속성(view), uint 타입의 리턴값(returns (uint))
        uint rand = uint(keccak256(abi.encodePacked(_str)));
        // 매개 변수로 받은 _str을 abi.encodePacked로 형 변환 후, 내장 함수 keccak256을 통해 해시화한다.
        return rand % dnaModulus;
        // 16자리 미만의 자릿수를 갖도록 변경해 리턴
    }
}

 

rand % dnaModulus 라는 계산이 어떻게 자릿수를 조절 할 수 있는지 잠시 짚고 넘어가자면

 

우리가 한 수를 어떤 수로 나누든 그 나머지를 구하면 항상 '나누는 수' 보다는 작은 값이 나온다.

 

예를 들어 어떤 수든, 크기가 얼마나 크든,  100으로 나누면 그 나머지는 항상 100보다 작은 0~99 사이의 값이 나온다.

 

이걸 자릿수만 바꿔서 이용한게 dnaModulus인데,

 

dnaModulus(10000000000000000)를 가지고 어떤 수를 나누든 이보다 작은 ( = 16자리 수 이하의) 

 

0~ 9999999999999999 사이의 값이 나오므로 결과값은 반드시 16 이하의 자릿수를 가지는 수가 나오는 것.

 

 

이제 방금 선언한 함수들을 묶어서 한 번에 실행해주는 함수를 선언해준다.

 

사용자 입장에서 내 좀비의 이름만 입력하면

 

(name이 memory 속성을 가지는 이유가 여기 있다. 따로 저장된 걸 불러오는게 아니라, 사용자가 입력한 값을 바로 사용하기 때문)

 

그걸 이용해 dna(숫자)를 만들고,

 

그걸로 Zombie struct를 만들어서  배열에 push 해주는 작업을 알아서 해주는 코드를 추가해주는 것이다.

 

// SPDX-License-Identifier: MIT
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 _name, uint _dna) private {
        zombies.push(Zombie(_name, _dna));
    }
    
    function _generateRandomDna(string memory _str) private view returns (uint) {
        // string 매개 변수, private 속성(private), view 속성(view), uint 타입의 리턴값(returns (uint))
        uint rand = uint(keccak256(abi.encodePacked(_str)));
        // 매개 변수로 받은 _str을 내장 함수 keccak256을 통해 해시화한다.
        return rand % dnaModulus;
        // 16자리 미만의 자릿수를 갖도록 변경해 리턴
    }
    
    function createRandomZombie(string memory _name) public {
        uint randDna = _generateRandomDna(_name);
        _createZombie(_name, randDna);
    }
}

 

4. 이벤트 선언

솔리디티의 이벤트는 js에서 addEventListener에 대응하는 요소라고 생각하면 된다.

 

컨트랙트에서 특정 사건이 발생할 경우, 이를 감지해 미리 정해둔 대응하는 액션을 취한다.

 

(addEventListener에서 콜백 함수가 발동하는 것과 비슷하게)

 

사용자가 createRandomZombie 함수를 호출해 _createZombie함수가 발동할 경우 이벤트가 발동하게끔 하고 싶다면

 

다음과 같이 이벤트를 선언 후, 함수 내부에 이벤트를 넣어주면 된다.

 

// SPDX-License-Identifier: MIT
pragma solidity >=0.5.0 <0.6.0;

contract ZombieFactory {

    event NewZombie(uint zombieId, string name, uint dna);
    // NewZombbie 이벤트 선언. 이 이벤트는 이름과, dna에 추가로 id 값을 가져야 한다.
    // 여기서 id 값은 zombies배열의 index값이다.
    
    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;
        // push method를 실행한 후의 리턴 값의 -1 한 값이 푸시 된 요소의 index가 된다.
        // 이해가 잘 안간다면 브라우저 콘솔을 열어서 push 메소드를 실행해보면 좋다.
        emit NewZombie(id, _name, _dna);
    }
    
    function _generateRandomDna(string memory _str) private view returns (uint) {
        // string 매개 변수, private 속성(private), view 속성(view), uint 타입의 리턴값(returns (uint))
        uint rand = uint(keccak256(abi.encodePacked(_str)));
        // 매개 변수로 받은 _str을 내장 함수 keccak256을 통해 해시화한다.
        return rand % dnaModulus;
        // 16자리 미만의 자릿수를 갖도록 변경해 리턴
    }
    
    function createRandomZombie(string memory _name) public {
        uint randDna = _generateRandomDna(_name);
        _createZombie(_name, randDna);
    }
}

 

이렇게 event 앞에 emit을 붙이고 인자값을 넣어 이벤트를 호출하면

 

_createZombie 함수가 실행될 때 newZombie 이벤트가 발동된다.

 

바꿔 말하면 앱에게 _createZombie함수가 실행되었다는 것을 알려준다.

 

앱에서는 이를 인식해 subscribe, on 메소드 등과 연동해 화면을 다시 렌더링하는 등의 작업을 할 수 있다. (#24)

'Solidity' 카테고리의 다른 글

Cryptozombie - Ch1. Making Zombie Factory  (2) 2023.12.03
Ch 3  (0) 2022.08.06
Ch2  (0) 2022.07.24