콘텐츠 대표 이미지 - 파이썬과 스트림릿으로NFT 데이터 분석 툴 만들기
🔗 블록체인 / Web3 × 프로그램개발

파이썬과 스트림릿으로
NFT 데이터 분석 툴 만들기

Python + Streamlit + OpenSea API = 나만의 NFT 대시보드 🚀
야 NFT 요즘도 사람들 하냐?? 🤔

ㅋㅋㅋ 시장은 살아있지~ 근데 데이터 분석 없이 투자하면 그냥 도박임

그럼 어떻게 분석해?? 툴 같은 거 있어?

직접 만들면 되지ㅋㅋ 파이썬이랑 스트림릿으로 30분이면 뚝딱임 진짜로

맞음ㅋㅋ 오늘 우리가 만들 거 딱 이거야. 파이썬(Python)이랑 스트림릿(Streamlit)을 활용해서 NFT 데이터를 실시간으로 긁어오고, 시각화해서 보여주는 나만의 분석 툴 제작! 코딩 좀 해봤으면 충분히 따라올 수 있고, 완성하면 진짜 쓸만한 Web3 툴이 생기는 거임 😎

이 글에서는 OpenSea API, Alchemy API 등 실제 NFT 데이터를 제공하는 서비스들을 연동하는 방법부터, 스트림릿으로 대시보드 UI 구성하는 법, 그리고 실제 분석에 쓸 수 있는 지표들까지 전부 다룰 거야. 레츠고 🔥


NFT 데이터 분석 툴 아키텍처 전체 데이터 흐름 한눈에 보기 📡 데이터 소스 OpenSea API Alchemy API CoinGecko API Etherscan API Moralis API REST / GraphQL 🐍 파이썬 처리 requests / httpx pandas 데이터 처리 numpy 연산 plotly 시각화 web3.py 연동 데이터 정제 · 분석 🎨 스트림릿 UI 차트 대시보드 필터 / 검색 실시간 업데이트 반응형 레이아웃 사이드바 컨트롤 웹 대시보드 렌더링 📊 분석 결과 바닥가 추이 거래량 분석 홀더 분포 희귀도 랭킹 고래 지갑 추적 인사이트 도출 🔄 실시간 데이터 파이프라인 · 로컬 또는 클라우드 배포 가능 Python 3.9+ · Streamlit 1.28+ · Web3.py 6.x
🧩 1. 왜 파이썬 + 스트림릿인가?

NFT 데이터 분석 툴 만들 때 선택지가 꽤 많음. React로 프론트 짜고 Node.js 백엔드 붙이는 방법도 있고, 그냥 엑셀로 정리하는 사람도 있고ㅋㅋ 근데 파이썬 + 스트림릿 조합이 왜 갑인지 설명해줄게.

🎯 파이썬 + 스트림릿 조합의 핵심 장점 1. 데이터 처리 최강자 — pandas, numpy, scipy 등 데이터 분석 생태계가 압도적으로 풍부함. NFT 거래 데이터 수만 건을 처리하는 데 파이썬만한 게 없음.

2. 스트림릿은 진짜 마법 — HTML/CSS/JS 하나도 몰라도 파이썬 코드 몇 줄로 웹 대시보드가 뚝딱 생김. 진짜임ㅋㅋ

3. Web3 라이브러리 풍부 — web3.py, eth-brownie 등 이더리움 연동 라이브러리가 잘 갖춰져 있음.

4. 배포 쉬움 — Streamlit Cloud에 GitHub 연동하면 무료로 배포 가능. 진입장벽 거의 없음.

스트림릿(Streamlit)은 2019년에 나온 오픈소스 파이썬 프레임워크인데, 데이터 과학자들이 빠르게 웹 앱을 만들 수 있도록 설계됐어. 2022년에 Snowflake가 8억 달러에 인수할 정도로 가치를 인정받은 툴임. 현재 GitHub 스타 수가 35,000개 이상이고, 매달 수백만 명이 사용하는 검증된 프레임워크야 💪

비교 항목 파이썬+스트림릿 React+Node.js Dune Analytics
개발 속도 ⚡ 매우 빠름 🐢 느림 ✅ 빠름(단, 제한적)
커스터마이징 ✅ 높음 ✅ 매우 높음 ❌ 낮음
데이터 처리 ✅ 최강 🔶 보통 ✅ SQL 기반
진입장벽 ✅ 낮음 ❌ 높음 ✅ 낮음
무료 배포 ✅ 가능 🔶 제한적 ✅ 가능

🛠️ 2. 개발 환경 세팅 (진짜 처음부터)

자 이제 본격적으로 세팅 들어가자. 파이썬 3.9 이상 버전이 설치되어 있다고 가정하고 진행할게. 없으면 python.org에서 최신 버전 받아와 😄

1
가상환경 생성 및 활성화
프로젝트마다 가상환경 쓰는 거 습관화하자. 패키지 충돌 방지에 필수임.
# 가상환경 생성
python -m venv nft-analyzer

# 활성화 (Windows)
nft-analyzer\Scripts\activate

# 활성화 (Mac/Linux)
source nft-analyzer/bin/activate
2
필수 패키지 설치
requirements.txt 만들어서 한 번에 설치하는 게 깔끔함.
# requirements.txt 내용
streamlit==1.28.0
pandas==2.1.0
numpy==1.24.0
requests==2.31.0
plotly==5.17.0
web3==6.11.0
python-dotenv==1.0.0
aiohttp==3.8.6
httpx==0.25.0
pillow==10.0.0

# 설치 명령어
pip install -r requirements.txt
3
프로젝트 폴더 구조 잡기
처음부터 구조 잘 잡아야 나중에 안 고생함ㅋㅋ
nft-analyzer/
├── app.py              # 메인 스트림릿 앱
├── .env                # API 키 (절대 깃허브에 올리면 안됨!!)
├── .gitignore
├── requirements.txt
├── config/
│   └── settings.py     # 설정값 관리
├── api/
│   ├── opensea.py      # OpenSea API 연동
│   ├── alchemy.py      # Alchemy API 연동
│   └── coingecko.py    # 가격 데이터
├── analysis/
│   ├── floor_price.py  # 바닥가 분석
│   ├── volume.py       # 거래량 분석
│   └── holders.py      # 홀더 분석
└── utils/
    ├── helpers.py      # 유틸 함수들
    └── cache.py        # 캐싱 로직
💡 팁! .env 파일에 API 키 저장하고 .gitignore에 꼭 추가해야 해. API 키 깃허브에 올렸다가 봇한테 털리는 사람 진짜 많음ㅋㅋ 조심조심
# .env 파일 예시
OPENSEA_API_KEY=your_opensea_api_key_here
ALCHEMY_API_KEY=your_alchemy_api_key_here
ETHERSCAN_API_KEY=your_etherscan_api_key_here
COINGECKO_API_KEY=your_coingecko_api_key_here

# .gitignore에 추가
.env
__pycache__/
*.pyc
.streamlit/secrets.toml

📡 3. NFT 데이터 API 연동하기

NFT 데이터를 가져오는 방법은 여러 가지인데, 주요 API들 특징이랑 사용법 알려줄게. 각각 장단점이 있으니까 용도에 맞게 골라 써야 함 🎯

