Web3.py Invalid Address 오류: EIP-55 체크섬 완벽 해결 가이드

어두운 공간에 놓인 컴퓨터 모니터와 키보드. 모니터 왼쪽에는 코드 편집기(VS Code)에 Web3.py 코드가 열려 있고, 오른쪽에는 노란색 블록체인 아이콘이 중앙에 크게 표시된 창이 띄워져 있다. 이는 'Invalid Address' 오류 해결 가이드에 대한 시각적 자료로, 블록체인 개발과 관련된 기술적인 문제를 다루는 분위기를 연출한다. 책상 위에는 필기도구와 커피잔이 놓여 있다.

Web3.py로 스마트 컨트랙트와 상호작용할 때 가장 빈번하게 마주치는 오류 중 하나가 바로 "invalid address" 오류에요. 이 오류는 주소 형식이 이더리움 표준을 따르지 않을 때 발생하는데, 특히 EIP-55 체크섬 주소 형식과 관련이 깊어요. 오늘은 이 문제를 완벽하게 해결하는 방법을 실전 코드와 함께 알아보겠어요.


개발 환경 세팅 (5분 완성)


먼저 필요한 패키지를 설치하고 기본 환경을 구성해요.

pip install web3 python-dotenv eth-account


프로젝트 구조를 다음과 같이 설정해요:

project/
├── .env
├── contracts/
│   └── SimpleToken.sol
├── abi/
│   └── SimpleToken.json
└── main.py


환경 변수 파일(.env)을 생성해요:

INFURA_URL=https://mainnet.infura.io/v3/YOUR_PROJECT_ID
PRIVATE_KEY=your_private_key_here
CONTRACT_ADDRESS=0x...


Invalid Address 오류 재현하기


먼저 오류가 발생하는 상황을 살펴보겠어요. 다음 코드는 전형적인 invalid address 오류를 발생시켜요:

from web3 import Web3
import json

# Web3 연결
w3 = Web3(Web3.HTTPProvider('https://mainnet.infura.io/v3/YOUR_KEY'))

# 잘못된 주소 형식들
wrong_addresses = [
    "0x5b38da6a701c568545dcfcb03fcb875f56beddc4",  # 소문자 주소
    "5B38Da6a701c568545dCfcB03FcB875f56beddC4",    # 0x 빠진 주소
    "0x5B38Da6a701c568545dCfcB03FcB875f56beddC",   # 길이 부족
]

# ABI 로드
with open('abi/SimpleToken.json', 'r') as file:
    abi = json.load(file)

# 오류 발생 코드
for addr in wrong_addresses:
    try:
        contract = w3.eth.contract(address=addr, abi=abi)
        print(f"성공: {addr}")
    except ValueError as e:
        print(f"오류 발생: {addr}")
        print(f"오류 메시지: {e}\n")


위 코드를 실행하면 다음과 같은 오류 메시지를 볼 수 있어요:

오류 발생: 0x5b38da6a701c568545dcfcb03fcb875f56beddc4
오류 메시지: {'code': -32602, 'message': 'invalid address'}


원인 분석: EIP-55 체크섬이란?


이더리움은 주소의 무결성을 검증하기 위해 EIP-55 체크섬 인코딩을 사용해요. 이는 주소의 대소문자를 특정 패턴으로 변환하여 타이핑 실수를 방지하는 메커니즘이에요.


체크섬 주소의 특징:

  • 40자의 16진수 문자열
  • 대소문자가 섞여 있음
  • 주소의 해시값을 기반으로 대소문자 결정


체크섬 검증 원리를 코드로 이해해보겠어요:

from web3 import Web3
import hashlib

def manual_checksum_verification(address):
    """EIP-55 체크섬 검증 과정을 수동으로 구현"""
    
    # 0x 제거하고 소문자로 변환
    address = address.lower().replace('0x', '')
    
    # Keccak-256 해시 계산
    hash_object = Web3.keccak(text=address)
    hash_hex = hash_object.hex()
    
    # 체크섬 주소 생성
    checksum = '0x'
    for i in range(len(address)):
        if hash_hex[i] in '89abcdef':  # 해시값이 8 이상이면 대문자
            checksum += address[i].upper()
        else:
            checksum += address[i]
    
    return checksum

