콘텐츠 대표 이미지 - 파이썬으로 NFT 보유자 전용 접근 API 개발하기: Web3 시대의 토큰 게이팅 완벽 가이드
🔐⛓️

파이썬으로 NFT 보유자 전용 접근 API 개발하기: Web3 시대의 토큰 게이팅 완벽 가이드

블록체인 기반 회원제 서비스를 구축하는 실전 개발 가이드

👋 안녕! 오늘은 정말 핫한 주제를 가지고 왔어. 바로 NFT 보유자만 접근할 수 있는 API를 파이썬으로 만드는 방법이야. 요즘 Web3 프로젝트들이 NFT를 일종의 '디지털 회원권'처럼 활용하는 거 많이 봤지? 특정 NFT를 가진 사람만 커뮤니티에 들어갈 수 있거나, 특별한 콘텐츠를 볼 수 있게 하는 거 말이야. 🎫


이런 개념을 '토큰 게이팅(Token Gating)'이라고 하는데, 생각보다 구현이 어렵지 않아. 파이썬과 몇 가지 라이브러리만 있으면 충분히 만들 수 있거든! 이 글에서는 실제로 동작하는 코드와 함께 단계별로 설명해줄게. 💻

NFT 🎨 Token ID: #1234 Owner: 0xABC... API Gateway Verification Layer ✓ Check Ownership 🔐 Grant Access Premium Content Exclusive Data Special Features VIP Services 🎉 Access Granted! NFT Token Gating Flow Blockchain-based Authentication System

🎯 왜 NFT 기반 접근 제어가 필요할까?

전통적인 웹 서비스는 ID/비밀번호나 OAuth 같은 방식으로 사용자를 인증했어. 하지만 Web3 시대에는 조금 다른 접근이 필요해. 🌐


NFT 기반 인증의 장점:


✅ 탈중앙화된 소유권 증명: 블록체인에 기록된 소유권은 위조가 불가능해. 중앙 서버가 해킹당해도 NFT 소유권 자체는 안전하지.


✅ 자동화된 회원 관리: NFT를 판매하거나 전송하면 자동으로 접근 권한이 이동해. 별도의 회원 탈퇴 처리가 필요 없어.


✅ 2차 시장 형성: 사용자들이 NFT를 거래할 수 있어서, 회원권 자체가 자산 가치를 가질 수 있어.


✅ 프로그래밍 가능한 권한: 스마트 컨트랙트로 복잡한 권한 로직을 구현할 수 있어.

실제로 많은 Web3 프로젝트들이 이런 방식을 활용하고 있어. 예를 들어 Bored Ape Yacht Club은 NFT 보유자에게만 특별한 이벤트 참여 권한을 주고, 일부 DAO들은 거버넌스 토큰 보유량에 따라 투표권을 차등 부여하지. 🏛️

🏗️ 시스템 아키텍처 설계하기

본격적으로 코딩에 들어가기 전에, 전체 시스템이 어떻게 동작할지 설계해보자. 📐


Step 1: 사용자 인증 플로우


1️⃣ 사용자가 지갑 주소로 로그인 요청
2️⃣ 서버가 랜덤 메시지(nonce) 생성
3️⃣ 사용자가 개인키로 메시지 서명
4️⃣ 서버가 서명 검증 → 지갑 주소 확인
5️⃣ 블록체인에서 해당 주소의 NFT 보유 여부 확인
6️⃣ JWT 토큰 발급 (NFT 보유 시)

Step 2: API 접근 제어


1️⃣ 클라이언트가 JWT 토큰과 함께 API 요청
2️⃣ 서버가 JWT 검증
3️⃣ 토큰에 포함된 지갑 주소 추출
4️⃣ 실시간으로 NFT 보유 여부 재확인 (선택사항)
5️⃣ 권한 확인 후 데이터 반환

💡 Tip: 매 요청마다 블록체인을 조회하면 느려질 수 있어. 캐싱 전략을 잘 세우는 게 중요해! 예를 들어 Redis에 5분간 NFT 보유 정보를 캐싱하면 성능과 보안의 균형을 맞출 수 있지.

🛠️ 개발 환경 세팅하기

자, 이제 실제로 코드를 작성해볼 시간이야! 먼저 필요한 라이브러리들을 설치해보자. 📦


필수 라이브러리 설치
pip install fastapi uvicorn web3 python-jose[cryptography] passlib python-multipart eth-account pydantic redis

각 라이브러리가 하는 역할을 간단히 설명하면:


라이브러리 역할
FastAPI 고성능 웹 API 프레임워크 🚀
web3.py 이더리움 블록체인과 통신
python-jose JWT 토큰 생성 및 검증
eth-account 지갑 서명 검증
redis 캐싱 및 세션 관리

⚠️ 주의: 실제 프로덕션 환경에서는 Infura나 Alchemy 같은 노드 프로바이더를 사용해야 해. 무료 티어로도 충분히 시작할 수 있으니 걱정 마!

🔐 지갑 서명 검증 구현하기

Web3 인증의 핵심은 지갑 서명 검증이야. 사용자가 정말 해당 지갑의 소유자인지 확인하는 과정이지. 🔑


auth_utils.py - 서명 검증 유틸리티
from eth_account.messages import encode_defunct
from web3 import Web3
import secrets
from datetime import datetime, timedelta
from jose import JWTError, jwt
from typing import Optional

# JWT 설정
SECRET_KEY = "your-secret-key-here-change-in-production"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60

class WalletAuth:
    def __init__(self):
        self.w3 = Web3()
        self.nonce_storage = {}  # 실제로는 Redis 사용 권장
    
    def generate_nonce(self, wallet_address: str) -> str:
        """
        로그인을 위한 랜덤 nonce 생성
        """
        nonce = secrets.token_hex(16)
        # nonce는 5분간 유효
        expiry = datetime.utcnow() + timedelta(minutes=5)
        self.nonce_storage[wallet_address.lower()] = {
            'nonce': nonce,
            'expiry': expiry
        }
        return nonce
    
    def verify_signature(self, wallet_address: str, signature: str, message: str) -> bool:
        """
        지갑 서명 검증
        """
        try:
            # 메시지를 이더리움 서명 형식으로 인코딩
            encoded_message = encode_defunct(text=message)
            
            # 서명으로부터 주소 복구
            recovered_address = self.w3.eth.account.recover_message(
                encoded_message,
                signature=signature
            )
            
            # 복구된 주소와 요청 주소 비교
            return recovered_address.lower() == wallet_address.lower()
        
        except Exception as e:
            print(f"서명 검증 실패: {e}")
            return False
    
    def create_access_token(self, wallet_address: str, nft_contract: str, token_id: int) -> str:
        """
        JWT 액세스 토큰 생성
        """
        expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
        to_encode = {
            "sub": wallet_address.lower(),
            "nft_contract": nft_contract.lower(),
            "token_id": token_id,
            "exp": expire
        }
        encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
        return encoded_jwt
    
    def verify_token(self, token: str) -> Optional[dict]:
        """
        JWT 토큰 검증 및 페이로드 반환
        """
        try:
            payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
            return payload
        except JWTError:
            return None

이 코드의 핵심 포인트를 설명해줄게:


🎲 Nonce 생성: 재사용 공격(replay attack)을 방지하기 위해 매번 새로운 랜덤 값을 생성해. 이 값은 한 번만 사용되고 5분 후 만료돼.


✍️ 서명 검증: encode_defunct로 메시지를 이더리움 표준 형식으로 변환하고, recover_message로 서명한 지갑 주소를 복구해. 이게 요청한 주소와 일치하면 인증 성공!


🎫 JWT 토큰: 인증 성공 후 JWT를 발급해서 이후 요청에서 사용해. 토큰에는 지갑 주소와 NFT 정보가 포함돼 있어.

⛓️ NFT 소유권 확인 구현하기

이제 가장 중요한 부분! 블록체인에서 실제로 NFT를 보유하고 있는지 확인하는 코드야. 🔍