주요 NFT 데이터 API 비교 OS OpenSea API 컬렉션 메타데이터 바닥가 · 거래량 NFT 목록 조회 이벤트 히스토리 무료: 4req/s 유료: 더 높은 한도 ⭐ 가장 많이 씀 AL Alchemy API NFT 소유권 조회 토큰 메타데이터 블록체인 이벤트 멀티체인 지원 무료: 300M CU/월 안정적인 인프라 🔗 Web3 연동 최적 MO Moralis API NFT 전송 이력 지갑 포트폴리오 DeFi 데이터 크로스체인 지원 무료: 40,000 req/일 멀티체인 강점 🌐 멀티체인 분석 CG CoinGecko API ETH 가격 데이터 NFT 컬렉션 가격 시장 캡 데이터 히스토리컬 데이터 무료: 30 req/분 가격 데이터 특화 💰 가격 분석 특화 * 무료 플랜 기준 · 2024년 기준 정보 · 변동 가능성 있음

이제 각 API 연동 코드 작성해보자. 먼저 OpenSea API부터 시작할게. OpenSea는 NFT 마켓플레이스 1위라서 데이터가 제일 풍부함 👑

# api/opensea.py
import requests
import os
from dotenv import load_dotenv
import pandas as pd
import time

load_dotenv()

class OpenSeaAPI:
    def __init__(self):
        self.api_key = os.getenv("OPENSEA_API_KEY")
        self.base_url = "https://api.opensea.io/api/v2"
        self.headers = {
            "accept": "application/json",
            "x-api-key": self.api_key
        }
    
    def get_collection(self, collection_slug: str) -> dict:
        """컬렉션 기본 정보 가져오기"""
        url = f"{self.base_url}/collections/{collection_slug}"
        response = requests.get(url, headers=self.headers)
        
        if response.status_code == 200:
            return response.json()
        else:
            raise Exception(f"API 오류: {response.status_code} - {response.text}")
    
    def get_collection_stats(self, collection_slug: str) -> dict:
        """컬렉션 통계 데이터 (바닥가, 거래량 등)"""
        url = f"{self.base_url}/collections/{collection_slug}/stats"
        response = requests.get(url, headers=self.headers)
        
        if response.status_code == 200:
            return response.json()
        else:
            raise Exception(f"통계 조회 실패: {response.status_code}")
    
    def get_collection_events(
        self, 
        collection_slug: str, 
        event_type: str = "sale",
        limit: int = 50
    ) -> pd.DataFrame:
        """
        컬렉션 이벤트 히스토리 (판매, 전송, 리스팅 등)
        event_type: 'sale', 'transfer', 'listing', 'offer', 'cancel'
        """
        url = f"{self.base_url}/events/collection/{collection_slug}"
        params = {
            "event_type": event_type,
            "limit": limit
        }
        
        all_events = []
        next_cursor = None
        
        while len(all_events) < limit:
            if next_cursor:
                params["next"] = next_cursor
            
            response = requests.get(url, headers=self.headers, params=params)
            
            if response.status_code == 200:
                data = response.json()
                events = data.get("asset_events", [])
                all_events.extend(events)
                next_cursor = data.get("next")
                
                if not next_cursor or not events:
                    break
                    
                time.sleep(0.25)  # Rate limit 방지
            else:
                break
        
        return self._events_to_dataframe(all_events)
    
    def _events_to_dataframe(self, events: list) -> pd.DataFrame:
        """이벤트 데이터를 pandas DataFrame으로 변환"""
        if not events:
            return pd.DataFrame()
        
        rows = []
        for event in events:
            row = {
                "event_type": event.get("event_type"),
                "timestamp": event.get("closing_date") or event.get("event_timestamp"),
                "token_id": event.get("nft", {}).get("identifier"),
                "price_eth": self._wei_to_eth(
                    event.get("payment", {}).get("quantity", 0)
                ),
                "seller": event.get("seller"),
                "buyer": event.get("buyer"),
                "transaction": event.get("transaction")
            }
            rows.append(row)
        
        df = pd.DataFrame(rows)
        if "timestamp" in df.columns:
            df["timestamp"] = pd.to_datetime(df["timestamp"], unit="s", errors="coerce")
        
        return df
    
    def _wei_to_eth(self, wei_value) -> float:
        """Wei를 ETH로 변환"""
        try:
            return int(wei_value) / 1e18
        except (ValueError, TypeError):
            return 0.0

이제 Alchemy API 연동 코드도 작성해보자. Alchemy는 특히 특정 지갑이 보유한 NFT 목록 조회할 때 진짜 편함 🔥

# api/alchemy.py
import requests
import os
from dotenv import load_dotenv
import pandas as pd

load_dotenv()

class AlchemyAPI:
    def __init__(self, network: str = "eth-mainnet"):
        self.api_key = os.getenv("ALCHEMY_API_KEY")
        self.network = network
        self.base_url = f"https://{network}.g.alchemy.com/nft/v3/{self.api_key}"
    
    def get_nfts_for_owner(self, owner_address: str) -> pd.DataFrame:
        """특정 지갑 주소가 보유한 NFT 전체 목록"""
        url = f"{self.base_url}/getNFTsForOwner"
        params = {
            "owner": owner_address,
            "withMetadata": "true",
            "pageSize": 100
        }
        
        all_nfts = []
        page_key = None
        
        while True:
            if page_key:
                params["pageKey"] = page_key
            
            response = requests.get(url, params=params)
            
            if response.status_code == 200:
                data = response.json()
                nfts = data.get("ownedNfts", [])
                all_nfts.extend(nfts)
                page_key = data.get("pageKey")
                
                if not page_key:
                    break
            else:
                break
        
        return self._nfts_to_dataframe(all_nfts)
    
    def get_owners_for_contract(self, contract_address: str) -> dict:
        """특정 NFT 컨트랙트의 모든 홀더 조회"""
        url = f"{self.base_url}/getOwnersForContract"
        params = {
            "contractAddress": contract_address,
            "withTokenBalances": "true"
        }
        
        response = requests.get(url, params=params)
        
        if response.status_code == 200:
            return response.json()
        else:
            raise Exception(f"홀더 조회 실패: {response.status_code}")
    
    def get_nft_metadata(self, contract_address: str, token_id: str) -> dict:
        """특정 NFT의 메타데이터 조회"""
        url = f"{self.base_url}/getNFTMetadata"
        params = {
            "contractAddress": contract_address,
            "tokenId": token_id,
            "refreshCache": "false"
        }
        
        response = requests.get(url, params=params)
        
        if response.status_code == 200:
            return response.json()
        else:
            return {}
    
    def _nfts_to_dataframe(self, nfts: list) -> pd.DataFrame:
        """NFT 목록을 DataFrame으로 변환"""
        if not nfts:
            return pd.DataFrame()
        
        rows = []
        for nft in nfts:
            contract = nft.get("contract", {})
            row = {
                "contract_address": contract.get("address"),
                "collection_name": contract.get("name"),
                "token_id": nft.get("tokenId"),
                "token_type": contract.get("tokenType"),
                "name": nft.get("name"),
                "description": nft.get("description"),
                "floor_price_eth": contract.get("openSeaMetadata", {}).get("floorPrice"),
                "image_url": nft.get("image", {}).get("cachedUrl"),
                "acquired_at": nft.get("acquiredAt", {}).get("blockTimestamp")
            }
            rows.append(row)
        
        return pd.DataFrame(rows)

📊 4. 데이터 분석 모듈 만들기

