상세 컨텐츠

본문 제목

Digging deeper into Solidity's syntax - LearnWeb3 번역

Blockchain

by 0xRobert 2025. 10. 5. 15:03

본문

Digging deeper into Solidity's syntax

솔리디티 입문 강의에서는 변수, 데이터 유형, 함수, 루프, 조건문, 배열 등 솔리디티의 기본 구문을 살펴보았습니다. 이번 강의에서는 솔리디티의 구문과 기타 기능을 더 깊이 있게 탐구하며, 계약과 더 많은 프로젝트를 구축해 나가면서 중요하게 다룰 내용들을 계속해서 알아보겠습니다.

Mappings

솔리디티의 매핑은 다른 프로그래밍 언어의 해시맵이나 사전과 유사하게 작동합니다. 키-값 쌍으로 데이터를 저장하는 데 사용됩니다.

매핑은 다음 구문을 사용하여 생성됩니다: mapping(keyType ⇒ valueType)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract Mapping {
    // Mapping from address to uint
    mapping(address => uint) public myMap;

    function get(address _addr) public view returns (uint) {
        // Mapping always returns a value.
        // If the value was never set, it will return the default value.
        // The default value for uint is 0
        return myMap[_addr];
    }

    function set(address _addr, uint _i) public {
        // Update the value at this address
        myMap[_addr] = _i;
    }

    function remove(address _addr) public {
        // Reset the value to the default value.
        delete myMap[_addr];
    }
}

중첩 매핑도 생성할 수 있습니다. 여기서 매핑의 각 값은 다른 매핑을 가리킵니다. 이를 위해 valueType을 매핑 자체로 설정합니다.

contract NestedMappings {
    // Mapping from address => (mapping from uint to bool)
    mapping(address => mapping(uint => bool)) public nestedMap;

    function get(address _addr1, uint _i) public view returns (bool) {
        // You can get values from a nested mapping
        // even when it is not initialized
        // The default value for a bool type is false
        return nestedMap[_addr1][_i];
    }

    function set(
        address _addr1,
        uint _i,
        bool _boo
    ) public {
        nestedMap[_addr1][_i] = _boo;
    }

    function remove(address _addr1, uint _i) public {
        delete nestedMap[_addr1][_i];
    }
}

열거형

Enum은 열거 가능(Enumerable)을 의미합니다. 이는 일련의 상수(멤버라고 함)에 대해 사람이 읽을 수 있는 이름을 포함하는 사용자 정의 유형입니다. 일반적으로 변수가 미리 정의된 몇 가지 값 중 하나만을 가질 수 있도록 제한하는 데 사용됩니다. 이는 사람이 읽을 수 있는 상수를 추상화한 것에 불과하므로, 실제로 내부적으로는 uint로 표현됩니다.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract Enum {
    // Enum representing different possible shipping states
    enum Status {
        Pending,
        Shipped,
        Accepted,
        Rejected,
        Canceled
    }

    // Declare a variable of the type Status
    // This can only contain one of the predefined values
    Status public status;

    // Since enums are internally represented by uints
    // This function will always return a uint
    // Pending = 0
    // Shipped = 1
    // Accepted = 2
    // Rejected = 3
    // Canceled = 4
    // Value higher than 4 cannot be returned
    function get() public view returns (Status) {
        return status;
    }

    // Pass the desired Status enum value as a uint
    function set(Status _status) public {
        status = _status;
    }

    // Update status enum value to a specific enum member, in this case, to the Canceled enum value
    function cancel() public {
        status = Status.Canceled; // Will set status = 4
    }
}

구조체

구조체 개념은 많은 고수준 프로그래밍 언어에 존재합니다. 관련 데이터를 한데 묶어 사용자 정의 데이터 유형을 정의하는 데 사용됩니다.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract TodoList {
    // Declare a struct which groups together two data types
    struct TodoItem {
        string text;
        bool completed;
    }

    // Create an array of TodoItem structs
    TodoItem[] public todos;

    function createTodo(string memory _text) public {
        // There are multiple ways to initialize structs

        // Method 1 - Call it like a function
        todos.push(TodoItem(_text, false));

        // Method 2 - Explicitly set its keys
        todos.push(TodoItem({ text: _text, completed: false }));

        // Method 3 - Initialize an empty struct, then set individual properties
        TodoItem memory todo;
        todo.text = _text;
        todo.completed = false;
        todos.push(todo);
    }

    // Update a struct value
    function update(uint _index, string memory _text) public {
        todos[_index].text = _text;
    }

    // Update completed
    function toggleCompleted(uint _index) public {
        todos[_index].completed = !todos[_index].completed;
    }
}