nft_checker.py - NFT 소유권 검증
from web3 import Web3
from typing import Optional, List
import json

# ERC-721 표준 ABI (필요한 함수만)
ERC721_ABI = json.loads('''[
    {
        "constant": true,
        "inputs": [{"name": "_owner", "type": "address"}],
        "name": "balanceOf",
        "outputs": [{"name": "balance", "type": "uint256"}],
        "type": "function"
    },
    {
        "constant": true,
        "inputs": [{"name": "_tokenId", "type": "uint256"}],
        "name": "ownerOf",
        "outputs": [{"name": "owner", "type": "address"}],
        "type": "function"
    },
    {
        "constant": true,
        "inputs": [
            {"name": "_owner", "type": "address"},
            {"name": "_index", "type": "uint256"}
        ],
        "name": "tokenOfOwnerByIndex",
        "outputs": [{"name": "tokenId", "type": "uint256"}],
        "type": "function"
    }
]''')

class NFTChecker:
    def __init__(self, provider_url: str):
        """
        provider_url: Infura, Alchemy 등의 RPC URL
        예: https://mainnet.infura.io/v3/YOUR-PROJECT-ID
        """
        self.w3 = Web3(Web3.HTTPProvider(provider_url))
        
        if not self.w3.is_connected():
            raise Exception("블록체인 노드 연결 실패!")
    
    def check_ownership(self, wallet_address: str, contract_address: str, 
                       token_id: Optional[int] = None) -> bool:
        """
        특정 NFT 소유 여부 확인
        
        Args:
            wallet_address: 확인할 지갑 주소
            contract_address: NFT 컨트랙트 주소
            token_id: 특정 토큰 ID (None이면 컬렉션 내 아무 NFT나 보유하면 OK)
        
        Returns:
            bool: 소유 여부
        """
        try:
            # 주소를 체크섬 형식으로 변환
            wallet_address = self.w3.to_checksum_address(wallet_address)
            contract_address = self.w3.to_checksum_address(contract_address)
            
            # 컨트랙트 인스턴스 생성
            contract = self.w3.eth.contract(
                address=contract_address,
                abi=ERC721_ABI
            )
            
            if token_id is not None:
                # 특정 토큰 ID의 소유자 확인
                owner = contract.functions.ownerOf(token_id).call()
                return owner.lower() == wallet_address.lower()
            else:
                # 컬렉션 내 NFT를 하나라도 보유하고 있는지 확인
                balance = contract.functions.balanceOf(wallet_address).call()
                return balance > 0
        
        except Exception as e:
            print(f"NFT 소유권 확인 실패: {e}")
            return False
    
    def get_owned_tokens(self, wallet_address: str, contract_address: str, 
                        max_tokens: int = 100) -> List[int]:
        """
        지갑이 보유한 모든 토큰 ID 조회
        """
        try:
            wallet_address = self.w3.to_checksum_address(wallet_address)
            contract_address = self.w3.to_checksum_address(contract_address)
            
            contract = self.w3.eth.contract(
                address=contract_address,
                abi=ERC721_ABI
            )
            
            balance = contract.functions.balanceOf(wallet_address).call()
            
            if balance == 0:
                return []
            
            # 보유한 토큰 ID 목록 가져오기
            token_ids = []
            for i in range(min(balance, max_tokens)):
                token_id = contract.functions.tokenOfOwnerByIndex(
                    wallet_address, i
                ).call()
                token_ids.append(token_id)
            
            return token_ids
        
        except Exception as e:
            print(f"토큰 목록 조회 실패: {e}")
            return []
    
    def check_multiple_collections(self, wallet_address: str, 
                                   contract_addresses: List[str]) -> dict:
        """
        여러 NFT 컬렉션에서 소유 여부 확인
        """
        results = {}
        for contract_address in contract_addresses:
            results[contract_address] = self.check_ownership(
                wallet_address, 
                contract_address
            )
        return results

이 코드는 ERC-721 표준을 따르는 모든 NFT에서 작동해. 주요 기능들을 살펴보자:


🎯 핵심 기능 설명

1. check_ownership()
가장 기본적인 함수야. 특정 지갑이 특정 NFT를 보유하고 있는지 확인해. token_id를 지정하면 정확히 그 NFT를, 지정하지 않으면 해당 컬렉션의 아무 NFT나 하나라도 있으면 true를 반환해.


2. get_owned_tokens()
지갑이 보유한 모든 토큰 ID를 가져와. 이건 "당신이 가진 NFT 목록"을 보여줄 때 유용해. 다만 가스비가 들지 않는 읽기 전용 호출이라 걱정 안 해도 돼! 💰


3. check_multiple_collections()
여러 NFT 컬렉션을 동시에 체크해. 예를 들어 "BAYC나 CryptoPunks 중 하나라도 있으면 VIP"같은 로직을 구현할 때 쓰지.

⚡ 성능 최적화 팁: 블록체인 조회는 느릴 수 있어. 실시간으로 매번 확인하는 대신, Redis에 결과를 캐싱하고 주기적으로 업데이트하는 방식을 추천해. 예를 들어 5분마다 백그라운드 작업으로 업데이트하면 사용자 경험이 훨씬 좋아져!

🚀 FastAPI로 API 엔드포인트 구축하기

이제 모든 조각을 합쳐서 실제 API를 만들어보자! FastAPI는 정말 직관적이고 빠르니까 금방 익숙해질 거야. ⚡


main.py - API 서버 메인 파일
from fastapi import FastAPI, Depends, HTTPException, status, Header
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel
from typing import Optional
import os
from dotenv import load_dotenv

from auth_utils import WalletAuth
from nft_checker import NFTChecker

# 환경 변수 로드
load_dotenv()

app = FastAPI(
    title="NFT Gated API",
    description="NFT 보유자 전용 API 서비스",
    version="1.0.0"
)

# 인증 스키마
security = HTTPBearer()

# 유틸리티 인스턴스
wallet_auth = WalletAuth()
nft_checker = NFTChecker(
    provider_url=os.getenv("WEB3_PROVIDER_URL")
)

# NFT 컨트랙트 주소 (환경 변수로 관리)
REQUIRED_NFT_CONTRACT = os.getenv("NFT_CONTRACT_ADDRESS")

# ============= 데이터 모델 =============

class NonceRequest(BaseModel):
    wallet_address: str

class LoginRequest(BaseModel):
    wallet_address: str
    signature: str
    message: str

class TokenResponse(BaseModel):
    access_token: str
    token_type: str = "bearer"

class UserInfo(BaseModel):
    wallet_address: str
    nft_balance: int
    owned_tokens: list

# ============= 인증 엔드포인트 =============

@app.post("/auth/nonce", summary="로그인용 Nonce 생성")
async def get_nonce(request: NonceRequest):
    """
    지갑 주소로 로그인용 nonce를 요청합니다.
    클라이언트는 이 nonce를 개인키로 서명해야 합니다.
    """
    nonce = wallet_auth.generate_nonce(request.wallet_address)
    message = f"재능넷 NFT 인증\n\nNonce: {nonce}\n\n이 서명은 로그인 목적으로만 사용됩니다."
    
    return {
        "message": message,
        "nonce": nonce,
        "wallet_address": request.wallet_address
    }

@app.post("/auth/login", response_model=TokenResponse, summary="지갑 서명으로 로그인")
async def login(request: LoginRequest):
    """
    지갑 서명을 검증하고 NFT 보유 여부를 확인한 후 JWT 토큰을 발급합니다.
    """
    # 1. 서명 검증
    if not wallet_auth.verify_signature(
        request.wallet_address,
        request.signature,
        request.message
    ):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="서명 검증 실패"
        )
    
    # 2. NFT 보유 여부 확인
    has_nft = nft_checker.check_ownership(
        request.wallet_address,
        REQUIRED_NFT_CONTRACT
    )
    
    if not has_nft:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="필요한 NFT를 보유하고 있지 않습니다"
        )
    
    # 3. 보유한 토큰 ID 가져오기
    owned_tokens = nft_checker.get_owned_tokens(
        request.wallet_address,
        REQUIRED_NFT_CONTRACT
    )
    
    # 4. JWT 토큰 생성
    access_token = wallet_auth.create_access_token(
        wallet_address=request.wallet_address,
        nft_contract=REQUIRED_NFT_CONTRACT,
        token_id=owned_tokens[0] if owned_tokens else 0
    )
    
    return TokenResponse(access_token=access_token)

