파이썬 requests로 오픈 API 활용하기 - 코인 시세·공공데이터 불러오기! JSON 파싱부터 API 키 관리까지 🌐
목차11개 항목
안녕하세요! Mojangcam입니다 😎
지난번에 정규표현식으로 문자열에서 원하는 정보를 뽑아내는 법을 배웠죠. 그런데 애초에 깔끔하게 정리된 데이터를 그냥 받아올 수 있다면 어떨까요? 그게 바로 오픈 API(Open API) 예요.
오늘은 파이썬 requests 라이브러리로 오픈 API를 호출해서 데이터를 받아오는 법을 처음부터 끝까지 해볼게요. 코인 시세를 실시간으로 가져오고, 공공데이터포털 API를 인증키까지 붙여서 호출하고, 마지막엔 타임아웃·재시도까지 넣어서 실무에서 안 터지는 코드로 만듭니다.
이거 익혀두면 활용처가 진짜 많아요. 환율 알림 봇, 날씨 알려주는 디스코드 봇, 매일 아침 시세를 엑셀로 저장하는 자동화 스크립트… 전부 오늘 배우는 30줄에서 시작합니다!

🧠 크롤링이랑 뭐가 다른데요?
예전에 다뤘던 웹 크롤링은 사람이 보는 HTML 페이지를 긁어와서 필요한 부분을 골라내는 방식이었어요. 화면 디자인이 바뀌면 코드가 깨지고, 태그 구조를 일일이 뜯어봐야 하죠.
오픈 API는 다릅니다. 서비스 제공자가 "이 주소로 요청하면 이런 데이터를 JSON으로 드릴게요"라고 공식적으로 약속해 둔 통로예요.
- JSON : 딕셔너리·리스트처럼 생긴 데이터 형식.
res.json()한 줄이면 파이썬 자료형으로 바뀝니다 - 엔드포인트(Endpoint) : 데이터를 받아올 주소. 예)
https://api.upbit.com/v1/ticker - 파라미터(Parameter) : "뭘 달라"고 알려주는 조건. 예)
markets=KRW-BTC - 인증키(API Key) : 누가 요청했는지 확인하는 열쇠. 필요 없는 API도 많아요
정리하면 크롤링은 눈치껏 긁어오기, API는 정식으로 받아오기 예요. 둘 다 되면 API가 무조건 낫습니다 👍
📌 준비물
- Python 3.10 이상 (requests 2.34부터 3.10+ 필요)
- requests 2.34.x — 2026년 8월 기준 최신은 2.34.2
- python-dotenv 1.2.x — 인증키를 코드 밖으로 빼기 위해 사용
pip install requests python-dotenv설치가 끝났으면 버전을 한 번 확인해 볼게요.
python -c "import requests; print(requests.__version__)"
# 2.34.2📌 1단계 - 세상에서 가장 짧은 API 호출
인증키가 아예 필요 없는 API부터 시작할게요. 업비트 현재가 조회 API는 키 없이 그냥 호출됩니다.
import requests
res = requests.get("https://api.upbit.com/v1/ticker?markets=KRW-BTC", timeout=5)
print(res.status_code) # 200
data = res.json() # JSON → 파이썬 리스트로 변환
print(data[0]["trade_price"])하나씩 뜯어볼게요 💡
- requests.get(url) : 해당 주소로 GET 요청을 보내고 응답 객체를 돌려줍니다
- timeout=5 : 5초 안에 응답이 없으면 포기. 이거 빼먹으면 프로그램이 영원히 멈출 수 있어요. 습관처럼 꼭 넣으세요
- res.status_code : 200이면 성공, 401은 인증 실패, 404는 주소 오류, 429는 너무 자주 요청함
- res.json() : 응답 본문을 파이썬 딕셔너리/리스트로 바꿔줍니다. 업비트 티커는 리스트로 오기 때문에
data[0]으로 첫 항목을 꺼냈어요
어떤 값들이 들어있는지 모르겠다면, 일단 키 목록부터 찍어보는 게 제일 빠릅니다.
print(data[0].keys())자주 쓰는 필드는 이 정도예요.
- market : 페어 코드 (예:
KRW-BTC) - trade_price : 현재가
- signed_change_rate : 전일 종가 대비 등락률 (부호 포함, 0.0213 = +2.13%)
- change :
RISE(상승) /FALL(하락) /EVEN(보합)
📌 2단계 - params로 깔끔하게, 여러 개 한 번에
주소 뒤에 ?markets=...를 문자열로 이어 붙이면 조건이 늘어날수록 지저분해집니다. requests는 params 딕셔너리를 주면 알아서 URL을 만들어줘요.
import requests
URL = "https://api.upbit.com/v1/ticker"
markets = ["KRW-BTC", "KRW-ETH", "KRW-XRP"]
res = requests.get(URL, params={"markets": ",".join(markets)}, timeout=5)
res.raise_for_status()
for t in res.json():
rate = t["signed_change_rate"] * 100
mark = "▲" if rate > 0 else ("▼" if rate < 0 else "-")
print(f"{t['market']:<9}{t['trade_price']:>14,.0f}원 {mark} {rate:+.2f}%")하나씩 뜯어볼게요 💡
- params={...} : 딕셔너리를 넘기면 requests가
?markets=KRW-BTC,KRW-ETH형태로 조립하고, 한글이나 특수문자도 알아서 URL 인코딩해 줍니다 - ",".join(markets) : 업비트는 페어를 쉼표로 구분한 문자열로 받습니다. 리스트를 그대로 넘기면 안 돼요
- res.raise_for_status() : 상태 코드가 400·500대면 예외를 발생시킵니다. 이게 없으면 에러 응답을 정상 데이터로 착각해서 엉뚱한 곳에서 터져요
:>14,.0f: 오른쪽 정렬 + 천 단위 콤마 + 소수점 없음. 숫자 표가 확 읽기 좋아집니다
참고로 업비트 현재가 API는 초당 10회 제한이 있어요. 반복문에서 마구 호출하면 429가 뜨니 주의하세요!
📌 3단계 - 인증키가 필요한 API (feat. .env)
공공데이터포털처럼 인증키(serviceKey)를 요구하는 API도 많죠. 여기서 절대 하면 안 되는 게 하나 있습니다.
# ❌ 이렇게 하지 마세요
SERVICE_KEY = "a1B2c3D4e5F6..."키를 코드에 그대로 박아두면 깃허브에 올리는 순간 그대로 유출돼요. 블로그에 코드 붙여넣다가 같이 올라가는 사고도 흔합니다 😱
그래서 python-dotenv를 씁니다. 프로젝트 폴더에 .env 파일을 만들고 키를 넣어두면, 코드는 파일에서 읽어오기만 해요.
# .env ← 코드와 같은 폴더에 만드세요
DATA_GO_KR_KEY=여기에_디코딩된_인증키를_붙여넣기
DATA_GO_KR_URL=http://apis.data.go.kr/서비스경로/오퍼레이션명import os
import requests
from dotenv import load_dotenv
load_dotenv() # .env 파일을 읽어 환경변수로 등록
SERVICE_KEY = os.getenv("DATA_GO_KR_KEY") # 코드에는 키가 남지 않아요
API_URL = os.getenv("DATA_GO_KR_URL")
params = {
"serviceKey": SERVICE_KEY,
"pageNo": 1,
"numOfRows": 5,
"type": "json",
}
res = requests.get(API_URL, params=params, timeout=10)
res.raise_for_status()
print(res.json())하나씩 뜯어볼게요 💡
- load_dotenv() :
.env파일의 내용을 환경변수로 올려줍니다. 스크립트 맨 위에서 딱 한 번 호출 - os.getenv("이름") : 환경변수를 읽어옵니다. 없으면
None이 나오니 값 체크를 해주면 더 안전해요 - params에 serviceKey : 공공데이터포털은 인증키도 파라미터로 받습니다
- type=json : 공공데이터포털 API 상당수는 기본이 XML이에요. 이 파라미터를 붙여야 JSON으로 옵니다 (API에 따라
_type인 경우도 있으니 활용 문서를 확인하세요)
그리고 .gitignore에 .env를 꼭 넣어두세요. 이거 안 하면 dotenv 쓴 의미가 없어집니다!
# .gitignore
.env💡 엔드포인트 주소와 파라미터 이름은 API마다 달라요. 공공데이터포털에서 원하는 API의 활용신청을 하면 상세 기능 문서에 요청 주소·파라미터·응답 예시가 전부 나옵니다. 그대로 옮겨 담으면 돼요.
📌 4단계 - 안 터지는 코드 만들기 (타임아웃·재시도)
외부 API는 언젠가 반드시 실패합니다. 서버가 잠깐 죽을 수도 있고, 와이파이가 끊길 수도 있어요. 자동화 스크립트라면 한 번 실패했다고 통째로 죽으면 안 되겠죠.
requests의 Session에 재시도 정책을 붙이면 간단하게 해결됩니다.
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def make_session(retries: int = 3) -> requests.Session:
session = requests.Session()
retry = Retry(
total=retries,
backoff_factor=0.5,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["GET"],
)
session.mount("https://", HTTPAdapter(max_retries=retry))
session.headers.update({"Accept": "application/json"})
return session하나씩 뜯어볼게요 💡
- Session() : 연결을 재사용해서 반복 호출이 훨씬 빨라지고, 헤더·쿠키를 한 번만 설정하면 됩니다
- total=3 : 최대 3번까지 다시 시도
- backoff_factor=0.5 : 재시도 간격을 0.5초 → 1초 → 2초로 점점 늘립니다. 서버에 예의를 갖추는 거예요 🙏
- status_forcelist : 이 상태 코드가 오면 재시도. 429(요청 과다)와 5xx(서버 오류)가 대상이에요
- allowed_methods=["GET"] : 조회 요청만 재시도. POST를 재시도하면 중복 등록 사고가 날 수 있어요
예외 처리도 종류별로 나눠 잡으면 원인 파악이 훨씬 쉬워집니다.
try:
res = session.get(URL, params=params, timeout=5)
res.raise_for_status()
except requests.exceptions.Timeout:
print("[!] 응답이 너무 느려요. timeout을 늘려보세요.")
except requests.exceptions.HTTPError as e:
print(f"[!] HTTP 에러: {e.response.status_code}")
except requests.exceptions.RequestException as e:
print(f"[!] 요청 실패: {e}")RequestException은 requests가 던지는 모든 예외의 부모예요. 맨 마지막에 두면 그물망 역할을 합니다.
📌 전체 코드 (복붙 OK)
# requests 2.34.x / python-dotenv 1.2.x / Python 3.10+
# pip install requests python-dotenv
import os
import requests
from dotenv import load_dotenv
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
load_dotenv()
UPBIT_TICKER_URL = "https://api.upbit.com/v1/ticker"
DATA_GO_KR_URL = os.getenv("DATA_GO_KR_URL")
DATA_GO_KR_KEY = os.getenv("DATA_GO_KR_KEY")
def make_session(retries: int = 3) -> requests.Session:
"""재시도 + 공통 헤더가 붙은 세션을 만들어요."""
session = requests.Session()
retry = Retry(
total=retries,
backoff_factor=0.5,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["GET"],
)
session.mount("https://", HTTPAdapter(max_retries=retry))
session.headers.update({"Accept": "application/json"})
return session
def get_coin_prices(session: requests.Session, markets: list[str]) -> list[dict]:
"""인증키가 필요 없는 API - 업비트 현재가 조회"""
params = {"markets": ",".join(markets)}
res = session.get(UPBIT_TICKER_URL, params=params, timeout=5)
res.raise_for_status()
return res.json()
def get_public_data(session: requests.Session, page: int = 1, rows: int = 5):
"""인증키가 필요한 API - 공공데이터포털 (.env에서 키를 읽어옵니다)"""
if not DATA_GO_KR_URL or not DATA_GO_KR_KEY:
print("[i] .env에 DATA_GO_KR_URL / DATA_GO_KR_KEY가 없어서 이 단계는 건너뜁니다.")
return None
params = {
"serviceKey": DATA_GO_KR_KEY,
"pageNo": page,
"numOfRows": rows,
"type": "json",
}
res = session.get(DATA_GO_KR_URL, params=params, timeout=10)
res.raise_for_status()
try:
return res.json()
except ValueError:
print("[!] JSON이 아니에요. 응답 앞부분:", res.text[:200])
return None
def print_tickers(tickers: list[dict]) -> None:
print("=== 업비트 현재가 ===")
for t in tickers:
rate = t["signed_change_rate"] * 100
mark = "▲" if rate > 0 else ("▼" if rate < 0 else "-")
print(f"{t['market']:<9}{t['trade_price']:>14,.0f}원 {mark} {rate:+.2f}%")
def main() -> None:
session = make_session()
try:
tickers = get_coin_prices(session, ["KRW-BTC", "KRW-ETH", "KRW-XRP"])
except requests.exceptions.Timeout:
print("[!] 응답이 너무 느려요. timeout을 늘려보세요.")
return
except requests.exceptions.HTTPError as e:
print(f"[!] HTTP 에러: {e.response.status_code}")
return
except requests.exceptions.RequestException as e:
print(f"[!] 요청 실패: {e}")
return
print_tickers(tickers)
data = get_public_data(session)
if data:
print("\n=== 공공데이터 응답(앞부분) ===")
print(str(data)[:300])
if __name__ == "__main__":
main()📌 실행 결과
python open_api.py=== 업비트 현재가 ===
KRW-BTC 158,420,000원 ▲ +2.13%
KRW-ETH 5,321,000원 ▼ -0.87%
KRW-XRP 4,185원 - +0.00%
[i] .env에 DATA_GO_KR_URL / DATA_GO_KR_KEY가 없어서 이 단계는 건너뜁니다.(가격은 실행 시점마다 달라져요!) .env에 공공데이터 키까지 채워 넣으면 두 번째 블록도 함께 출력됩니다.