뷰 함수와 순수 함수

지금까지 작성한 함수 중 일부는 함수 헤더에 뷰(view) 또는 순수(pure) 키워드 중 하나를 명시하는 것을 눈치챘을 것입니다. 이는 함수의 특정 동작을 나타내는 특수 키워드입니다.

값을 반환하는 게터 함수는 뷰 또는 순수로 선언할 수 있습니다.

  • 뷰: 상태 값을 변경하지 않는 함수
  • 퓨어: 상태 값을 변경하지 않으며 상태 값을 읽지도 않는 함수
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract ViewAndPure {
    // Declare a state variable
    uint public x = 1;

    // Promise not to modify the state (but can read state)
    function addToX(uint y) public view returns (uint) {
        return x + y;
    }

    // Promise not to modify or read from state
    function add(uint i, uint j) public pure returns (uint) {
        return i + j;
    }
}

수정자

수정자는 함수 호출 전후에 실행될 수 있는 코드입니다. 특정 함수에 대한 접근 제한, 입력 매개변수 검증, 특정 유형의 공격 방어 등에 흔히 사용됩니다.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract Modifiers {
    address public owner;

    constructor() {
        // Set the contract deployer as the owner of the contract
        owner = msg.sender;
    }

    // Create a modifier that only allows a function to be called by the owner
    modifier onlyOwner() {
        require(msg.sender == owner, "You are not the owner");

        // Underscore is a special character used inside modifiers
        // Which tells Solidity to execute the function the modifier is used on
        // at this point
        // Therefore, this modifier will first perform the above check
        // Then run the rest of the code
        _;
    }

    // Create a function and apply the onlyOwner modifier on it
    function changeOwner(address _newOwner) public onlyOwner {
        // We will only reach this point if the modifier succeeds with its checks
        // So the caller of this transaction must be the current owner
        owner = _newOwner;
    }
}

이벤트

이벤트는 계약이 이더리움 블록체인에 로깅을 수행할 수 있게 합니다. 특정 계약에 대한 로그는 나중에 파싱되어 프론트엔드 인터페이스 업데이트를 수행하는 데 활용될 수 있습니다. 일반적으로 프론트엔드 인터페이스가 특정 이벤트를 수신하여 사용자 인터페이스를 업데이트하거나, 저렴한 저장 형태로 사용하기 위해 활용됩니다.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract Events {
    // Declare an event which logs an address and a string
    event TestCalled(address sender, string message);

    function test() public {
        // Log an event
        emit TestCalled(msg.sender, "Someone called test()!");
    }
}

생성자

생성자는 계약이 처음 배포될 때 실행되는 선택적 함수입니다. 다른 함수와 달리 생성자는 계약 배포 후에는 수동으로 다시 호출할 수 없으므로 단 한 번만 실행됩니다. 계약 배포 전에 생성자에 인수를 전달할 수도 있습니다.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract X {
    string public name;

    // You will need to provide a string argument when deploying the contract
    constructor(string memory _name) {
        // This will be set immediately when the contract is deployed
        name = _name;
    }
}

계약 상속

상속은 한 계약이 다른 계약의 속성과 메서드를 물려받는 절차입니다. Solidity는 다중 상속을 지원합니다. 계약은 is 키워드를 사용하여 다른 계약을 상속할 수 있습니다.

자식 계약에 의해 재정의될 수 있는 함수를 가진 부모 계약은 가상 함수로 선언되어야 합니다.

부모 함수를 재정의할 자식 계약은 override 키워드를 사용해야 합니다.

부모 계약들이 동일한 이름의 메서드나 속성을 공유하는 경우 상속 순서가 중요합니다.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

/* Graph of inheritance
  A
 /  \
B    C
|\  /|
| \/ |
| /\ | 
D    E

*/

contract A {
    // Declare a virtual function foo() which can be overridden by children
    function foo() public pure virtual returns (string memory) {
        return "A";
    }
}

contract B is A {
    // Override A.foo();
    // But also allow this function to be overridden by further children
    // So we specify both keywords - virtual and override
    function foo() public pure virtual override returns (string memory) {
        return "B";
    }
}

contract C is A {
    // Similar to contract B above
    function foo() public pure virtual override returns (string memory) {
        return "C";
    }
}