# ============= 보호된 엔드포인트 =============

async def verify_nft_holder(
    credentials: HTTPAuthorizationCredentials = Depends(security)
) -> dict:
    """
    JWT 토큰을 검증하고 NFT 보유 여부를 확인하는 의존성 함수
    """
    token = credentials.credentials
    
    # JWT 검증
    payload = wallet_auth.verify_token(token)
    if not payload:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="유효하지 않은 토큰"
        )
    
    wallet_address = payload.get("sub")
    nft_contract = payload.get("nft_contract")
    
    # 실시간 NFT 보유 확인 (선택사항 - 캐싱 권장)
    has_nft = nft_checker.check_ownership(wallet_address, nft_contract)
    if not has_nft:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="NFT를 더 이상 보유하고 있지 않습니다"
        )
    
    return payload

@app.get("/api/user/info", response_model=UserInfo, summary="사용자 정보 조회")
async def get_user_info(user: dict = Depends(verify_nft_holder)):
    """
    인증된 사용자의 정보를 반환합니다. (NFT 보유자 전용)
    """
    wallet_address = user["sub"]
    nft_contract = user["nft_contract"]
    
    owned_tokens = nft_checker.get_owned_tokens(wallet_address, nft_contract)
    
    return UserInfo(
        wallet_address=wallet_address,
        nft_balance=len(owned_tokens),
        owned_tokens=owned_tokens
    )

@app.get("/api/premium/content", summary="프리미엄 콘텐츠")
async def get_premium_content(user: dict = Depends(verify_nft_holder)):
    """
    NFT 보유자만 접근 가능한 프리미엄 콘텐츠
    """
    return {
        "message": "축하합니다! NFT 보유자 전용 콘텐츠입니다.",
        "data": {
            "exclusive_data": "여기에 특별한 데이터가...",
            "special_features": ["기능1", "기능2", "기능3"],
            "vip_benefits": "VIP 혜택 정보"
        },
        "wallet": user["sub"]
    }

@app.get("/api/public/info", summary="공개 정보")
async def get_public_info():
    """
    누구나 접근 가능한 공개 엔드포인트
    """
    return {
        "message": "이것은 공개 정보입니다",
        "required_nft": REQUIRED_NFT_CONTRACT,
        "description": "해당 NFT를 보유하면 프리미엄 기능에 접근할 수 있습니다"
    }

# ============= 헬스체크 =============

@app.get("/health")
async def health_check():
    """
    서버 상태 확인
    """
    return {
        "status": "healthy",
        "blockchain_connected": nft_checker.w3.is_connected()
    }

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

와! 코드가 좀 길지? 하지만 구조는 단순해. 하나씩 뜯어보자. 🔬


📋 API 엔드포인트 구조

인증 관련 엔드포인트:


• POST /auth/nonce - 로그인용 랜덤 메시지 생성
• POST /auth/login - 서명 검증 후 JWT 발급


보호된 엔드포인트 (NFT 보유자 전용):


• GET /api/user/info - 사용자 정보 조회
• GET /api/premium/content - 프리미엄 콘텐츠


공개 엔드포인트:


• GET /api/public/info - 누구나 접근 가능
• GET /health - 서버 상태 확인

🎓 FastAPI의 장점: Depends() 함수를 사용하면 인증 로직을 재사용하기 정말 쉬워. verify_nft_holder 의존성만 추가하면 자동으로 NFT 보유 여부를 확인해주거든!

⚙️ 환경 설정 및 배포 준비

코드는 완성됐어! 이제 실제로 실행하기 위한 설정을 해보자. 🔧


.env - 환경 변수 파일
# Web3 Provider (Infura, Alchemy 등)
WEB3_PROVIDER_URL=https://mainnet.infura.io/v3/YOUR_PROJECT_ID

# NFT 컨트랙트 주소
NFT_CONTRACT_ADDRESS=0x1234567890123456789012345678901234567890

# JWT 시크릿 키 (반드시 변경!)
JWT_SECRET_KEY=your-super-secret-key-change-this-in-production

# Redis 설정 (선택사항)
REDIS_HOST=localhost
REDIS_PORT=6379

# 서버 설정
API_HOST=0.0.0.0
API_PORT=8000

requirements.txt - 의존성 목록
fastapi==0.104.1
uvicorn[standard]==0.24.0
web3==6.11.3
python-jose[cryptography]==3.3.0
eth-account==0.10.0
pydantic==2.5.0
python-multipart==0.0.6
python-dotenv==1.0.0
redis==5.0.1

이제 서버를 실행해보자!


서버 실행 명령어
# 개발 모드 (자동 리로드)
uvicorn main:app --reload --host 0.0.0.0 --port 8000

# 프로덕션 모드
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4

서버가 실행되면 http://localhost:8000/docs로 접속해봐. FastAPI가 자동으로 생성한 멋진 API 문서를 볼 수 있어! 📚

API Request Flow Client Web3 Wallet 1. Request Nonce API Server FastAPI 2. Generate Nonce Blockchain Ethereum 3. Return Message Sign Message with Private Key 4. Send Signature Verify Signature Check NFT Ownership 5. Query NFT 6. Return Ownership Issue JWT Token ✓ Access Granted 7. Return Token Protected API Premium Content User Dashboard Exclusive Features 8. Access with JWT

🎨 프론트엔드 연동 예제

백엔드만 있으면 뭐해? 실제로 사용자가 접속할 프론트엔드도 필요하지! 간단한 JavaScript 예제를 보여줄게. 💻


frontend.html - 웹 클라이언트 예제
<!DOCTYPE html>
<html>
<head>
    <title>NFT Gated Access</title>
    <script src="https://cdn.ethers.io/lib/ethers-5.7.umd.min.js"></script>
</head>
<body>
    <h1>NFT 보유자 전용 서비스</h1>
    <button id="connectBtn">지갑 연결</button>
    <div id="status"></div>
    <div id="content"></div>

    <script>
        const API_URL = 'http://localhost:8000';
        let provider, signer, walletAddress;

        document.getElementById('connectBtn').addEventListener('click', connectWallet);

        async function connectWallet() {
            try {
                // MetaMask 연결
                if (typeof window.ethereum === 'undefined') {
                    alert('MetaMask를 설치해주세요!');
                    return;
                }

                provider = new ethers.providers.Web3Provider(window.ethereum);
                await provider.send("eth_requestAccounts", []);
                signer = provider.getSigner();
                walletAddress = await signer.getAddress();

                document.getElementById('status').innerHTML = 
                    `연결된 지갑: ${walletAddress}`;

                // 로그인 프로세스 시작
                await login();
            } catch (error) {
                console.error('지갑 연결 실패:', error);
                alert('지갑 연결에 실패했습니다.');
            }
        }

        async function login() {
            try {
                // 1. Nonce 요청
                const nonceResponse = await fetch(`${API_URL}/auth/nonce`, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ wallet_address: walletAddress })
                });
                const nonceData = await nonceResponse.json();

                // 2. 메시지 서명
                const signature = await signer.signMessage(nonceData.message);

                // 3. 로그인 요청
                const loginResponse = await fetch(`${API_URL}/auth/login`, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({
                        wallet_address: walletAddress,
                        signature: signature,
                        message: nonceData.message
                    })
                });

                if (!loginResponse.ok) {
                    const error = await loginResponse.json();
                    alert(`로그인 실패: ${error.detail}`);
                    return;
                }

                const loginData = await loginResponse.json();
                const accessToken = loginData.access_token;

                // 토큰 저장
                localStorage.setItem('access_token', accessToken);

                document.getElementById('status').innerHTML += 
                    '<br>✅ 로그인 성공!';

                // 프리미엄 콘텐츠 로드
                await loadPremiumContent(accessToken);

            } catch (error) {
                console.error('로그인 실패:', error);
                alert('로그인에 실패했습니다.');
            }
        }

        async function loadPremiumContent(token) {
            try {
                const response = await fetch(`${API_URL}/api/premium/content`, {
                    headers: {
                        'Authorization': `Bearer ${token}`
                    }
                });

                if (!response.ok) {
                    throw new Error('콘텐츠 로드 실패');
                }

                const data = await response.json();
                
                document.getElementById('content').innerHTML = `
                    <h2>🎉 프리미엄 콘텐츠</h2>
                    <p>${data.message}</p>
                    <pre>${JSON.stringify(data.data, null, 2)}</pre>
                `;

            } catch (error) {
                console.error('콘텐츠 로드 실패:', error);
            }
        }

        // 페이지 로드 시 저장된 토큰 확인
        window.addEventListener('load', async () => {
            const token = localStorage.getItem('access_token');
            if (token) {
                // 토큰이 있으면 자동으로 콘텐츠 로드
                await loadPremiumContent(token);
            }
        });
    </script>