API로 데이터 가져왔으면 이제 분석 로직 짜야지! NFT 분석에서 제일 중요한 지표들 위주로 만들어볼게 📈

🎯 NFT 분석의 핵심 지표 5가지 ① 바닥가(Floor Price) — 컬렉션에서 가장 싸게 팔리는 NFT 가격. 시장 진입 기준점.
② 거래량(Volume) — 일정 기간 내 총 거래 금액. 시장 활성도 지표.
③ 홀더 분포(Holder Distribution) — 소수가 대부분 보유하면 고래 리스크 높음.
④ 리스팅 비율(Listing Rate) — 전체 공급량 대비 판매 등록 비율. 낮을수록 희소성 높음.
⑤ 희귀도(Rarity Score) — 각 NFT의 특성 조합 희귀도. 가격 프리미엄 결정.
# analysis/floor_price.py
import pandas as pd
import numpy as np
from typing import List, Dict

class FloorPriceAnalyzer:
    
    def analyze_price_trend(self, sales_df: pd.DataFrame) -> Dict:
        """
        판매 이벤트 데이터로 가격 트렌드 분석
        
        Returns:
            dict: 분석 결과 (이동평균, 변동성, 추세 등)
        """
        if sales_df.empty:
            return {}
        
        # 날짜별 최저가 (바닥가 근사치)
        daily_floor = (
            sales_df
            .groupby(sales_df["timestamp"].dt.date)["price_eth"]
            .min()
            .reset_index()
        )
        daily_floor.columns = ["date", "floor_price"]
        daily_floor = daily_floor.sort_values("date")
        
        # 7일 이동평균
        daily_floor["ma7"] = (
            daily_floor["floor_price"]
            .rolling(window=7, min_periods=1)
            .mean()
        )
        
        # 30일 이동평균
        daily_floor["ma30"] = (
            daily_floor["floor_price"]
            .rolling(window=30, min_periods=1)
            .mean()
        )
        
        # 변동성 (표준편차 / 평균)
        volatility = (
            daily_floor["floor_price"].std() / 
            daily_floor["floor_price"].mean()
        ) * 100
        
        # 추세 판단 (최근 7일 vs 이전 7일)
        recent_7d = daily_floor.tail(7)["floor_price"].mean()
        prev_7d = daily_floor.iloc[-14:-7]["floor_price"].mean() if len(daily_floor) >= 14 else None
        
        trend = "데이터 부족"
        trend_pct = 0
        
        if prev_7d and prev_7d > 0:
            trend_pct = ((recent_7d - prev_7d) / prev_7d) * 100
            if trend_pct > 5:
                trend = "📈 상승"
            elif trend_pct < -5:
                trend = "📉 하락"
            else:
                trend = "➡️ 횡보"
        
        return {
            "daily_data": daily_floor,
            "current_floor": daily_floor["floor_price"].iloc[-1] if not daily_floor.empty else 0,
            "avg_floor_30d": daily_floor.tail(30)["floor_price"].mean(),
            "max_floor": daily_floor["floor_price"].max(),
            "min_floor": daily_floor["floor_price"].min(),
            "volatility_pct": round(volatility, 2),
            "trend": trend,
            "trend_pct": round(trend_pct, 2)
        }
    
    def calculate_support_resistance(self, prices: pd.Series) -> Dict:
        """지지선/저항선 계산 (단순 분위수 기반)"""
        return {
            "support_1": prices.quantile(0.1),
            "support_2": prices.quantile(0.25),
            "resistance_1": prices.quantile(0.75),
            "resistance_2": prices.quantile(0.9),
            "median": prices.median()
        }
# analysis/holders.py
import pandas as pd
import numpy as np
from typing import Dict

class HolderAnalyzer:
    
    def analyze_distribution(self, owners_data: dict) -> Dict:
        """
        홀더 분포 분석 - 고래 리스크 평가
        """
        owners = owners_data.get("owners", [])
        
        if not owners:
            return {}
        
        # 각 홀더의 보유량 추출
        holdings = []
        for owner in owners:
            balances = owner.get("tokenBalances", [])
            total_held = sum(int(b.get("balance", 0)) for b in balances)
            holdings.append({
                "address": owner.get("ownerAddress"),
                "count": total_held
            })
        
        df = pd.DataFrame(holdings)
        total_supply = df["count"].sum()
        
        # 상위 홀더 비중
        df_sorted = df.sort_values("count", ascending=False)
        top10_count = df_sorted.head(10)["count"].sum()
        top50_count = df_sorted.head(50)["count"].sum()
        top100_count = df_sorted.head(100)["count"].sum()
        
        # 지니 계수 계산 (불평등 지수, 0=완전평등, 1=완전불평등)
        gini = self._calculate_gini(df["count"].values)
        
        # 고래 리스크 평가
        whale_risk = self._assess_whale_risk(
            top10_pct=(top10_count / total_supply) * 100,
            gini=gini
        )
        
        # 분포 구간별 홀더 수
        distribution = {
            "1개 보유": len(df[df["count"] == 1]),
            "2-5개 보유": len(df[(df["count"] >= 2) & (df["count"] <= 5)]),
            "6-10개 보유": len(df[(df["count"] >= 6) & (df["count"] <= 10)]),
            "11-50개 보유": len(df[(df["count"] >= 11) & (df["count"] <= 50)]),
            "51개 이상 보유": len(df[df["count"] > 50])
        }
        
        return {
            "total_holders": len(df),
            "total_supply": total_supply,
            "avg_holding": round(df["count"].mean(), 2),
            "median_holding": df["count"].median(),
            "top10_pct": round((top10_count / total_supply) * 100, 2),
            "top50_pct": round((top50_count / total_supply) * 100, 2),
            "top100_pct": round((top100_count / total_supply) * 100, 2),
            "gini_coefficient": round(gini, 4),
            "whale_risk": whale_risk,
            "distribution": distribution,
            "top_holders": df_sorted.head(20).to_dict("records")
        }
    
    def _calculate_gini(self, values: np.ndarray) -> float:
        """지니 계수 계산"""
        values = np.sort(values)
        n = len(values)
        if n == 0 or values.sum() == 0:
            return 0
        cumsum = np.cumsum(values)
        return (2 * np.sum((np.arange(1, n+1)) * values) / 
                (n * cumsum[-1])) - (n + 1) / n
    
    def _assess_whale_risk(self, top10_pct: float, gini: float) -> str:
        """고래 리스크 레벨 평가"""
        if top10_pct > 50 or gini > 0.8:
            return "🔴 매우 높음 - 고래 집중도 위험"
        elif top10_pct > 35 or gini > 0.65:
            return "🟠 높음 - 주의 필요"
        elif top10_pct > 20 or gini > 0.5:
            return "🟡 보통 - 일반적 수준"
        else:
            return "🟢 낮음 - 분산 양호"

🎨 5. 스트림릿으로 대시보드 UI 만들기

드디어 제일 재밌는 파트임ㅋㅋ 스트림릿으로 실제 대시보드 UI 만들어볼 거야. 스트림릿의 진짜 매력은 파이썬 코드 그대로 쓰면서 웹 UI가 자동으로 생긴다는 거임 🤩

스트림릿 처음 써보는데 어려워?? 😅

진짜 쉬움ㅋㅋ st.write() 하면 텍스트 나오고 st.line_chart() 하면 차트 나오고 그게 끝임