// When inheriting from multiple contracts, if a function is defined multiple times, the right-most parent contract's function is used.
contract D is B, C {
    // D.foo() returns "C"
    // since C is the right-most parent with function foo();
    // override (B,C) means we want to override a method that exists in two parents
    function foo() public pure override (B, C) returns (string memory) {
        // super is a special keyword that is used to call functions
        // in the parent contract
        return super.foo();
    }
}

contract E is C, B {
    // E.foo() returns "B"
    // since B is the right-most parent with function foo();
    function foo() public pure override (C, B) returns (string memory) {
        return super.foo();
    }
}

ETH 전송

계약에서 다른 주소로 ETH를 전송하는 방법은 세 가지가 있습니다. 여기서는 각 방법의 장단점을 분석하고 세 가지 중 가장 적합한 방법을 결론 내리겠습니다.

  1. send 함수 사용

send 함수는 지정된 양의 이더를 주소로 전송합니다. 이 저수준 함수는 실행에 필요한 가스(gas)를 2300만 제공하며, 이 양은 이벤트 발생에만 충분합니다. 실행에 더 많은 가스가 필요하면 실패합니다. send 함수는 실패 시 예외를 발생시키지 않고 false를 반환합니다. 따라서 항상 send 결과값을 확인하고 실패 시 수동으로 처리해야 합니다.

send 함수 사용

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract SendEther {
    function sendEth(address payable _to) public payable {
        // Just forward the ETH received in this payable function
        // to the given address
        uint amountToSend = msg.value;

        bool sent = _to.send(amountToSend);
        require(sent == true, "Failed to send ETH");
    }
}

  1. call 함수 사용

call 함수는 이더를 전송하고 남은 가스를 모두 전달하는 저수준 함수입니다. 즉, call을 사용하면 이더를 전송하고 2300 가스 이상이 필요한 더 복잡한 작업을 실행할 수 있습니다. 그러나 send와 마찬가지로, 전송이 실패해도 call은 자동으로 트랜잭션을 롤백하지 않습니다. 대신 false를 반환합니다. 따라서 항상 결과를 확인하고 실패 사례를 수동으로 처리해야 합니다. 또한 call은 남은 가스를 모두 전달하기 때문에 신중하게 사용하지 않으면 재진입 공격(re-entrancy attack)이 발생할 수 있습니다.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract SendEther {
    function sendEth(address payable _to) public payable {
        // Just forward the ETH received in this payable function
        // to the given address
        uint amountToSend = msg.value;
        // call returns a bool value specifying success or failure
        (bool success, bytes memory data) = _to.call{value: msg.value}("");
        require(success == true, "Failed to send ETH");
    }
}
  1. 전송 함수 사용

전송 함수는 또한 지정된 양의 이더를 주소로 보냅니다. send나 call과 달리, 전송은 전송이 실패할 경우 자동으로 예외를 발생시키고 트랜잭션을 되돌립니다. 이는 결과를 수동으로 확인하거나 실패 사례를 처리할 필요가 없기 때문에 전송을 더 안전하고 사용하기 쉽게 만듭니다. 그러나 send와 마찬가지로, 전송도 실행을 위해 2300 가스의 할당량만 제공합니다.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract SendEther {
    function sendEth(address payable _to) public payable {
        // Just forward the ETH received in this payable function
        // to the given address
        uint amountToSend = msg.value;

        // Use the transfer method to send the ETH.
        _to.transfer(msg.value);
    }
}

.transfer 메서드를 사용하면 코드를 더 간단하고 직관적으로 만들 수 있습니다. .transfer 메서드는 잠재적인 재진입 공격을 방지하는 데 도움이 되므로 더 안전하다고 여겨집니다.

** 참고: .transfer를 사용할 때는 2300 가스만 전달하여 재진입 공격을 방지한다는 점을 반드시 인지해야 합니다. 수신자 계약이 특정 작업을 수행해야 하는 경우 이 가스량이 충분하지 않을 수 있습니다. **

ETH 수신

개인 키(예: MetaMask)로 제어되는 외부 소유 계정(EOA)에서 ETH를 수신하는 경우, 모든 ETH 전송을 자동으로 수락할 수 있으므로 특별한 조치가 필요하지 않습니다.

그러나 직접 ETH 전송을 수신할 수 있도록 계약을 작성하는 경우, 아래 함수 중 최소한 하나를 포함해야 합니다:

  • receive() external payable
  • fallback() external payable