</body>
</html>

이 코드는 ethers.js를 사용해서 MetaMask와 연동하고, 우리가 만든 API와 통신해. 실제로 동작하는 모습을 보면 정말 신기할 거야! ✨


프론트엔드 동작 순서:


1️⃣ 사용자가 "지갑 연결" 버튼 클릭
2️⃣ MetaMask 팝업이 뜨고 연결 승인
3️⃣ 서버에서 nonce 받아오기
4️⃣ MetaMask로 메시지 서명
5️⃣ 서명과 함께 로그인 요청
6️⃣ JWT 토큰 받아서 localStorage에 저장
7️⃣ 토큰으로 프리미엄 콘텐츠 요청
8️⃣ 화면에 콘텐츠 표시

🔒 보안 고려사항

Web3 개발에서 보안은 정말정말 중요해. 한 번 실수하면 돈이 날아갈 수도 있거든! ⚠️


반드시 지켜야 할 보안 원칙들:


🔐 1. 절대 개인키를 서버에 저장하지 마!
서버는 오직 공개 주소와 서명만 다뤄야 해. 개인키는 사용자의 지갑에만 있어야 하고, 절대 네트워크를 통해 전송되면 안 돼.


🔐 2. Nonce는 반드시 일회용으로!
같은 nonce를 재사용하면 재생 공격(replay attack)에 취약해져. 우리 코드에서는 5분 후 만료되도록 했지만, 더 짧게 설정해도 좋아.


🔐 3. JWT 시크릿 키는 강력하게!
최소 32자 이상의 랜덤 문자열을 사용하고, 절대 코드에 하드코딩하지 마. 환경 변수로 관리해야 해.


🔐 4. HTTPS는 필수!
프로덕션 환경에서는 반드시 HTTPS를 사용해야 해. 서명이 중간에 가로채이면 큰일나거든.


🔐 5. Rate Limiting 적용!
무차별 대입 공격을 막기 위해 API 호출 횟수를 제한해야 해. FastAPI에서는 slowapi 라이브러리를 쓰면 쉽게 구현할 수 있어.

rate_limiter.py - Rate Limiting 예제
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from fastapi import Request

limiter = Limiter(key_func=get_remote_address)

# main.py에 추가
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

# 엔드포인트에 적용
@app.post("/auth/login")
@limiter.limit("5/minute")  # 분당 5회 제한
async def login(request: Request, login_data: LoginRequest):
    # ... 기존 코드

⚡ 성능 최적화 전략

블록체인 조회는 느릴 수 있어. 사용자 경험을 개선하기 위한 최적화 방법들을 알아보자! 🚀


1. Redis 캐싱 구현
cache_manager.py - Redis 캐싱
import redis
import json
from typing import Optional
from datetime import timedelta

class CacheManager:
    def __init__(self, redis_host='localhost', redis_port=6379):
        self.redis_client = redis.Redis(
            host=redis_host,
            port=redis_port,
            decode_responses=True
        )
    
    def cache_nft_ownership(self, wallet_address: str, contract_address: str, 
                           has_nft: bool, ttl_seconds: int = 300):
        """
        NFT 소유권 정보를 캐싱 (기본 5분)
        """
        key = f"nft:{contract_address}:{wallet_address}"
        self.redis_client.setex(
            key,
            timedelta(seconds=ttl_seconds),
            json.dumps({"has_nft": has_nft})
        )
    
    def get_cached_ownership(self, wallet_address: str, 
                            contract_address: str) -> Optional[bool]:
        """
        캐시된 소유권 정보 조회
        """
        key = f"nft:{contract_address}:{wallet_address}"
        cached = self.redis_client.get(key)
        
        if cached:
            data = json.loads(cached)
            return data.get("has_nft")
        return None
    
    def invalidate_cache(self, wallet_address: str, contract_address: str):
        """
        캐시 무효화 (NFT 전송 시 호출)
        """
        key = f"nft:{contract_address}:{wallet_address}"
        self.redis_client.delete(key)

# 사용 예제
cache = CacheManager()

def check_ownership_with_cache(wallet_address: str, contract_address: str) -> bool:
    # 1. 캐시 확인
    cached_result = cache.get_cached_ownership(wallet_address, contract_address)
    if cached_result is not None:
        return cached_result
    
    # 2. 캐시 미스 - 블록체인 조회
    has_nft = nft_checker.check_ownership(wallet_address, contract_address)
    
    # 3. 결과 캐싱
    cache.cache_nft_ownership(wallet_address, contract_address, has_nft)
    
    return has_nft

2. 배치 처리로 다중 조회 최적화

여러 사용자의 NFT를 한 번에 확인해야 할 때는 배치 처리가 효율적이야.


batch_checker.py - 배치 NFT 확인
from typing import List, Dict
import asyncio
from concurrent.futures import ThreadPoolExecutor

class BatchNFTChecker:
    def __init__(self, nft_checker: NFTChecker, max_workers: int = 10):
        self.nft_checker = nft_checker
        self.executor = ThreadPoolExecutor(max_workers=max_workers)
    
    async def check_multiple_owners(self, wallet_addresses: List[str], 
                                   contract_address: str) -> Dict[str, bool]:
        """
        여러 지갑의 NFT 소유 여부를 병렬로 확인
        """
        loop = asyncio.get_event_loop()
        
        tasks = [
            loop.run_in_executor(
                self.executor,
                self.nft_checker.check_ownership,
                wallet,
                contract_address
            )
            for wallet in wallet_addresses
        ]
        
        results = await asyncio.gather(*tasks)
        
        return {
            wallet: result 
            for wallet, result in zip(wallet_addresses, results)
        }

# 사용 예제
batch_checker = BatchNFTChecker(nft_checker)

@app.post("/api/admin/check-batch")
async def check_batch_ownership(wallet_addresses: List[str]):
    """
    관리자용: 여러 지갑의 NFT 보유 여부를 한 번에 확인
    """
    results = await batch_checker.check_multiple_owners(
        wallet_addresses,
        REQUIRED_NFT_CONTRACT
    )
    return results

💡 성능 팁: Web3 Provider를 선택할 때 Alchemy를 추천해. Infura보다 빠르고 안정적이거든. 특히 무료 티어에서도 초당 요청 제한이 넉넉해서 개발하기 좋아!

🎭 다양한 NFT 표준 지원하기

지금까지는 ERC-721만 다뤘는데, 실제로는 다양한 NFT 표준이 있어. 각각 조금씩 다르게 처리해야 해. 🔄


표준 특징 사용 예시
ERC-721 각 토큰이 고유함 CryptoPunks, BAYC
ERC-1155 대체 가능/불가능 혼합 게임 아이템, 티켓
ERC-998 조합 가능한 NFT 캐릭터 + 장비

