서울 리전의 작은 VPS 한 대가 키움증권 미국주식 조건검색을 실시간으로 감시하고, 조건에 새로 걸린 종목의 시세를 계속 갱신해서 올려둡니다. 이 브랜드 사이트의 관리자 페이지는 그 서버에 명령을 전달하고 설정을 바꾸는 역할만 합니다. 코드 수정이나 파일 관리는 일부러 사이트에 넣지 않았습니다 — 웹 화면이 할 수 있는 일이 적을수록, 웹 화면이 뚫렸을 때 잃을 것도 적습니다.
01왜 만들었나
조건검색식은 이미 영웅문Global에 열 개 넘게 만들어 두고 쓰고 있었습니다. 문제는 그 결과를 보려면 PC 앞에 앉아 프로그램을 켜두고 있어야 한다는 점이었습니다. 미국장은 한국 시간으로 밤에 열리고, 프리마켓까지 치면 감시해야 하는 시간은 하루의 절반을 넘습니다. 그래서 감시는 꺼지지 않는 서버에 맡기고, 저는 어디서든 브라우저로 결과만 확인하는 구조가 필요했습니다. 동시에 이 구조가 나중에 주문 실행까지 이어질 수 있어야 했습니다.
02구조 — 세 개의 층, 한 방향의 신뢰
전체는 세 층으로 나뉩니다. 관리자 페이지, 그 사이에서 우체통 역할을 하는 Cloudflare Worker + D1, 그리고 실제로 키움과 통신하는 VPS입니다. 관리자 페이지와 VPS는 서로를 직접 부르지 않습니다. 둘 다 가운데 층에 글을 쓰고, 가운데 층에서 글을 읽을 뿐입니다.
관리자 페이지 (브라우저) │ 명령 전달 · 설정 수정 ▲ 1초마다 조회만 ▼ │ Cloudflare Worker + D1 — 명령 큐 · 종목 테이블 · 상태값 ▲ 2초 폴링: 명령 수령 / 결과 · 시세 업로드 │ VPS (Vultr Seoul · systemd 상주) — 키움 호출은 오직 여기서만 │ REST: 토큰 · 시세 · 차트 / WebSocket: 조건검색 실시간 ▼ 키움증권 미국주식 API
전체 구조 — 관리자 페이지와 VPS는 서로를 직접 호출하지 않는다
03커맨드 큐 — 서버가 먼저 묻는다
집에 있는 PC든 VPS든, 밖에서 안으로 요청을 밀어넣으려면 포트를 열어야 합니다. 그 대신 방향을 뒤집었습니다. 관리자 페이지에서 "연결 확인", "검색식 조회", "실시간 검색 시작/중지", "차트 조회" 같은 버튼을 누르면 명령은 D1에 pending 상태로 쌓이기만 합니다. VPS가 2초마다 "할 일 있어?"라고 묻고, 있으면 in_progress로 바꿔서 가져간 뒤 실행하고, 결과를 다시 올려놓습니다. 관리자 페이지는 그 결과가 올라올 때까지 기다렸다가 화면에 그립니다.
# 사이트: 명령을 적어두기만 한다 POST commands { type: "start_search", payload: { seq } } → status: pending # VPS: 2초마다 먼저 묻는다 (이 폴링 자체가 생존 신호 역할도 겸함) GET next-command → status: in_progress POST commands/:id/result { ok, data } → status: done
이 폴링이 곧 하트비트이기도 해서, 별도의 생존 신호 루프를 따로 두지 않았습니다. 마지막으로 물어본 시각이 곧 서버가 살아있던 마지막 시각입니다. 사이트발 수동 명령과 서버 내부의 자동 스케줄이 같은 조건검색을 동시에 건드리지 않도록, 시작·중지는 하나의 잠금(lock)으로 직렬화했습니다.
04미국주식 조건검색 — 문서에 한 줄로 적힌 함정들
처음에는 국내주식 조건검색으로 연동을 검증했습니다. 동작은 했지만, 나중에 붙일 주문 엔진(LOG_01)이 미국주식용이라는 걸 떠올리고 시장을 통일하기로 하면서 다시 연결했습니다. 미국주식 쪽은 공식 문서와 실계좌 테스트를 번갈아 보며 몇 가지를 확정해야 했습니다.
- 다른 문으로 들어가야 한다 — 로그인은 계좌 단위라 어느 웹소켓 경로에서든 되지만, 미국주식 조건검색 프레임은 국내용과 다른 전용 경로에서만 응답합니다.
- 목록을 먼저 물어봐야 한다 — 같은 연결 안에서 조건식 목록조회(
GCNSRLST)를 한 번 거치지 않으면, 실시간 조회 요청(GCNSRREQ)은 에러도 없이 조용히 무시됩니다. 문서에는 "시스템 내부구조상"이라는 한 줄로만 적혀 있었습니다. - 실시간 이벤트에는 가격이 없다 — 편입(
I)·이탈(D) 신호와 종목코드, 거래소 구분만 옵니다. 현재가와 등락률은 시세 조회 TR을 따로 불러서 채워야 합니다. - 서두르면 막힌다 — 스무 종목 이상을 연달아 조회하면 곧바로
429 Too Many Requests가 돌아옵니다. 조회마다 짧은 간격을 두고, 막히면 점점 길게 기다렸다가 다시 시도합니다.
그리고 한 번 리스트에 올라온 종목은 조건을 벗어나도 지우지 않고 "이탈" 표시만 남깁니다. 잠깐 조건을 스쳐간 종목이 오히려 나중에 가장 궁금한 종목일 때가 많기 때문입니다.
051초마다 바뀌는 것처럼 — 두 개의 속도 분리
종목 리스트의 가격이 거의 실시간으로 움직이길 원했지만, 그렇다고 화면을 연 사람 수만큼 키움 API를 두드리면 안 됩니다. 그래서 속도를 둘로 나눴습니다. VPS는 감시 중인 종목들을 라운드로빈으로 한 바퀴씩 돌며 시세를 조회해서 D1에 계속 덮어씁니다. 관리자 페이지는 1초마다 D1만 읽습니다. 화면을 몇 개 띄우든 키움 쪽 호출량은 VPS의 순회 속도 하나로만 결정됩니다.
첫 구현에는 실수가 있었습니다. 순회를 한 바퀴 돌 때마다 접근 토큰을 새로 발급받고 있었던 겁니다. 토큰은 24시간 유효한데, 종목이 몇 개 없으면 1~2초마다 재발급을 요청하는 셈이었습니다. 토큰을 캐싱해서 재사용하도록 고친 뒤에야 순회가 안정적으로 돌기 시작했습니다.
06재시작해도 끊기지 않게 — 기억은 사이트가 한다
토큰 문제를 고친 뒤에도 가격이 멈춰 있었습니다. 원인은 상태 불일치였습니다. 코드를 배포하느라 VPS 프로세스를 재시작하면 서버의 기억은 전부 초기화되는데, D1에는 "이 검색식 감시 중"이라는 값이 그대로 남아 있었습니다. 화면은 계속 "감시 중"이라고 말하고, 서버는 할 일이 없다며 쉬고 있었던 겁니다. 연결 확인은 정상인데 리스트만 안 바뀌는, 가장 헷갈리는 형태의 고장이었습니다.
그래서 원칙을 정했습니다. 무엇을 감시해야 하는지는 사이트가 기억하고, 서버는 깨어나자마자 그걸 물어본다. VPS는 시작하는 순간 사이트에 직전 상태를 묻고, 감시 중이던 검색식이 있으면 곧바로 다시 구독합니다. 같은 이유로 새로고침한 관리자 페이지도 로컬 상태가 아니라 서버 상태를 기준으로 화면을 복원합니다.
키움 세션이 하루 단위로 끊기는 것에 맞춘 평일 루틴도 이 원칙 위에서 돕니다.
# 평일(KST) 일일 루틴
16:50 실시간 감시 해제 → 오늘의 종목 리스트를 스냅샷으로 보관 → 삭제목록 초기화
17:00 활성 종목 리스트 비움
17:01 마지막으로 쓰던 "기본 검색식"으로 자동 재연결
보관된 스냅샷은 날짜별 "저장된 목록"으로 남아 나중에 복기할 수 있고, 리스트에서 직접 지운 종목은 같은 조건에 다시 걸려도 그날은 돌아오지 않습니다. 종목마다 빨강·노랑·초록 신호등 체크를 따로 남길 수 있게 해서, 눈으로 훑으며 느낀 인상도 데이터로 쌓이게 했습니다.
07차트에서 배운 것 — 키가 있어도 문이 열리진 않는다
종목을 누르면 1일봉과 1분봉 캔들차트, 거래량이 뜨도록 했습니다. 처음에는 "클릭할 때만 필요한 조회니까 서버를 거치지 말고 사이트가 바로 부르자"고 설계했습니다. 앱키도 이미 사이트 설정에 있으니 가능해 보였습니다. 배포 직전, 화면에 IP를 등록하라는 오류가 떴습니다.
키움 앱키는 미리 등록해둔 IP에서 온 요청만 받습니다. VPS는 고정 IP라 등록할 수 있지만, Cloudflare Worker는 요청마다 전 세계 엣지 중 어디서든 나갈 수 있어서 애초에 등록할 IP가 없습니다. 키만 있으면 되는 게 아니라, 부르는 자리 자체가 허락돼 있어야 했던 겁니다. 게다가 키움은 동시접속을 허용하지 않아서, 두 곳에서 따로 세션을 여는 것 자체가 위험했습니다.
결국 차트 조회도 다른 명령과 똑같이 커맨드 큐를 거치도록 되돌렸습니다. 응답이 1~2초 늦어졌지만, 사이트 쪽 코드는 오히려 줄었고 규칙은 하나로 정리됐습니다 — 키움을 부르는 모든 길은 VPS 한 곳을 지난다.
08다음 — 판단을 거드는 AI, 실행하는 엔진
지금까지 만든 것은 "무엇을 볼지"를 대신 찾아주는 층입니다. 다음은 그 위에 두 가지를 올리는 일입니다.
- AI 판단 층 — VPS에 AI API를 연결해, 조건검색에 새로 걸린 종목의 시세·차트·거래량 맥락을 읽고 진입 여부와 조건을 제안하게 합니다. 최종 판단의 범위와 한도는 사람이 정하고, AI는 그 안에서만 움직이도록 설계할 예정입니다.
- 주문 실행 층 — LOG_01 스탑 엔진에서 실거래로 다듬어온 주문·정정·복구 로직을 서버 쪽으로 옮겨옵니다. 관리자 페이지의 종목 상세 화면에 비워둔 "주문 설정(스탑로스 등)" 자리가 그 입구가 됩니다.
이 두 층 모두 지금의 원칙을 그대로 따릅니다. 키움 호출은 VPS에서만, 사이트는 명령과 설정만, 그리고 상태의 기준은 한 곳에만.
09지금 이 자리
조건검색 실시간 감시, 1초 단위 시세 갱신, 재시작 자동 복구, 평일 일일 루틴, 삭제·아카이브, 만족도 신호등까지는 실제로 돌려보며 확인했습니다. 일봉·분봉 차트는 구조를 바로잡고 배포를 앞두고 있고, 분봉 시간축이 미국 장중 시간과 정확히 맞는지는 실사용에서 확인할 차례입니다.
여섯 번의 배포 동안 매번 무언가가 어긋났고, 매번 그 어긋남이 다음 원칙이 됐습니다. 토큰은 아껴 쓴다, 기억은 한 곳에 둔다, 키움은 한 문으로만 부른다. 지금은 그렇게 반복(Iterate)으로 다져진 바닥 위에, AI와 주문이라는 다음 흐름(Flow)을 올릴 준비를 하는 단계입니다.