구글 앱스크립트를 Go로 이식한 경험
주식 자동매매 웹 콘솔을 오랫동안 구글 앱스크립트(GAS)로 운영했습니다. 무료에, 배포도 "웹 앱으로 게시" 클릭 한 번. 시작하기엔 최고죠. 그런데 봇이 진지해질수록 벽에 부딪혔고, 결국 Go로 이식했습니다. 그 과정에서 배운 것을 정리합니다.
앱스크립트의 구조와 한계
GAS 웹 앱은 진입점이 딱 두 개입니다.
function doGet(e) { /* GET 요청 → HTML 반환 */ }
function doPost(e) { /* POST 요청 → JSON 처리 */ }
간단하지만, 자동매매에서는 이 단순함이 곧 한계가 됩니다.
- 실행 시간 제한 — 한 번 호출이 6분(트리거는 더 짧음)을 넘기면 강제 종료됩니다.
while(true)매매 루프를 못 돕니다. - 상태 유지 불가 — 호출 사이에 메모리가 사라집니다. 상태는 매번
PropertiesService나 스프레드시트에 읽고 씁니다. - 동시성 제어 부재 — 트리거가 겹치면 같은 로직이 중복 실행되기 쉽습니다.
- 디버깅과 버전 관리의 불편함 — 로컬 도구 체인, git, 테스트가 자연스럽지 않습니다.
이식 전략 — 진입점을 라우터로
GAS의 doGet/doPost 두 함수를 Go net/http 라우팅에 그대로 대응시키는 게 출발점입니다.
func main() {
http.HandleFunc("/", handleHome) // doGet 자리 — HTML
http.HandleFunc("/api", handleAPI) // doPost 자리 — JSON
http.Handle("/static/", http.StripPrefix("/static/",
http.FileServer(http.Dir("./static/"))))
log.Fatal(http.ListenAndServe(":9329", nil))
}
GAS에서 e.parameter.action으로 분기하던 코드는 Go에서 요청 바디를 디코드해 switch로 옮깁니다. 원본의 액션 이름을 그대로 유지하면 프런트엔드는 손대지 않아도 됩니다.
type APIRequest struct {
Action string `json:"action"`
Payload interface{} `json:"payload"`
}
func handleAPI(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
respondWithError(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req APIRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondWithError(w, "Invalid JSON format", http.StatusBadRequest)
return
}
switch req.Action {
case "getParameters":
handleGetParameters(w, r)
case "saveParameters":
handleSaveParameters(w, req.Payload)
case "startBot":
handleStartBot(w)
case "stopBot":
handleStopBot(w)
default:
respondWithError(w, "Unknown action", http.StatusBadRequest)
}
}
응답 포맷도 원본과 맞춥니다. { status, data, ok, error } 형태를 유지하면 기존 JS 클라이언트가 그대로 동작합니다.
상태를 되찾다 — 봇을 진짜로 "실행"
가장 큰 수확은 긴 실행 상태를 코드가 직접 소유할 수 있게 된 것입니다. GAS에서는 불가능했던 "봇을 켜고 백그라운드에서 돌린다"가 Go에서는 자연스럽습니다.
func handleStartBot(w http.ResponseWriter) {
botInstance.mu.Lock()
defer botInstance.mu.Unlock()
ctx, cancel := context.WithCancel(context.Background())
botInstance.cancelFunc = cancel
botInstance.IsRunning = true
go botInstance.TradingEngine.Run(ctx) // 백그라운드 매매 루프
respondWithJSON(w, APIResponse{Status: "started"}, http.StatusOK)
}
context로 취소 신호를 주면 "정지" 버튼이 실제로 루프를 멈춥니다. GAS의 스프레드시트-폴링 흉내가 아니라, 진짜 장기 실행 프로세스입니다. 설정은 스프레드시트 대신 로컬 파일(또는 SQLite)에 저장하니 조회도 즉각적이죠.
전환으로 얻은 것
- 6분 제한 소멸 — 무한 루프 매매가 가능해짐
- 인메모리 상태 — 호출마다 외부 저장소를 왕복하지 않아도 됨
- 단일 실행 파일 —
go build결과물 하나로 어디서든 실행, 외부 런타임 불필요 - 표준 도구 체인 — git, 로컬 디버깅, 테스트, 타입 안정성
남는 트레이드오프
공짜는 아닙니다. GAS의 "게시 클릭 한 번" 배포는 사라지고, 이제 서버(또는 항상 켜진 PC)를 직접 운영해야 합니다. 구글 계정 기반 인증도 직접 붙여야 하고요. 소규모·저빈도 작업이라면 GAS가 여전히 합리적입니다. 하지만 상태를 들고 오래 도는 봇이라면, 이식의 대가는 충분히 돌려받습니다.
이 글은 아키텍처 이식 경험을 다루며, 특정 투자 성과를 보장하지 않습니다.
댓글
댓글 쓰기