JS 렌더링 · AJAX · fetch · API vs 스크래핑 · 공공데이터포털
박수현 · aSSIST University · 2026
🎨 이미지 프롬프트(배경): "Two divergent paths — one through dense HTML forest with manual scraping, the other through clean API gateway with structured JSON streams, teal and purple tones, editorial illustration, 16:9, no text"
📖 도서 매핑: Ch.5 §1~2 + Ch.8 §1~3
회차 1 의 그림이 이번 회차의 출발점
| 패턴 | 동작 | 난이도 |
|---|---|---|
| SSR (Server-Side Rendering) | 서버가 매 요청마다 데이터 + HTML 합쳐서 응답 | 쉬움 |
| SSG (Static Site Generation) | 빌드 타임에 모든 HTML 미리 생성 (정적 파일) | 간단함 |
| CSR (Client-Side Rendering) | 빈 HTML + JS 만 응답 → 브라우저가 데이터 가져와 그림 | 어려움 |
| SPA (Single Page Application) | 한 HTML 로 시작, 페이지 전환마다 JS 가 화면만 갈아끼움 | 어려움 — CSR 의 일종 |
🔑 회차 3의 BS4가 100% 통하는 영역 = SSR + SSG.
AJAX = 새로고침 없이 서버와 데이터를 주고받는 기법 (원래 XML, 지금은 JSON). DOM = 브라우저가 만든 HTML 트리.
// 페이지 로드 후, JS가 추가 데이터 요청
async function loadProducts() {
const r = await fetch(
"/api/products?page=1");
const data = await r.json();
// → DOM 에 동적으로 추가
renderProducts(data.items);
}
loadProducts();
서버는 처음에 빈 컨테이너만 보냄.
JS 가 fetch → JSON 받기 → DOM 채우기.

🔑 JSON API 가 보이면 무조건 그쪽으로.

상품 카드들이 격자로. 평범한 쇼핑 페이지.

절차: F12 → Network 탭 → 상단 필터 Fetch/XHR 클릭
페이지가 로드되면서 여러 JSON 요청 발생.
상품 데이터는 JSON API 안에.
XHR = XMLHttpRequest, JS 가 만든 비동기 요청 (브라우저가 서버에 데이터 추가로 받는 통로)
📖 도서 참조: Ch.5 §1 — "JS가 늦게 채우는 데이터"
스크래핑은 마지막 카드
1
공식 Open API 가 있고 한도가 충분한가?
✅ "예" → 공식 API가장 안전·안정 (회차 5 — 공공데이터포털 등)
2
Network XHR/Fetch 에 숨은 JSON API 가 있나?
⚠️ "예" → JSON API 직접 호출회색지대 — 사이트 약관·robots.txt 점검 후 사용
3
위 둘 다 없다 — 마지막 카드
✅ HTML 스크래핑회차 3 (정적) · 회차 6 (Selenium)
🔑 우선순위
공식 API > 숨은 JSON > HTML 스크래핑 — 위에서부터 시도. 한 단계 내려갈수록 윤리·법적 위험 증가.

서버가 요리(데이터) 를 만들어 응답.
스크래핑은 손님이 주방에 들어가서 직접 그릇 들고 나오는 격.
| 항목 | 공식 API | 스크래핑 |
|---|---|---|
| 합법성 | ✅ 명확 | ⚠️ 회색 |
| 안정성 | ✅ 계약된 스키마 | ❌ DOM(HTML 트리) 변경 취약 |
| 속도 | ✅ 1초 100건 | 🐢 1초 1~5건 |
| 인증 | API 키 / OAuth | 헤더 위장 (User-Agent 를 브라우저처럼 꾸미기) |
| 변경 알림 | ✅ deprecation 메일 (deprecation = 기능 폐기 사전 공지) | ❌ 갑자기 깨짐 |
| 데이터 양 | 한도 있음 | HTML 닿는 만큼 |
| 비용 | 일부 유료 | 무료 (서버 비용은 자기) |
🎯 연구자 입장
첫 시도는 항상 API. 안 되면 그제서야 스크래핑.
한국 연구자의 첫 API — 법적으로 가장 안전

data.go.kr — 회원가입 후 API 키 발급.
총 7만+ 데이터셋 제공.

