이번에는 muabow.com에 작은 RESTful API를 붙였다.
지금 공개된 경로는 두 가지다.
GET /api/device-statusGET /api/visits
첫 번째는 라즈베리파이의 현재 상태를 JSON으로 돌려주고,
두 번째는 오늘 방문 수와 총 방문 수를 간단하게 응답한다.
단순히 화면만 보는 것이 아니라, 이제는 브라우저 밖에서도 같은 정보를 읽을 수 있게 된 셈이다.
RESTful API는 무엇인가
아주 어렵게 말할 필요는 없다.
RESTful API는 대체로 아래 감각으로 이해하면 충분하다.
- URL은 무엇을 다루는지 드러나게 만든다.
- HTTP 메서드는 행동을 나눈다.
- 응답은 보통 JSON으로 준다.
- 서버 상태를 억지로 기억하기보다, 요청 하나만 보고 처리할 수 있게 만든다.
예를 들면 이런 식이다.
GET /api/device-status
GET /api/visits
POST /api/visits
이번 사이트에서는 읽기 중심이기 때문에 GET 위주로 두었고,
방문자 수 집계만 내부 스크립트에서 POST /api/visits를 사용한다.
왜 붙였는가
이전까지는 실시간 정보가 있어도 브라우저 화면 안에서만 보였다.
예를 들어 /info 대시보드는 잘 돌아가지만,
다른 프로그램이나 터미널에서 같은 정보를 재사용하려면 다시 긁어야 했다.
그래서 방향을 이렇게 잡았다.
- 대시보드가 보는 최신 장치 상태를 그대로 JSON으로도 공개한다.
- 사이트 footer에서 쓰는 방문자 수도 JSON으로 읽을 수 있게 둔다.
- 앞으로 실시간 기능이 늘어나면 같은
/api/...패턴으로 확장한다.
즉 이번 API는 거대한 백엔드가 아니라,
이미 운영 중인 사이트 기능을 바깥에서도 읽기 좋게 정리한 첫걸음이다.
지금 구조
현재 흐름은 이렇다.
Go collector
-> NATS subject: muabow.info.metrics
-> Go web server keeps latest payload
-> /info HTML dashboard
-> /info/events SSE stream
-> /api/device-status JSON API
-> /api/visits visit counter API
즉 수집은 Go가 하고,
실시간 전달은 NATS와 SSE가 맡고,
공개 API는 같은 Go 서비스가 그대로 응답한다.
어떤 API를 열었는가
현재 공개 엔드포인트는 아래와 같다.
1. 장치 상태
GET https://muabow.com/api/device-status
응답 예시는 이런 형태다.
{
"collectedAt": "2026-05-15T03:14:14+09:00",
"hostname": "device-host",
"os": "linux",
"arch": "arm64",
"uptimeSeconds": 388411,
"load": {
"one": 0.05,
"five": 0.04,
"fifteen": 0.10
},
"cpu": {
"usagePercent": 0,
"cores": 4
},
"memory": {
"totalBytes": 4246405120,
"availableBytes": 3561111552,
"usedBytes": 685293568,
"usedPercent": 16.14
},
"disk": {
"path": "/",
"totalBytes": 62408265728,
"freeBytes": 51915681792,
"usedBytes": 10492583936,
"usedPercent": 16.81
},
"temperatureC": 54,
"services": [
{ "name": "nginx", "active": "active" },
{ "name": "cloudflared", "active": "active" },
{ "name": "nats", "active": "active" },
{ "name": "admin-app", "active": "active" },
{ "name": "info-app", "active": "active" }
]
}
2. 방문자 수
GET https://muabow.com/api/visits
응답은 단순하다.
{
"today": 12,
"total": 248
}
Go에서는 어떻게 구현했는가
핵심 아이디어는 새로 계산하지 않는 것이다.
이미 Go 서비스는 NATS에서 장치 상태를 받아 최신 payload를 메모리에 들고 있었다.
그래서 /api/device-status는 그 최신 JSON을 그대로 돌려주면 된다.
구현 흐름은 이렇다.
broker가 NATS 메시지를 받을 때 최신 payload를 저장한다.latestPayload()로 가장 최근 JSON을 꺼낸다.GET /api/device-status요청이 오면 그 JSON을 응답한다.
핸들러는 대략 이런 구조다.
mux.HandleFunc("/api/device-status", func(w http.ResponseWriter, r *http.Request) {
handleDeviceStatus(w, r, b)
})
그리고 실제 응답은 아래처럼 매우 얇게 처리한다.
func handleDeviceStatus(w http.ResponseWriter, r *http.Request, b *broker) {
if r.Method != http.MethodGet {
w.Header().Set("Allow", "GET")
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
payload := b.latestPayload()
if len(payload) == 0 {
http.Error(w, "device status unavailable", http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
_, _ = w.Write(payload)
}
포인트는 두 가지다.
GET만 허용한다.- 아직 수집값이 없으면
503 Service Unavailable을 준다.
이 정도만 해도 API의 태도가 꽤 분명해진다.
방문자 수 API는 어떻게 만들었는가
방문자 수는 메모리에만 두면 재부팅 때 사라진다.
그래서 JSON 파일 하나를 저장소처럼 사용했다.
방문자 수는 로컬 JSON 파일 하나에 저장한다.
동작은 단순하다.
GET /api/visits는 현재 수치 조회POST /api/visits는 오늘 방문과 총 방문을 증가
사이트 footer에서는 브라우저가 이 API를 사용한다.
fetch("/api/visits", options)
그리고 같은 브라우저에서 하루에 한 번만 증가시키기 위해 localStorage 키를 사용했다.
즉 방문자 수는 “로그 기반 분석”이 아니라,
사이트 안에 가볍게 붙는 작은 카운터라는 성격에 가깝다.
nginx는 왜 필요한가
Go 앱은 외부에 직접 열지 않았다.
실제로는 라즈베리파이 로컬에서만 아래 주소로 뜬다.
내부 루프백 주소의 Go 앱 포트
그리고 nginx가 공개 경로만 프록시한다.
현재 설정은 이런 식이다.
location /api/device-status {
proxy_pass http://127.0.0.1:<internal-app-port>;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /api/visits {
proxy_pass http://127.0.0.1:<internal-app-port>;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
이 구조의 장점은 분명하다.
- 공개 URL은
muabow.com하나로 유지된다. - 내부 앱은 로컬 포트에만 묶어둘 수 있다.
- 정적 사이트와 작은 동적 기능을 한 도메인에서 같이 운영할 수 있다.
따라 하려면 무엇이 필요한가
비슷하게 만들고 싶다면 순서는 이 정도면 충분하다.
- 장치 상태를 수집하는 작은 Go 앱을 만든다.
- 그 값을 JSON 구조체로 정리한다.
- 최신 payload를 메모리에 유지한다.
GET /api/...핸들러를 붙인다.nginx에서 해당 경로를 Go 앱으로 프록시한다.curl로 실제 응답을 확인한다.
테스트는 아래처럼 바로 할 수 있다.
curl https://muabow.com/api/device-status
curl https://muabow.com/api/visits
라즈베리파이 내부에서는 이렇게 볼 수 있다.
curl http://127.0.0.1:<internal-app-port>/api/device-status
curl http://127.0.0.1/api/device-status
이번 구현에서 중요했던 점
이번에 일부러 지킨 기준은 이렇다.
- API 경로는
/api/...아래로 모은다. - 읽기용 경로는
GET만 허용한다. - 브라우저 화면에서 보이는 값과 API 응답이 따로 놀지 않게 한다.
- 이미 있는 Go 서비스 위에 얇게 덧붙인다.
이게 꽤 중요하다.
화면용 데이터와 API용 데이터를 따로 계산하기 시작하면,
나중에 값이 달라지거나 관리 포인트가 늘어나기 쉽다.
이번에는 같은 원본 payload를 재사용해서 그 문제를 피했다.
앞으로 늘릴 수 있는 것
이제 패턴은 만들어졌다.
나중에는 이런 것도 같은 방식으로 붙일 수 있다.
GET /api/recent-eventsGET /api/photo-feedGET /api/deploy-statusGET /api/services
즉 muabow.com은 정적 블로그이면서도,
조금씩 자기 상태를 내놓는 작은 서버로 자라나는 중이다.
정리
이번 작업으로 RESTful API를 거창하지 않게 시작했다.
핵심은 단순하다.
- 이미 수집하고 있는 데이터를 재사용한다.
- 의미가 드러나는
/api/...경로로 공개한다. nginx로 안전하게 프록시한다.- 브라우저와 터미널에서 같은 정보를 읽을 수 있게 한다.
지금은 device-status와 visits 두 개뿐이지만,
앞으로 붙을 실시간 기능들의 공통 입구로는 꽤 괜찮은 출발점이다.