Go 웹 애플리케이션에서 static contents를 서빙하는 표준 패턴
복잡한 nginx 설정 파일, CDN 연동, 그리고 정적 파일 경로 문제로 골머리를 앓으신 적 있으신가요? Go 표준 라이브러리만으로도 정적 파일(CSS, JavaScript, 이미지 등)을 우아하게 서빙할 수 있습니다. 이번 글에서는 Go 웹 애플리케이션에서 static contents를 서빙하는 표준 패턴을 알아보겠습니다. http.FileServer, http.Dir, 그리고 http.StripPrefix의 조합만으로 프로덕션 레벨의 static file serving을 구현할 수 있습니다.
이 글은 GOTH Stack 웹 애플리케이션 개발 과정에서 발견한 패턴을 Claude Sonnet 을 이용해 정리한 것입니다.
문제 상황: URL과 파일 시스템 경로의 불일치#
웹 애플리케이션을 개발하다 보면 다음과 같은 구조를 자주 마주치게 됩니다:
프로젝트/
└── web/
└── static/
├── css/
│ └── style.css
├── js/
│ └── app.js
└── images/
└── logo.png
파일 시스템 경로: web/static/css/style.css
원하는 URL: /static/css/style.css
여기서 핵심 질문이 나옵니다:
“왜
web/static/css/style.css파일을/static/css/style.cssURL로 접근할 수 있을까?”
해답: http.StripPrefix와 http.FileServer#
Go 표준 라이브러리는 이 문제를 해결하는 우아한 패턴을 제공합니다. 코드부터 보겠습니다:
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
)
func main() {
r := chi.NewRouter()
// Static file serving 설정
fileServer := http.FileServer(http.Dir("./web/static"))
r.Handle("/static/*", http.StripPrefix("/static/", fileServer))
http.ListenAndServe(":8080", r)
}
단 3줄의 코드로 정적 파일 서빙이 완성됩니다. 각 부분을 자세히 살펴보겠습니다.
핵심 구성 요소#
1. http.Dir - 파일 시스템 루트 지정#
fileServer := http.FileServer(http.Dir("./web/static"))
역할: 파일 서버의 “루트 디렉토리"를 지정합니다.
http.Dir("./web/static"): 현재 디렉토리 기준으로web/static/폴더를 루트로 설정- 이 폴더 외부의 파일에는 접근할 수 없습니다 (보안)
2. http.StripPrefix - URL 경로 변환#
r.Handle("/static/*", http.StripPrefix("/static/", fileServer))
역할: URL에서 prefix를 제거한 후 파일 서버로 전달합니다.
- 브라우저 요청:
GET /static/css/style.css StripPrefix처리 후:css/style.css- FileServer가 찾는 파일:
./web/static+css/style.css=./web/static/css/style.css✅
동작 흐름 상세 분석#
브라우저에서 CSS 파일을 요청하는 전체 과정을 단계별로 살펴보겠습니다:
1. 브라우저 요청
GET /static/css/style.css
2. Chi Router 매칭
패턴 "/static/*" 매치 ✓
3. http.StripPrefix 처리
"/static/css/style.css" → "css/style.css"
(앞의 "/static/" 제거)
4. http.FileServer 처리
루트: "./web/static"
요청: "css/style.css"
파일: "./web/static/css/style.css"
5. 파일 전송
Content-Type: text/css
Status: 200 OK
💡 포인트: StripPrefix가 없다면 FileServer는 ./web/static/static/css/style.css를 찾게 되어 404 에러가 발생합니다!
실전 예제: Tailwind CSS 서빙#
실제 프로젝트에서 Tailwind CSS를 서빙하는 전체 코드를 보겠습니다:
디렉토리 구조#
프로젝트/
├── cmd/
│ └── server/
│ └── main.go
├── web/
│ └── static/
│ ├── css/
│ │ ├── input.css (소스)
│ │ └── output.css (빌드 결과)
│ ├── js/
│ └── images/
└── internal/
└── view/
└── layout/
└── base.templ
서버 코드 (cmd/server/main.go)#
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Compress(5)) // GZIP 압축
// Static files serving
fileServer := http.FileServer(http.Dir("./web/static"))
r.Handle("/static/*", http.StripPrefix("/static/", fileServer))
// HTML 페이지
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/static/css/output.css">
</head>
<body>
<h1 class="text-3xl font-bold">Hello GOTH!</h1>
</body>
</html>
`))
})
http.ListenAndServe(":8080", r)
}
HTML 템플릿 (internal/view/layout/base.templ)#
<head>
<meta charset="UTF-8"/>
<title>My App</title>
<!-- Tailwind CSS -->
<link rel="stylesheet" href="/static/css/output.css"/>
<!-- Custom JavaScript -->
<script src="/static/js/app.js"></script>
</head>
💡 포인트: HTML에서는 /static/으로 시작하는 URL을 사용하지만, 실제 파일은 web/static/에 있습니다. 매핑은 서버가 자동으로 처리합니다!
보안 고려사항#
✅ 안전한 패턴#
// ✅ GOOD: web/static 폴더만 서빙
fileServer := http.FileServer(http.Dir("./web/static"))
r.Handle("/static/*", http.StripPrefix("/static/", fileServer))
이 설정에서는:
web/static/폴더 내부의 파일만 접근 가능../../../etc/passwd같은 경로 탐색 공격 차단- 소스 코드(
.go파일) 접근 불가
❌ 위험한 패턴#
// ❌ BAD: 프로젝트 루트 전체를 노출
fileServer := http.FileServer(http.Dir("."))
r.Handle("/static/*", http.StripPrefix("/static/", fileServer))
이렇게 하면 /static/../cmd/server/main.go 같은 요청으로 소스 코드가 노출될 수 있습니다!
고급 패턴: Cache-Control 헤더 추가#
프로덕션 환경에서는 브라우저 캐싱을 위해 Cache-Control 헤더를 추가하는 것이 좋습니다:
func cacheControlMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// CSS, JS, 이미지는 1년간 캐싱
w.Header().Set("Cache-Control", "public, max-age=31536000")
next.ServeHTTP(w, r)
})
}
// 사용
fileServer := http.FileServer(http.Dir("./web/static"))
r.Handle("/static/*",
cacheControlMiddleware(
http.StripPrefix("/static/", fileServer),
),
)
표준 라이브러리 vs 외부 패키지#
| 방식 | 장점 | 단점 |
|---|---|---|
| 표준 라이브러리 | 의존성 없음, 단순함, 검증됨 | 고급 기능 직접 구현 필요 |
| nginx | 강력한 성능, 풍부한 기능 | 추가 서버 필요, 설정 복잡 |
| CDN | 전 세계 캐싱, 빠른 속도 | 비용, 외부 의존성 |
💡 추천: 개발/스테이징 환경에서는 Go 표준 라이브러리로 충분합니다. 프로덕션에서 트래픽이 많다면 CDN을 앞단에 추가하세요.
실제 프로젝트 체크리스트#
GOTH 스택 프로젝트에서 static file serving을 설정할 때 확인할 사항:
-
web/static/디렉토리 생성 -
.gitignore에 빌드 결과물 추가 (output.css등) -
http.StripPrefix정확히 설정 - Templ 템플릿에서
/static/경로로 참조 - 브라우저 개발자 도구로 200 OK 응답 확인
- GZIP 압축 미들웨어 추가 (선택)
- Cache-Control 헤더 설정 (프로덕션)
디버깅 팁#
문제가 생겼을 때 확인할 사항:
1. 404 Not Found가 나올 때#
# 파일이 실제로 존재하는지 확인
ls -la web/static/css/output.css
# 서버 로그 확인 (Chi의 Logger 미들웨어 사용 시)
# GET /static/css/output.css - 404 Not Found
원인: 경로 불일치 또는 StripPrefix 오류
2. 파일이 업데이트되지 않을 때#
브라우저 캐시가 원인일 수 있습니다:
# 강제 새로고침
# macOS: Cmd + Shift + R
# Windows/Linux: Ctrl + Shift + R
해결: 개발 중에는 Cache-Control: no-cache 설정
3. 경로 확인 도구#
# HTTP 요청 테스트
curl -I http://localhost:8080/static/css/output.css
# 응답 예시:
# HTTP/1.1 200 OK
# Content-Type: text/css
# Content-Length: 17234
마치며#
Go 표준 라이브러리의 http.FileServer와 http.StripPrefix는 단순하지만 강력합니다. 복잡한 설정 없이 3줄의 코드로 정적 파일 서빙을 구현할 수 있습니다.
핵심 개념:
http.Dir(): 파일 시스템 루트 지정http.StripPrefix(): URL에서 prefix 제거- 두 가지를 조합하여 URL과 파일 경로 매핑
GOTH 스택의 철학처럼, “복잡한 도구 대신 표준 라이브러리를 먼저 고려하라"는 원칙이 여기서도 빛을 발합니다.
다음 프로젝트에서 정적 파일을 서빙할 일이 있다면, nginx 설정 파일을 열기 전에 Go 표준 라이브러리를 먼저 시도해보세요! 🚀