+/=)가 %2B%2F%3D 로 변환된 형태 — URL 에 직접 넣어 GET 호출할 때requests.get(url, params={...}) 처럼 라이브러리가 인코딩해줄 때📌 보통 Decoding 키를 코드에 넣고, requests 가
params=로 자동 인코딩.
import requests, pandas as pd
from datetime import datetime, timedelta
API_KEY = "여기에-decoding-키-붙여넣기"
# base_time 은 02/05/08/11/14/17/20/23
# 8회 발표. 어제 0500 발표분이 안정적
yesterday = (
datetime.now() - timedelta(days=1)
).strftime("%Y%m%d")
base_time = "0500"
url = (
"http://apis.data.go.kr/1360000/"
"VilageFcstInfoService_2.0/getVilageFcst"
)
params = {
"serviceKey": API_KEY,
"numOfRows": 1000, "pageNo": 1,
"dataType": "JSON",
"base_date": yesterday,
"base_time": base_time,
"nx": 60, "ny": 127, # 서울 종로
}
r = requests.get(
url, params=params, timeout=10)
data = r.json()
items = (
data["response"]["body"]
["items"]["item"]
)
df = pd.DataFrame(items)
print(df.head())
df.to_csv(
"weather.csv",
index=False,
encoding="utf-8-sig",
)
📌 격자 좌표 (
nx,ny) 는 위경도가 아닌 기상청 5 km 격자값. 서울 종로 ≈ (60, 127). 다른 지역은 기상청 「격자좌표 파일」 참조.
🎯 30줄로 7일치 예보 1,000건. 회차 3의 50페이지 스크래핑보다 100배 빠름.
💬 Antigravity / ChatGPT 입력 air_quality.py
📋 prompt prompts/ch08_prompts.md 📄 example examples/ch08/air_quality.py
Python 스크립트 air_quality.py 를 만들어 줘.
목적: 공공데이터포털의 한국환경공단 "측정소별
실시간 측정정보 조회" API 로 서울 강남구 측정소의
최근 24건 미세먼지 데이터를 받아 DataFrame 으로
출력한다.
요구사항:
1. 라이브러리: requests, pandas, python-dotenv
2. .env 의 PUBLIC_DATA_KEY_DECODED (디코딩된
일반 인증키) 를 load_dotenv() 로 읽기
3. 엔드포인트:
http://apis.data.go.kr/B552584/
ArpltnInforInqireSvc/getMsrstnAcctoRltmMesureDnsty
4. 쿼리 파라미터:
- serviceKey: 환경변수값
- numOfRows: 24
- pageNo: 1
- stationName: "강남구"
- dataTerm: "DAILY"
- ver: "1.3"
- returnType: "json" (XML 기본 → JSON 강제)
5. 요청: requests.get(..., timeout=10)
→ resp.raise_for_status() → resp.json()
6. 응답 경로:
data["response"]["body"]["items"]
를 pd.DataFrame 으로 변환
7. df[["dataTime","pm10Value","pm25Value"]].head()
를 print
에러 처리:
- HTTP 오류 → 메시지 출력 후 종료
- items 가 없거나 빈 리스트면 안내 후 종료
- PUBLIC_DATA_KEY_DECODED 미설정이면 안내 후 종료
환경: Python 3.11, venv. 코드만, 한국어 주석 최소.
returnType: "json" 이 빠지면 AI 가 XML 응답을 받게 코드 작성data["response"]["body"]["items"] 명시 → 공공데이터 특유의 깊은 nesting 안전 통과💡 다른 공공 API 응용 — 동일 프롬프트에서:
- 엔드포인트 URL 만 교체
- 파라미터 키 이름만 교체
- 응답 경로만 교체
하면 어떤 공공데이터든 30 줄 안에 호출.
📖 출처:
ch08_prompts.md§3 — 공공데이터포털 정복.
{
"response": {
"header": {
"resultCode": "00",
"resultMsg": "NORMAL_SERVICE"
},
"body": {
"dataType": "JSON",
"items": {
"item": [
{
"baseDate": "20260501",
"baseTime": "0600",
"category": "TMP",
"fcstValue": "18",
"fcstDate": "20260502"
},
...
]
}
}
}
}
data["response"]["body"]["items"]["item"] 가 레코드 리스트📌 resultCode "00" 이 정상. 자주 만나는 비정상 코드: -
30NO_AUTHORIZED— 키가 잘못됨 -22LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS— 일일 한도 초과 -33DEADLINE_HAS_EXPIRED— 활용신청 만료
API Key (가장 단순)
params = {"serviceKey": API_KEY}
requests.get(url, params=params)
Bearer Token (OAuth 후)
headers = {"Authorization": f"Bearer {TOKEN}"}
requests.get(url, headers=headers)
OAuth 2.0 — 3 단계: 1) 앱 등록 (Client ID + Secret) → 2) 사용자 동의 받아 인가 코드 발급 → 3) 코드를 토큰으로 교환해 Bearer 호출
HMAC (Hash-based Message Authentication Code) — 비밀키 + 요청 내용을 해시 → Authorization 헤더에 서명 동봉. 키 자체를 송신하지 않음 (보안 ↑↑, 거래소·결제 API)
| 방식 | 만나는 곳 |
|---|---|
| API Key | 공공데이터, OpenWeather |
| Bearer | OpenAI, Claude, GitHub |
| OAuth | Google, Naver Login |
| HMAC | AWS, 거래소, 카카오페이 |
| 상태 | 의미 | 해결 |
|---|---|---|
| 401 Unauthorized | 키 없음/오타 | 키 확인 |
| 403 Forbidden | 키 있음 but 권한 부족 | 활용신청 승인 확인 |
| 429 Too Many Requests | 한도 초과 | sleep + 지수적 대기 |
| 500/502 Server Error | 서버 일시 장애 | retry |
resultCode != "00" |
응답 구조에 에러 | resultMsg 출력 |
# 안전한 호출 + 메시지 점검
r = requests.get(url, params=params, timeout=10)
r.raise_for_status() # HTTP 에러 → 예외
data = r.json()
header = data["response"]["header"]
if header["resultCode"] != "00":
raise RuntimeError(header["resultMsg"])
같은 데이터, 다른 합법성
opendart.fss.or.krdata.krx.co.kryfinance)🎯 권장
공식 우선 — 회차 7 윤리 파트와 직결. 비공식은 개인 학습용.
https://opendart.fss.or.kr/uss/umt/EgovMberInsertView.dohttps://opendart.fss.or.kr/guide/main.do?apiGrpCd=DS001| 번호 | API명 | 상세기능 | 개발가이드 |
|---|---|---|---|
| 1 | 공시검색 | 공시 유형별, 회사별, 날짜별 등 여러가지 조건으로 공시보고서 검색기능을 제공합니다. | 바로가기 |
| 2 | 기업개황 | DART에 등록되어있는 기업의 개황정보를 제공합니다. | 바로가기 |
| 3 | 공시서류원본파일 | 공시보고서 원본파일을 제공합니다. | 바로가기 |
| 4 | 고유번호 | DART에 등록되어있는 공시대상회사의 고유번호,회사명,종목코드, 최근변경일자를 파일로 제공합니다. | 바로가기 |
🎯 Session 5 적용 포인트: "공식 API 우선" 원칙에 맞는 대표 사례가 OpenDART.
| 메서드 | 요청 URL | 인코딩 | 출력포맷 |
|---|---|---|---|
| GET | https://opendart.fss.or.kr/api/list.json |
UTF-8 | JSON |
| GET | https://opendart.fss.or.kr/api/list.xml |
UTF-8 | XML |
| 요청키 | 명칭 | 타입 | 필수여부 | 값설명 |
|---|---|---|---|---|
crtfc_key |
API 인증키 | STRING(40) | Y | 발급받은 인증키(40자리) |
corp_code |
고유번호 | STRING(8) | N | 공시대상회사의 고유번호(8자리), 개발가이드 > 공시정보 > 고유번호 참고 |
bgn_de |
시작일 | STRING(8) | N | 검색시작 접수일자(YYYYMMDD), 기본값=end_de, corp_code 없으면 검색기간 3개월 제한 |
end_de |
종료일 | STRING(8) | N | 검색종료 접수일자(YYYYMMDD), 기본값=당일 |
last_reprt_at |
최종보고서 검색여부 | STRING(1) | N | Y or N, 기본값=N(정정이 있으면 최종정정만 검색) |
pblntf_ty |
공시유형 | STRING(1) | N | A~J (A정기, B주요사항, C발행, D지분, E기타, F외부감사, G펀드, H자산유동화, I거래소, J공정위) |
pblntf_detail_ty |
공시상세유형 | STRING(4) | N | 상세 유형 코드 |
corp_cls |
법인구분 | STRING(1) | N | Y(유가), K(코스닥), N(코넥스), E(기타), 미입력 시 전체 |
sort |
정렬 | STRING(4) | N | date(접수일), crp(회사명), rpt(보고서명), 기본값=date |
sort_mth |
정렬방법 | STRING(4) | N | asc / desc, 기본값=desc |
page_no |
페이지 번호 | STRING(5) | N | 기본값=1 |
page_count |
페이지별 건수 | STRING(3) | N | 1~100, 기본값=10, 최대=100 |
| 응답키 | 명칭 | 출력설명 |
|---|---|---|
status |
에러 및 정보 코드 | 메시지 설명 참조 |
message |
에러 및 정보 메시지 | 메시지 설명 참조 |
page_no |
페이지 번호 | 페이지 번호 |
page_count |
페이지별 건수 | 페이지별 건수 |
total_count |
총 건수 | 총 건수 |
total_page |
총 페이지 수 | 총 페이지 수 |
list |
목록 | 공시 목록 배열 |
corp_cls |
법인구분 | Y/K/N/E |
corp_name |
종목명(법인명) | 상장사 종목명 또는 법인명 |
corp_code |
고유번호 | 8자리 |
stock_code |
종목코드 | 6자리 |
report_nm |
보고서명 | 공시구분+보고서명+기타정보(기재정정/첨부정정/첨부추가 등) |
rcept_no |
접수번호 | 14자리 |
flr_nm |
공시 제출인명 | 공시 제출인명 |
rcept_dt |
접수일자 | YYYYMMDD |
rm |
비고 | 유/코/채/넥/공/연/정/철 조합 |
공시뷰어 연결 예시:
https://dart.fss.or.kr/dsaf001/main.do?rcpNo=접수번호
https://opendart.fss.or.kr/api/list.json?crtfc_key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&bgn_de=20200117&end_de=20200117&corp_cls=Y&page_no=1&page_count=10
{
"status": "000",
"message": "정상",
"page_no": 1,
"page_count": 10,
"total_count": 210,
"total_page": 21,
"list": [
{
"corp_code": "00120182",
"corp_name": "NH투자증권",
"stock_code": "005940",
"corp_cls": "Y",
"report_nm": "[첨부추가]일괄신고추가서류(파생결합증권-주가연계증권)",
"rcept_no": "20200117000559",
"flr_nm": "NH투자증권",
"rcept_dt": "20200117",
"rm": ""
},
{
"corp_code": "00878915",
"corp_name": "iM금융지주",
"stock_code": "139130",
"corp_cls": "Y",
"report_nm": "소송등의판결ㆍ결정(자회사의 주요경영사항)",
"rcept_no": "20200117800593",
"flr_nm": "iM금융지주",
"rcept_dt": "20200117",
"rm": "유"
},
{
"corp_code": "00120571",
"corp_name": "롯데칠성음료",
"stock_code": "005300",
"corp_cls": "Y",
"report_nm": "타법인주식및출자증권취득결정",
"rcept_no": "20200117800584",
"flr_nm": "롯데칠성음료",
"rcept_dt": "20200117",
"rm": "유정"
}
// ... (총 10건, 원문 예제와 동일 구조)
]
}
✅ 해석 포인트:
status=000이면 정상,list배열에서rcept_no로 공시뷰어 URL을 바로 생성할 수 있습니다.
| 메서드 | 요청 URL | 인코딩 | 출력포멧 |
|---|---|---|---|
| GET | https://opendart.fss.or.kr/api/corpCode.xml |
UTF-8 | Zip FILE (binary) |
| 요청키 | 명칭 | 타입 | 필수여부 | 값설명 |
|---|---|---|---|---|
crtfc_key |
API 인증키 | STRING(40) | Y | 발급받은 인증키(40자리) |
| 응답키 | 명칭 | 출력설명 |
|---|---|---|
status |
에러 및 정보 코드 | 메시지 설명 참조 |
message |
에러 및 정보 메시지 | 메시지 설명 참조 |
corp_code |
고유번호 | 공시대상회사의 고유번호(8자리), ZIP 안 XML 정보 |
corp_name |
정식명칭 | 정식회사명칭, ZIP 안 XML 정보 |
corp_eng_name |
영문 정식명칭 | 영문정식회사명칭, ZIP 안 XML 정보 |
stock_code |
종목코드 | 상장회사 주식 종목코드(6자리), ZIP 안 XML 정보 |
modify_date |
최종변경일자 | 기업개황정보 최종변경일자(YYYYMMDD), ZIP 안 XML 정보 |
zip으로 바꾸면 정상 확인 가능API 인증키🔗 개발가이드:
https://opendart.fss.or.kr/guide/detail.do?apiGrpCd=DS001&apiId=2019018
import yfinance as yf
import pandas as pd
import matplotlib.pyplot as plt
from datetime import date
# 삼성전자 (005930.KS) — 2025-01-01 ~ 오늘
today = date.today().strftime("%Y-%m-%d")
df = yf.download("005930.KS", start="2025-01-01", end=today, auto_adjust=False)
# 1) 데이터 저장 (캔들 차트용 OHLCV 포함)
df.to_csv("output/price_data/samsung_2025_to_today.csv", encoding="utf-8-sig")
# 2) 가장 간단한 시계열 시각화 (종가)
close = df["Close"]
ax = close.plot(figsize=(10, 4), title="Samsung Electronics Close (2025~Today)")
ax.set_xlabel("Date")
ax.set_ylabel("KRW")
plt.tight_layout()
plt.savefig("output/charts/samsung_2025_to_today_close.png", dpi=160)
plt.show()
🎯 회차 8 시계열 분석의 베이스 데이터.
삼성전자 2025년 1월 1일부터 현재까지의 주가 데이터 시각화

