환경  nginx 1.30.4 stable · 1.31.4 mainline 기준 · 2026-08-24 확인

프런트 앞단 nginx 에 location /api/ 한 블록을 더했다. 문법 검사도 통과했고 reload 도 조용히 끝났는데 브라우저는 404 만 돌려준다. 백엔드 접근 로그를 열어 보면 요청은 멀쩡히 도착해 있고, 경로가 /api/users 로 찍혀 있다. 그 서버가 기다리는 건 /users 다.

슬래시 한 칸이 요청 경로를 갈라 놓는다

같은 location, 같은 업스트림인데 백엔드가 받는 경로가 달라진다. 범인은 proxy_pass 주소의 끝이다. 여기에 슬래시가 하나 붙었느냐 아니냐로 nginx 는 아예 다른 두 가지 일을 한다.

붙지 않은 쪽은 클라이언트가 보낸 요청 URI 를 그대로 넘긴다. 붙은 쪽은 요청 URI 에서 location 과 맞아떨어진 앞부분을 잘라내고 그 자리에 지정한 URI 를 끼워 넣는다. location /api/ 와 proxy_pass http://app/ 이 만나면 앞의 /api/ 가 / 로 바뀌니, /api/users 는 /users 가 되어 나간다.

설정 파일에서 두 줄은 거의 똑같아 보인다. 나란히 놓고 결과를 적어야 차이가 보인다.

요청                     proxy_pass http://app;      proxy_pass http://app/;
-----------------------  --------------------------  ------------------------
/api/users               /api/users 로 간다            /users 로 간다
/api/                    /api/ 로 간다                 / 로 간다
/api/v1/orders?id=3      /api/v1/orders?id=3           /v1/orders?id=3
디렉티브의 URI 부분       없다                          있다 (/)
location 에 맞은 앞부분   그대로 둔다                   지정한 URI 로 갈아 끼운다
백엔드가 /api 를 안다면    맞는다                        404
백엔드가 루트에서 받는다면  404                           맞는다

location 은 그대로 두고 proxy_pass 끝만 바꿨을 때

슬래시가 없으면 경로를 그대로 넘기고, 있으면 location 에 맞은 만큼을 잘라 낸다.

기준은 슬래시가 아니라 'URI 가 붙었는가' 다

슬래시로만 외워 두면 경로를 더 붙이는 순간 어긋난다. nginx 가 보는 건 proxy_pass 에 URI 부분이 있느냐 없느냐다. http://app 은 URI 가 없고, http://app/ 은 URI 가 / 이며, http://app/v2/ 는 URI 가 /v2/ 다. 슬래시는 URI 가 붙었다는 표시일 뿐이다.

URI 가 있으면 치환이 일어난다. 이 치환은 경로 단위가 아니라 문자열 단위다. location 에 맞은 앞부분을 떼고 적어 둔 문자열을 그대로 이어 붙인다. 끝 슬래시를 하나 빠뜨리면 /v2users 처럼 단어가 붙어 버리는 이유가 여기 있다.

location 쪽 슬래시도 같이 봐야 한다. location /api 로 써 두면 잘려 나가는 건 /api 까지다. 뒤에 따라오던 슬래시가 남아 //users 가 된다. 두 줄의 끝을 맞춰 두는 습관 하나로 이 부류는 거의 사라진다.

# 백엔드가 /users 를 기다린다 - 프리픽스를 떼어 낸다
location /api/ {
    proxy_pass http://app/;      # /api/users  ->  /users
}

# 다른 프리픽스 아래로 보낸다
location /api/ {
    proxy_pass http://app/v2/;   # /api/users  ->  /v2/users
}

# 끝 슬래시를 빠뜨리면 문자열이 그대로 이어 붙는다
location /api/ {
    proxy_pass http://app/v2;    # /api/users  ->  /v2users
}

# location 쪽 슬래시가 빠져도 어긋난다
location /api {
    proxy_pass http://app/;      # /api/users  ->  //users
}

location 과 proxy_pass 의 끝은 항상 같이 읽는다

이 규칙이 아예 통하지 않는 자리가 있다

치환이 성립하려면 요청 URI 중 어디를 잘라낼지가 정해져야 한다. 정해지지 않는 경우가 셋 있고, 셋 다 운영 설정에서 흔히 만난다.

첫째는 정규식으로 쓴 location, 그리고 named location 이다. 무엇이 매칭됐는지 앞부분을 특정할 수 없으니 이 자리에서는 proxy_pass 를 URI 없이 쓴다. 경로를 잘라내야 한다면 그 일은 rewrite 로 옮긴다.

둘째는 proxy_pass 주소에 변수를 쓴 경우다. 이때 디렉티브에 URI 를 적으면 그 URI 가 원래 요청 URI 를 통째로 대신한다. 앞부분만 갈아 끼우는 게 아니라 전부 갈린다.

셋째는 같은 location 안에서 rewrite 로 URI 를 이미 바꾸고 break 로 그 설정을 계속 쓰는 경우다. 그러면 proxy_pass 에 적은 URI 는 무시되고 바뀐 요청 URI 전체가 넘어간다. 한 블록에서 두 군데가 경로를 손대고 있으면 어느 쪽이 이겼는지부터 본다.