헐 그게 다야?? 개쉽네

물론 더 복잡하게 만들 수 있지만 기본은 진짜 그 정도임 ㅎㅎ
# app.py - 메인 스트림릿 앱
import streamlit as st
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from datetime import datetime, timedelta
import time

from api.opensea import OpenSeaAPI
from api.alchemy import AlchemyAPI
from analysis.floor_price import FloorPriceAnalyzer
from analysis.holders import HolderAnalyzer

# ===== 페이지 설정 =====
st.set_page_config(
    page_title="NFT 데이터 분석 툴",
    page_icon="🔮",
    layout="wide",
    initial_sidebar_state="expanded"
)

# ===== 커스텀 CSS =====
st.markdown("""
<style>.tx-header{background:linear-gradient(135deg,#1a1a2e 0%,#16213e 50%,#0f3460 100%);
border-radius:18px;
padding:40px 30px;
text-align:center;
margin-bottom:32px;}
.tx-header-sub{color:#a8b2d8;
font-size:1rem;
margin-top:8px;
letter-spacing:2px;}
.tx-badge{display:inline-block;
background:rgba(100,200,255,0.15);
color:#64c8ff;
border:1px solid rgba(100,200,255,0.3);
border-radius:20px;
padding:4px 14px;
font-size:0.8rem;
margin-bottom:16px;}
.tx-section-title{font-size:1.4rem;
font-weight:700;
color:#1a1a2e;
border-left:4px solid #6c63ff;
padding-left:12px;
margin:36px 0 16px 0;}
.tx-highlight{background:linear-gradient(90deg,#e8f4fd,#f0e8ff);
border-radius:10px;
padding:16px 20px;
margin:18px 0;}
.tx-tip{background:#f0fdf4;
border-left:4px solid #4ade80;
border-radius:0 10px 10px 0;
padding:14px 18px;
margin:16px 0;}
.tx-warn{background:#fff7ed;
border-left:4px solid #fb923c;
border-radius:0 10px 10px 0;
padding:14px 18px;
margin:16px 0;}
.tx-code-wrap pre{margin:0;
color:#cdd6f4;
font-size:0.88rem;
line-height:1.7;}
.tx-step{display:flex;
align-items:flex-start;
gap:14px;
margin:14px 0;}
.tx-step-num{min-width:32px;
height:32px;
background:linear-gradient(135deg,#6c63ff,#a78bfa);
border-radius:50%;
display:flex;
align-items:center;
justify-content:center;
color:#fff;
font-weight:700;
font-size:0.9rem;}
.tx-step-content{flex:1;}
.tx-divider{border:none;
border-top:1px solid #e8eaf0;
margin:32px 0;}
.tx-table{width:100%;
border-collapse:collapse;
margin:16px 0;
font-size:0.9rem;}
.tx-table th{background:#f3f0ff;
color:#6c63ff;
padding:10px 12px;
text-align:left;
border-bottom:2px solid #d8d0ff;}
.tx-table td{padding:10px 12px;
border-bottom:1px solid #f0eeff;
color:#444;}
.tx-table tr:hover td{background:#faf8ff;}
.tx-chat{background:#f8f9ff;
border-radius:14px;
padding:16px 20px;
margin:16px 0;
font-size:0.95rem;
line-height:1.8;
color:#333;}
.tx-chat-bubble{display:inline-block;
background:#6c63ff;
color:#fff;
border-radius:18px 18px 18px 4px;
padding:8px 16px;
margin:4px 0;
font-size:0.9rem;}
.tx-chat-bubble-right{display:inline-block;
background:#e8f4fd;
color:#1a1a2e;
border-radius:18px 18px 4px 18px;
padding:8px 16px;
margin:4px 0;
font-size:0.9rem;}
.tx-flow{display:flex;
flex-wrap:wrap;
gap:10px;
margin:18px 0;
align-items:center;}
.tx-flow-item{background:#f3f0ff;
border:1px solid #d8d0ff;
border-radius:8px;
padding:8px 14px;
font-size:0.85rem;
color:#6c63ff;
font-weight:600;}
.tx-flow-arrow{color:#a78bfa;
font-size:1.2rem;
font-weight:700;}
.tx-result-box{background:linear-gradient(135deg,#f8f9ff,#f0fdf4);
border:1px solid #d8d0ff;
border-radius:12px;
padding:18px 22px;
margin:18px 0;}
.tx-emoji-title{font-size:1.1rem;
font-weight:700;
color:#6c63ff;
margin-bottom:8px;}</style>
""", unsafe_allow_html=True)

# ===== 헤더 =====
st.markdown("""
<div class="main-header">
    <h1 style="color: #e8f0ff; margin: 0;">🔮 NFT 데이터 분석 대시보드</h1>
    <p style="color: #a8b2d8; margin: 8px 0 0 0;">
        실시간 NFT 시장 데이터 분석 · Python + Streamlit
    </p>
</div>
""", unsafe_allow_html=True)

# ===== 사이드바 =====
with st.sidebar:
    st.markdown("### ⚙️ 분석 설정")
    
    collection_slug = st.text_input(
        "컬렉션 슬러그 입력",
        value="boredapeyachtclub",
        help="OpenSea URL에서 컬렉션 이름 부분 (예: boredapeyachtclub)"
    )
    
    analysis_period = st.selectbox(
        "분석 기간",
        options=["7일", "30일", "90일"],
        index=1
    )
    
    show_whale_analysis = st.checkbox("고래 분석 포함", value=True)
    show_rarity = st.checkbox("희귀도 분석 포함", value=False)
    
    analyze_btn = st.button("🚀 분석 시작", type="primary", use_container_width=True)
    
    st.divider()
    st.markdown("### 📌 인기 컬렉션")
    popular = {
        "BAYC": "boredapeyachtclub",
        "CryptoPunks": "cryptopunks",
        "Azuki": "azuki",
        "Doodles": "doodles-official",
        "CloneX": "clonex"
    }
    for name, slug in popular.items():
        if st.button(name, use_container_width=True):
            collection_slug = slug