📌 자주 나는 에러
1. SERVICE KEY IS NOT REGISTERED ERROR
- 원인 : 공공데이터포털 인증키는 인코딩 키와 디코딩 키 두 가지로 발급돼요. 어느 쪽을 써야 하는지가 호출 방식에 따라 다릅니다
- 해결 :
params={"serviceKey": ...}처럼 파라미터로 넘길 때는 디코딩 키, URL 문자열에 직접 이어 붙일 때는 인코딩 키를 쓰세요. 방금 발급받았다면 반영까지 1시간 정도 걸리기도 합니다
2. requests.exceptions.JSONDecodeError / Expecting value: line 1 column 1
- 원인 : 응답이 JSON이 아니에요. XML이거나 에러 페이지(HTML)일 가능성이 큽니다
- 해결 :
print(res.text[:200])으로 실제로 뭐가 왔는지 먼저 확인하세요. 공공데이터포털이면type=json(또는_type=json) 파라미터가 빠졌을 확률이 높아요
3. 429 Too Many Requests
- 원인 : 요청 수 제한 초과. 업비트 현재가 API는 초당 10회예요
- 해결 : 반복문에
time.sleep(0.2)를 넣거나, 위에서 만든 Retry + backoff_factor를 적용하세요. 여러 종목이 필요하면 한 번에 한 개씩 부르지 말고markets에 쉼표로 묶어서 한 번에 요청하는 게 정답입니다
4. 프로그램이 아무 반응 없이 멈춰 있어요
- 원인 :
timeout을 안 넣었어요. requests는 기본 타임아웃이 없습니다 - 해결 : 모든 요청에
timeout=5처럼 값을 지정하세요. 예외 없이요!
5. ModuleNotFoundError: No module named 'dotenv'
- 원인 : 패키지 이름은
python-dotenv인데 import 이름은dotenv라서 헷갈리기 쉬워요 - 해결 :
pip install python-dotenv로 설치하세요. 가상환경(venv)을 쓰고 있다면 활성화 상태인지도 확인!
📌 마무리
오늘은 requests로 오픈 API를 호출하는 흐름 전체를 잡아봤어요. 정리하면 이렇습니다 🙌
requests.get(url, params=..., timeout=...)으로 요청raise_for_status()로 실패를 즉시 잡고res.json()으로 파이썬 자료형 변환- 인증키는
.env+ python-dotenv,.gitignore에.env추가 - Session + Retry로 가끔 실패해도 알아서 다시 시도
이 5줄만 몸에 익히면 세상 대부분의 API를 다룰 수 있어요. 진짜예요!
다음 편에서는 오늘 배운 걸 한 단계 더 밀어붙여서 파이썬 웹 크롤링 실전 - 뉴스 수집 자동화를 해볼 예정이에요. API가 없는 사이트에서 데이터를 가져와 정리하는 방법을 다룹니다. 기대해주세요! 🙌