# 테스트
test_addr = "0x5b38da6a701c568545dcfcb03fcb875f56beddc4"
checksum_addr = manual_checksum_verification(test_addr)
print(f"원본: {test_addr}")
print(f"체크섬: {checksum_addr}")


해결 방법 1: toChecksumAddress() 사용


Web3.py는 주소를 체크섬 형식으로 변환하는 내장 함수를 제공해요:

from web3 import Web3
import json
from eth_account import Account

class SmartContractManager:
    def __init__(self, provider_url):
        self.w3 = Web3(Web3.HTTPProvider(provider_url))
        
        if not self.w3.is_connected():
            raise Exception("Web3 연결 실패")
    
    def safe_contract_init(self, address, abi_path):
        """안전한 컨트랙트 초기화"""
        
        # 주소 전처리
        if not address.startswith('0x'):
            address = '0x' + address
        
        # 주소 길이 확인
        if len(address) != 42:
            raise ValueError(f"잘못된 주소 길이: {len(address)}")
        
        # 체크섬 주소로 변환
        try:
            checksum_address = self.w3.to_checksum_address(address)
        except Exception as e:
            raise ValueError(f"체크섬 변환 실패: {e}")
        
        # ABI 로드
        with open(abi_path, 'r') as file:
            abi = json.load(file)
        
        # 컨트랙트 객체 생성
        contract = self.w3.eth.contract(
            address=checksum_address, 
            abi=abi
        )
        
        return contract

# 사용 예제
manager = SmartContractManager('https://mainnet.infura.io/v3/YOUR_KEY')

# 다양한 형식의 주소 테스트
test_addresses = [
    "0x5b38da6a701c568545dcfcb03fcb875f56beddc4",  # 소문자
    "5B38Da6a701c568545dCfcB03FcB875f56beddC4",    # 0x 없음
    "0X5B38DA6A701C568545DCFCB03FCB875F56BEDDC4",  # 대문자
]

for addr in test_addresses:
    try:
        contract = manager.safe_contract_init(addr, 'abi/Token.json')
        print(f"✓ 성공: {contract.address}")
    except Exception as e:
        print(f"✗ 실패: {addr} - {e}")


해결 방법 2: 주소 유효성 검증 유틸리티


더 강력한 주소 검증 시스템을 구축해보겠어요:

from web3 import Web3
from typing import Optional, Union
import re

class AddressValidator:
    """이더리움 주소 유효성 검증 클래스"""
    
    def __init__(self):
        self.w3 = Web3()
        self.address_pattern = re.compile(r'^(0x)?[0-9a-fA-F]{40}$')
    
    def is_valid_format(self, address: str) -> bool:
        """주소 형식 검증"""
        return bool(self.address_pattern.match(address))
    
    def normalize_address(self, address: str) -> Optional[str]:
        """주소 정규화 및 체크섬 적용"""
        
        # 빈 문자열 체크
        if not address:
            return None
        
        # 공백 제거
        address = address.strip()
        
        # 0x 접두사 처리
        if not address.startswith('0x'):
            address = '0x' + address
        
        # 형식 검증
        if not self.is_valid_format(address):
            return None
        
        try:
            # 체크섬 주소로 변환
            return self.w3.to_checksum_address(address)
        except:
            return None
    
    def validate_and_convert(self, address: str) -> dict:
        """종합 검증 결과 반환"""
        
        result = {
            'original': address,
            'is_valid': False,
            'checksum_address': None,
            'errors': []
        }
        
        # 빈 값 체크
        if not address:
            result['errors'].append('주소가 비어있어요')
            return result
        
        # 길이 체크
        clean_addr = address.replace('0x', '').replace('0X', '')
        if len(clean_addr) != 40:
            result['errors'].append(f'주소 길이 오류: {len(clean_addr)}자')
            return result
        
        # 16진수 체크
        try:
            int(clean_addr, 16)
        except ValueError:
            result['errors'].append('올바른 16진수가 아니에요')
            return result
        
        # 체크섬 변환
        normalized = self.normalize_address(address)
        if normalized:
            result['is_valid'] = True
            result['checksum_address'] = normalized
        else:
            result['errors'].append('체크섬 변환 실패')
        
        return result

