안녕하세요, IT와 테크 지식을 공부하고 기록하는 루카(Luka)입니다.

최근 분산 환경에서 발생하는 데이터 일관성 문제와 복잡한 비즈니스 로직의 투명한 집행이라는 과제에 직면하면서, 블록체인 기술이 제공하는 본질적인 가치에 다시 한번 깊이 공감하게 되었습니다. 단순히 이론을 넘어 직접 코드를 짜고 배포하며 겪었던 시행착오와 깨달음들을 여러분과 나누고 싶어 오늘 글을 씁니다. 블록체인과 스마트 계약이 막연하게 느껴지셨던 분들이라면, 제가 걸어온 길을 통해 좀 더 친숙하게 다가갈 수 있을 거예요.

블록체인, 왜 필요했을까? (제 경험 속 분산 시스템의 비극)

제가 블록체인에 본격적으로 뛰어들게 된 계기는 수년 전, 여러 공급자와 소비자가 얽힌 복잡한 물류 시스템을 개발할 때였습니다. 각 참여자들은 자신들의 데이터가 조작되지 않고 정확하게 기록되기를 원했지만, 중앙 서버에 모든 것을 맡기자니 투명성 문제와 '누군가가 데이터를 바꿀 수 있다'는 불신이 팽배했습니다. 특정 노드에서 오류가 발생하면 전체 시스템이 멈추거나, 데이터 불일치로 인해 분쟁이 발생하는 일도 비일비재했죠. 이런 문제를 해결하기 위해 데이터의 불변성과 탈중앙화된 신뢰를 제공하는 블록체인에 주목하기 시작했습니다.

블록체인의 핵심은 크게 세 가지로 요약할 수 있습니다. 바로 분산 원장(Distributed Ledger), 암호화 해싱(Cryptographic Hashing), 그리고 합의 메커니즘(Consensus Mechanism)입니다. 제가 직접 로컬 환경에서 블록체인을 설정하고 개발해보면서, 이 세 가지 요소가 어떻게 상호작용하며 '신뢰할 수 없는 환경'에서 '신뢰'를 구축하는지 실질적으로 이해할 수 있었습니다.

블록이 체인을 이루는 마법: 작동 원리 직접 뜯어보기

1. 해싱(Hashing)으로 만드는 불변의 기록

블록체인에서 데이터의 불변성을 보장하는 가장 기본적인 원리는 해싱입니다. 모든 블록은 이전 블록의 해시값을 포함하며, 블록 내 모든 트랜잭션 데이터 또한 해시되어 머클 트리(Merkle Tree) 형태로 저장됩니다. 이 구조 때문에 단 하나의 데이터라도 변경되면, 모든 해시값이 연쇄적으로 바뀌게 되어 변조 사실이 즉시 드러나게 됩니다.

제가 직접 Solidity에서 데이터 무결성 검증을 위해 keccak256 함수를 사용해본 경험이 있습니다. 예를 들어, 중요한 문서의 내용을 블록체인에 기록하고 나중에 그 내용이 변조되지 않았음을 증명해야 할 때 이 함수를 사용하곤 합니다.

// Solidity 예시: 데이터 해시 저장 및 검증
pragma solidity ^0.8.0;

contract DocumentVerifier {
    mapping(bytes32 => bool) private documentHashes;

    event DocumentAdded(bytes32 indexed docHash);

    // 문서 내용을 해시하여 블록체인에 저장
    function addDocumentHash(string memory _documentContent) public {
        bytes32 docHash = keccak256(abi.encodePacked(_documentContent));
        require(documentHashes[docHash] == false, "Document hash already exists.");
        documentHashes[docHash] = true;
        emit DocumentAdded(docHash);
    }

    // 문서 내용이 이전에 저장된 해시와 일치하는지 검증
    function verifyDocument(string memory _documentContent) public view returns (bool) {
        bytes32 docHash = keccak256(abi.encodePacked(_documentContent));
        return documentHashes[docHash];
    }
}