# ===== 메인 분석 로직 =====
if analyze_btn or "analysis_data" in st.session_state:
    
    opensea = OpenSeaAPI()
    alchemy = AlchemyAPI()
    floor_analyzer = FloorPriceAnalyzer()
    holder_analyzer = HolderAnalyzer()
    
    # 데이터 로딩 (캐싱 활용)
    @st.cache_data(ttl=300)  # 5분 캐시
    def load_collection_data(slug: str):
        with st.spinner("📡 컬렉션 데이터 로딩 중..."):
            collection = opensea.get_collection(slug)
            stats = opensea.get_collection_stats(slug)
            events = opensea.get_collection_events(slug, "sale", limit=200)
        return collection, stats, events
    
    try:
        collection, stats, events_df = load_collection_data(collection_slug)
        
        # ===== 컬렉션 기본 정보 =====
        col_info = collection
        st.markdown(f"## 📋 {col_info.get('name', collection_slug)}")
        
        # 핵심 지표 카드 (4열)
        col1, col2, col3, col4 = st.columns(4)
        
        total_stats = stats.get("total", {})
        
        with col1:
            floor = total_stats.get("floor_price", 0)
            st.metric(
                label="🏷️ 바닥가",
                value=f"{floor:.3f} ETH",
                delta=None
            )
        
        with col2:
            volume = total_stats.get("volume", 0)
            st.metric(
                label="📊 총 거래량",
                value=f"{volume:,.1f} ETH"
            )
        
        with col3:
            sales = total_stats.get("num_sales", 0)
            st.metric(
                label="🔄 총 거래 수",
                value=f"{sales:,}건"
            )
        
        with col4:
            owners = total_stats.get("num_owners", 0)
            st.metric(
                label="👥 홀더 수",
                value=f"{owners:,}명"
            )
        
        st.divider()
        
        # ===== 가격 추이 차트 =====
        st.markdown("### 📈 가격 추이 분석")
        
        if not events_df.empty:
            price_analysis = floor_analyzer.analyze_price_trend(events_df)
            
            if price_analysis and "daily_data" in price_analysis:
                daily_df = price_analysis["daily_data"]
                
                # Plotly 차트 생성
                fig = go.Figure()
                
                fig.add_trace(go.Scatter(
                    x=daily_df["date"],
                    y=daily_df["floor_price"],
                    mode="lines+markers",
                    name="일별 최저가",
                    line=dict(color="#6c63ff", width=2),
                    marker=dict(size=4)
                ))
                
                fig.add_trace(go.Scatter(
                    x=daily_df["date"],
                    y=daily_df["ma7"],
                    mode="lines",
                    name="7일 이동평균",
                    line=dict(color="#f59e0b", width=2, dash="dash")
                ))
                
                fig.add_trace(go.Scatter(
                    x=daily_df["date"],
                    y=daily_df["ma30"],
                    mode="lines",
                    name="30일 이동평균",
                    line=dict(color="#10b981", width=2, dash="dot")
                ))
                
                fig.update_layout(
                    title="바닥가 추이 (ETH)",
                    xaxis_title="날짜",
                    yaxis_title="가격 (ETH)",
                    hovermode="x unified",
                    plot_bgcolor="rgba(0,0,0,0)",
                    paper_bgcolor="rgba(0,0,0,0)",
                    legend=dict(
                        orientation="h",
                        yanchor="bottom",
                        y=1.02,
                        xanchor="right",
                        x=1
                    )
                )
                
                st.plotly_chart(fig, use_container_width=True)
                
                # 분석 요약
                col_a, col_b, col_c = st.columns(3)
                with col_a:
                    st.info(f"**추세**: {price_analysis.get('trend', 'N/A')}")
                with col_b:
                    st.info(f"**변동성**: {price_analysis.get('volatility_pct', 0):.1f}%")
                with col_c:
                    trend_pct = price_analysis.get('trend_pct', 0)
                    delta_str = f"+{trend_pct:.1f}%" if trend_pct >= 0 else f"{trend_pct:.1f}%"
                    st.info(f"**7일 변화**: {delta_str}")
        
        # ===== 거래량 분석 =====
        st.markdown("### 📊 거래량 분석")
        
        if not events_df.empty:
            # 일별 거래량
            daily_volume = (
                events_df
                .groupby(events_df["timestamp"].dt.date)
                .agg(
                    volume=("price_eth", "sum"),
                    count=("price_eth", "count"),
                    avg_price=("price_eth", "mean")
                )
                .reset_index()
            )
            
            fig_vol = px.bar(
                daily_volume,
                x="timestamp",
                y="volume",
                title="일별 거래량 (ETH)",
                color="volume",
                color_continuous_scale="Purples"
            )
            fig_vol.update_layout(
                plot_bgcolor="rgba(0,0,0,0)",
                paper_bgcolor="rgba(0,0,0,0)"
            )
            st.plotly_chart(fig_vol, use_container_width=True)
        
    except Exception as e:
        st.error(f"❌ 데이터 로딩 실패: {str(e)}")
        st.info("💡 API 키 설정을 확인하거나 컬렉션 슬러그를 다시 확인해주세요.")
💡 스트림릿 핵심 꿀팁! @st.cache_data(ttl=300) 데코레이터 쓰면 API 호출 결과를 5분간 캐싱해줌. API 요청 횟수 줄이고 속도도 빨라짐. 진짜 필수임ㅋㅋ

🐋 6. 고래 지갑 추적 기능 구현

NFT 시장에서 고래(Whale) 추적은 진짜 중요한 알파임 🐋 대형 홀더들이 언제 팔고 사는지 알면 시장 방향성 예측에 도움이 됨. 물론 100% 맞는 건 아니지만 참고 지표로 쓸 수 있어.

# analysis/whale_tracker.py
import pandas as pd
import numpy as np
from typing import List, Dict
from api.alchemy import AlchemyAPI
from api.opensea import OpenSeaAPI

class WhaleTracker:
    
    def __init__(self):
        self.alchemy = AlchemyAPI()
        self.opensea = OpenSeaAPI()
        
        # 알려진 고래/기관 지갑 주소 (예시)
        self.known_whales = {
            "0x...": "유명 컬렉터 A",
            "0x...": "NFT 펀드 B",
            # 실제 주소로 교체 필요
        }
    
    def identify_whales(
        self, 
        holders_df: pd.DataFrame, 
        threshold_pct: float = 1.0
    ) -> pd.DataFrame:
        """
        전체 공급량의 threshold_pct% 이상 보유한 지갑을 고래로 분류
        """
        total_supply = holders_df["count"].sum()
        threshold = total_supply * (threshold_pct / 100)
        
        whales = holders_df[holders_df["count"] >= threshold].copy()
        whales["holding_pct"] = (whales["count"] / total_supply * 100).round(2)
        whales["label"] = whales["address"].map(
            self.known_whales
        ).fillna("미확인 고래")
        
        return whales.sort_values("count", ascending=False)
    
    def analyze_whale_activity(
        self, 
        events_df: pd.DataFrame,
        whale_addresses: List[str]
    ) -> Dict:
        """
        고래들의 최근 거래 활동 분석
        """
        if events_df.empty:
            return {}
        
        # 고래가 판매자인 경우
        whale_sells = events_df[
            events_df["seller"].isin(whale_addresses)
        ].copy()
        
        # 고래가 구매자인 경우
        whale_buys = events_df[
            events_df["buyer"].isin(whale_addresses)
        ].copy()
        
        # 최근 7일 활동
        cutoff = pd.Timestamp.now() - pd.Timedelta(days=7)
        recent_sells = whale_sells[whale_sells["timestamp"] >= cutoff]
        recent_buys = whale_buys[whale_buys["timestamp"] >= cutoff]
        
        # 고래 순매수/순매도 판단
        buy_volume = recent_buys["price_eth"].sum()
        sell_volume = recent_sells["price_eth"].sum()
        net_flow = buy_volume - sell_volume
        
        sentiment = "🐋 고래 순매수 (강세 신호)" if net_flow > 0 else "🐋 고래 순매도 (약세 신호)"
        
        return {
            "total_whale_sells_7d": len(recent_sells),
            "total_whale_buys_7d": len(recent_buys),
            "whale_sell_volume_7d": round(sell_volume, 3),
            "whale_buy_volume_7d": round(buy_volume, 3),
            "net_flow_7d": round(net_flow, 3),
            "whale_sentiment": sentiment,
            "recent_whale_sells": whale_sells.head(10),
            "recent_whale_buys": whale_buys.head(10)
        }
    
    def get_wallet_portfolio(self, wallet_address: str) -> Dict:
        """특정 지갑의 NFT 포트폴리오 분석"""
        nfts_df = self.alchemy.get_nfts_for_owner(wallet_address)
        
        if nfts_df.empty:
            return {"total_nfts": 0, "collections": {}}
        
        # 컬렉션별 보유 현황
        collection_summary = (
            nfts_df
            .groupby("collection_name")
            .agg(
                count=("token_id", "count"),
                avg_floor=("floor_price_eth", "mean")
            )
            .reset_index()
        )
        
        # 추정 포트폴리오 가치
        nfts_df["estimated_value"] = nfts_df["floor_price_eth"].fillna(0)
        total_value = nfts_df["estimated_value"].sum()
        
        return {
            "total_nfts": len(nfts_df),
            "total_collections": nfts_df["collection_name"].nunique(),
            "estimated_value_eth": round(total_value, 4),
            "collection_summary": collection_summary,
            "nfts": nfts_df
        }