multi_standard_checker.py - 다중 표준 지원
import json
from enum import Enum

class NFTStandard(Enum):
    ERC721 = "ERC721"
    ERC1155 = "ERC1155"

# ERC-1155 ABI
ERC1155_ABI = json.loads('''[
    {
        "constant": true,
        "inputs": [
            {"name": "_owner", "type": "address"},
            {"name": "_id", "type": "uint256"}
        ],
        "name": "balanceOf",
        "outputs": [{"name": "", "type": "uint256"}],
        "type": "function"
    }
]''')

class MultiStandardNFTChecker:
    def __init__(self, provider_url: str):
        self.w3 = Web3(Web3.HTTPProvider(provider_url))
    
    def detect_standard(self, contract_address: str) -> NFTStandard:
        """
        컨트랙트의 NFT 표준 자동 감지
        """
        contract_address = self.w3.to_checksum_address(contract_address)
        
        # ERC-165 supportsInterface로 확인
        erc165_abi = json.loads('''[{
            "constant": true,
            "inputs": [{"name": "interfaceId", "type": "bytes4"}],
            "name": "supportsInterface",
            "outputs": [{"name": "", "type": "bool"}],
            "type": "function"
        }]''')
        
        try:
            contract = self.w3.eth.contract(
                address=contract_address,
                abi=erc165_abi
            )
            
            # ERC-721 인터페이스 ID: 0x80ac58cd
            is_erc721 = contract.functions.supportsInterface(
                bytes.fromhex('80ac58cd')
            ).call()
            
            if is_erc721:
                return NFTStandard.ERC721
            
            # ERC-1155 인터페이스 ID: 0xd9b67a26
            is_erc1155 = contract.functions.supportsInterface(
                bytes.fromhex('d9b67a26')
            ).call()
            
            if is_erc1155:
                return NFTStandard.ERC1155
        
        except Exception as e:
            print(f"표준 감지 실패: {e}")
        
        return NFTStandard.ERC721  # 기본값
    
    def check_ownership_any_standard(self, wallet_address: str, 
                                    contract_address: str,
                                    token_id: Optional[int] = None) -> bool:
        """
        NFT 표준을 자동 감지하고 소유권 확인
        """
        standard = self.detect_standard(contract_address)
        
        if standard == NFTStandard.ERC721:
            return self._check_erc721(wallet_address, contract_address, token_id)
        elif standard == NFTStandard.ERC1155:
            return self._check_erc1155(wallet_address, contract_address, token_id)
        
        return False
    
    def _check_erc721(self, wallet_address: str, contract_address: str,
                     token_id: Optional[int]) -> bool:
        """ERC-721 소유권 확인"""
        # 이전에 만든 NFTChecker 로직 사용
        pass
    
    def _check_erc1155(self, wallet_address: str, contract_address: str,
                      token_id: int) -> bool:
        """ERC-1155 소유권 확인"""
        if token_id is None:
            raise ValueError("ERC-1155는 token_id가 필수입니다")
        
        wallet_address = self.w3.to_checksum_address(wallet_address)
        contract_address = self.w3.to_checksum_address(contract_address)
        
        contract = self.w3.eth.contract(
            address=contract_address,
            abi=ERC1155_ABI
        )
        
        balance = contract.functions.balanceOf(wallet_address, token_id).call()
        return balance > 0

이제 어떤 NFT 표준이든 자동으로 감지해서 처리할 수 있어! 🎉

🌐 멀티체인 지원하기

이더리움만 지원하면 아쉽잖아? Polygon, BSC, Arbitrum 같은 다른 체인도 지원해보자! 🔗


multichain_config.py - 멀티체인 설정
from enum import Enum
from typing import Dict

class Chain(Enum):
    ETHEREUM = 1
    POLYGON = 137
    BSC = 56
    ARBITRUM = 42161
    OPTIMISM = 10

class MultiChainConfig:
    # 각 체인의 RPC URL
    RPC_URLS: Dict[Chain, str] = {
        Chain.ETHEREUM: "https://mainnet.infura.io/v3/YOUR_KEY",
        Chain.POLYGON: "https://polygon-rpc.com",
        Chain.BSC: "https://bsc-dataseed.binance.org",
        Chain.ARBITRUM: "https://arb1.arbitrum.io/rpc",
        Chain.OPTIMISM: "https://mainnet.optimism.io"
    }
    
    # 각 체인의 블록 탐색기
    EXPLORERS: Dict[Chain, str] = {
        Chain.ETHEREUM: "https://etherscan.io",
        Chain.POLYGON: "https://polygonscan.com",
        Chain.BSC: "https://bscscan.com",
        Chain.ARBITRUM: "https://arbiscan.io",
        Chain.OPTIMISM: "https://optimistic.etherscan.io"
    }

class MultiChainNFTChecker:
    def __init__(self):
        self.checkers: Dict[Chain, NFTChecker] = {}
        
        # 각 체인별 체커 초기화
        for chain, rpc_url in MultiChainConfig.RPC_URLS.items():
            self.checkers[chain] = NFTChecker(rpc_url)
    
    def check_ownership_multichain(self, wallet_address: str,
                                   nft_configs: List[Dict]) -> Dict:
        """
        여러 체인에서 NFT 소유 여부 확인
        
        nft_configs 예시:
        [
            {"chain": Chain.ETHEREUM, "contract": "0x...", "token_id": 123},
            {"chain": Chain.POLYGON, "contract": "0x...", "token_id": None}
        ]
        """
        results = {}
        
        for config in nft_configs:
            chain = config["chain"]
            contract = config["contract"]
            token_id = config.get("token_id")
            
            checker = self.checkers[chain]
            has_nft = checker.check_ownership(
                wallet_address,
                contract,
                token_id
            )
            
            results[f"{chain.name}:{contract}"] = has_nft
        
        return results

# API에 적용
multichain_checker = MultiChainNFTChecker()

@app.post("/auth/login-multichain")
async def login_multichain(request: LoginRequest, chain: Chain):
    """
    특정 체인의 NFT로 로그인
    """
    # 서명 검증 (체인 무관)
    if not wallet_auth.verify_signature(
        request.wallet_address,
        request.signature,
        request.message
    ):
        raise HTTPException(status_code=401, detail="서명 검증 실패")
    
    # 해당 체인에서 NFT 확인
    checker = multichain_checker.checkers[chain]
    has_nft = checker.check_ownership(
        request.wallet_address,
        REQUIRED_NFT_CONTRACT
    )
    
    if not has_nft:
        raise HTTPException(status_code=403, detail="NFT 미보유")
    
    # JWT 발급
    token = wallet_auth.create_access_token(
        wallet_address=request.wallet_address,
        nft_contract=REQUIRED_NFT_CONTRACT,
        token_id=0
    )
    
    return {"access_token": token, "chain": chain.name}

이제 사용자가 어느 체인의 NFT를 가지고 있든 로그인할 수 있어! 🌍

📊 모니터링과 로깅

프로덕션 환경에서는 무슨 일이 일어나는지 추적하는 게 중요해. 제대로 된 로깅 시스템을 구축해보자! 🔍


logger_config.py - 로깅 설정
import logging
from logging.handlers import RotatingFileHandler
import json
from datetime import datetime
from typing import Any, Dict

class JSONFormatter(logging.Formatter):
    """
    JSON 형식으로 로그를 출력하는 포매터
    """
    def format(self, record: logging.LogRecord) -> str:
        log_data = {
            "timestamp": datetime.utcnow().isoformat(),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage(),
            "module": record.module,
            "function": record.funcName,
            "line": record.lineno
        }
        
        # 추가 필드가 있으면 포함
        if hasattr(record, 'wallet_address'):
            log_data['wallet_address'] = record.wallet_address
        if hasattr(record, 'nft_contract'):
            log_data['nft_contract'] = record.nft_contract
        if hasattr(record, 'action'):
            log_data['action'] = record.action
        
        return json.dumps(log_data)

