매일 아침 KOSPI/KOSDAQ 지수, 외국인·기관 순매수 상위, 환율, 미국 빅테크 시세, 그리고 한국 투자자들의 해외 결제 현황까지 한 화면에 모아 보는 작은 대시보드를 만들어보고 싶었다. 티스토리 블로그에 임베드해두고 매일 아침 커피와 함께 한 번씩 확인할 수 있는, 그런 단순한 것.
그런데 단순할 줄 알았던 이 프로젝트는 "데이터를 불러오는 단 한 줄"에서 일주일을 잡아먹었다. 다음은 그 과정에서 발로 직접 밟아본 함정들의 기록이다. 비슷한 걸 만들어보려는 누군가가 같은 함정을 한 번이라도 덜 밟길 바라며.
스택 한 줄 요약
Frontend: 단순 HTML/CSS/JS, Tistory 임베드용
Backend: Cloudflare Pages + Workers (무료 플랜)
데이터 소스: 네이버 금융, Yahoo Finance, 공공데이터포털(예탁원 외화증권 결제정보)
1. Cloudflare Functions가 내 파일을 모른다
처음엔 Cloudflare Pages Functions의 표준 패턴인 functions/api/[[path]].js 캐치올 라우터로 구성했다. 그런데 wrangler pages deploy를 돌릴 때마다 출력에 이 줄만 떴다.
Uploaded 0 files (5 already uploaded)
그리고 어디에도 "Compiled Worker successfully" 문구는 없었다. /api/health를 호출하면 함수가 응답하는 게 아니라 정적 자산이 돌아왔다. Functions가 빌드되지 않고 있다는 뜻이다. 파일은 분명히 있고, 내용도 맞고, 권한도 맞는데 wrangler가 그 파일을 함수로 인식하지 못했다.
30번쯤 재배포해본 끝에 Pages Advanced Mode로 갈아탔다. functions/ 폴더 대신 _worker.js 단일 파일에 모든 라우팅을 직접 작성하는 방식이다.
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
if (!url.pathname.startsWith('/api')) {
return env.ASSETS.fetch(request);
}
// ... 라우팅 ...
}
};
그리고 _routes.json으로 어떤 경로가 Worker로 들어갈지 명시했다.
{"version":1, "include":["/api/*"], "exclude":[]}
이 순간 비로소 "Compiled Worker successfully"가 떴다. [[path]].js 파일명에 들어간 대괄호가 어떤 단계에서 발목을 잡은 것 같은데, 정확히 어디인지 끝까지 알아내지 못했다.
교훈: Cloudflare Pages Functions가 침묵하면, 더 묻지 말고 _worker.js Advanced Mode로 옮겨라.
2. macOS 한글 폴더명의 비밀 — NFD vs NFC
프로젝트 폴더 이름이 "동학개미 지표"였다. 한글이 들어가는 게 무슨 문제냐 싶었는데, bash에서 cd "동학개미 지표"가 "No such file or directory"로 떨어졌다. ls로 분명히 그 폴더가 보이는데도.
macOS 파일시스템은 한글 파일명을 NFD(자모 분리: ㄷ+ㅗ+ㅇ...) 형태로 저장하는데, 내가 타이핑한 한글은 NFC(완성형: 동) 형태로 입력된다. 같은 글자처럼 보여도 바이트 시퀀스가 다르다.
해결은 글로브로 우회하는 것이었다.
for d in */; do
if [[ "$d" != "outputs/" && "$d" != "uploads/" ]]; then
cd "$d"
break
fi
done
또 일부 파일은 rm이 "Permission denied"를 뱉었다. 결국 해당 파일은 못 지우고 빈 stub으로 덮어쓰는 식으로 마무리했다.
3. 네이버 모바일 JSON API의 변덕
지수와 환율은 네이버 금융 모바일 페이지의 비공식 JSON 엔드포인트에서 가져오는 게 보통의 정답이다. KOSPI는 잘 됐다.
GET https://m.stock.naver.com/api/index/KOSPI/basic
그런데 같은 패턴으로 환율과 미국 종목을 시도하니 400, 404, 406이 줄줄이 떨어졌다. 헤더를 바꿔도, User-Agent를 바꿔도 안 통했다. 네이버는 같은 도메인 안에서도 엔드포인트마다 다른 검증 규칙을 적용하고 있었다.
결국 환율과 미국 종목은 Yahoo Finance v8 chart endpoint로 갈아탔다.
GET https://query1.finance.yahoo.com/v8/finance/chart/{symbol}?range=3mo&interval=1d
여기에는 모든 게 다 들어 있다. meta.regularMarketPrice, meta.chartPreviousClose, 그리고 종가 배열까지. 스파크라인까지 한 번에 그릴 수 있다.
교훈: 무료 데이터 소스를 다룰 때 가장 큰 적은 "일관성의 부재"다. 하나의 출처를 끝까지 믿지 말고, 첫 시도가 실패하면 두 번째 소스를 항상 준비해둬라.
4. 네이버 HTML 스크래핑의 함정들
외국인·기관 순매수 상위는 네이버 데스크톱 페이지의 EUC-KR HTML을 스크래핑해야 했다. 여기서 만난 함정만 다섯 가지다.
(1) 인코딩. 응답이 EUC-KR이라 그대로 UTF-8로 디코딩하면 글자가 다 깨졌다. new TextDecoder('euc-kr').decode(buffer)로 강제 디코딩해야 한다.
(2) table 클래스명 오해. 처음에 다른 블로그를 참고해 <table class="type_5">를 찾도록 작성했는데, 실제 응답에는 type_r1으로 들어 있었다. 페이지 구조가 그 사이 바뀐 것이다. 디버그 라우트를 따로 만들어 실제 HTML 일부를 응답에 같이 실어 보내고 나서야 알아냈다.
(3) investor_gubun 값이 KOSPI/KOSDAQ에서 뒤집힘. KOSPI에선 외국인=1000, 기관=9000인데, KOSDAQ에선 같은 의미로 9000, 1000을 써야 했다. 같은 도메인 같은 메뉴인데 시장별로 코드가 반대로 매핑되어 있었다. 어떤 사양 문서에도 적혀 있지 않다.
(4) 코스닥은 숨은 iframe 전용 페이지로만 응답. 일반 URL로 부르면 외부 껍데기만 오고 실제 데이터는 별도 iframe URL을 호출해야 한다. 두 시장이 같은 메뉴에서 같은 UX를 제공하면서 내부 구조는 전혀 다르다.
(5) HTML 자체의 오타. <a class="tltle"> 같은 식으로 title도 아니고 tltle이라는 오타가 살아 있다. 정규식이 이걸 같이 잡도록 너그럽게 작성해야 한다.
이 단계만 디버깅에 사흘이 들었다.
5. 가장 큰 좌절 — 공공데이터포털과 예탁원(KSD)
대시보드의 핵심 카드 중 하나는 "한국 투자자의 해외주식 결제 상위"였다. 이건 다른 어디서도 구할 수 없고 한국예탁결제원(KSD)이 공식적으로 발표하는 데이터다. 공공데이터포털을 통해 API로 제공된다고 적혀 있어서 가볍게 끝낼 줄 알았다.
(1) 존재하지 않는 호스트
처음에 어떤 자료를 보고 opendata.ksd.or.kr이라는 호스트로 호출했다. 응답은 항상 5xx. 직접 브라우저로 접속해보니 "DNS_PROBE_FINISHED_NXDOMAIN" — 도메인 자체가 존재하지 않았다. 예전엔 있었는지, 처음부터 잘못된 정보였는지 알 수 없지만, 인터넷에 떠도는 일부 KSD API 안내 글들은 더 이상 유효하지 않은 호스트를 가리키고 있었다.
(2) 진짜 호스트는 공공데이터포털 통합 게이트웨이
정답은 KSD가 직접 호스팅하는 게 아니라 공공데이터포털 통합 게이트웨이를 거치는 것이었다.
https://apis.data.go.kr/1160100/GetDrForeSecuSettInfoService_V2/getMarkForeSecuSettStat_V2
1160100이 예탁원의 기관 코드, 그 뒤가 서비스명과 오퍼레이션명이다. 인증키는 공공데이터포털 마이페이지에서 발급받아 serviceKey 쿼리 파라미터로 붙인다.
(3) 406 Not Acceptable
호스트를 고쳐도 응답이 자꾸 406으로 떨어졌다. 정확한 키를 발급받았는데도. 원인은 Cloudflare Worker의 fetch가 기본으로 붙이는 Accept 헤더가 너무 화려해서 서버가 거부한 것이었다.
// 실패
fetch(url)
// → 기본 Accept 헤더: 너무 복잡하고 정확하지 않음
// 성공
fetch(url, {
headers: {
'User-Agent': 'curl/8.4.0',
'Accept': '*/*',
}
})
공공데이터 게이트웨이는 응답 협상에 민감했다. 헤더를 Accept: */* 한 줄로 단순화하니 통과됐다.
(4) 주말과 휴일에는 데이터가 비어 있다
주말에 테스트해보면 응답은 200인데 items가 텅 비어 있었다. KSD 결제 데이터는 영업일에만 집계되기 때문이다. 그래서 백엔드에서 오늘부터 과거 5영업일까지 거꾸로 훑으며 비어 있지 않은 첫 응답을 채택하도록 탐색 로직을 넣었다.
(5) 그리고 가장 깊은 좌절 — 종목별 데이터는 없다
위의 모든 문제를 해결하고 나서야 비로소 깨달았다. 공공데이터 API로 받을 수 있는 KSD 외화증권 결제정보는 시장(국가) 단위 집계까지가 끝이다.
이 서비스에 들어 있는 두 오퍼레이션은 다음과 같다.
getMarkForeSecuSettStat_V2 — 시장(국가)별 결제 통계 (미국 전체, 일본 전체, 홍콩 전체, …)
getForeSecuSettInfo_V2 — 이름은 "종목별"처럼 들리지만 실제 응답은 통화/시장 코드 단위 집계 (ISIN/종목명 필드 자체가 없음)
다시 말해 "한국 투자자가 오늘 NVIDIA를 얼마나 결제했는지" 같은 종목 단위 숫자는 공공 API로 제공되지 않는다. 그건 KSD 자체 포털인 SEIBro(seibro.or.kr)에서만 화면으로 조회할 수 있고, 그것조차 명시적 API가 아닌 내부 AJAX 호출이라 프로그램적 접근을 권장하지 않는다.
나는 카드 제목을 "한국 투자자 해외주식 결제 상위"에서 "국가별 외화증권 결제 상위"로 바꿨다. 보고 싶었던 그림은 결국 못 그렸다.
대안들을 따져봤지만
혹시 다른 길이 있나 싶어 따져본 옵션들도 결국 다 막혔다.
SEIBro 직접 스크래핑 — POST + 세션 쿠키 + XML payload. Cloudflare Worker에서 구현 가능하지만 비공식이고 안내문에 "프로그램적 접근 금지"가 명시되어 있다.
증권사 API (한국투자증권 KIS Developers 등) — 본인 계좌 거래만 보여주고, 전체 한국 투자자 집계는 제공하지 않는다.
FnGuide / WiseFn — 정확한 종목별 데이터를 갖고 있지만 유료. 개인 블로그 임베드용으론 비용이 맞지 않는다.
증권사 리서치 페이지 크롤링 — 일부 회사가 "이번 주 해외주식 순매수 TOP10" 보고서를 발행하지만 형식이 자주 바뀌고 비공식이다.
결국 그 자리에는 "한국인 선호 해외주식 watchlist 100종목 중 오늘 가장 많이 움직인 5종목"이라는 우회 카드를 채워 넣기로 했다. 진짜 결제 데이터가 아니라 "한국인이 자주 사는 종목들의 오늘자 변동"이지만, 사용자 체감으로는 비슷한 정보를 준다. 다만 카드 제목과 부제에는 그 사실을 솔직히 적었다 — 우회는 우회라고.
6. 무엇을 배웠는가
이 프로젝트의 백엔드는 결국 _worker.js 한 파일 24KB짜리로 끝났다. 라우트가 6개, 데이터 소스가 3개. 그걸 만드는 데 일주일이 걸렸다.
돌아보면 본질적인 교훈은 세 가지다.
첫째, 비공식 데이터는 첫 시도가 실패하면 곧바로 다른 소스를 준비하라. 네이버 모바일 JSON이 한 엔드포인트에서 안 통하면 같은 엔드포인트의 다른 변형도 시간 문제다. Yahoo Finance 같은 백업을 항상 같이 가져가는 게 안전하다.
둘째, 공공데이터 API는 "있다는 사실"과 "내가 필요한 그 데이터가 있다는 사실"이 전혀 다른 이야기다. 서비스 목록에 KSD 외화증권 결제 정보가 있다고 해서 종목 단위 데이터가 있다는 뜻이 아니다. 시작하기 전에 응답 스키마와 샘플 응답을 반드시 한 번 눈으로 확인하라. 명세서가 흐릿하면 그건 대개 데이터가 흐릿하다는 뜻이다.
셋째, 우회는 정직하게 표기하라. "종목별 결제금액(예탁원)"이라고 적고 싶었던 카드에 watchlist 기반 변동률을 채워 넣으면서, 카드 제목과 부제를 솔직하게 바꿨다. 사용자(=나)도 속이지 않는 게 결국 오래 가는 대시보드를 만든다.
대시보드는 현재 6개 카드 — KOSPI/KOSDAQ 지수, 환율, 미국 빅테크 시세, 외국인 순매수 TOP, 기관 순매수 TOP, 국가별 외화증권 결제 상위 — 로 운영 중이고, "한국인 선호 해외주식 변동 TOP" 카드가 곧 추가될 예정이다. 공공데이터 API와 비슷한 작업을 시도하시는 분들께 작은 참고가 되길 바란다.
반응형
'Ai 취미생활' 카테고리의 다른 글
| Hermes Agent란? Gemini AI 연동 설치 방법 완전 정복 (2026년 최신) (2) | 2026.05.20 |
|---|---|
| VS Code에서 Gemma 4 로컬 AI 쓰는 방법 - GitHub Copilot 없이 (0) | 2026.05.06 |
| 코딩 몰라도 됩니다 - 안티그레비티 바이브 코딩으로 바이낸스 자동매매 프로그램 만들기 (3) | 2026.04.30 |
| 안티그레비티 + Gemma 4 연동 가이드 — 맥미니 M4에서 완전 무료 로컬 AI 개발환경 만들기 (1) | 2026.04.29 |
| Google 안티그레비티(Antigravity) 완벽 가이드 — AI 에이전트 IDE 설치부터 실전 사용법까지 (0) | 2026.04.29 |