🐋 고래 지갑 분석 대시보드 예시 상위 홀더 보유 비중 · 7일 거래 활동 · 순매수/매도 흐름 홀더 분포 홀더 분포 상위 10명 (60%) 11~50명 (28%) 나머지 (12%) 7일 고래 거래 활동 (ETH) 120 월 200 화 160 수 280 목 220 금 100 토 180 일 * 예시 데이터 · 실제 분석 결과는 API 연동 후 확인 가능
🔢 7. 희귀도(Rarity) 점수 계산 알고리즘

NFT 희귀도 계산은 가격 프리미엄에 직결되는 중요한 분석임. 희귀한 특성을 가진 NFT일수록 더 비싸게 거래되는 경향이 있거든. 대표적인 희귀도 계산 방식 두 가지 알려줄게 🎲

📐 희귀도 계산 방식 비교 ① Statistical Rarity (통계적 희귀도)
각 특성의 출현 빈도를 곱해서 계산. 직관적이지만 특성 수가 다른 NFT 비교에 불리함.
공식: Rarity = Π (특성 출현 횟수 / 전체 공급량)

② Rarity Score (희귀도 점수)
OpenRarity, Rarity.tools 등에서 사용하는 방식. 각 특성의 역빈도 합산.
공식: Score = Σ (전체 공급량 / 해당 특성 보유 NFT 수)
# analysis/rarity.py
import pandas as pd
import numpy as np
from typing import List, Dict

class RarityCalculator:
    
    def __init__(self, total_supply: int):
        self.total_supply = total_supply
    
    def calculate_rarity_scores(
        self, 
        nfts_metadata: List[Dict]
    ) -> pd.DataFrame:
        """
        NFT 컬렉션 전체의 희귀도 점수 계산
        
        Args:
            nfts_metadata: 각 NFT의 메타데이터 리스트
                          [{"token_id": "1", "attributes": [{"trait_type": "Background", "value": "Blue"}, ...]}, ...]
        
        Returns:
            DataFrame: token_id, rarity_score, rarity_rank 포함
        """
        # 특성별 출현 빈도 계산
        trait_counts = {}
        
        for nft in nfts_metadata:
            attributes = nft.get("attributes", [])
            for attr in attributes:
                trait_type = attr.get("trait_type", "Unknown")
                value = str(attr.get("value", "None"))
                key = f"{trait_type}:{value}"
                trait_counts[key] = trait_counts.get(key, 0) + 1
        
        # 각 NFT의 희귀도 점수 계산
        results = []
        
        for nft in nfts_metadata:
            token_id = nft.get("token_id") or nft.get("identifier")
            attributes = nft.get("attributes", [])
            
            # Rarity Score 방식
            rarity_score = 0
            trait_scores = []
            
            for attr in attributes:
                trait_type = attr.get("trait_type", "Unknown")
                value = str(attr.get("value", "None"))
                key = f"{trait_type}:{value}"
                
                count = trait_counts.get(key, 1)
                score = self.total_supply / count
                rarity_score += score
                
                trait_scores.append({
                    "trait_type": trait_type,
                    "value": value,
                    "count": count,
                    "frequency_pct": round((count / self.total_supply) * 100, 2),
                    "score": round(score, 2)
                })
            
            # 특성 없는 경우 패널티 (Missing Trait)
            # 특성 종류 수 파악
            all_trait_types = set(
                attr.get("trait_type") 
                for nft_data in nfts_metadata 
                for attr in nft_data.get("attributes", [])
            )
            nft_trait_types = set(
                attr.get("trait_type") 
                for attr in attributes
            )
            missing_traits = all_trait_types - nft_trait_types
            
            # 없는 특성에 대한 점수 추가 (OpenRarity 방식)
            for missing in missing_traits:
                missing_key = f"{missing}:None"
                missing_count = trait_counts.get(missing_key, 0)
                if missing_count > 0:
                    rarity_score += self.total_supply / missing_count
            
            results.append({
                "token_id": token_id,
                "rarity_score": round(rarity_score, 2),
                "trait_count": len(attributes),
                "trait_details": trait_scores
            })
        
        # DataFrame 생성 및 랭킹 부여
        df = pd.DataFrame(results)
        df = df.sort_values("rarity_score", ascending=False)
        df["rarity_rank"] = range(1, len(df) + 1)
        
        # 희귀도 등급 분류
        df["rarity_tier"] = pd.cut(
            df["rarity_rank"],
            bins=[0, 
                  len(df)*0.01, 
                  len(df)*0.05, 
                  len(df)*0.15, 
                  len(df)*0.35, 
                  len(df)],
            labels=["💎 Legendary", "🔮 Epic", "🌟 Rare", "✨ Uncommon", "⚪ Common"]
        )
        
        return df
    
    def get_collection_trait_stats(
        self, 
        nfts_metadata: List[Dict]
    ) -> pd.DataFrame:
        """컬렉션 전체 특성 통계"""
        rows = []
        
        for nft in nfts_metadata:
            for attr in nft.get("attributes", []):
                rows.append({
                    "trait_type": attr.get("trait_type"),
                    "value": str(attr.get("value")),
                    "token_id": nft.get("token_id")
                })
        
        if not rows:
            return pd.DataFrame()
        
        df = pd.DataFrame(rows)
        
        stats = (
            df.groupby(["trait_type", "value"])
            .agg(count=("token_id", "count"))
            .reset_index()
        )
        
        stats["frequency_pct"] = (
            stats["count"] / self.total_supply * 100
        ).round(2)
        
        stats["rarity_score"] = (
            self.total_supply / stats["count"]
        ).round(2)
        
        return stats.sort_values(
            ["trait_type", "count"], 
            ascending=[True, True]
        )

⚡ 8. 성능 최적화 & 캐싱 전략

API 호출이 많아지면 속도가 느려지고 요금도 나올 수 있음. 캐싱 전략 잘 짜야 해 💨

# utils/cache.py
import streamlit as st
import pandas as pd
import json
import hashlib
import os
from datetime import datetime, timedelta
from typing import Any, Optional

