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.Router.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를 사용하는 것을 강력히 추천합니다.

  1. 미들웨어 관리가 필요할 때: 인증(JWT, OAuth), CORS, 로깅, 압축(Gzip), 타임아웃 등 여러 미들웨어를 체계적으로 적용해야 하는 경우.
  2. API 엔드포인트가 많고 구조화가 필요할 때: API v1/v2 버전 분리, /users, /posts 등 도메인별로 라우터를 쪼개서 파일/패키지 단위로 관리하고 싶을 때.
  3. Context 관리가 중요할 때: chi는 URL 파라미터 등을 context에 담아 핸들러로 넘겨주는 패턴이 매우 잘 정립되어 있습니다.
  4. 표준 호환성을 지키고 싶을 때: Gin이나 Fiber 같은 프레임워크는 자체 Context 객체를 사용해 표준 라이브러리와 100% 호환되지 않습니다. 반면 chi는 표준 http.Handler를 그대로 쓰므로, 나중에 다른 라이브러리로 갈아타거나 표준 라이브러리와 섞어 쓰기에 부담이 없습니다.

결론#

작고 단순한 마이크로서비스나 토이 프로젝트라면 Go 1.22의 향상된 net/http 로도 충분합니다.

하지만 실무 레벨의 백엔드 서버를 구축한다면 chi 가 사실상의 표준(De facto standard)에 가깝습니다. Go의 단순함(Simplicity)을 해치지 않으면서도, 대규모 애플리케이션에 필요한 구조화 도구(Router Group, Middleware Stack)를 완벽하게 제공하기 때문입니다.