코드 실행 결과:
samsung_2025_to_today_close.png
2026년 1월 1일부터 현재까지의 주가 데이터에 2026년 1월 한달간의 공시자료 오버레이

코드 실행 결과:
samsung_2026_price_disclosures.png
댓글 1만 건 → 분류·감성·요약 = LLM 1~2분

LLM 은 빠르고 정확, but 비용 + 추론 비용 + 데이터 외부 송신.
회차 7 에서 데이터 외부 송신 위험 을 자세히.
| 비교 항목 | 전통 NLP | LLM API |
|---|---|---|
| 초기 셋업 | 수일~수주 | 수분 |
| 학습 데이터 | 직접 라벨링 필요 | 불필요 |
| 정확도 | 도메인 한정 | 범용 고정밀 |
| 비용 | 인건비·서버 | API 호출당 과금 |
| 데이터 외부 송신 | 없음 (로컬) | 외부 전송 발생 |
| 한국어 품질 | 형태소 정밀 | 문맥 우수 |
| 재현성 | 동일 입력 = 동일 출력 | 모델 업데이트로 변동 |
📌 언제 전통 NLP 가 더 나은가 — 의료·법률 등 외부 송신 금지 데이터, 비용 민감, 동일 입력 = 동일 출력 보장 필요한 연구.
import os
from anthropic import Anthropic
# ANTHROPIC_API_KEY 환경변수 자동 사용
client = Anthropic()
text = (
"배송이 빠르고 포장도 깔끔했어요. "
"다만 제품 색이 사진과 좀 달라요."
)
resp = client.messages.create(
model="claude-haiku-4-5",
max_tokens=200,
messages=[{
"role": "user",
"content": (
"다음 리뷰를 분석해 JSON 으로 응답해.\n"
'{"sentiment": '
'"positive|negative|neutral", '
'"topics": [...], '
'"summary": "..."}\n\n'
f"리뷰: {text}"
),
}],
)
print(resp.content[0].text)
{
"sentiment": "neutral",
"topics": ["배송", "포장", "색상"],
"summary": "배송·포장 만족, 색상 불일치 불만"
}
Anthropic() — ANTHROPIC_API_KEY 환경변수 자동 인식, 첫 줄로 클라이언트 준비model="claude-haiku-4-5" — 가장 저렴·빠른 모델 (분류·요약에 충분)max_tokens=200 — 응답 길이 상한 (비용 제어)resp.content[0].text → 문자열 → json.loads() 로 파이썬 dict 변환📌 회차 8 에서 1만 건 일괄 분류 + 비용 최적화 다룸.
💬 Antigravity / ChatGPT 입력 llm_sentiment.py
📋 prompt prompts/ch11_prompts.md 📄 example examples/ch11/llm_sentiment.py
Python 스크립트 llm_sentiment.py 를 만들어 줘.
목적: 한국어 리뷰 한 건의 감성·측면별 평가·
칭찬/불만 키워드를 OpenAI LLM 에 JSON 으로 요청해
dict 로 반환한다.
요구사항:
1. 라이브러리: openai, python-dotenv, 표준 json
2. .env 의 OPENAI_API_KEY 를 load_dotenv() 로 읽고
OpenAI 클라이언트 생성
3. 함수 시그니처:
sentiment_of(text: str) -> dict
4. 모델: gpt-4o-mini,
response_format={"type": "json_object"}
5. 메시지 구성:
- system: "한국어 리뷰의 감성을 분석한다.
JSON 만 반환한다."
- user: 리뷰 본문 + 다음 JSON 스키마로 응답:
{
"sentiment": "positive|negative|neutral",
"score": -1.0 ~ 1.0 사이 실수,
"aspects": {
"delivery":"positive|negative|neutral|not_mentioned",
"quality": "...",
"price": "...",
"service": "..."
},
"praise": [핵심 칭찬 키워드],
"complaint": [핵심 불만 키워드]
}
6. 반환:
json.loads(resp.choices[0].message.content)
7. 하단에서 예시 리뷰로 호출 후 결과 print
에러 처리:
- JSON 파싱 실패 시 기본 구조 (모든 필드를
neutral/not_mentioned, score=0.0,
praise=[], complaint=[]) 로 반환
- API 실패 시 동일한 기본 구조 반환
환경: Python 3.11, venv. 코드만, 한국어 주석 최소.
response_format={"type": "json_object"} 와 함께 사용해 응답 일관성 극대화neutral 과 not_mentioned 분리 — "리뷰에 안 나온 측면" 과 "중립적으로 언급된 측면" 을 구분 → 통계가 정확해짐💡 연구 응용:
- 리뷰 → 의료 면담록·민원·인터뷰 전사
- aspects 4개 → 본인 연구 차원 (예: 감정/대응/만족)
으로 바꾸면 정성 데이터 코딩 자동화.
📖 출처:
ch11_prompts.md§2 — 두 가지 접근 (감성사전 vs LLM).
from openai import OpenAI
client = OpenAI() # OPENAI_API_KEY 환경변수
resp = client.chat.completions.create(
model="gpt-4o-mini",
response_format={"type": "json_object"},
messages=[
{"role": "system", "content": "리뷰를 JSON 으로 분석해."},
{"role": "user", "content": text},
],
)
print(resp.choices[0].message.content)
| Anthropic (Claude) | OpenAI (GPT) | |
|---|---|---|
| 강점 | 긴 컨텍스트(200K~1M 토큰), 분석·코드 정확성 | 생태계 광범위, JSON mode (응답을 항상 유효 JSON 으로 강제) |
| 가격 | Haiku 매우 저렴 | gpt-4o-mini 매우 저렴 |
| API key 변수 | ANTHROPIC_API_KEY |
OPENAI_API_KEY |
🎯 둘 다 .env 에 키 보관 — 코드에 직접 박지 말 것.
| 모델 | 입력 1M 토큰 | 출력 1M 토큰 | 1만 리뷰 추정 |
|---|---|---|---|
| Claude Haiku 4.5 | $1.00 | $5.00 | $1~$3 (사용량 따라) |
| Claude Sonnet 4.6 | $3.00 | $15.00 | ~ $5 전후 |
| Claude Opus 4.7 | $5.00 | $25.00 | $8~$15 (요금 변동 가능) |
| GPT-4o-mini | $0.15 | $0.60 | ~ $0.3 |
| GPT-4o | $2.50 | $10.00 | ~ $4 |
📌 2026-05 기준 — 강의일에
anthropic.com/pricing/openai.com/api/pricing에서 재확인 필수🎯 학위논문 가이드
첫 시도 — Haiku / 4o-mini. 정확도 부족하면 Sonnet 으로. Opus 는 샘플 1,000건 검증용.
공공데이터 + LLM 1회씩
df.head() 캡처weather.csv (또는 tour.csv)books_with_genre.csvnotebook.ipynb"스크래핑은 마지막. 먼저 API 가 있는지 본다."