Windows 콘솔 앱에 웹 대시보드 붙이기
콘솔에서 도는 봇은 상태를 보려면 창을 열어 로그를 노려봐야 합니다. 외출 중엔 확인도 못 하죠. 무거운 프런트엔드 스택 없이, 표준 net/http만으로 실시간 웹 대시보드를 붙인 경험을 정리합니다. 의존성은 0개, 결과물은 여전히 단일 실행 파일입니다.
구조 — 상태 스냅샷을 폴링
실시간이라고 WebSocket부터 꺼낼 필요 없습니다. 봇 상태를 JSON으로 내주는 엔드포인트 하나(/api/state)를 만들고, 브라우저가 몇 초마다 폴링하면 충분합니다. 단순하고, 연결이 끊겨도 알아서 복구됩니다.
func apiState(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(GlobalDash.Snapshot())
}
관건은 Snapshot()이 경쟁 상태 없이 현재 상태의 사본을 만드는 것입니다. HTTP 핸들러는 여러 요청이 동시에 들어오고, 봇 엔진은 매 틱 상태를 갱신하니까요.
표시 상태는 액터 goroutine이 소유
여기서도 뮤텍스 대신 채널을 씁니다. 표시용 상태를 단일 액터 goroutine이 소유하고, 갱신도 조회도 전부 채널로만 오갑니다.
type Dash struct {
cmds chan func(*dashCore) // 상태 변경 요청
query chan chan StateSnapshot // 스냅샷 요청
}
func (d *Dash) run() {
core := &dashCore{}
for {
select {
case fn := <-d.cmds:
fn(core) // 엔진의 갱신을 순차 적용
case rep := <-d.query:
rep <- core.snapshot() // 읽기 전용 사본을 반환
}
}
}
// HTTP 핸들러에서 호출
func (d *Dash) Snapshot() StateSnapshot {
rep := make(chan StateSnapshot)
d.query <- rep
return <-rep
}
엔진은 매 틱 UpdateTick(...)으로 잔고·포지션·신호를 밀어 넣고, 브라우저는 /api/state로 사본을 받아 갑니다. 둘 다 락 없이 안전합니다. 로그도 같은 액터가 최근 100개만 유지하도록 관리하면, 메모리가 무한정 늘지 않습니다.
외부 접속 — 바인딩과 인증
집 밖에서도 보려면 localhost가 아니라 모든 인터페이스에 바인딩해야 합니다.
addr := fmt.Sprintf("0.0.0.0:%d", port) // localhost 아님
http.ListenAndServe(addr, mux)
그런데 0.0.0.0으로 여는 순간, 봇 제어 API가 인터넷에 노출됩니다. 반드시 인증 미들웨어를 답니다. access_key가 설정돼 있으면 쿼리 파라미터나 헤더로 검증하고, 비어 있으면 (로컬 전용으로) 통과시킵니다.
func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if serverAccessKey == "" { // 키 미설정 = 무인증(로컬 전용)
next(w, r)
return
}
provided := r.URL.Query().Get("key")
if provided == "" {
provided = r.Header.Get("X-Access-Key")
}
// 타이밍 공격 방지: 상수 시간 비교
if subtle.ConstantTimeCompare([]byte(provided), []byte(serverAccessKey)) != 1 {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next(w, r)
}
}
subtle.ConstantTimeCompare가 포인트입니다. ==로 문자열을 비교하면 일치하는 접두사 길이에 따라 응답 시간이 미세하게 달라져, 이론상 키를 한 글자씩 추정당할 수 있습니다. 상수 시간 비교로 이 타이밍 누출을 막습니다. 모든 라우트를 이 미들웨어로 감쌉니다.
mux.HandleFunc("/api/state", authMiddleware(apiState))
mux.HandleFunc("/api/force-exit", authMiddleware(apiForceExit))
노출 범위는 최소로. 정말 외부에서 봇을 제어할 필요가 없다면 조회 엔드포인트만 열고, 실거래 환경이라면 그 앞에 HTTPS 리버스 프록시나 VPN을 두는 편이 안전합니다.
덤: 웹에서 콘솔 창까지 제어
Windows라면 대시보드 버튼으로 콘솔 창을 보이거나 숨길 수 있습니다. user32.dll을 직접 호출하면 됩니다.
var user32 = syscall.NewLazyDLL("user32.dll")
var showWindow = user32.NewProc("ShowWindow")
const swHide, swShowMin = 0, 2
func hideMyConsole() bool {
hwnd, _, _ := kernel32.NewProc("GetConsoleWindow").Call()
if hwnd == 0 {
return false
}
showWindow.Call(hwnd, swHide)
return true
}
/api/window?action=hide 같은 엔드포인트에 연결하면, 브라우저에서 봇 창을 숨기고 트레이 앱처럼 조용히 돌릴 수 있습니다. 시작할 때 자동으로 최소화해 두면 더 깔끔하고요. 이 부분만 OS 종속이라 window_ctrl_windows.go처럼 빌드 태그로 분리해 두면 다른 OS 빌드가 깨지지 않습니다.
정리
- 실시간 UI는 상태 스냅샷 폴링으로 충분하다 — WebSocket 없이도
- 표시 상태는 액터 goroutine이 소유해 락 없이 안전하게
- 외부 접속은
0.0.0.0바인딩 + 상수 시간 비교 인증을 세트로 - OS 종속 코드(콘솔 제어)는 빌드 태그로 격리
표준 라이브러리만으로, 콘솔 봇이 어디서나 열어 보는 대시보드를 갖게 됩니다. 적은 코드로 운영 체감이 크게 달라지는, 가성비 높은 작업입니다.
이 글은 모니터링 UI 구현을 다루며, 특정 투자 성과와 무관한 기술 설명입니다.
댓글
댓글 쓰기