# 사용 예제
validator = AddressValidator()

test_cases = [
    "0x5b38da6a701c568545dcfcb03fcb875f56beddc4",
    "5b38da6a701c568545dcfcb03fcb875f56beddc4",
    "0x5b38da6a701c568545dcfcb03fcb875f56bedd",  # 짧음
    "0xGGGGda6a701c568545dcfcb03fcb875f56beddc4",  # 잘못된 문자
    "",  # 빈 문자열
]

for addr in test_cases:
    result = validator.validate_and_convert(addr)
    print(f"\n입력: {addr}")
    print(f"유효: {result['is_valid']}")
    if result['checksum_address']:
        print(f"체크섬: {result['checksum_address']}")
    if result['errors']:
        print(f"오류: {', '.join(result['errors'])}")


실전 스마트 컨트랙트 연동


이제 실제 토큰 컨트랙트와 상호작용하는 완전한 예제를 만들어보겠어요:

from web3 import Web3
from web3.middleware import geth_poa_middleware
from eth_account import Account
import json
import os
from dotenv import load_dotenv

load_dotenv()

class TokenInteraction:
    """ERC-20 토큰 상호작용 클래스"""
    
    def __init__(self, network='mainnet'):
        self.network = network
        self.setup_connection()
        
    def setup_connection(self):
        """네트워크 연결 설정"""
        
        if self.network == 'mainnet':
            rpc_url = os.getenv('MAINNET_RPC')
        elif self.network == 'goerli':
            rpc_url = os.getenv('GOERLI_RPC')
        else:
            raise ValueError(f"지원하지 않는 네트워크: {self.network}")
        
        self.w3 = Web3(Web3.HTTPProvider(rpc_url))
        
        # POA 네트워크용 미들웨어 (테스트넷)
        if self.network != 'mainnet':
            self.w3.middleware_onion.inject(geth_poa_middleware, layer=0)
        
        if not self.w3.is_connected():
            raise Exception("Web3 연결 실패")
        
        print(f"연결 성공: {self.network}")
        print(f"블록 번호: {self.w3.eth.block_number}")
    
    def load_contract(self, contract_address: str, abi_path: str):
        """컨트랙트 로드 (주소 검증 포함)"""
        
        # 주소 정규화
        if not contract_address.startswith('0x'):
            contract_address = '0x' + contract_address
        
        # 체크섬 적용
        try:
            checksum_address = self.w3.to_checksum_address(contract_address)
        except Exception as e:
            raise ValueError(f"잘못된 컨트랙트 주소: {e}")
        
        # ABI 로드
        with open(abi_path, 'r') as f:
            abi = json.load(f)
        
        # 컨트랙트 객체 생성
        contract = self.w3.eth.contract(address=checksum_address, abi=abi)
        
        # 컨트랙트 코드 확인 (배포되었는지)
        code = self.w3.eth.get_code(checksum_address)
        if code == b'':
            raise ValueError(f"해당 주소에 컨트랙트가 없어요: {checksum_address}")
        
        return contract
    
    def safe_address_param(self, address: str) -> str:
        """함수 파라미터용 주소 검증"""
        
        # None 체크
        if address is None:
            raise ValueError("주소가 None이에요")
        
        # 문자열 타입 체크
        if not isinstance(address, str):
            address = str(address)
        
        # 체크섬 변환
        return self.w3.to_checksum_address(address)
    
    def get_balance(self, token_contract, owner_address: str) -> dict:
        """토큰 잔액 조회"""
        
        # 주소 검증
        owner = self.safe_address_param(owner_address)
        
        try:
            # 잔액 조회
            balance = token_contract.functions.balanceOf(owner).call()
            
            # decimals 조회
            decimals = token_contract.functions.decimals().call()
            
            # 심볼 조회
            symbol = token_contract.functions.symbol().call()
            
            # 실제 잔액 계산
            actual_balance = balance / (10 ** decimals)
            
            return {
                'address': owner,
                'balance_wei': balance,
                'balance': actual_balance,
                'symbol': symbol,
                'decimals': decimals
            }
            
        except Exception as e:
            raise Exception(f"잔액 조회 실패: {e}")

