Go: net/http vs. chi
Go 언어의 웹 생태계에서 net/http (표준 라이브러리) 와 chi 는 가장 “Go스러운(Idiomatic)” 선택지로 꼽힙니다. Gin이나 Echo 같은 “웹 프레임워크"와 달리, chi는 표준 라이브러리의 인터페이스를 그대로 따르면서 부족한 라우팅 기능을 보강해주는 “라우터"이기 때문입니다.
특히 Go 1.22 버전부터 net/http의 라우팅 기능이 대폭 강화되었기 때문에, 이 둘의 차이를 명확히 이해하는 것이 중요합니다.
이 글은 요청에 대한 Gemini 3 Pro 의 답변입니다.
1. net/http vs chi 요약 비교#
| 특징 | net/http (Go 1.22+) |
chi |
|---|---|---|
| 외부 의존성 | 없음 (Zero dependency) | 있음 (But 매우 가벼움) |
| 인터페이스 | http.Handler |
http.Handler (100% 호환) |
| 라우팅 (Path) | 지원 (/items/{id}) |
강력하게 지원 (Regex 등 포함) |
| 메서드 매칭 | 지원 (POST /items) |
지원 (r.Post("/items", ...)) |
| 미들웨어 관리 | 직접 구현 필요 (복잡함) | 매우 강력하고 간편함 (Use) |
| 라우팅 그룹 | 직접 구현 필요 | 기본 지원 (Group) |
| 속도 | 빠름 | 매우 빠름 (Radix Tree 기반) |
2. 왜 앱이 커질수록 chi가 유리한가?#
앱이 복잡해진다는 것은 엔드포인트가 많아지고, 공통 로직(인증, 로깅, CORS 등)이 복잡해진다는 뜻입니다. 이 지점에서 chi가 net/http보다 압도적인 강점을 가집니다.
핵심 차이 1: 미들웨어 (Middleware) 체이닝#
복잡한 앱은 모든 요청에 대해 로깅을 남기고, 특정 경로(예: /admin)에는 인증을 요구해야 합니다.
chi:.Use()메서드 하나로 깔끔하게 파이프라인을 구축합니다.net/http: 미들웨어를 적용하려면 핸들러를 함수로 감싸고 또 감싸야 해서 코드가 깊어지고(nesting) 읽기 어려워집니다.
핵심 차이 2: 라우팅 그룹 (Routing Groups) 및 서브 라우팅#
API 버전 관리(v1, v2)나 도메인별 분리(users, posts)가 필요할 때 구조화가 필수적입니다.
chi:r.Route나r.Group을 사용해 코드를 블록 단위로 격리할 수 있습니다.net/http:ServeMux를 여러 개 만들어 서로 연결해야 하는데, 직관적이지 않고 관리가 번거롭습니다.
3. 코드 비교: “관리자 전용 API 만들기”#
상황: /admin 하위 경로는 모두 인증 미들웨어가 필요하고, 일반 경로는 필요 없습니다.
A. chi 사용 시 (구조적이고 명확함)#
func main() {
r := chi.NewRouter()
// 1. 모든 요청에 적용되는 전역 미들웨어
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
// 2. 일반 공개 API
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Welcome"))
})
// 3. 관리자 전용 그룹 (여기에만 인증 미들웨어 적용)
r.Route("/admin", func(r chi.Router) {
r.Use(AuthMiddleware) // 이 블록 내부에만 적용됨 ✨
r.Get("/users", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Admin: User List"))
})
r.Delete("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
// chi는 URL 파라미터를 아주 쉽게 가져옴
userID := chi.URLParam(r, "id")
w.Write([]byte("Deleting user " + userID))
})
})
http.ListenAndServe(":3000", r)
}
장점: 미들웨어의 적용 범위가 눈에 확 들어옵니다. 코드가 들여쓰기(indentation)를 통해 논리적으로 그룹화됩니다.
B. net/http (Go 1.22+) 사용 시 (다소 평면적이고 수동적)#
func main() {
mux := http.NewServeMux()
// 핸들러 정의
publicHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Welcome"))
})
adminUserList := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Admin: User List"))
})
// 라우팅 등록
mux.Handle("GET /", LogMiddleware(publicHandler)) // 전역 미들웨어를 일일이 감싸야 함
// 관리자 그룹을 흉내내려면?
// 경로마다 미들웨어를 체이닝해야 하거나, 별도의 mux를 만들어 마운트해야 함
// 여기서는 AuthMiddleware(LogMiddleware(...)) 처럼 감싸야 함. 가독성이 떨어짐.
mux.Handle("GET /admin/users", LogMiddleware(AuthMiddleware(adminUserList)))
// Path parameter (Go 1.22 기능)
mux.Handle("DELETE /admin/users/{id}", LogMiddleware(AuthMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
id := r.PathValue("id")
w.Write([]byte("Deleting user " + id))
}))))
http.ListenAndServe(":3000", mux)
}
단점: 미들웨어가 많아질수록 Middleware1(Middleware2(Handler)) 형태가 되어 코드가 지저분해집니다. 그룹화된 로직을 한눈에 파악하기 어렵습니다.
4. 언제 chi를 선택해야 할까?#
다음 중 하나라도 해당된다면 net/http 대신 chi를 사용하는 것을 강력히 추천합니다.
- 미들웨어 관리가 필요할 때: 인증(JWT, OAuth), CORS, 로깅, 압축(Gzip), 타임아웃 등 여러 미들웨어를 체계적으로 적용해야 하는 경우.
- API 엔드포인트가 많고 구조화가 필요할 때: API v1/v2 버전 분리,
/users,/posts등 도메인별로 라우터를 쪼개서 파일/패키지 단위로 관리하고 싶을 때. - Context 관리가 중요할 때: chi는 URL 파라미터 등을
context에 담아 핸들러로 넘겨주는 패턴이 매우 잘 정립되어 있습니다. - 표준 호환성을 지키고 싶을 때: Gin이나 Fiber 같은 프레임워크는 자체
Context객체를 사용해 표준 라이브러리와 100% 호환되지 않습니다. 반면 chi는 표준http.Handler를 그대로 쓰므로, 나중에 다른 라이브러리로 갈아타거나 표준 라이브러리와 섞어 쓰기에 부담이 없습니다.
결론#
작고 단순한 마이크로서비스나 토이 프로젝트라면 Go 1.22의 향상된 net/http 로도 충분합니다.
하지만 실무 레벨의 백엔드 서버를 구축한다면 chi 가 사실상의 표준(De facto standard)에 가깝습니다. Go의 단순함(Simplicity)을 해치지 않으면서도, 대규모 애플리케이션에 필요한 구조화 도구(Router Group, Middleware Stack)를 완벽하게 제공하기 때문입니다.