# 정규식 location - 여기서는 URI 를 붙이지 않는다
location ~ ^/api/(.*)$ {
    proxy_pass http://app;
}

# 경로를 자를 일은 rewrite 가 맡는다
location ~ ^/api/(.*)$ {
    rewrite    ^/api/(.*)$ /$1 break;
    proxy_pass http://app;
}

# 변수를 쓰면 적어 둔 URI 가 요청 URI 를 통째로 대신한다
location /api/ {
    proxy_pass http://127.0.0.1:9000$request_uri;
}

앞부분을 특정할 수 없는 자리에서는 치환을 기대하지 않는다

정규식 location 안에서는 proxy_pass 에 URI 를 붙이지 않는다. 경로를 자르는 일은 rewrite 가 맡는다.

그래서 어느 쪽을 언제 쓰나

고르는 기준은 하나다. 백엔드가 어떤 경로를 기다리는가.

백엔드가 /api 프리픽스까지 알고 라우팅하는 구조면 아무것도 하지 않는 쪽이 맞다. URI 없이 넘기면 경로가 손상되지 않는다. 반대로 백엔드는 루트에서 서비스하고 /api 는 순전히 앞단 사정이라면, 슬래시를 붙여 프리픽스를 떼어 낸다.

프리픽스를 떼는 쪽을 골랐다면 리다이렉트도 함께 본다. 백엔드가 /login 으로 302 를 주면 브라우저는 /api 가 빠진 주소로 튄다. nginx 는 proxy_redirect 기본값이 location 과 proxy_pass 를 근거로 Location 헤더를 고쳐 주는데, 경로를 rewrite 로 직접 조립했다면 그 기본값이 맞아떨어지지 않을 수 있다.

정리하면 이런 모양이 된다.

백엔드가 이런 모양이면                     proxy_pass 를 이렇게 쓴다
---------------------------------------  ----------------------------------
/api 프리픽스까지 알고 라우팅한다           URI 없이  http://app
루트(/)에서 서비스한다                     끝에 슬래시  http://app/
다른 프리픽스 아래에 있다                   http://app/v2/  (끝 슬래시까지)
location 이 정규식이거나 named location    URI 없이 + rewrite ... break
proxy_pass 주소에 변수를 쓴다               $request_uri 로 경로를 직접 조립

설정에서 시작하지 말고 백엔드가 기다리는 경로에서 시작한다

고치기 전에 백엔드가 뭘 받는지부터 본다

설정을 이리저리 바꿔 가며 새로고침하는 게 제일 느리다. 백엔드가 받은 경로 한 줄만 확인하면 슬래시를 붙일지 뗄지가 그 자리에서 갈린다.

애플리케이션 로그에 경로가 안 남는다면 받은 것을 그대로 찍어 주는 서버를 임시로 세우고 업스트림을 잠깐 그쪽으로 돌린다. 넘어가는 경로를 눈으로 보는 게 추측보다 빠르다.

nginx 접근 로그만 봐서는 답이 안 나온다. 거기 남는 건 클라이언트가 보낸 경로지 업스트림으로 나간 경로가 아니다. 그 둘이 다르다는 게 지금 문제의 전부다.

# 받은 경로를 그대로 찍어 주는 임시 백엔드
python3 -m http.server 9000
#   "GET /users HTTP/1.0"      -> 프리픽스가 떨어져 나갔다
#   "GET /api/users HTTP/1.0"  -> 그대로 넘어갔다

# 문법 확인하고 반영
nginx -t && nginx -s reload

# 브라우저 캐시를 피해 직접 친다
curl -i http://localhost/api/users

넘어간 경로를 확인하고 나서 설정을 손댄다

조치 후 확인할 것

  • 백엔드 접근 로그에서 경로가 /api/users 로 왔는지 /users 로 왔는지 본다. 여기서 이미 답이 갈린다.
  • location 과 proxy_pass 의 끝 슬래시를 나란히 놓고 맞춘다. 한쪽만 붙어 있으면 //users 나 /v2users 가 나온다.
  • 정규식 location 이나 named location 이면 proxy_pass 에서 URI 를 빼고, 경로 정리는 rewrite ... break 로 옮긴다.
  • 프리픽스를 떼는 설정이라면 로그인처럼 302 를 주는 경로를 한 번씩 눌러 Location 헤더에 /api 가 살아 있는지 확인한다.
  • nginx -t 로 문법을 확인하고 reload 한 뒤, 브라우저 캐시가 아니라 curl 로 다시 친다.

설정 파일에서 슬래시 하나는 좀처럼 눈에 띄지 않는다. 리뷰에서도 잘 안 걸리고, 배포한 다음 404 로 처음 드러난다. location 과 proxy_pass 는 늘 두 줄을 같이 읽고 끝을 맞춰 두자. 백엔드가 기다리는 경로만 먼저 정하면 어느 쪽을 쓸지는 저절로 정해진다.