# 사용 예제
token_manager = TokenInteraction('mainnet')

# USDT 컨트랙트 (메인넷)
usdt_address = "0xdac17f958d2ee523a2206206994597c13d831ec7"
user_address = "0x5b38da6a701c568545dcfcb03fcb875f56beddc4"

try:
    # 컨트랙트 로드
    usdt = token_manager.load_contract(usdt_address, 'abi/USDT.json')
    
    # 잔액 조회
    balance_info = token_manager.get_balance(usdt, user_address)
    
    print(f"\n토큰: {balance_info['symbol']}")
    print(f"잔액: {balance_info['balance']:.6f}")
    print(f"주소: {balance_info['address']}")
    
except Exception as e:
    print(f"오류 발생: {e}")


프론트엔드 연동 (React + ethers.js)


웹 애플리케이션에서도 동일한 문제가 발생할 수 있어요. React와 ethers.js를 사용한 해결 방법을 살펴보겠어요:

// utils/addressValidator.js
import { ethers } from 'ethers';

export const validateAndChecksum = (address) => {
  try {
    // 빈 값 체크
    if (!address || address.trim() === '') {
      return { valid: false, error: '주소가 비어있어요' };
    }
    
    // 0x 접두사 추가
    let normalized = address.trim();
    if (!normalized.startsWith('0x')) {
      normalized = '0x' + normalized;
    }
    
    // ethers.js의 getAddress는 자동으로 체크섬 적용
    const checksumAddress = ethers.utils.getAddress(normalized);
    
    return {
      valid: true,
      address: checksumAddress,
      original: address
    };
    
  } catch (error) {
    return {
      valid: false,
      error: error.message || '유효하지 않은 주소에요',
      original: address
    };
  }
};

// 컨트랙트 연결 훅
export const useContract = (contractAddress, abi) => {
  const [contract, setContract] = useState(null);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const initContract = async () => {
      try {
        // 주소 검증
        const validation = validateAndChecksum(contractAddress);
        if (!validation.valid) {
          throw new Error(validation.error);
        }
        
        // Provider 설정
        const provider = new ethers.providers.Web3Provider(window.ethereum);
        const signer = provider.getSigner();
        
        // 컨트랙트 초기화
        const contractInstance = new ethers.Contract(
          validation.address,
          abi,
          signer
        );
        
        setContract(contractInstance);
        setError(null);
        
      } catch (err) {
        setError(err.message);
        setContract(null);
      }
    };
    
    if (contractAddress && abi) {
      initContract();
    }
    
  }, [contractAddress, abi]);
  
  return { contract, error };
};


테스트넷 배포 및 검증


Goerli 테스트넷에서 실제로 테스트해보는 과정이에요:

from web3 import Web3
from eth_account import Account
import json