def setup_logger(name: str = "nft_api") -> logging.Logger:
    """
    로거 설정
    """
    logger = logging.getLogger(name)
    logger.setLevel(logging.INFO)
    
    # 콘솔 핸들러
    console_handler = logging.StreamHandler()
    console_handler.setFormatter(JSONFormatter())
    logger.addHandler(console_handler)
    
    # 파일 핸들러 (10MB마다 로테이션, 최대 5개 파일)
    file_handler = RotatingFileHandler(
        'nft_api.log',
        maxBytes=10*1024*1024,
        backupCount=5
    )
    file_handler.setFormatter(JSONFormatter())
    logger.addHandler(file_handler)
    
    return logger

# 로거 인스턴스
logger = setup_logger()

# 사용 예제
def log_authentication_attempt(wallet_address: str, success: bool, 
                               nft_contract: str = None):
    """
    인증 시도 로깅
    """
    extra = {
        'wallet_address': wallet_address,
        'action': 'authentication',
        'success': success
    }
    
    if nft_contract:
        extra['nft_contract'] = nft_contract
    
    if success:
        logger.info("Authentication successful", extra=extra)
    else:
        logger.warning("Authentication failed", extra=extra)

# API에 적용
@app.post("/auth/login")
async def login(request: LoginRequest):
    try:
        # ... 기존 로직
        
        # 성공 로깅
        log_authentication_attempt(
            wallet_address=request.wallet_address,
            success=True,
            nft_contract=REQUIRED_NFT_CONTRACT
        )
        
        return {"access_token": token}
    
    except Exception as e:
        # 실패 로깅
        log_authentication_attempt(
            wallet_address=request.wallet_address,
            success=False
        )
        logger.error(f"Login error: {str(e)}", exc_info=True)
        raise

Prometheus 메트릭 추가하기

더 나아가서 Prometheus로 메트릭을 수집하면 시스템 상태를 실시간으로 모니터링할 수 있어!


metrics.py - Prometheus 메트릭
from prometheus_client import Counter, Histogram, Gauge
from prometheus_fastapi_instrumentator import Instrumentator

# 메트릭 정의
authentication_attempts = Counter(
    'nft_auth_attempts_total',
    'Total number of authentication attempts',
    ['status', 'chain']
)

nft_check_duration = Histogram(
    'nft_check_duration_seconds',
    'Time spent checking NFT ownership',
    ['chain']
)

active_users = Gauge(
    'nft_active_users',
    'Number of currently authenticated users'
)

# FastAPI에 Prometheus 추가
instrumentator = Instrumentator()

@app.on_event("startup")
async def startup():
    instrumentator.instrument(app).expose(app)

# 사용 예제
@app.post("/auth/login")
async def login(request: LoginRequest):
    with nft_check_duration.labels(chain='ethereum').time():
        has_nft = nft_checker.check_ownership(
            request.wallet_address,
            REQUIRED_NFT_CONTRACT
        )
    
    if has_nft:
        authentication_attempts.labels(status='success', chain='ethereum').inc()
        active_users.inc()
    else:
        authentication_attempts.labels(status='failed', chain='ethereum').inc()
    
    # ... 나머지 로직

이제 http://localhost:8000/metrics에서 메트릭을 확인할 수 있어! Grafana와 연동하면 멋진 대시보드도 만들 수 있지. 📈

🧪 테스트 코드 작성하기

좋은 개발자는 테스트를 작성해! 특히 블록체인 관련 코드는 실수하면 돈이 날아갈 수 있으니까 더더욱 중요해. ✅


test_auth.py - 인증 테스트
import pytest
from fastapi.testclient import TestClient
from eth_account import Account
from main import app

client = TestClient(app)

@pytest.fixture
def test_wallet():
    """테스트용 지갑 생성"""
    account = Account.create()
    return {
        'address': account.address,
        'private_key': account.key.hex()
    }

def test_get_nonce(test_wallet):
    """Nonce 생성 테스트"""
    response = client.post("/auth/nonce", json={
        "wallet_address": test_wallet['address']
    })
    
    assert response.status_code == 200
    data = response.json()
    assert 'nonce' in data
    assert 'message' in data
    assert data['wallet_address'] == test_wallet['address']

def test_login_with_valid_signature(test_wallet):
    """유효한 서명으로 로그인 테스트"""
    # 1. Nonce 받기
    nonce_response = client.post("/auth/nonce", json={
        "wallet_address": test_wallet['address']
    })
    nonce_data = nonce_response.json()
    
    # 2. 메시지 서명
    account = Account.from_key(test_wallet['private_key'])
    signed_message = account.sign_message(
        encode_defunct(text=nonce_data['message'])
    )
    
    # 3. 로그인 시도
    login_response = client.post("/auth/login", json={
        "wallet_address": test_wallet['address'],
        "signature": signed_message.signature.hex(),
        "message": nonce_data['message']
    })
    
    # NFT가 없으면 403, 있으면 200
    assert login_response.status_code in [200, 403]

def test_login_with_invalid_signature(test_wallet):
    """잘못된 서명으로 로그인 테스트"""
    nonce_response = client.post("/auth/nonce", json={
        "wallet_address": test_wallet['address']
    })
    nonce_data = nonce_response.json()
    
    # 잘못된 서명
    login_response = client.post("/auth/login", json={
        "wallet_address": test_wallet['address'],
        "signature": "0x" + "0" * 130,  # 가짜 서명
        "message": nonce_data['message']
    })
    
    assert login_response.status_code == 401

def test_protected_endpoint_without_token():
    """토큰 없이 보호된 엔드포인트 접근"""
    response = client.get("/api/user/info")
    assert response.status_code == 403  # Forbidden

def test_protected_endpoint_with_invalid_token():
    """잘못된 토큰으로 보호된 엔드포인트 접근"""
    response = client.get(
        "/api/user/info",
        headers={"Authorization": "Bearer invalid_token"}
    )
    assert response.status_code == 401  # Unauthorized

test_nft_checker.py - NFT 체커 테스트
import pytest
from unittest.mock import Mock, patch
from nft_checker import NFTChecker

@pytest.fixture
def mock_web3():
    """Web3 모킹"""
    with patch('nft_checker.Web3') as mock:
        mock_instance = Mock()
        mock_instance.is_connected.return_value = True
        mock.return_value = mock_instance
        yield mock_instance

def test_check_ownership_with_nft(mock_web3):
    """NFT 보유 시 테스트"""
    # 컨트랙트 모킹
    mock_contract = Mock()
    mock_contract.functions.balanceOf.return_value.call.return_value = 1
    mock_web3.eth.contract.return_value = mock_contract
    
    checker = NFTChecker("http://test")
    result = checker.check_ownership(
        "0x1234567890123456789012345678901234567890",
        "0x0987654321098765432109876543210987654321"
    )
    
    assert result == True

def test_check_ownership_without_nft(mock_web3):
    """NFT 미보유 시 테스트"""
    mock_contract = Mock()
    mock_contract.functions.balanceOf.return_value.call.return_value = 0
    mock_web3.eth.contract.return_value = mock_contract
    
    checker = NFTChecker("http://test")
    result = checker.check_ownership(
        "0x1234567890123456789012345678901234567890",
        "0x0987654321098765432109876543210987654321"
    )
    
    assert result == False

def test_get_owned_tokens(mock_web3):
    """보유 토큰 목록 조회 테스트"""
    mock_contract = Mock()
    mock_contract.functions.balanceOf.return_value.call.return_value = 3
    mock_contract.functions.tokenOfOwnerByIndex.return_value.call.side_effect = [
        100, 200, 300
    ]
    mock_web3.eth.contract.return_value = mock_contract
    
    checker = NFTChecker("http://test")
    tokens = checker.get_owned_tokens(
        "0x1234567890123456789012345678901234567890",
        "0x0987654321098765432109876543210987654321"
    )
    
    assert tokens == [100, 200, 300]