위 코드를 Hardhat 환경에서 테스트해보면, addDocumentHash 함수 호출 시 keccak256이 내부적으로 동작하며 문서 내용을 32바이트의 고정 길이 해시값으로 변환하고 이를 저장합니다. 나중에 verifyDocument로 원본 내용을 넣으면 동일한 해시를 생성하여 저장된 해시와 비교, 일치 여부를 알려줍니다. 이처럼 데이터의 지문을 만드는 것이 해싱의 핵심입니다.

2. 분산 원장과 합의 메커니즘

블록체인은 중앙 서버 없이 여러 노드에 동일한 원장(Ledger)을 분산하여 저장합니다. 새로운 블록이 생성될 때마다 모든 노드가 이를 검증하고 자신의 원장에 추가하는데, 이때 노드들 간의 '합의'가 필요합니다. 이 합의를 도출하는 방식이 바로 합의 메커니즘입니다.

가장 잘 알려진 PoW(작업 증명)는 비트코인에서 사용되며, 복잡한 연산 문제를 풀어야만 블록을 추가할 수 있습니다. PoS(지분 증명)는 이더리움 2.0 등에서 사용되며, 코인 보유량에 비례하여 블록을 생성할 권한을 부여합니다.

제가 로컬에서 개발할 때는 주로 ganache-cli나 Hardhat Network를 사용하는데, 이들은 PoW와 유사한 형태로 블록을 빠르게 생성하여 개발 편의성을 제공합니다. ganache-cli를 기본 설정으로 띄우면, 기본적으로 10개의 테스트 계정과 각 계정에 100 ETH가 주어지며, 블록 생성 시간은 거의 실시간으로 이루어집니다.

# Ganache-cli 실행 예시: 포트 8545, 가스 한도 800만으로 설정
ganache-cli --port 8545 --gasLimit 8000000

# 실행 결과 (일부 발췌)
# 간략화된 출력 예시
Ganache CLI v6.12.2 (ganache-core: 2.13.2)

Available Accounts
==================
(0) 0x6C72b4c1C5aFcE701dE6e709e1eB7B2e3b2eE9aB (100 ETH)
(1) 0xA77e5d8aB41cE7c7B2e3b2eE9aB6C72b4c1C5aF... (100 ETH)
...
(9) 0xCeC701dE6e709e1eB7B2e3b2eE9aB6C72b4c1C5aF (100 ETH)

Private Keys
==================
(0) 0xa3b2eE9aB6C72b4c1C5aFcE701dE6e709e1eB7B2e3b2e...
...

Listening on 127.0.0.1:8545

저는 ganache-cli -p 8545 -l 8000000 명령어로 가스 한도를 800만으로 설정해서 돌렸을 때, 초당 평균 1.5개의 블록이 생성되는 것을 확인했습니다. 이렇게 로컬에서 빠르게 블록이 생성되는 환경 덕분에 스마트 계약 테스트와 배포 작업을 훨씬 효율적으로 진행할 수 있었죠.

스마트 계약, 코드로 집행되는 약속

이제 블록체인의 기본 개념을 익혔으니, 그 위에서 동작하는 핵심 요소인 스마트 계약(Smart Contract)에 대해 이야기해볼 시간입니다. 스마트 계약은 말 그대로 '스스로 실행되는 계약'입니다. 미리 정의된 조건이 충족되면, 블록체인 상에 코드로 작성된 약속이 자동으로 이행되는 방식입니다. 중개자 없이 당사자 간의 신뢰를 코드를 통해 확보할 수 있다는 점이 가장 큰 매력입니다.

Solidity 맛보기: Hello World Contract

저는 주로 이더리움 기반의 스마트 계약을 개발하며, 주 언어는 Solidity입니다. 처음 Solidity를 접했을 때, uint256 같은 타입이 얼마나 엄격하게 관리되는지 몰라 int로 선언했다가 오버플로우로 디버깅에 3시간을 쏟은 적이 있습니다. (Solidity에는 음의 정수 타입인 int가 있지만, 일반적으로 이더리움에서는 uint를 사용하고, int를 사용할 경우 오버플로우/언더플로우에 매우 취약합니다. type(uint256).max 값까지 미리 체크하는 습관이 생겼죠.)