class CacheManager:
    """
    다층 캐싱 전략:
    1. Streamlit 세션 캐시 (가장 빠름, 세션 내 유지)
    2. 파일 캐시 (디스크, 앱 재시작 후에도 유지)
    3. API 응답 캐시 (TTL 기반)
    """
    
    def __init__(self, cache_dir: str = ".cache"):
        self.cache_dir = cache_dir
        os.makedirs(cache_dir, exist_ok=True)
    
    def _get_cache_key(self, *args) -> str:
        """캐시 키 생성"""
        key_str = "_".join(str(a) for a in args)
        return hashlib.md5(key_str.encode()).hexdigest()
    
    def _get_cache_path(self, key: str) -> str:
        return os.path.join(self.cache_dir, f"{key}.json")
    
    def get(self, key: str, ttl_minutes: int = 30) -> Optional[Any]:
        """캐시에서 데이터 가져오기"""
        cache_path = self._get_cache_path(key)
        
        if not os.path.exists(cache_path):
            return None
        
        with open(cache_path, "r") as f:
            cached = json.load(f)
        
        # TTL 체크
        cached_at = datetime.fromisoformat(cached["cached_at"])
        if datetime.now() - cached_at > timedelta(minutes=ttl_minutes):
            os.remove(cache_path)
            return None
        
        return cached["data"]
    
    def set(self, key: str, data: Any) -> None:
        """캐시에 데이터 저장"""
        cache_path = self._get_cache_path(key)
        
        # DataFrame은 JSON 직렬화 처리
        if isinstance(data, pd.DataFrame):
            serializable_data = data.to_dict("records")
        else:
            serializable_data = data
        
        with open(cache_path, "w") as f:
            json.dump({
                "cached_at": datetime.now().isoformat(),
                "data": serializable_data
            }, f, default=str)
    
    def clear_expired(self, ttl_minutes: int = 60) -> int:
        """만료된 캐시 정리"""
        cleared = 0
        for filename in os.listdir(self.cache_dir):
            if filename.endswith(".json"):
                filepath = os.path.join(self.cache_dir, filename)
                try:
                    with open(filepath, "r") as f:
                        cached = json.load(f)
                    cached_at = datetime.fromisoformat(cached["cached_at"])
                    if datetime.now() - cached_at > timedelta(minutes=ttl_minutes):
                        os.remove(filepath)
                        cleared += 1
                except Exception:
                    pass
        return cleared


# 스트림릿 캐시 데코레이터 활용 예시
@st.cache_data(ttl=300, show_spinner="데이터 로딩 중...")
def fetch_collection_data(collection_slug: str, period_days: int):
    """
    컬렉션 데이터 캐싱 (5분 TTL)
    같은 파라미터로 5분 내 재호출 시 캐시 반환
    """
    from api.opensea import OpenSeaAPI
    opensea = OpenSeaAPI()
    
    collection = opensea.get_collection(collection_slug)
    stats = opensea.get_collection_stats(collection_slug)
    events = opensea.get_collection_events(
        collection_slug, 
        "sale", 
        limit=min(period_days * 10, 300)
    )
    
    return collection, stats, events


@st.cache_resource
def get_api_clients():
    """
    API 클라이언트 싱글톤 (앱 전체에서 재사용)
    cache_resource는 객체를 앱 수명 동안 유지
    """
    from api.opensea import OpenSeaAPI
    from api.alchemy import AlchemyAPI
    
    return {
        "opensea": OpenSeaAPI(),
        "alchemy": AlchemyAPI()
    }
⚠️ 주의! Streamlit의 @st.cache_data와 @st.cache_resource는 용도가 다름.
cache_data: 데이터(DataFrame, dict 등) 캐싱 — 직렬화 가능한 객체
cache_resource: 리소스(DB 연결, API 클라이언트 등) 캐싱 — 싱글톤 패턴

🚀 9. 배포하기 (Streamlit Cloud)

로컬에서 잘 돌아가면 이제 배포해서 어디서든 접근할 수 있게 만들자! Streamlit Cloud 쓰면 무료로 배포 가능함 🌐

1
GitHub 레포지토리 생성 및 코드 업로드
.env 파일은 절대 올리면 안 됨! .gitignore 확인 필수
2
Streamlit Cloud에서 시크릿 설정
share.streamlit.io → 앱 설정 → Secrets에 API 키 입력
# .streamlit/secrets.toml (로컬 테스트용, 깃허브 올리면 안됨!)
OPENSEA_API_KEY = "your_key_here"
ALCHEMY_API_KEY = "your_key_here"
ETHERSCAN_API_KEY = "your_key_here"

# 코드에서 시크릿 접근 방법
import streamlit as st

# Streamlit Cloud 배포 시
api_key = st.secrets["OPENSEA_API_KEY"]

# 로컬 개발 시 (환경변수 폴백)
import os
api_key = st.secrets.get("OPENSEA_API_KEY") or os.getenv("OPENSEA_API_KEY")
3
requirements.txt 최신화 확인
배포 환경에서 패키지 버전 맞춰야 오류 안 남
4
share.streamlit.io에서 앱 배포
GitHub 연동 → 레포 선택 → 브랜치 선택 → app.py 경로 입력 → Deploy!
💡 배포 꿀팁! Streamlit Cloud 무료 플랜은 앱이 일정 시간 비활성화되면 슬립 모드로 들어감. 항상 켜두려면 유료 플랜 쓰거나, Render.com이나 Railway.app 같은 대안 서비스 써도 됨. 재능넷(https://www.jaenung.net) 같은 플랫폼에서 파이썬 개발자 찾아서 배포 도움받는 것도 방법임ㅋㅋ

📱 10. 완성된 대시보드 기능 총정리
localhost:8501 · NFT 데이터 분석 대시보드 ⚙️ 분석 설정 boredapeyachtclub 분석 기간: 30일 🚀 분석 시작 📌 인기 컬렉션 BAYC CryptoPunks Azuki 🔮 NFT 데이터 분석 대시보드 Bored Ape Yacht Club · 실시간 분석 🏷️ 바닥가 12.5 ETH 📊 총 거래량 847K ETH 🔄 총 거래 수 42,891건 👥 홀더 수 6,432명 📈 바닥가 추이 (ETH) 📊 거래량 분석 🐋 고래 분석 요약 고래 리스크: 🟠 높음 상위 10명 보유: 38.2% 7일 순매수: +124.5 ETH 🐋 고래 순매수 (강세 신호)

완성된 NFT 분석 툴에는 다음 기능들이 들어가 있음 🎉

기능 카테고리 세부 기능 사용 API
📊 가격 분석 바닥가 추이, 이동평균, 변동성, 지지/저항선 OpenSea
📈 거래량 분석 일별/주별 거래량, 평균 거래가, 거래 건수 OpenSea
👥 홀더 분석 홀더 분포, 지니계수, 고래 리스크 평가 Alchemy
🐋 고래 추적 대형 홀더 식별, 매수/매도 활동, 순매수 흐름 Alchemy + OpenSea
💎 희귀도 분석 Rarity Score 계산, 등급 분류, 특성 통계 Alchemy
💰 가격 환산 ETH → USD 실시간 환산 CoinGecko
🔍 지갑 분석 특정 지갑 NFT 포트폴리오, 추정 가치 Alchemy

🔧 11. 실전 트러블슈팅 & 자주 나오는 에러

개발하다 보면 에러 만나는 게 당연함ㅋㅋ 자주 나오는 에러들이랑 해결법 정리해줄게 🛠️

❌ 에러 1: Rate Limit 초과 429 Too Many Requests
→ 해결: API 호출 사이에 time.sleep(0.25) 추가. 캐싱 적극 활용. 유료 플랜 고려.
❌ 에러 2: 스트림릿 무한 재실행 위젯 값 바꿀 때마다 전체 스크립트가 재실행되는 현상
→ 해결: st.session_state 활용해서 상태 관리. 무거운 연산은 @st.cache_data로 캐싱.
❌ 에러 3: JSON 직렬화 오류 Object of type Timestamp is not JSON serializable
→ 해결: json.dumps(data, default=str) 사용하거나 DataFrame 저장 시 df.to_json() 활용.
❌ 에러 4: Web3 연결 오류
ConnectionError: Could not connect to the endpoint

→ 해결: Alchemy/Infura RPC URL 확인. 네트워크 이름 정확히 입력 (eth-mainnet, polygon-mainnet 등).
# 에러 핸들링 베스트 프랙티스
import streamlit as st
import logging

# 로깅 설정
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def safe_api_call(func, *args, **kwargs):
    """
    API 호출 안전하게 래핑
    에러 발생 시 None 반환 + 사용자에게 알림
    """
    try:
        return func(*args, **kwargs)
    except Exception as e:
        error_msg = str(e)
        logger.error(f"API 호출 실패: {error_msg}")
        
        if "429" in error_msg:
            st.warning("⚠️ API 요청 한도 초과. 잠시 후 다시 시도해주세요.")
        elif "401" in error_msg or "403" in error_msg:
            st.error("❌ API 키가 유효하지 않습니다. 설정을 확인해주세요.")
        elif "404" in error_msg:
            st.warning("⚠️ 컬렉션을 찾을 수 없습니다. 슬러그를 확인해주세요.")
        else:
            st.error(f"❌ 오류 발생: {error_msg}")
        
        return None


# 재시도 로직 (exponential backoff)
import time
from functools import wraps

def retry_with_backoff(max_retries: int = 3, base_delay: float = 1.0):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_retries):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if "429" in str(e) and attempt < max_retries - 1:
                        delay = base_delay * (2 ** attempt)
                        logger.info(f"Rate limit. {delay}초 후 재시도 ({attempt+1}/{max_retries})")
                        time.sleep(delay)
                    else:
                        raise
        return wrapper
    return decorator