class ContractDeployer:
    """스마트 컨트랙트 배포 및 검증"""
    
    def __init__(self, private_key, rpc_url):
        self.w3 = Web3(Web3.HTTPProvider(rpc_url))
        self.account = Account.from_key(private_key)
        
    def deploy_and_verify(self, bytecode, abi):
        """컨트랙트 배포 후 주소 검증"""
        
        # 트랜잭션 구성
        Contract = self.w3.eth.contract(abi=abi, bytecode=bytecode)
        
        # 가스 추정
        gas_estimate = Contract.constructor().estimate_gas({
            'from': self.account.address
        })
        
        # 배포 트랜잭션 생성
        tx = Contract.constructor().build_transaction({
            'from': self.account.address,
            'gas': gas_estimate,
            'gasPrice': self.w3.eth.gas_price,
            'nonce': self.w3.eth.get_transaction_count(self.account.address),
        })
        
        # 서명 및 전송
        signed_tx = self.account.sign_transaction(tx)
        tx_hash = self.w3.eth.send_raw_transaction(signed_tx.rawTransaction)
        
        # 영수증 대기
        receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash)
        
        # 배포된 주소 검증
        deployed_address = receipt.contractAddress
        checksum_address = self.w3.to_checksum_address(deployed_address)
        
        print(f"배포 완료: {checksum_address}")
        print(f"트랜잭션: {tx_hash.hex()}")
        
        return checksum_address


보안 고려사항


주소 처리 시 반드시 고려해야 할 보안 사항들이에요:

  1. Zero Address 체크: 0x0000...0000 주소로의 전송 방지
  2. Contract vs EOA 구분: 컨트랙트 주소와 일반 주소 구분
  3. Reentrancy 방지: 외부 호출 시 주소 검증
  4. Access Control: 관리자 주소 하드코딩 금지

def security_checks(w3, address):
    """보안 검증 함수"""
    
    # 체크섬 적용
    safe_address = w3.to_checksum_address(address)
    
    # Zero address 체크
    if safe_address == '0x0000000000000000000000000000000000000000':
        raise ValueError("Zero address는 사용할 수 없어요")
    
    # 컨트랙트 여부 확인
    code = w3.eth.get_code(safe_address)
    is_contract = len(code) > 0
    
    return {
        'address': safe_address,
        'is_contract': is_contract,
        'is_eoa': not is_contract
    }


흔한 에러와 해결법


1. ENS 도메인 처리

def resolve_ens_or_address(w3, input_string):
    """ENS 도메인 또는 주소 처리"""
    
    # .eth로 끝나면 ENS
    if input_string.endswith('.eth'):
        address = w3.ens.address(input_string)
        if address:
            return w3.to_checksum_address(address)
        raise ValueError(f"ENS 도메인을 찾을 수 없어요: {input_string}")
    
    # 일반 주소
    return w3.to_checksum_address(input_string)


2. 멀티체인 주소 처리

def multichain_address_handler(chain, address):
    """체인별 주소 처리"""
    
    chains = {
        'ethereum': Web3.to_checksum_address,
        'bsc': Web3.to_checksum_address,
        'polygon': Web3.to_checksum_address,
    }
    
    if chain in chains:
        return chains[chain](address)
    
    raise ValueError(f"지원하지 않는 체인: {chain}")


메인넷 체크리스트


메인넷 배포 전 반드시 확인해야 할 사항들이에요:

  • 모든 주소에 체크섬 적용 확인
  • Zero address 체크 로직 구현
  • 주소 타입 검증 (EOA vs Contract)
  • 테스트넷에서 충분한 테스트
  • 가스비 최적화 확인
  • 에러 핸들링 구현
  • 로깅 시스템 구축

중요 경고: 메인넷 배포 전 반드시 전문 감사를 받으세요. 실제 자금을 다룰 때는 극도로 신중해야 해요. 모든 책임은 사용자 본인에게 있어요.


마치며


Invalid address 오류는 단순해 보이지만 블록체인 개발에서 자주 마주치는 중요한 문제에요. Web3.py의 to_checksum_address() 함수를 적절히 활용하고, 체계적인 주소 검증 시스템을 구축하면 안정적인 디앱을 개발할 수 있어요.


테스트넷에서 충분히 테스트한 후 메인넷에 적용하는 것을 잊지 마세요. 블록체인 트랜잭션은 되돌릴 수 없으니 항상 신중하게 접근해야 해요.


이더리움 스마트컨트랙트 101: Solidity로 Hello World 구현하는 방법