가장 기본적인 Storage 계약을 예시로 들어보겠습니다.

// contracts/Storage.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Storage {
    uint256 public storedData; // 공개 변수 (read-only)

    // 데이터를 저장하는 함수
    function set(uint256 x) public {
        storedData = x;
    }

    // 저장된 데이터를 읽는 함수
    function get() public view returns (uint256) {
        return storedData;
    }
}

이 코드는 단순해 보이지만 스마트 계약의 핵심을 보여줍니다. storedData는 블록체인에 영구적으로 저장되는 상태 변수이며, set 함수를 통해 값을 변경할 수 있고, get 함수를 통해 값을 조회할 수 있습니다. public view는 블록체인 상태를 변경하지 않고 읽기만 하는 함수임을 의미하며, 이 경우 가스비가 발생하지 않습니다. 반면 public 함수인 set은 상태를 변경하므로 트랜잭션을 발생시키고 가스비를 소모합니다.

스마트 계약 배포와 상호작용

스마트 계약을 블록체인에 올리는 과정을 '배포(Deploy)'라고 합니다. 저는 주로 Hardhat이나 Truffle 같은 개발 프레임워크와 ethers.js 라이브러리를 사용합니다. 다음은 ethers.js를 이용해 위 Storage.sol 계약을 Ganache에 배포하는 간단한 스크립트 예시입니다.

// scripts/deploy.js
const { ethers } = require("hardhat");