테스트를 실행하려면:


테스트 실행
# 모든 테스트 실행
pytest

# 커버리지 포함
pytest --cov=. --cov-report=html

# 특정 테스트만 실행
pytest test_auth.py::test_login_with_valid_signature -v

🚢 Docker로 배포하기

개발이 끝났으면 이제 배포할 차례! Docker를 사용하면 어디서든 똑같이 동작하는 환경을 만들 수 있어. 🐳


Dockerfile
FROM python:3.11-slim

# 작업 디렉토리 설정
WORKDIR /app

# 시스템 의존성 설치
RUN apt-get update && apt-get install -y \
    gcc \
    && rm -rf /var/lib/apt/lists/*

# Python 의존성 복사 및 설치
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 애플리케이션 코드 복사
COPY . .

# 포트 노출
EXPOSE 8000

# 헬스체크
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD python -c "import requests; requests.get('http://localhost:8000/health')"

# 애플리케이션 실행
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

docker-compose.yml
version: '3.8'

services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      - WEB3_PROVIDER_URL=${WEB3_PROVIDER_URL}
      - NFT_CONTRACT_ADDRESS=${NFT_CONTRACT_ADDRESS}
      - JWT_SECRET_KEY=${JWT_SECRET_KEY}
      - REDIS_HOST=redis
    depends_on:
      - redis
    restart: unless-stopped
    volumes:
      - ./logs:/app/logs

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    restart: unless-stopped

  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
    restart: unless-stopped

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:
      - grafana_data:/var/lib/grafana
    restart: unless-stopped

volumes:
  redis_data:
  prometheus_data:
  grafana_data:

배포 명령어
# 이미지 빌드 및 컨테이너 실행
docker-compose up -d

# 로그 확인
docker-compose logs -f api

# 컨테이너 상태 확인
docker-compose ps

# 중지
docker-compose down

# 완전 삭제 (볼륨 포함)
docker-compose down -v

이제 서버 어디에서든 docker-compose up -d 한 줄이면 전체 시스템이 실행돼! 🎉

💡 실전 활용 사례

이론은 충분히 배웠으니, 실제로 어떻게 활용할 수 있는지 구체적인 예시를 보여줄게! 🌟


사례 1: NFT 보유자 전용 커뮤니티


특정 NFT 컬렉션 보유자만 접근할 수 있는 프라이빗 포럼이나 디스코드 채널을 만들 수 있어. 예를 들어 BAYC 보유자만 들어갈 수 있는 VIP 라운지 같은 거지.


구현 포인트:
• 디스코드 봇과 연동해서 NFT 검증
• 주기적으로 보유 여부 재확인 (NFT 판매 시 자동 퇴출)
• 여러 컬렉션 중 하나라도 보유하면 접근 허용

사례 2: 토큰 게이팅 콘텐츠 플랫폼


크리에이터가 자신의 NFT를 발행하고, 그 NFT 보유자에게만 독점 콘텐츠를 제공하는 플랫폼. 일종의 Web3 버전 Patreon이라고 볼 수 있어.


구현 포인트:
• 크리에이터별로 다른 NFT 컨트랙트 지원
• 티어별 콘텐츠 접근 권한 (골드/실버/브론즈 NFT)
• NFT 메타데이터에 만료일 설정 가능

사례 3: DAO 거버넌스 시스템


NFT를 투표권으로 사용하는 DAO를 만들 수 있어. 각 NFT가 1표를 가지거나, 희귀도에 따라 가중치를 다르게 줄 수도 있지.


구현 포인트:
• 스냅샷 기능 (특정 블록 높이에서의 보유 상태 기록)
• 위임 투표 지원
• 제안 생성 및 투표 API

🎓 재능넷 활용 팁: 이런 NFT 기반 시스템을 구축하는 걸 재능으로 등록할 수도 있어! Web3 개발자는 수요가 많은데 공급이 부족해서, 재능넷에서 프로젝트를 받아보는 것도 좋은 방법이야. 특히 NFT 프로젝트들은 기술적 구현에 어려움을 겪는 경우가 많거든.

🔮 고급 기능 추가하기

기본 기능은 다 구현했으니, 이제 좀 더 고급 기능들을 추가해보자! ⚡


1. 동적 권한 관리

NFT의 속성(traits)에 따라 다른 권한을 부여할 수 있어. 예를 들어 "레어" 속성을 가진 NFT는 더 많은 기능에 접근할 수 있게 하는 거지.


dynamic_permissions.py
from typing import Dict, List
import requests

class NFTMetadataChecker:
    def __init__(self, nft_checker: NFTChecker):
        self.nft_checker = nft_checker
    
    def get_token_metadata(self, contract_address: str, token_id: int) -> Dict:
        """
        NFT 메타데이터 조회
        """
        # ERC-721 tokenURI 함수 호출
        contract = self.nft_checker.w3.eth.contract(
            address=contract_address,
            abi=[{
                "constant": true,
                "inputs": [{"name": "_tokenId", "type": "uint256"}],
                "name": "tokenURI",
                "outputs": [{"name": "", "type": "string"}],
                "type": "function"
            }]
        )
        
        token_uri = contract.functions.tokenURI(token_id).call()
        
        # IPFS URI를 HTTP로 변환
        if token_uri.startswith('ipfs://'):
            token_uri = token_uri.replace('ipfs://', 'https://ipfs.io/ipfs/')
        
        # 메타데이터 다운로드
        response = requests.get(token_uri)
        return response.json()
    
    def check_trait(self, metadata: Dict, trait_type: str, 
                   trait_value: str) -> bool:
        """
        특정 속성 보유 여부 확인
        """
        attributes = metadata.get('attributes', [])
        for attr in attributes:
            if attr.get('trait_type') == trait_type:
                return attr.get('value') == trait_value
        return False
    
    def calculate_rarity_score(self, metadata: Dict) -> float:
        """
        희귀도 점수 계산 (간단한 예시)
        """
        attributes = metadata.get('attributes', [])
        
        # 속성 개수가 적을수록 희귀
        base_score = 100 - len(attributes) * 5
        
        # 특정 희귀 속성 보너스
        rare_traits = ['Legendary', 'Epic', 'Mythic']
        for attr in attributes:
            if attr.get('value') in rare_traits:
                base_score += 50
        
        return max(0, base_score)

class PermissionManager:
    def __init__(self, metadata_checker: NFTMetadataChecker):
        self.metadata_checker = metadata_checker
    
    def get_user_permissions(self, wallet_address: str, 
                            contract_address: str,
                            token_id: int) -> List[str]:
        """
        사용자의 권한 목록 반환
        """
        permissions = ['basic_access']  # 기본 권한
        
        # 메타데이터 조회
        metadata = self.metadata_checker.get_token_metadata(
            contract_address,
            token_id
        )
        
        # 희귀도 기반 권한
        rarity_score = self.metadata_checker.calculate_rarity_score(metadata)
        
        if rarity_score >= 80:
            permissions.append('premium_access')
        if rarity_score >= 90:
            permissions.append('vip_access')
        
        # 특정 속성 기반 권한
        if self.metadata_checker.check_trait(metadata, 'Background', 'Gold'):
            permissions.append('golden_lounge')
        
        if self.metadata_checker.check_trait(metadata, 'Type', 'Founder'):
            permissions.append('founder_benefits')
        
        return permissions

# API에 적용
permission_manager = PermissionManager(
    NFTMetadataChecker(nft_checker)
)

@app.get("/api/user/permissions")
async def get_permissions(user: dict = Depends(verify_nft_holder)):
    """
    사용자의 권한 목록 조회
    """
    wallet_address = user["sub"]
    nft_contract = user["nft_contract"]
    token_id = user["token_id"]
    
    permissions = permission_manager.get_user_permissions(
        wallet_address,
        nft_contract,
        token_id
    )
    
    return {
        "wallet_address": wallet_address,
        "token_id": token_id,
        "permissions": permissions
    }

# 권한 체크 데코레이터
def require_permission(required_permission: str):
    async def permission_checker(user: dict = Depends(verify_nft_holder)):
        permissions = permission_manager.get_user_permissions(
            user["sub"],
            user["nft_contract"],
            user["token_id"]
        )
        
        if required_permission not in permissions:
            raise HTTPException(
                status_code=403,
                detail=f"'{required_permission}' 권한이 필요합니다"
            )
        
        return user
    
    return permission_checker

# 사용 예제
@app.get("/api/vip/exclusive")
async def vip_content(user: dict = Depends(require_permission('vip_access'))):
    """
    VIP 전용 콘텐츠 (희귀도 90+ NFT 보유자만)
    """
    return {
        "message": "VIP 전용 콘텐츠입니다!",
        "data": "초특급 비밀 정보..."
    }

2. 시간 기반 접근 제어

NFT를 일정 기간 동안만 보유해야 접근할 수 있게 하거나, 특정 시간대에만 접근을 허용할 수도 있어.


time_based_access.py
from datetime import datetime, timedelta
from typing import Optional

class TimeBasedAccessControl:
    def __init__(self, nft_checker: NFTChecker):
        self.nft_checker = nft_checker
        self.holding_records = {}  # 실제로는 DB 사용
    
    def record_first_holding(self, wallet_address: str, 
                            contract_address: str,
                            token_id: int):
        """
        NFT를 처음 보유한 시점 기록
        """
        key = f"{wallet_address}:{contract_address}:{token_id}"
        if key not in self.holding_records:
            self.holding_records[key] = datetime.utcnow()
    
    def get_holding_duration(self, wallet_address: str,
                            contract_address: str,
                            token_id: int) -> Optional[timedelta]:
        """
        NFT 보유 기간 조회
        """
        key = f"{wallet_address}:{contract_address}:{token_id}"
        first_holding = self.holding_records.get(key)
        
        if not first_holding:
            return None
        
        return datetime.utcnow() - first_holding
    
    def check_minimum_holding_period(self, wallet_address: str,
                                    contract_address: str,
                                    token_id: int,
                                    min_days: int) -> bool:
        """
        최소 보유 기간 확인
        """
        duration = self.get_holding_duration(
            wallet_address,
            contract_address,
            token_id
        )
        
        if not duration:
            return False
        
        return duration.days >= min_days
    
    def check_time_window(self, start_hour: int, end_hour: int) -> bool:
        """
        특정 시간대 확인 (UTC 기준)
        """
        current_hour = datetime.utcnow().hour
        return start_hour <= current_hour < end_hour

# API에 적용
time_access = TimeBasedAccessControl(nft_checker)

@app.get("/api/veteran/content")
async def veteran_content(user: dict = Depends(verify_nft_holder)):
    """
    장기 보유자 전용 콘텐츠 (30일 이상 보유)
    """
    is_veteran = time_access.check_minimum_holding_period(
        user["sub"],
        user["nft_contract"],
        user["token_id"],
        min_days=30
    )
    
    if not is_veteran:
        raise HTTPException(
            status_code=403,
            detail="30일 이상 NFT를 보유해야 접근할 수 있습니다"
        )
    
    return {
        "message": "장기 보유자 감사 콘텐츠",
        "bonus": "특별 보상"
    }

@app.get("/api/happy-hour/deals")
async def happy_hour_deals(user: dict = Depends(verify_nft_holder)):
    """
    해피아워 특가 (UTC 14:00-16:00만 접근 가능)
    """
    if not time_access.check_time_window(14, 16):
        raise HTTPException(
            status_code=403,
            detail="해피아워 시간(14:00-16:00 UTC)에만 접근 가능합니다"
        )
    
    return {
        "message": "해피아워 특가!",
        "deals": ["50% 할인", "2+1 이벤트"]
    }

🎓 배운 내용 정리

와! 여기까지 정말 긴 여정이었어. 이제 우리가 배운 내용을 정리해보자. 📝


핵심 개념 요약:


1️⃣ 토큰 게이팅: NFT를 디지털 회원권처럼 사용해서 특정 서비스나 콘텐츠에 대한 접근을 제어하는 방식


2️⃣ 지갑 서명 인증: 비밀번호 대신 지갑의 개인키로 서명해서 본인 인증을 하는 Web3 방식의 로그인


3️⃣ NFT 소유권 검증: 블록체인에서 직접 NFT 보유 여부를 확인해서 위조 불가능한 인증 구현


4️⃣ JWT 토큰: 인증 성공 후 발급받는 토큰으로, 이후 API 요청에서 사용자를 식별


5️⃣ 캐싱 전략: 블록체인 조회를 최소화하기 위해 Redis 같은 캐시를 활용


구현한 주요 기능:


✅ 지갑 서명 기반 인증 시스템
✅ ERC-721/ERC-1155 NFT 소유권 검증
✅ JWT 기반 세션 관리
✅ 멀티체인 지원 (Ethereum, Polygon, BSC 등)
✅ 동적 권한 관리 (NFT 속성 기반)
✅ 시간 기반 접근 제어
✅ Redis 캐싱으로 성능 최적화
✅ Prometheus 메트릭 수집
✅ Docker 컨테이너화
✅ 포괄적인 테스트 코드

🚀 다음 단계로 나아가기

여기까지 따라왔다면 이미 훌륭한 NFT 게이팅 시스템을 만들 수 있어! 하지만 더 발전시킬 수 있는 방향들이 있어. 🌟


추가로 구현해볼 만한 기능들:


🔔 웹훅 시스템: NFT 전송이 발생하면 자동으로 알림을 받아서 권한을 업데이트


📱 모바일 앱 지원: WalletConnect를 사용해서 모바일 지갑과 연동


🎨 NFT 갤러리: 사용자가 보유한 NFT를 예쁘게 보여주는 UI


💬 소셜 기능: NFT 보유자끼리 채팅하거나 커뮤니티 형성


🎁 에어드랍 시스템: 특정 NFT 보유자에게 자동으로 토큰이나 NFT 배포


📊 분석 대시보드: 사용자 통계, 인기 NFT 컬렉션 등을 시각화

💼 재능넷 활용: 이런 고급 기능들을 구현하는 프로젝트를 재능넷에서 찾아볼 수도 있고, 반대로 당신이 이 기술을 재능으로 등록해서 다른 사람들을 도와줄 수도 있어. Web3 개발자는 정말 수요가 많거든!

🎯 마무리하며

자, 이제 정말 끝이야! 파이썬으로 NFT 보유자 전용 접근 API를 만드는 방법을 처음부터 끝까지 다뤄봤어. 🎉


처음에는 복잡해 보였을 수도 있지만, 하나씩 뜯어보면 그렇게 어렵지 않지? 핵심은 지갑 서명 검증과 블록체인 NFT 조회, 그리고 JWT 토큰 관리야. 이 세 가지만 제대로 이해하면 나머지는 응용이거든.


Web3는 아직 초기 단계라서 새로운 패턴과 베스트 프랙티스가 계속 나오고 있어. 이 글에서 배운 내용을 기반으로 자신만의 창의적인 서비스를 만들어보길 바라! ✨


혹시 구현하다가 막히는 부분이 있으면 커뮤니티에 물어보는 것도 좋은 방법이야. Stack Overflow, Discord, 그리고 재능넷의 지식인의 숲 같은 곳에서 많은 도움을 받을 수 있을 거야.


마지막으로, 보안은 절대 타협하지 마! 특히 블록체인 관련 코드는 한 번 실수하면 돌이킬 수 없는 결과를 낳을 수 있으니까, 항상 신중하게 개발하고 충분히 테스트해야 해. 🔒


그럼 행운을 빌어! 멋진 Web3 프로젝트를 만들어보길 바랄게! 🚀

🎊🎉🎊

Happy Coding in Web3!

댓글 작성

이 글에 대한 여러분의 생각을 들려주세요

댓글 0
댓글을 불러오는 중입니다.

아직 댓글이 없습니다.