receive()는 msg.data가 빈 값일 때 호출되며, 그 외의 경우에는 fallback()이 사용됩니다.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract ReceiveEther {
    /*
    Which function is called, fallback() or receive()?

           send Ether
               |
         msg.data is empty?
              / \
            yes  no
            /     \
receive() exists?  fallback()
         /   \
        yes   no
        /      \
    receive()   fallback()
    */

    // Function to receive Ether. msg.data must be empty
    receive() external payable {}

    // Fallback function is called when msg.data is not empty
    fallback() external payable {}

    function getBalance() public view returns (uint) {
        return address(this).balance;
    }
}

다른 계약 호출

계약은 다른 계약의 인스턴스에서 함수를 호출하는 방식으로 다른 계약을 호출할 수 있습니다(예: A.foo(x, y, z)). 이를 위해서는 계약에 존재하는 함수를 알려주는 A에 대한 인터페이스가 필요합니다. Solidity의 인터페이스는 헤더 파일처럼 동작하며, 프론트엔드에서 계약을 호출할 때 사용해 온 ABI와 유사한 역할을 합니다. 이를 통해 계약은 외부 계약 호출 시 함수 인자와 반환값을 인코딩 및 디코딩하는 방법을 알 수 있습니다.

💡
참고: 사용하는 인터페이스는 반드시 포괄적일 필요는 없습니다. 즉, 외부 계약에 존재하는 모든 함수를 포함할 필요는 없으며, 향후 호출할 가능성이 있는 함수만 포함하면 됩니다.

외부 ERC20 계약이 존재한다고 가정하고, 우리 계약에서 특정 주소의 잔액을 확인하기 위해 balanceOf 함수를 호출하고자 합니다.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

interface MinimalERC20 {
    // Just include the functions we are interested in
    // in the interface
    function balanceOf(address account) external view returns (uint256);
}

contract MyContract {
    MinimalERC20 externalContract;

    constructor(address _externalContract) {
        // Initialize a MinimalERC20 contract instance
        externalContract = MinimalERC20(_externalContract);
    }

    function mustHaveSomeBalance() public {
        // Require that the caller of this transaction has a non-zero
        // balance of tokens in the external ERC20 contract
        uint balance = externalContract.balanceOf(msg.sender);
        require(balance > 0, "You don't own any tokens of external contract");
    }
}

계약서 가져오기

코드 가독성을 유지하기 위해 Solidity 코드를 여러 파일로 분할할 수 있습니다. 다음과 같은 폴더 구조를 가정해 보겠습니다:

├── Import.sol
└── Foo.sol

Foo.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract Foo {
    string public name = "Foo";
}

우리는 Foo를 가져올 수 있으며 Import.sol에서 다음과 같이 사용할 수 있습니다.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

// import Foo.sol from current directory
import "./Foo.sol";

contract Import {
    // Initialize Foo.sol
    Foo public foo = new Foo();

    // Test Foo.sol by getting it's name.
    function getFooName() public view returns (string memory) {
        return foo.name();
    }
}

참고: Hardhat을 사용할 때 npm을 통해 계약을 노드 모듈로 설치한 후 node_modules 폴더에서 계약을 가져올 수도 있습니다. 이러한 경우에도 로컬 가져오기로 간주됩니다. 기술적으로 패키지를 설치할 때 계약을 로컬 머신에 다운로드하기 때문입니다.

라이브러리

라이브러리는 Solidity의 계약과 유사하지만 몇 가지 제한 사항이 있습니다. 라이브러리는 상태 변수를 포함할 수 없으며 ETH를 전송할 수 없습니다. 또한 라이브러리는 네트워크에 단 한 번만 배포됩니다. 즉, 다른 사람이 게시한 라이브러리를 사용하는 경우, 이더리움이 해당 라이브러리가 이미 과거에 다른 사람에 의해 배포되었음을 인식하므로 네트워크에 코드를 배포할 때 가스 비용을 지불할 필요가 없습니다.

일반적으로 라이브러리는 계약에 보조 함수를 추가하는 데 사용됩니다. 솔리디티 세계에서 극히 흔히 사용되는 라이브러리는 SafeMath로, 수학적 연산이 정수 언더플로우나 오버플로우를 일으키지 않도록 보장합니다.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

library SafeMath {
    function add(uint x, uint y) internal pure returns (uint) {
        uint z = x + y;
        // If z overflowed, throw an error
        require(z >= x, "uint overflow");
        return z;
    }
}

contract TestSafeMath {
    function testAdd(uint x, uint y) public pure returns (uint) {
        return SafeMath.add(x, y);
    }
}

 

 

블록체인 트위터 : https://x.com/yonggal05739034 

 

 

관련글 더보기