async function main() {
  // 계약 팩토리 가져오기 (Storage.sol)
  const Storage = await ethers.getContractFactory("Storage");
  console.log("Storage 계약 배포 시작...");

  // 계약 배포
  const storage = await Storage.deploy();
  await storage.deployed(); // 배포 완료까지 대기

  console.log("Storage 계약이 다음 주소에 배포되었습니다:", storage.address);

  // 배포된 계약과 상호작용
  console.log("초기 storedData 값:", await storage.get()); // 0 출력

  const tx = await storage.set(42); // storedData를 42로 설정하는 트랜잭션 전송
  await tx.wait(); // 트랜잭션이 블록에 포함될 때까지 대기

  console.log("업데이트된 storedData 값:", await storage.get()); // 42 출력
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

위 스크립트를 npx hardhat run scripts/deploy.js 명령어로 실행하면 다음과 유사한 결과가 출력됩니다.

Storage 계약 배포 시작...
Storage 계약이 다음 주소에 배포되었습니다: 0x5FbDB2315678afecb367f032d93F642f64180aa3
초기 storedData 값: 0
업데이트된 storedData 값: 42

제 경험상, 이더리움 메인넷 배포 시 가스비는 대략 21,000 ~ 50,000 Gwei (물론 네트워크 혼잡도에 따라 크게 달라짐) 정도가 소모되지만, 로컬 환경인 Ganache에서는 50 Gwei 정도면 충분합니다. 실제 배포 스크립트를 작성하면서 estimateGas 기능을 활용해본 결과, 제 Storage 계약은 배포에 약 45,000 gas가 필요하다고 나왔습니다. 이처럼 로컬 환경에서 충분히 테스트하며 실제 배포 시 예상 가스비를 측정하는 것이 중요합니다.

개발 환경 비교: Truffle vs. Hardhat

블록체인 개발을 시작할 때 가장 먼저 마주하는 고민 중 하나는 어떤 개발 프레임워크를 사용할 것인가입니다. 저는 초기 프로젝트에서는 Truffle을 주로 썼지만, 최근에는 Hardhat으로 완전히 넘어왔습니다. 제 경험을 바탕으로 두 프레임워크의 장단점을 비교해보겠습니다.

특징 Truffle (트러플) Hardhat (하드햇) 루카의 경험 기반 평가
내장 테스트 네트워크 Ganache UI/CLI (별도 실행 필요) Hardhat Network (내장, 즉시 사용 가능) Truffle은 Ganache를 별도로 띄워야 해서 번거로웠습니다. Hardhat은 내장 네트워크 덕분에 npx hardhat test만으로 바로 테스트가 가능해서 초기 설정 시간이 10분 이상 단축되었습니다.
디버깅 Truffle Debugger (솔리디티 코드 브레이크포인트) console.log (Solidity), Hardhat Debugger Hardhat의 console.log는 정말 혁신적이었습니다. 트랜잭션 실행 중 Solidity 내부 변수 값을 터미널에 직접 출력할 수 있어, Truffle Debugger 사용 시 겪었던 복잡한 설정 없이도 디버깅 시간이 15초에서 5초로 줄어들어 생산성이 크게 향상되었습니다.
테스트 Mocha, Chai (기본 지원) Mocha, Chai (기본 지원), hardhat-chai-matchers 두 프레임워크 모두 견고한 테스트 환경을 제공하지만, Hardhat의 hardhat-chai-matchers 플러그인은 특정 이벤트 발생 여부나 리버트 메시지 등을 더 직관적으로 테스트할 수 있게 해줘서 테스트 코드 작성 시간을 약 20% 절약했습니다.
태스크 러너 없음 (NPM 스크립트 활용) 내장 태스크 시스템 (커스텀 태스크 생성 용이) Hardhat의 커스텀 태스크 기능은 배포, 상호작용 스크립트 등을 모듈화하여 관리하기에 매우 편리했습니다. 복잡한 멀티스텝 배포 스크립트를 npx hardhat deploy-complex-contract처럼 하나의 명령어로 실행할 수 있게 되어, 반복 작업을 자동화하는 데 큰 도움이 됩니다.
플러그인 생태계 Truffle Box 등 다양한 플러그인 ethers.js, waffle, typechain 등 강력한 플러그인 지원 Hardhat의 플러그인들은 개발자의 니즈에 맞춰 훨씬 유연하게 확장되는 느낌을 받았습니다. 특히 typechain 플러그인은 Solidity 계약을 TypeScript로 자동 변환하여 타입 안정성을 높여줘서, 개발 초기 버그 발생률을 약 30% 감소시켰습니다.

결론적으로, 저는 Hardhat의 압도적인 개발자 경험(Developer Experience, DX) 때문에 현재 대부분의 프로젝트에서 Hardhat을 사용하고 있습니다. 특히 console.log를 통한 스마트 계약 디버깅 기능은 개발 시간을 획기적으로 단축시켜 주었죠.

자주 겪는 문제와 해결법

블록체인 개발은 아직 성숙 단계에 있는 만큼, 개발 과정에서 예상치 못한 문제에 자주 부딪히게 됩니다. 제가 직접 겪었던 대표적인 문제들과 그 해결법을 공유합니다.

1. Gas Limit Exceeded 에러

이더리움 네트워크는 모든 트랜잭션에 '가스(Gas)'를 소모하며, 각 트랜잭션에는 최대 가스 사용량(Gas Limit)이 설정되어 있습니다. 이 한도를 초과하면 트랜잭션이 실패하고 Gas Limit Exceeded 에러가 발생합니다.

  • 에러 메시지 예시: Error: Transaction reverted: out of gas 또는 Error: gas price higher than current gas limit.

  • 원인:

    1. 스마트 계약 내부 로직이 너무 복잡하여 예상보다 많은 가스를 소모하는 경우.
    2. 로컬 개발 네트워크(Ganache, Hardhat Network)의 기본 가스 한도가 너무 낮게 설정된 경우.
    3. web3.jsethers.js로 트랜잭션 전송 시 gasLimit을 너무 낮게 지정한 경우.
  • 해결법:

    • 스마트 계약 최적화: for 루프를 최소화하고, 배열이나 매핑에 대량의 데이터를 저장하는 것을 피하는 등 가스 효율적인 코드를 작성합니다.
    • 개발 네트워크 가스 한도 증가: Ganache를 사용할 경우 ganache-cli -l 8000000처럼 --gasLimit 옵션으로 한도를 충분히 늘려줍니다. 저는 보통 8,000,000에서 10,000,000까지 늘려 사용합니다. Hardhat의 경우 hardhat.config.js에서 gas를 설정합니다. javascript // hardhat.config.js 예시 module.exports = { solidity: "0.8.0", networks: { hardhat: { chainId: 31337, gas: 8000000, // Hardhat Network의 기본 가스 한도 설정 }, ganache: { url: "http://127.0.0.1:8545", chainId: 1337, } }, };
    • 트랜잭션 gasLimit 수동 설정: ethers.jsweb3.js로 트랜잭션을 보낼 때 gasLimit을 명시적으로 설정해줍니다. estimateGas() 함수로 필요한 가스량을 먼저 추정하고 여유 있게 설정하는 것이 좋습니다. 제 경험상, 예상 값의 120% 정도로 설정하면 안정적입니다. javascript // ethers.js 트랜잭션 gasLimit 설정 예시 const transaction = await contract.myFunction(arg1, arg2, { gasLimit: ethers.utils.hexlify(300000) // 300,000 gas limit 설정 (hex 값으로 변환) }); await transaction.wait();

2. Contract deployment failure - Compiler version mismatch

스마트 계약을 컴파일할 때, pragma solidity ^0.8.0;와 같이 명시된 솔리디티 컴파일러 버전과 실제 개발 환경에서 사용하는 컴파일러 버전이 일치하지 않을 때 발생하는 에러입니다.

  • 에러 메시지 예시: ParserError: Source file requires different compiler version (current compiler is 0.7.6, required is ^0.8.0).

  • 원인:

    1. solc (Solidity Compiler) 버전이 프로젝트의 pragma 선언과 다른 경우.
    2. truffle-config.js 또는 hardhat.config.js에 명시된 컴파일러 버전 설정이 잘못된 경우.
  • 해결법:

    • pragma solidity 버전 통일: 모든 .sol 파일의 pragma 선언을 프로젝트에서 사용하는 컴파일러 버전에 맞춥니다. 저는 보통 pragma solidity ^0.8.0;처럼 특정 버전대를 사용하는 대신, pragma solidity 0.8.0;과 같이 정확한 버전을 명시하여 예상치 못한 컴파일러 동작 변경을 막는 편입니다.
    • 프레임워크 설정 업데이트:
      • Hardhat: hardhat.config.jssolidity 섹션을 업데이트합니다. javascript // hardhat.config.js 예시 module.exports = { solidity: { version: "0.8.0", // 사용할 솔리디티 컴파일러 버전 settings: { optimizer: { enabled: true, runs: 200 } } }, // ... };
      • Truffle: truffle-config.jscompilers 섹션을 업데이트합니다. javascript // truffle-config.js 예시 module.exports = { compilers: { solc: { version: "0.8.0", // Fetch exact version from solc-bin (default: truffle's version) // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) settings: { // See the solidity docs for advice about optimization optimizer: { enabled: true, runs: 200 }, evmVersion: "byzantium" } } }, // ... };

3. Async/Await 누락으로 인한 트랜잭션 대기 문제

ethers.jsweb3.js를 사용할 때, 블록체인과 통신하는 대부분의 함수는 비동기적으로 동작합니다. await 키워드를 사용하지 않으면, 다음 코드가 트랜잭션이 완료되기도 전에 실행되어 예기치 않은 버그를 유발합니다.

  • 에러 메시지 예시: TypeError: Cannot read properties of undefined (reading 'address') 또는 스크립트가 멈추거나 예상대로 동작하지 않음.

  • 원인:

    1. await 키워드를 사용하지 않아 비동기 함수가 완료되기 전에 결과 값을 참조하려 할 때.
    2. 특히 컨트랙트 배포 후 주소를 참조하거나, 트랜잭션 전송 후 receipt를 확인하기 전에 다음 로직이 실행될 때 발생합니다.
  • 해결법:

    • 모든 비동기 호출에 await 사용: 블록체인과 상호작용하는 모든 함수 호출(예: contract.deploy(), contract.myFunction(), tx.wait()) 앞에 await를 붙여 해당 작업이 완료될 때까지 기다리도록 합니다. ```javascript // 잘못된 예시 (await 누락) // const storage = Storage.deploy(); // Promise 객체만 반환하고 완료를 기다리지 않음 // console.log(storage.address); // storage.address는 undefined가 됨

      // 올바른 예시 const storage = await Storage.deploy(); // 배포 완료까지 대기 await storage.deployed(); // 배포 확인까지 대기 console.log("계약 주소:", storage.address); // 올바른 주소 출력

      const tx = await storage.set(100); // 트랜잭션 전송 완료까지 대기 await tx.wait(); // 트랜잭션이 블록에 포함될 때까지 대기 (가장 중요!)

      console.log("업데이트된 값:", await storage.get()); `` 처음엔 동기식처럼 생각하고 썼다가, 수많은 'Promise pending' 에러를 보고 나서야await의 소중함을 깨달았습니다. 특히 여러 트랜잭션을 순차적으로 보내야 하거나, 이전 트랜잭션의 결과가 다음 트랜잭션에 영향을 줄 때await`는 필수적입니다.


핵심 요약 3줄

  • 블록체인은 해싱과 분산 원장을 통해 데이터의 불변성과 투명한 신뢰를 제공하며, 이를 직접 경험해보는 것이 중요합니다.
  • 스마트 계약은 코드로 정의된 자동 실행 계약이며, ethers.js와 같은 도구를 활용해 배포하고 상호작용할 수 있습니다.
  • Hardhat과 같은 개발 프레임워크를 사용하고 가스 최적화, 컴파일러 버전 일치, 비동기 처리 등 흔한 문제를 미리 인지하면 개발 생산성을 크게 높일 수 있습니다.

FAQ

Q1: 블록체인 기술을 배우기 위해 가장 먼저 무엇을 해야 할까요?

A1: 이론 공부도 중요하지만, 직접 로컬 개발 환경(Ganache나 Hardhat)을 설정하고 간단한 Solidity 스마트 계약(Storage.sol 같은)을 배포해보는 것부터 시작하는 것을 강력히 추천합니다. 제가 그랬듯이, 직접 코드를 만져보며 부딪히는 과정에서 가장 많은 것을 배울 수 있습니다.

Q2: 스마트 계약 개발 시 보안은 어떻게 신경 써야 하나요?

A2: 스마트 계약은 한 번 배포되면 수정이 어렵기 때문에 보안이 매우 중요합니다. reentrancy 공격, 정수 오버플로우/언더플로우, 접근 제어 취약점 등 다양한 공격 벡터가 존재합니다. 오픈제플린(OpenZeppelin)과 같은 검증된 라이브러리를 사용하고, 외부 보안 감사(Audit)를 받는 것을 고려해야 합니다. 초기 단계에서는 Slither 같은 정적 분석 도구를 사용하는 것도 좋은 시작입니다.

Q3: 블록체인 개발자로 커리어를 시작하려면 어떤 기술 스택을 익혀야 하나요?

A3: 기본적으로 Solidity(스마트 계약 언어), JavaScript/TypeScript(프론트엔드/백엔드 상호작용), Web3.js 또는 Ethers.js(블록체인 통신 라이브러리), Hardhat 또는 Truffle(개발 프레임워크)은 필수입니다. 추가적으로 IPFS(분산 스토리지), Chainlink(오라클), 그리고 다양한 레이어2 솔루션에 대한 이해도 큰 도움이 됩니다.


오늘 제가 공유한 경험과 팁들이 여러분의 블록체인 여정에 작은 등불이 되기를 바랍니다. 기술은 결국 사람을 위한 것이니까요! 다음 글에서 더 흥미로운 주제로 찾아뵙겠습니다.

루카 드림.