# 사용 예시
@retry_with_backoff(max_retries=3, base_delay=1.0)
def fetch_with_retry(url: str, headers: dict) -> dict:
    import requests
    response = requests.get(url, headers=headers, timeout=10)
    response.raise_for_status()
    return response.json()

💡 12. 더 발전시키기 - 고급 기능 아이디어

기본 툴 완성했으면 이제 더 발전시켜볼 수 있는 아이디어들 공유할게. 이걸 다 구현하면 진짜 프로 수준의 NFT 분석 툴이 됨 🚀

🤖 AI 가격 예측 모델 통합

scikit-learn이나 Prophet 라이브러리로 바닥가 예측 모델 만들기. 과거 가격 데이터 + 거래량 + 홀더 수 변화를 피처로 사용. 단, 예측은 참고용으로만 써야 함 (투자 조언 아님!).

📧 알림 시스템 구축

바닥가가 특정 가격 이하로 떨어지거나, 고래가 대량 매도할 때 텔레그램/이메일 알림 보내기. Python의 schedule 라이브러리 + Telegram Bot API 조합으로 구현 가능.

🔗 멀티체인 지원 확장

이더리움 외에 Polygon, Solana, Arbitrum 등 다른 체인의 NFT도 분석하기. Moralis API는 멀티체인을 잘 지원함. Solana는 Helius API 추천.

📊 포트폴리오 트래커

여러 지갑 주소를 등록해두고 전체 NFT 포트폴리오 가치를 실시간으로 추적. 수익/손실 계산, 취득 원가 대비 현재 바닥가 비교 기능.

🕸️ 소셜 센티멘트 분석

Twitter/X API나 Reddit API로 특정 NFT 컬렉션 관련 소셜 미디어 언급량, 감성 분석(긍정/부정) 데이터를 가격 데이터와 함께 시각화. VADER나 TextBlob 라이브러리 활용.

🗄️ 데이터베이스 연동

SQLite나 PostgreSQL 연동해서 히스토리컬 데이터 축적. 장기 트렌드 분석 가능. SQLAlchemy + pandas의 to_sql() 메서드로 쉽게 구현.


🎓 13. 마무리 & 학습 로드맵

자 이제 파이썬 + 스트림릿으로 NFT 데이터 분석 툴 만드는 방법 전부 다 알려줬음ㅋㅋ 처음엔 복잡해 보여도 하나씩 따라가다 보면 진짜 완성됨. 이런 툴 만들어서 재능넷(https://www.jaenung.net)에서 Web3 개발 재능으로 등록해두면 의뢰도 들어올 수 있음 💪

🗺️ NFT 분석 툴 개발 학습 로드맵 1 기초 세팅 Python 환경 구성 가상환경 설정 패키지 설치 API 키 발급 프로젝트 구조 ⏱️ 1~2일 2 API 연동 OpenSea API Alchemy API 데이터 파싱 에러 핸들링 캐싱 구현 ⏱️ 2~3일 3 분석 로직 가격 분석 홀더 분석 고래 추적 희귀도 계산 pandas 활용 ⏱️ 3~5일 4 UI 구현 스트림릿 기초 Plotly 차트 레이아웃 구성 사이드바 설정 반응형 디자인 ⏱️ 2~3일 5 배포 GitHub 업로드 시크릿 설정 Streamlit Cloud 도메인 연결 ⏱️ 1일 총 예상 기간: 약 1~2주 (기존 파이썬 경험자 기준)
이거 다 만들면 진짜 쓸 수 있는 거야?? 🤩

당연하지ㅋㅋ 실제로 이런 툴 만들어서 NFT 투자 리서치에 쓰는 사람들 많음. 물론 툴이 수익을 보장해주진 않지만 데이터 기반 의사결정은 확실히 도움됨

API 키 발급은 어디서 해?

OpenSea는 opensea.io/developers, Alchemy는 alchemy.com, CoinGecko는 coingecko.com/en/api 에서 무료로 발급 가능! 다 무료 플랜 있음 👍
🎯 핵심 요약 - 이것만 기억하자! ① 파이썬 + 스트림릿 = 빠른 개발, 쉬운 배포, 강력한 데이터 처리
② OpenSea API = 가격/거래량 데이터, Alchemy API = 온체인 데이터
③ 캐싱 필수 = @st.cache_data로 API 호출 최소화
④ 에러 핸들링 = Rate Limit, 연결 오류 대비 재시도 로직 구현
⑤ 보안 중요 = API 키는 절대 깃허브에 올리지 말 것!
⑥ 데이터는 참고용 = 분석 결과는 투자 조언이 아님, 항상 DYOR(Do Your Own Research)

이 글에서 다룬 코드들은 실제 동작하는 코드 기반으로 작성됐지만, API 버전 업데이트나 정책 변경에 따라 수정이 필요할 수 있어. 공식 문서 항상 참고하는 습관 들이자 📚

Web3 개발은 빠르게 변하는 분야라 꾸준히 업데이트 따라가는 게 중요함. 스트림릿도 계속 새 기능 추가되고 있고, OpenSea API도 v2로 업그레이드되면서 기능이 많이 좋아졌어. 이 글 기준 시점은 2024년 하반기야 📅

자 이제 진짜 만들어봐! 코드 짜다가 막히는 거 있으면 공식 문서랑 스택오버플로우가 최고의 친구임ㅋㅋ 화이팅 🔥🔥🔥

댓글 작성

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

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

아직 댓글이 없습니다.