Windows 콘솔 앱에 웹 대시보드 붙이기

🌐 English

콘솔에서 도는 봇은 상태를 보려면 창을 열어 로그를 노려봐야 합니다. 외출 중엔 확인도 못 하죠. 무거운 프런트엔드 스택 없이, 표준 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 구현을 다루며, 특정 투자 성과와 무관한 기술 설명입니다.

댓글

이 블로그의 인기 게시물

한국투자증권 KIS API로 실시간 시세 받기 (WebSocket 실전)

파이썬으로 업비트 API 연동하기 — 시세 조회부터 주문까지 기초

Go로 자동매매 신호봇 프레임워크 설계하기