이미지 로딩 중...
AI Generated
2025. 11. 14. · 2 Views
Nginx Location 블록과 Rewrite 규칙 완벽 가이드
웹 서버 라우팅의 핵심인 Nginx Location 블록과 Rewrite 규칙을 실무 예제와 함께 알아봅니다. URL 매칭, 우선순위, 정규표현식 활용법부터 복잡한 리다이렉션 규칙까지 단계별로 학습할 수 있습니다.
목차
- Location 블록 기본 문법 - URL 매칭의 시작점
- Location 매칭 우선순위 - 충돌 없는 라우팅 설계
- Rewrite 지시자 기본 - URL 변환의 마법
- Rewrite 플래그 심화 - last, break, redirect, permanent
- Try_files 지시자 - 우아한 폴백 처리
- 정규표현식 캡처 그룹 - 동적 URL 변환
- 조건부 Rewrite (if 지시자) - 고급 라우팅 제어
- Map 지시자 - 효율적인 변수 매핑
- Return 지시자 - 빠른 응답과 리다이렉트
- 정규표현식 Location - 패턴 기반 라우팅
- Named Location - 내부 리다이렉션 전용
- 실전 예제 - SPA와 API를 함께 서빙
1. Location 블록 기본 문법 - URL 매칭의 시작점
시작하며
여러분이 웹 서버를 운영하다가 "/api" 경로는 백엔드로, "/static" 경로는 정적 파일로 보내야 하는 상황을 겪어본 적 있나요? 또는 특정 URL 패턴에만 인증을 적용하고 싶었던 적이 있나요?
이런 문제는 실제 개발 현장에서 매일 발생합니다. 서비스가 커질수록 URL 라우팅 규칙은 복잡해지고, 잘못된 설정은 404 에러나 보안 문제로 이어질 수 있습니다.
바로 이럴 때 필요한 것이 Nginx의 Location 블록입니다. Location 블록을 제대로 이해하면 복잡한 라우팅 규칙도 명확하게 정의할 수 있습니다.
개요
간단히 말해서, Location 블록은 특정 URL 패턴에 대한 처리 방법을 정의하는 Nginx의 핵심 구성 요소입니다. 클라이언트가 요청한 URL에 따라 다른 백엔드 서버로 프록시하거나, 정적 파일을 서빙하거나, 캐싱 정책을 다르게 적용할 수 있습니다.
예를 들어, API 엔드포인트는 Node.js 서버로 보내고, 이미지 파일은 CDN으로 리다이렉트하는 경우에 매우 유용합니다. 전통적인 방법으로는 웹 서버마다 복잡한 설정 파일을 작성해야 했다면, Nginx는 직관적인 Location 블록으로 이를 간단하게 처리할 수 있습니다.
Location 블록의 핵심 특징은 크게 세 가지입니다: 첫째, 다양한 매칭 방식(완전 일치, 접두사 일치, 정규표현식)을 지원하고, 둘째, 우선순위 규칙이 명확하며, 셋째, 중첩 구조로 복잡한 라우팅도 표현할 수 있습니다. 이러한 특징들이 유연하면서도 예측 가능한 라우팅 설정을 가능하게 합니다.
코드 예제
# 완전 일치: 정확히 /api와 일치할 때만
location = /api {
return 200 "Exact match";
}
# 접두사 일치: /images로 시작하는 모든 경로
location /images/ {
root /var/www/static;
# /images/logo.png -> /var/www/static/images/logo.png
}
# 우선순위 접두사: ^~는 정규표현식보다 먼저 매칭
location ^~ /admin {
auth_basic "Admin Area";
proxy_pass http://admin-backend;
}
# 정규표현식 (대소문자 구분): \. 확장자 매칭
location ~ \.(jpg|png|gif)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
# 정규표현식 (대소문자 무시)
location ~* \.(css|js)$ {
gzip on;
expires 1y;
}
설명
이것이 하는 일: Location 블록은 들어오는 HTTP 요청의 URI를 검사하여, 정의된 패턴과 매칭되면 해당 블록 내의 설정을 적용합니다. 첫 번째로, 완전 일치(=)는 가장 높은 우선순위를 가집니다.
location = /api는 정확히 "/api"일 때만 매칭되며, "/api/users"나 "/api/"는 매칭되지 않습니다. 이는 홈페이지나 특정 엔드포인트를 빠르게 처리할 때 유용합니다.
그 다음으로, 우선순위 접두사(^~)와 일반 접두사가 평가됩니다. ^~는 정규표현식 검사를 건너뛰고 즉시 매칭을 확정합니다.
예를 들어 location ^~ /admin은 "/admin"으로 시작하는 모든 경로를 매칭하되, 정규표현식 검사 없이 바로 처리합니다. 이는 관리자 페이지처럼 보안이 중요한 경로에서 성능과 명확성을 동시에 얻을 수 있습니다.
마지막으로, 정규표현식(~, ~)이 평가됩니다. ~는 대소문자를 구분하고, ~는 무시합니다.
location ~ \.(jpg|png)$는 확장자가 jpg나 png인 파일을 매칭합니다. 정규표현식은 여러 위치에 정의될 수 있으며, 설정 파일에서 나타나는 순서대로 평가되어 첫 번째 매칭에서 멈춥니다.
여러분이 이 코드를 사용하면 API 요청은 백엔드로, 정적 파일은 캐싱과 함께 직접 서빙하고, 관리자 영역은 인증을 적용하는 등 복잡한 라우팅을 명확하게 구현할 수 있습니다. 성능 최적화(정확한 매칭으로 빠른 처리), 보안 강화(경로별 접근 제어), 유지보수성 향상(직관적인 구조) 등의 이점을 얻을 수 있습니다.
실전 팁
💡 우선순위를 외우세요: = (완전일치) > ^~ (우선순위 접두사) > ~ 또는 ~* (정규표현식, 파일 순서) > 일반 접두사. 디버깅할 때 이 순서를 모르면 예상과 다른 블록이 실행될 수 있습니다.
💡 정적 파일 경로에는 ^~를 사용하세요. /static 같은 경로는 정규표현식 검사가 불필요하므로 ^~로 성능을 높일 수 있습니다.
💡 정규표현식은 설정 파일 순서가 중요합니다. 여러 개가 매칭될 수 있을 때 첫 번째로 나타나는 것이 선택되므로, 더 구체적인 패턴을 먼저 배치하세요.
💡 디버깅 시 return 200 "matched location X";를 임시로 추가하여 어느 블록이 실행되는지 확인하세요.
💡 root와 alias의 차이를 이해하세요. location /images/에서 root는 경로를 붙이지만(/var/www/images/), alias는 대체합니다. 잘못 사용하면 404가 발생합니다.
2. Location 매칭 우선순위 - 충돌 없는 라우팅 설계
시작하며
여러분이 /api/v1/users라는 요청이 들어왔을 때, location /api, location ~ /api/v1, location /api/v1/users 중 어느 블록이 실행될지 헷갈린 적 있나요? 특히 여러 개발자가 동시에 설정을 추가하다 보면 의도하지 않은 블록이 실행되는 경우가 생깁니다.
이런 문제는 Nginx의 우선순위 규칙을 정확히 이해하지 못해서 발생합니다. 잘못된 우선순위 설계는 인증이 빠지거나, 캐싱이 적용되지 않거나, 심지어 프로덕션에서 404 에러를 유발할 수 있습니다.
바로 이럴 때 필요한 것이 Location 매칭 우선순위에 대한 명확한 이해입니다. 우선순위 규칙을 알면 복잡한 설정도 예측 가능하게 만들 수 있습니다.
개요
간단히 말해서, Nginx는 요청 URI에 대해 여러 Location 블록을 정해진 순서대로 검사하며, 가장 먼저 매칭되거나 가장 구체적인 블록을 선택합니다. 우선순위 규칙은 완전히 결정론적입니다.
완전 일치가 최우선이고, 없으면 정규표현식보다 우선하는 접두사(^~)를 찾고, 없으면 정규표현식을 파일 순서대로 검사하며, 마지막으로 가장 긴 접두사 매칭을 선택합니다. 예를 들어, 보안이 중요한 /admin 경로는 ^~로 정의하여 다른 정규표현식이 간섭하지 못하게 할 수 있습니다.
전통적인 웹 서버에서는 설정 순서가 실행 순서인 경우가 많았지만, Nginx는 우선순위 알고리즘으로 더 명확하고 예측 가능한 동작을 제공합니다. Location 우선순위의 핵심 특징은 세 가지입니다: 첫째, 완전 일치(=)가 있으면 즉시 검색을 중단하고 해당 블록을 사용합니다.
둘째, 정규표현식은 나타나는 순서대로 평가되므로 설정 파일 구조가 중요합니다. 셋째, 여러 접두사가 매칭될 때는 가장 긴 것이 선택됩니다.
이러한 규칙들이 복잡한 라우팅도 일관되게 동작하도록 보장합니다.
코드 예제
# 우선순위 1: 완전 일치 (가장 높음)
location = / {
# 정확히 "/" 요청만 매칭
return 200 "Homepage exact match";
}
# 우선순위 2: 우선순위 접두사 (정규표현식보다 우선)
location ^~ /api/internal {
# /api/internal로 시작하면 정규표현식 검사 없이 즉시 매칭
deny all;
}
# 우선순위 3: 정규표현식 (파일 순서대로)
location ~ ^/api/v[0-9]+/ {
# /api/v1/, /api/v2/ 등 매칭 (이게 먼저 나오면 우선)
proxy_pass http://versioned-api;
}
location ~* \.(jpg|png)$ {
# 위의 정규표현식과 안 겹치면 이게 평가됨
expires max;
}
# 우선순위 4: 일반 접두사 (가장 긴 것 선택)
location /api/ {
# /api/로 시작하지만 위의 규칙들에 안 걸린 경우
proxy_pass http://default-api;
}
location / {
# 가장 짧은 접두사, 아무것도 안 걸리면 여기
try_files $uri $uri/ =404;
}
설명
이것이 하는 일: Nginx는 요청이 들어오면 모든 Location 블록을 우선순위 알고리즘에 따라 평가하여, 실행할 단 하나의 블록을 선택합니다. 첫 번째로, 완전 일치(=) 블록을 검사합니다.
location = /는 정확히 "/"일 때만 매칭되며, 매칭되면 즉시 검색을 중단하고 해당 블록을 실행합니다. 이는 홈페이지처럼 자주 요청되는 경로를 최고 속도로 처리하기 위한 최적화입니다.
그 다음으로, 모든 접두사 매칭을 찾고 가장 긴 것을 기억합니다. 이때 ^~ 수정자가 있는 접두사가 가장 길었다면, 정규표현식 검사를 건너뛰고 즉시 해당 블록을 사용합니다.
예를 들어 location ^~ /api/internal은 내부 API 경로를 보호하기 위해 다른 정규표현식이 간섭하지 못하도록 합니다. 만약 ^~ 블록이 선택되지 않았다면, 설정 파일에 나타나는 순서대로 정규표현식(~, ~*) 블록을 평가합니다.
첫 번째로 매칭되는 정규표현식이 있으면 그것을 사용하고 검색을 중단합니다. 따라서 location ~ ^/api/v[0-9]+/가 location ~ ^/api/보다 먼저 나와야 버전별 API가 올바르게 처리됩니다.
마지막으로, 정규표현식도 매칭되지 않았다면 앞서 기억해둔 가장 긴 접두사 블록을 사용합니다. location /api/와 location /가 모두 매칭된다면 더 긴 /api/가 선택됩니다.
여러분이 이 우선순위를 이해하면 수십 개의 Location 블록이 있어도 어느 것이 실행될지 정확히 예측할 수 있습니다. 디버깅 시간 단축, 보안 설정의 확실성, 성능 최적화(완전 일치로 빠른 처리) 등의 이점을 얻을 수 있습니다.
실전 팁
💡 보안이 중요한 경로는 ^~를 사용하세요. /admin이나 /internal-api 같은 경로가 다른 정규표현식에 의해 우회되는 것을 방지할 수 있습니다.
💡 정규표현식 순서를 신중하게 배치하세요. 더 구체적인 패턴(~ ^/api/v2/special)을 일반적인 패턴(~ ^/api) 위에 두어야 합니다.
💡 nginx -T로 실제 적용되는 설정을 확인하세요. include 파일들이 어떤 순서로 병합되는지 보면 정규표현식 순서를 파악할 수 있습니다.
💡 헷갈릴 때는 curl과 함께 테스트하세요. curl -i http://localhost/api/v1/test로 응답 헤더를 보면 어느 블록이 처리했는지 알 수 있습니다(add_header로 표시 추가).
💡 가장 긴 접두사가 선택된다는 것을 활용하세요. /api/v1/special과 /api/v1이 모두 있으면 더 구체적인 것이 선택되므로, 예외 케이스를 쉽게 처리할 수 있습니다.
3. Rewrite 지시자 기본 - URL 변환의 마법
시작하며
여러분이 웹사이트를 리뉴얼하면서 /old-product-page를 /products로 변경해야 하는데, 기존 URL을 사용하는 사용자들도 자동으로 새 페이지로 이동시켜야 하는 상황을 겪어본 적 있나요? 또는 /user/123을 내부적으로 /index.php?user_id=123으로 변환해야 할 때가 있었나요?
이런 문제는 실무에서 매우 흔합니다. SEO를 위해 깔끔한 URL을 유지하면서도, 내부적으로는 레거시 시스템과 호환되어야 하거나, 리다이렉션 없이 투명하게 URL을 변환해야 하는 경우가 많습니다.
바로 이럴 때 필요한 것이 Nginx의 Rewrite 지시자입니다. Rewrite를 제대로 활용하면 클라이언트는 모르게 URL을 변환하거나, 명시적으로 리다이렉트할 수 있습니다.
개요
간단히 말해서, Rewrite 지시자는 요청 URI를 정규표현식으로 매칭하여 다른 URI로 변환하거나 리다이렉트하는 Nginx의 강력한 기능입니다. 정규표현식 캡처 그룹을 사용하여 URL의 일부를 추출하고, 새로운 URL에 재구성할 수 있습니다.
예를 들어, /product/electronics/laptop-123을 /shop?category=electronics&id=laptop-123으로 내부 변환하여 레거시 백엔드와 호환성을 유지하면서도 사용자에게는 깔끔한 URL을 제공할 수 있습니다. 전통적인 방법으로는 애플리케이션 코드에서 라우팅을 처리해야 했다면, Nginx Rewrite는 웹 서버 레벨에서 이를 처리하여 애플리케이션 부담을 줄이고 성능을 높입니다.
Rewrite의 핵심 특징은 네 가지입니다: 첫째, PCRE 정규표현식을 지원하여 복잡한 패턴 매칭이 가능합니다. 둘째, last, break, redirect, permanent 플래그로 동작 방식을 세밀하게 제어할 수 있습니다.
셋째, 캡처 그룹($1, $2 등)으로 URL 부분을 재사용할 수 있습니다. 넷째, 조건부 실행(if와 함께)으로 복잡한 비즈니스 로직도 구현할 수 있습니다.
이러한 특징들이 유연하고 강력한 URL 관리를 가능하게 합니다.
코드 예제
# 기본 문법: rewrite 패턴 대체 [플래그];
# 내부 변환 (클라이언트는 모름)
location /products {
# /products/electronics/123 -> /shop.php?cat=electronics&id=123
rewrite ^/products/([^/]+)/(\d+)$ /shop.php?cat=$1&id=$2 last;
}
# 영구 리다이렉트 (301, SEO 전달)
rewrite ^/old-page$ /new-page permanent;
# 임시 리다이렉트 (302)
rewrite ^/temp-sale$ /promotions redirect;
# break: 현재 블록에서 rewrite 중단
location /api {
rewrite ^/api/v1/(.*)$ /v1-endpoint/$1 break;
proxy_pass http://backend;
# http://backend/v1-endpoint/users로 프록시됨
}
# last: rewrite 후 location 재검색
rewrite ^/user/(\d+)$ /profile?id=$1 last;
# 새로운 URI로 location 블록 다시 찾음
설명
이것이 하는 일: Rewrite 지시자는 요청 URI를 정규표현식으로 검사하여, 매칭되면 캡처 그룹을 활용해 새로운 URI를 생성하고, 플래그에 따라 내부 처리 또는 클라이언트 리다이렉트를 수행합니다. 첫 번째로, 정규표현식 매칭이 수행됩니다.
^/products/([^/]+)/(\d+)$는 "/products/" 다음에 슬래시가 아닌 문자들(카테고리)과 숫자들(ID)을 캡처합니다. 예를 들어 "/products/electronics/123"이 요청되면 $1은 "electronics", $2는 "123"이 됩니다.
그 다음으로, 캡처된 값들을 사용하여 새 URI를 생성합니다. /shop.php?cat=$1&id=$2는 "/shop.php?cat=electronics&id=123"으로 변환됩니다.
이 시점에서 플래그가 동작 방식을 결정합니다. last 플래그는 rewrite를 수행한 후 모든 Location 블록을 처음부터 다시 검색합니다.
변환된 URI("/shop.php?cat=electronics&id=123")로 가장 적합한 Location 블록을 찾아 그곳의 설정을 적용합니다. break 플래그는 현재 Location 블록 내에서만 rewrite를 중단하고, 현재 블록의 나머지 지시자들을 계속 실행합니다.
주로 proxy_pass와 함께 사용됩니다. redirect와 permanent 플래그는 클라이언트에게 HTTP 302 또는 301 응답을 보내 새 URL로 이동하게 합니다.
브라우저 주소창이 변경되며, permanent는 SEO 점수를 새 URL로 전달합니다. 여러분이 이 코드를 사용하면 레거시 URL을 유지하면서 새 구조로 마이그레이션하거나, RESTful한 깔끔한 URL을 제공하면서 내부적으로는 쿼리 파라미터를 사용하는 등의 유연성을 얻을 수 있습니다.
SEO 최적화(영구 리다이렉트로 점수 전달), 사용자 경험 개선(깔끔한 URL), 레거시 호환성 유지 등의 이점이 있습니다.
실전 팁
💡 last vs break를 명확히 구분하세요. last는 Location 재검색으로 무한 루프 위험이 있고, break는 proxy_pass 같은 콘텐츠 핸들러와 함께 사용합니다.
💡 무한 루프를 조심하세요. rewrite ^/(.*)$ /$1 last;처럼 자기 자신을 계속 매칭하면 "rewrite or internal redirection cycle" 에러가 발생합니다.
💡 SEO 마이그레이션엔 permanent를 사용하세요. 301 리다이렉트는 검색엔진에게 페이지가 영구 이동했음을 알려 랭킹을 보존합니다.
💡 정규표현식은 Location과 달리 URI 부분만 매칭합니다. 쿼리 스트링은 자동으로 보존되거나 명시적으로 처리해야 합니다($args 변수 사용).
💡 디버깅 시 error_log에 rewrite_log on;을 설정하면 모든 rewrite 과정이 로그에 기록되어 문제를 빠르게 찾을 수 있습니다.
4. Rewrite 플래그 심화 - last, break, redirect, permanent
시작하며
여러분이 Rewrite 규칙을 작성했는데 무한 루프에 빠지거나, proxy_pass로 전달되어야 할 요청이 404가 되거나, 리다이렉트가 예상과 다르게 동작하는 상황을 겪어본 적 있나요? 특히 last와 break의 차이를 정확히 몰라서 헤맨 경험이 있을 것입니다.
이런 문제는 Rewrite 플래그의 동작 원리를 정확히 이해하지 못해서 발생합니다. 잘못된 플래그 선택은 성능 저하, 무한 루프, 잘못된 리다이렉션 등 심각한 문제를 일으킬 수 있습니다.
바로 이럴 때 필요한 것이 각 플래그의 정확한 동작 방식에 대한 이해입니다. 플래그를 제대로 알면 복잡한 라우팅 시나리오도 안정적으로 구현할 수 있습니다.
개요
간단히 말해서, Rewrite 플래그는 URL 변환 후 Nginx가 어떻게 처리를 계속할지를 결정하는 제어 지시자입니다. 네 가지 플래그(last, break, redirect, permanent)는 각각 명확히 다른 역할을 합니다.
last는 변환된 URI로 Location 블록을 재검색하고, break는 현재 블록에서 계속 진행하며, redirect/permanent는 클라이언트에게 HTTP 리다이렉션을 보냅니다. 예를 들어, proxy_pass를 사용하는 API 게이트웨이에서는 break를 써야 하고, SEO를 유지하며 도메인을 변경할 때는 permanent를 사용합니다.
전통적인 Apache의 mod_rewrite와 비교하면, Nginx는 더 명확하고 예측 가능한 플래그 시스템을 제공하여 복잡한 규칙도 이해하기 쉽게 만듭니다. 플래그의 핵심 특징은 세 가지입니다: 첫째, last와 break는 내부 처리(클라이언트는 모름)이고 redirect/permanent는 외부 리다이렉션입니다.
둘째, last는 최대 10회까지 Location 재검색을 허용하며 초과 시 500 에러를 반환합니다. 셋째, break는 proxy_pass, fastcgi_pass 같은 콘텐츠 핸들러와 조합할 때 필수적입니다.
이러한 특징들이 다양한 라우팅 패턴을 정확하게 구현할 수 있게 합니다.
코드 예제
# LAST: Location 재검색 (server, location 블록에서 사용)
server {
location /shop {
# URI를 변환하고 Location 블록 재검색
rewrite ^/shop/(.*)$ /products/$1 last;
# 여기서 멈추고 /products에 맞는 Location 찾음
}
location /products {
# 위의 rewrite 후 이 블록이 실행됨
root /var/www/store;
}
}
# BREAK: 현재 블록 내에서 rewrite 중단
location /api {
# URI 변환 후 현재 블록의 proxy_pass 실행
rewrite ^/api/v2/(.*)$ /v2/$1 break;
proxy_pass http://backend:8080;
# http://backend:8080/v2/users 로 프록시됨
# Location 재검색하지 않음!
}
# REDIRECT: 임시 리다이렉트 (302)
location /temp-promo {
rewrite ^/temp-promo$ https://example.com/sale redirect;
# 클라이언트가 302 받고 새 URL로 이동
}
# PERMANENT: 영구 리다이렉트 (301, SEO 전달)
server {
server_name old-domain.com;
rewrite ^/(.*)$ https://new-domain.com/$1 permanent;
# 검색엔진이 랭킹을 새 도메인으로 이전
}
설명
이것이 하는 일: Rewrite 플래그는 URI 변환이 완료된 후 Nginx의 요청 처리 흐름을 제어하여, 내부 재검색, 현재 블록 계속, 또는 클라이언트 리다이렉션 중 하나를 수행합니다. 첫 번째로, last 플래그를 살펴봅시다.
rewrite ^/shop/(.*)$ /products/$1 last;가 실행되면 URI가 "/shop/laptops"에서 "/products/laptops"로 변환되고, Nginx는 모든 Location 블록을 처음부터 다시 검색하여 "/products"에 매칭되는 블록을 찾아 실행합니다. 이는 마치 클라이언트가 처음부터 "/products/laptops"를 요청한 것처럼 동작합니다.
단, 무한 루프 방지를 위해 최대 10회로 제한되며, 초과 시 "rewrite or internal redirection cycle" 에러가 발생합니다. 그 다음으로, break 플래그는 완전히 다르게 동작합니다.
rewrite ^/api/v2/(.*)$ /v2/$1 break; 후에는 Location 재검색을 하지 않고, 현재 Location 블록의 나머지 지시자(proxy_pass, fastcgi_pass 등)를 계속 실행합니다. 이는 리버스 프록시 시나리오에서 필수적입니다.
예를 들어 "/api/v2/users"를 "http://backend:8080/v2/users"로 프록시할 때, break를 써야 정확한 경로가 전달됩니다. redirect와 permanent 플래그는 클라이언트와 상호작용합니다.
redirect는 HTTP 302(임시 이동) 응답을 보내 브라우저가 새 URL로 재요청하게 합니다. permanent는 HTTP 301(영구 이동)을 보내며, 검색엔진이 이를 인식하여 SEO 점수를 새 URL로 이전합니다.
예를 들어 도메인 변경 시 rewrite ^/(.*)$ https://new-domain.com/$1 permanent;를 사용하면 Google 랭킹이 보존됩니다. 플래그를 생략하면 기본적으로 내부 변환 후 현재 블록의 다음 rewrite 규칙으로 진행합니다.
모든 rewrite를 처리한 후에는 변환된 URI로 현재 블록을 계속 실행합니다. 여러분이 각 플래그를 정확히 사용하면 API 게이트웨이(break로 정확한 프록시), 마이크로서비스 라우팅(last로 유연한 내부 전달), SEO 최적화(permanent로 도메인 마이그레이션) 등을 안정적으로 구현할 수 있습니다.
성능 최적화(불필요한 재검색 방지), 보안(무한 루프 방지), SEO 개선 등의 이점을 얻습니다.
실전 팁
💡 proxy_pass와 함께라면 무조건 break를 사용하세요. last를 쓰면 변환된 URI로 Location 재검색이 일어나 엉뚱한 블록이 실행될 수 있습니다.
💡 무한 루프를 피하려면 rewrite 전후의 URI가 다른 Location에 매칭되는지 확인하세요. 같은 Location에 계속 매칭되면 10회 제한에 걸립니다.
💡 도메인 변경이나 URL 구조 변경 시 permanent를 사용하세요. 301은 브라우저가 캐싱하므로, 테스트 시에는 redirect로 하다가 확정 후 permanent로 변경하세요.
💡 플래그 없이 여러 rewrite를 연속 실행할 수 있습니다. 조건부 변환(if와 함께)에서 유용하지만, 가독성을 위해 최소화하세요.
💡 return 지시자를 고려하세요. 단순 리다이렉트는 return 301 https://new-site.com$request_uri;가 rewrite보다 빠르고 명확합니다.
5. Try_files 지시자 - 우아한 폴백 처리
시작하며
여러분이 정적 파일을 서빙하는데, 파일이 없으면 404 대신 index.html을 보여주거나 백엔드로 요청을 전달해야 하는 상황을 겪어본 적 있나요? 특히 React나 Vue 같은 SPA를 배포할 때 모든 경로를 index.html로 폴백해야 하는 경우가 있습니다.
이런 문제는 프론트엔드 라우팅과 서버 라우팅이 충돌할 때 발생합니다. 전통적인 방법으로는 복잡한 if 문이나 rewrite 규칙을 작성해야 했지만, 가독성이 떨어지고 유지보수가 어렵습니다.
바로 이럴 때 필요한 것이 try_files 지시자입니다. Try_files를 사용하면 여러 옵션을 순서대로 시도하며 우아하게 폴백 처리를 할 수 있습니다.
개요
간단히 말해서, try_files는 주어진 여러 파일이나 디렉토리 옵션을 순서대로 확인하여, 처음 존재하는 것을 서빙하거나 마지막 폴백 옵션을 실행하는 지시자입니다. 파일 시스템을 실제로 확인하여 존재 여부를 판단하므로, 복잡한 if 조건이나 정규표현식 없이도 직관적으로 폴백 로직을 구현할 수 있습니다.
예를 들어, 정적 파일이 있으면 서빙하고, 없으면 디렉토리 인덱스를 찾고, 그마저 없으면 백엔드 API로 프록시하는 로직을 한 줄로 표현할 수 있습니다. 전통적인 Apache의 mod_rewrite 복잡한 조건문과 비교하면, Nginx의 try_files는 훨씬 읽기 쉽고 성능도 우수합니다.
try_files의 핵심 특징은 네 가지입니다: 첫째, 실제 파일 시스템을 체크하므로 정확합니다. 둘째, 여러 옵션을 공백으로 구분하여 순서대로 시도합니다.
셋째, 마지막 파라미터는 폴백으로, Location이나 응답 코드가 될 수 있습니다. 넷째, $uri 변수로 요청 URI를 참조할 수 있습니다.
이러한 특징들이 SPA 배포, 정적 사이트 호스팅, API 폴백 등 다양한 시나리오를 간결하게 처리할 수 있게 합니다.
코드 예제
# SPA 배포: 모든 경로를 index.html로 폴백
location / {
root /var/www/react-app;
# 1) 정확한 파일 존재 확인
# 2) 디렉토리면 index 확인
# 3) 없으면 index.html 서빙 (SPA 라우팅)
try_files $uri $uri/ /index.html;
}
# API 폴백: 정적 파일 우선, 없으면 백엔드
location /assets {
root /var/www/static;
# 파일 있으면 서빙, 없으면 @backend로 점프
try_files $uri @backend;
}
location @backend {
# 명명된 location: try_files 폴백 전용
proxy_pass http://api-server:3000;
}
# 다중 폴백: 캐시 -> 원본 -> 404
location /images {
root /var/cache;
try_files $uri $uri/ @origin =404;
}
location @origin {
proxy_pass http://image-server;
}
# 쿼리 파라미터 보존
location /api {
try_files $uri $uri/ /index.php?$args;
# /api/users?page=2 -> /index.php?page=2
}
설명
이것이 하는 일: try_files는 왼쪽부터 순서대로 각 옵션이 파일 시스템에 실제 존재하는지 확인하고, 처음 발견한 것을 즉시 서빙하거나, 모두 없으면 마지막 옵션을 폴백으로 실행합니다. 첫 번째로, try_files $uri $uri/ /index.html;에서 $uri는 현재 요청 URI를 의미합니다.
예를 들어 "/about/team"이 요청되면 먼저 "/var/www/react-app/about/team" 파일이 존재하는지 확인합니다. 이는 실제 파일 시스템 조회이므로 정확합니다.
그 다음으로, 첫 번째 옵션이 없으면 두 번째 $uri/(디렉토리)를 확인합니다. "/var/www/react-app/about/team/" 디렉토리가 존재하고 그 안에 index.html이나 index.htm 같은 인덱스 파일이 있는지 봅니다.
디렉토리 인덱싱이 활성화되어 있으면 디렉토리 목록을 보여줄 수도 있습니다. 마지막으로, 모든 옵션이 실패하면 세 번째 "/index.html"로 내부 리다이렉트합니다.
이는 파일 존재 확인 없이 무조건 실행되는 폴백입니다. SPA의 경우 index.html이 모든 클라이언트 측 라우팅을 처리하므로, 이것이 완벽한 해결책입니다.
특별한 케이스로 try_files $uri @backend;처럼 명명된 Location(@로 시작)을 폴백으로 사용할 수 있습니다. 이는 복잡한 프록시 로직을 분리하여 가독성을 높입니다.
또한 =404 같은 응답 코드를 직접 반환할 수도 있습니다. 여러분이 try_files를 사용하면 SPA 배포가 한 줄로 해결되고, CDN과 오리진 서버 간 폴백, 정적 파일 우선 서빙 후 API 폴백 등 복잡한 시나리오를 간결하게 구현할 수 있습니다.
코드 가독성 향상, 유지보수 간소화, if 문 제거로 인한 성능 개선 등의 이점을 얻습니다.
실전 팁
💡 SPA 배포는 항상 try_files $uri $uri/ /index.html; 패턴을 사용하세요. React Router, Vue Router가 모두 이 방식으로 동작합니다.
💡 if 문을 피하고 try_files를 사용하세요. Nginx 공식 문서도 "If is Evil"이라고 경고합니다. if는 예측 불가능한 동작을 유발할 수 있습니다.
💡 명명된 Location(@backend)을 활용하여 복잡한 폴백 로직을 분리하세요. 코드 재사용성과 가독성이 크게 향상됩니다.
💡 마지막 파라미터는 반드시 존재하는 파일이나 Location이어야 합니다. 그렇지 않으면 내부 에러가 발생할 수 있습니다.
💡 쿼리 파라미터는 자동으로 보존되지만, 명시적으로 전달하려면 $args나 $query_string을 사용하세요.
6. 정규표현식 캡처 그룹 - 동적 URL 변환
시작하며
여러분이 /blog/2024/05/my-first-post를 /posts?year=2024&month=05&slug=my-first-post로 변환해야 하는데, URL의 각 부분을 어떻게 추출하고 재구성할지 고민한 적 있나요? 특히 다양한 패턴의 레거시 URL을 새 구조로 마이그레이션할 때 이런 상황이 자주 발생합니다.
이런 문제는 URL 구조 변경, API 버전 관리, SEO 친화적 URL 생성 등 실무에서 매우 흔합니다. 하드코딩된 변환 규칙으로는 유연성이 부족하고, 유지보수가 어렵습니다.
바로 이럴 때 필요한 것이 정규표현식 캡처 그룹입니다. 캡처 그룹을 제대로 활용하면 URL의 동적인 부분을 추출하여 원하는 형태로 재구성할 수 있습니다.
개요
간단히 말해서, 정규표현식 캡처 그룹은 괄호()로 묶인 패턴 부분을 매칭하여 $1, $2 같은 변수에 저장하고, rewrite나 다른 지시자에서 재사용할 수 있게 하는 기능입니다. PCRE(Perl Compatible Regular Expressions) 문법을 지원하므로 매우 강력하고 유연한 패턴 매칭이 가능합니다.
예를 들어, 사용자 프로필 URL /user/john123/posts/5에서 사용자명과 게시물 ID를 추출하여 /profile.php?user=john123&post_id=5로 변환할 수 있습니다. 전통적인 문자열 치환과 비교하면, 정규표현식은 패턴 기반이므로 수천 개의 다른 URL을 하나의 규칙으로 처리할 수 있습니다.
캡처 그룹의 핵심 특징은 네 가지입니다: 첫째, 괄호로 묶인 부분이 자동으로 $1, $2, ... $9까지 저장됩니다.
둘째, 중첩된 괄호는 왼쪽 괄호 순서대로 번호가 매겨집니다. 셋째, 캡처하지 않는 그룹(?:)을 사용하여 매칭만 하고 저장하지 않을 수 있습니다.
넷째, 명명된 캡처 그룹(?P<name>)으로 가독성을 높일 수 있습니다. 이러한 특징들이 복잡한 URL 변환도 정확하고 간결하게 표현할 수 있게 합니다.
코드 예제
# 기본 캡처: 날짜별 블로그 URL 변환
location /blog {
# /blog/2024/05/my-post -> /posts.php?year=2024&month=05&slug=my-post
rewrite ^/blog/(\d{4})/(\d{2})/(.+)$ /posts.php?year=$1&month=$2&slug=$3 last;
# $1=2024, $2=05, $3=my-post
}
# 복잡한 캡처: 사용자 프로필과 액션
location /user {
# /user/john123/photos/15 -> /profile.php?user=john123§ion=photos&id=15
rewrite ^/user/([a-zA-Z0-9_]+)/([^/]+)/(\d+)$
/profile.php?user=$1§ion=$2&id=$3 last;
}
# 옵션 부분 매칭: 버전이 있을 수도, 없을 수도
location /api {
# /api/v2/users 또는 /api/users 모두 매칭
rewrite ^/api/(?:v(\d+)/)?(.+)$ /api.php?version=$1&endpoint=$2 last;
# version 없으면 $1은 빈 문자열
}
# 확장자 추출 및 변환
location /files {
# /files/document.pdf -> /serve.php?file=document&type=pdf
rewrite ^/files/([^.]+)\.(.+)$ /serve.php?file=$1&type=$2 last;
}
# 명명된 캡처 (가독성 향상)
location ~ ^/product/(?P<category>[^/]+)/(?P<id>\d+) {
# /product/electronics/123
proxy_pass http://backend/api?cat=$category&id=$id;
}
설명
이것이 하는 일: 정규표현식 엔진이 패턴을 매칭할 때 괄호로 묶인 부분을 별도로 기억하여 번호가 매겨진 변수에 저장하고, 이를 rewrite, proxy_pass 등에서 참조할 수 있게 합니다. 첫 번째로, 패턴 매칭이 수행됩니다.
^/blog/(\d{4})/(\d{2})/(.+)$는 "/blog/" 다음에 4자리 숫자(연도), 2자리 숫자(월), 그리고 나머지 모든 문자(슬러그)를 기대합니다. "/blog/2024/05/my-first-post"가 요청되면 정규표현식 엔진이 각 괄호 부분을 순서대로 추출합니다.
그 다음으로, 추출된 값들이 자동으로 변수에 할당됩니다. 첫 번째 괄호 (\d{4})에 매칭된 "2024"는 $1에, 두 번째 괄호 (\d{2})에 매칭된 "05"는 $2에, 세 번째 괄호 (.+)에 매칭된 "my-first-post"는 $3에 저장됩니다.
이 변수들은 같은 지시자 블록 내에서 자유롭게 사용할 수 있습니다. 새로운 URI를 생성할 때 이 변수들이 치환됩니다.
/posts.php?year=$1&month=$2&slug=$3는 "/posts.php?year=2024&month=05&slug=my-first-post"로 확장됩니다. 이렇게 하나의 규칙으로 수천 개의 다른 날짜와 슬러그 조합을 처리할 수 있습니다.
고급 기능으로 비캡처 그룹 (?:v(\d+)/)?를 사용하면 그룹 자체는 변수에 저장되지 않지만, 내부의 괄호는 캡처됩니다. 이는 옵션 부분을 매칭할 때 유용합니다.
명명된 캡처 (?P<name>패턴)을 사용하면 $1 대신 $name처럼 의미 있는 이름으로 참조할 수 있어 가독성이 크게 향상됩니다. 여러분이 캡처 그룹을 마스터하면 레거시 URL 마이그레이션, RESTful API 라우팅, SEO 친화적 URL 변환 등을 간결하고 유지보수하기 쉬운 규칙으로 구현할 수 있습니다.
코드 중복 제거(하나의 패턴으로 무한한 조합 처리), 유연성(새로운 URL 패턴 쉽게 추가), 성능(컴파일된 정규표현식의 빠른 매칭) 등의 이점을 얻습니다.
실전 팁
💡 캡처 그룹은 최대 9개($1~$9)까지만 사용할 수 있습니다. 더 많이 필요하면 규칙을 분리하거나 명명된 캡처를 사용하세요.
💡 특수 문자는 이스케이프하세요. .은 모든 문자를 의미하므로 리터럴 점은 \.로 써야 합니다. /files/doc.pdf를 매칭하려면 ([^.]+)\.([^.]+)$처럼 사용하세요.
💡 탐욕적 vs 비탐욕적 매칭을 이해하세요. (.+)는 최대한 많이 매칭하고, (.+?)는 최소한만 매칭합니다. /path/to/file에서 마지막 부분만 원하면 ^.+/(.+)$를 사용하세요.
💡 온라인 정규표현식 테스터(regex101.com)를 활용하세요. 패턴을 미리 테스트하고 캡처 그룹이 올바르게 동작하는지 확인할 수 있습니다.
💡 명명된 캡처는 가독성을 크게 높입니다. (?P<year>\d{4})처럼 쓰면 6개월 후에도 코드를 쉽게 이해할 수 있습니다.
7. 조건부 Rewrite (if 지시자) - 고급 라우팅 제어
시작하며
여러분이 특정 User-Agent에서만 모바일 페이지로 리다이렉트하거나, 특정 IP 대역에서만 베타 기능에 접근할 수 있게 하거나, HTTP 요청만 HTTPS로 강제 전환해야 하는 상황을 겪어본 적 있나요? 이런 조건부 라우팅은 실무에서 매우 자주 필요합니다.
이런 문제는 단순한 URL 패턴 매칭으로는 해결할 수 없습니다. 요청 헤더, 변수 값, 쿼리 파라미터 등을 검사하여 동적으로 처리 방법을 결정해야 합니다.
바로 이럴 때 필요한 것이 if 지시자입니다. 하지만 Nginx의 if는 "If is Evil"이라는 경고가 있을 정도로 주의가 필요하므로, 올바른 사용법을 정확히 알아야 합니다.
개요
간단히 말해서, if 지시자는 변수나 조건을 평가하여 참일 때만 내부의 지시자들을 실행하는 조건문입니다. 하지만 일반 프로그래밍 언어의 if와는 다릅니다.
Nginx의 if는 설정 파일 파싱 단계가 아닌 요청 처리 단계에서 평가되며, 예상치 못한 부작용이 있을 수 있습니다. 안전하게 사용할 수 있는 경우는 return이나 rewrite와 함께 사용할 때, 그리고 특정 변수($request_method, $http_*, $args 등)를 검사할 때입니다.
예를 들어, 모바일 User-Agent 감지, HTTP to HTTPS 리다이렉트, 특정 쿼리 파라미터 기반 라우팅 등에 유용합니다. 전통적인 웹 서버에서는 복잡한 모듈을 작성해야 했던 조건부 로직을, Nginx는 if로 간단히 구현할 수 있지만, 과도한 사용은 성능과 안정성을 해칠 수 있습니다.
if 지시자의 핵심 특징은 네 가지입니다: 첫째, 단순 비교(=, !=), 정규표현식 매칭(, *), 파일 존재 확인(-f, !-f), 디렉토리 확인(-d, !-d)을 지원합니다. 둘째, return이나 rewrite와 함께 사용하면 안전합니다.*, !, !
셋째, 복잡한 조건은 map 지시자로 대체하는 것이 좋습니다. 넷째, if 내부에서 proxy_pass 같은 일부 지시자는 예상대로 동작하지 않을 수 있습니다.
이러한 특징들을 이해하고 제한된 범위에서만 사용해야 안정적인 설정을 만들 수 있습니다.
코드 예제
# 안전한 사용: HTTP to HTTPS 리다이렉트
server {
listen 80;
server_name example.com;
if ($scheme = http) {
return 301 https://$host$request_uri;
}
}
# 모바일 감지 및 리다이렉트
location / {
if ($http_user_agent ~* (mobile|android|iphone)) {
rewrite ^(.*)$ https://m.example.com$1 redirect;
}
root /var/www/desktop;
}
# 특정 쿼리 파라미터 검사
location /api {
if ($arg_debug = "true") {
return 200 "Debug mode enabled\n";
}
proxy_pass http://backend;
}
# 파일 존재 확인 (안전함)
location /legacy {
if (!-f $request_filename) {
rewrite ^/legacy/(.+)$ /new-path/$1 last;
}
}
# 위험한 사용 (피해야 함!)
location /bad-example {
if ($request_method = POST) {
proxy_pass http://backend; # 동작 안 할 수 있음!
}
}
# 올바른 대안: map 사용
map $request_method $backend {
default "";
POST http://backend;
}
location /good-example {
if ($backend != "") {
proxy_pass $backend;
}
}
설명
이것이 하는 일: if 지시자는 요청 처리 중에 조건을 평가하여, 참이면 블록 내부의 return, rewrite 같은 지시자를 실행하고, 거짓이면 건너뜁니다. 첫 번째로, 조건 평가가 수행됩니다.
if ($scheme = http)는 현재 요청이 HTTP 프로토콜을 사용하는지 확인합니다. $scheme 변수는 Nginx가 자동으로 설정하는 내장 변수로, "http" 또는 "https" 값을 가집니다.
단순 문자열 비교 =를 사용하며, 대소문자를 구분합니다. 그 다음으로, 정규표현식 매칭을 사용할 수 있습니다.
if ($http_user_agent ~* (mobile|android|iphone))는 User-Agent 헤더($http_user_agent)에 "mobile", "android", "iphone" 중 하나가 포함되는지 대소문자 무시하고 검사합니다. ~*는 대소문자 무시 정규표현식 매칭이고, ~는 대소문자 구분 매칭입니다.
파일 시스템 검사도 가능합니다. if (!-f $request_filename)는 요청된 파일이 실제로 존재하지 않는지 확인합니다.
-f는 일반 파일 존재 확인이고, -d는 디렉토리, -e는 파일이나 디렉토리 모두, -x는 실행 가능 파일입니다. 앞에 !를 붙이면 부정입니다.
하지만 if는 위험할 수 있습니다. Nginx의 if는 설정 블록을 임시로 재작성하는 방식으로 동작하므로, proxy_pass, fastcgi_pass 같은 지시자를 if 내부에 직접 쓰면 예상대로 동작하지 않을 수 있습니다.
안전한 패턴은 if 내부에서 return이나 rewrite만 사용하는 것입니다. 복잡한 조건은 map 지시자로 변수를 설정하고, 그 변수를 if로 검사하는 방식이 더 안전합니다.
여러분이 if를 올바르게 사용하면 HTTP to HTTPS 강제 전환(보안 강화), 모바일 사용자 리다이렉트(사용자 경험 개선), 디버그 모드 활성화(개발 편의성) 등을 구현할 수 있습니다. 하지만 과도한 사용은 피하고, 가능하면 map, geo, split_clients 같은 더 안전한 대안을 먼저 고려해야 합니다.
실전 팁
💡 if 내부에서는 return과 rewrite만 사용하세요. proxy_pass나 다른 복잡한 지시자는 예측 불가능한 동작을 유발할 수 있습니다.
💡 여러 조건을 AND로 결합하려면 변수를 연결하세요. set $test ""; if ($a) { set $test "A"; } if ($test = "A") { if ($b) { ... } } 같은 패턴을 사용합니다.
💡 HTTP to HTTPS 리다이렉트는 예외적으로 안전합니다. if ($scheme = http) { return 301 https://$host$request_uri; }는 Nginx 공식 문서에서도 권장하는 패턴입니다.
💡 복잡한 조건은 map으로 대체하세요. User-Agent 기반 분기는 map $http_user_agent $mobile { ~*(mobile|android) 1; default 0; }로 처리하면 더 효율적입니다.
💡 공식 문서의 "If is Evil" 페이지를 읽으세요. 어떤 경우에 안전하고 어떤 경우에 위험한지 명확히 이해할 수 있습니다.
8. Map 지시자 - 효율적인 변수 매핑
시작하며
여러분이 수십 개의 다른 User-Agent를 감지하여 각각 다른 백엔드로 라우팅하거나, 국가별 IP를 다른 CDN으로 보내거나, 파일 확장자에 따라 다른 캐시 정책을 적용해야 하는 상황을 겪어본 적 있나요? if 문을 수십 개 중첩하면 설정 파일이 지저분해지고 성능도 나빠집니다.
이런 문제는 복잡한 조건부 로직을 if로만 처리하려 할 때 발생합니다. if는 느리고, 가독성이 떨어지며, 여러 조건을 조합하기 어렵습니다.
바로 이럴 때 필요한 것이 map 지시자입니다. Map을 사용하면 복잡한 조건부 로직을 선언적이고 효율적으로 표현할 수 있으며, 설정 파일의 가독성도 크게 향상됩니다.
개요
간단히 말해서, map 지시자는 입력 변수를 검사하여 매칭되는 패턴에 따라 출력 변수를 설정하는 효율적인 매핑 메커니즘입니다. 해시 테이블 기반으로 동작하므로 수천 개의 매핑 규칙이 있어도 O(1) 시간 복잡도로 빠르게 처리됩니다.
정규표현식, 와일드카드, CIDR 표기법 등 다양한 패턴을 지원하므로 User-Agent 감지, IP 기반 라우팅, 확장자별 처리 등 거의 모든 조건부 로직을 대체할 수 있습니다. 예를 들어, 100개의 다른 봇 User-Agent를 감지하여 별도 백엔드로 보내는 로직을 깔끔하게 구현할 수 있습니다.
전통적인 if 체인과 비교하면, map은 선언적이고, 캐싱되며, 훨씬 빠릅니다. 설정 변경도 쉽고, 재사용도 간편합니다.
map의 핵심 특징은 다섯 가지입니다: 첫째, http 블록에서만 정의하며 모든 server와 location에서 사용할 수 있습니다. 둘째, 결과는 자동으로 캐싱되어 같은 요청에서 여러 번 참조해도 한 번만 평가됩니다.
셋째, 정규표현식(~, ~), 와일드카드(.example.com), CIDR(192.168.1.0/24) 등 다양한 패턴을 지원합니다. 넷째, default 값을 설정하여 매칭되지 않는 경우를 처리할 수 있습니다.
다섯째, include로 외부 파일에서 매핑을 로드하여 관리를 쉽게 할 수 있습니다. 이러한 특징들이 복잡한 조건부 로직을 우아하고 효율적으로 처리할 수 있게 합니다.
코드 예제
# 기본 map: User-Agent 기반 모바일 감지
map $http_user_agent $is_mobile {
default 0;
~*mobile 1;
~*android 1;
~*iphone 1;
~*ipad 1;
}
server {
location / {
if ($is_mobile) {
rewrite ^ https://m.example.com$request_uri redirect;
}
}
}
# 확장자별 캐시 시간 설정
map $uri $cache_duration {
default "no-cache";
~*\.(jpg|png|gif|ico)$ "30d";
~*\.(css|js)$ "1y";
~*\.(woff2|ttf)$ "max";
}
location /assets {
expires $cache_duration;
}
# IP 기반 백엔드 라우팅
map $remote_addr $backend_pool {
default http://prod-backend;
192.168.1.0/24 http://internal-backend;
10.0.0.0/8 http://dev-backend;
}
location /api {
proxy_pass $backend_pool;
}
# 복잡한 조건: 여러 map 조합
map $request_method $method_flag {
default "";
POST "P";
PUT "U";
}
map $http_content_type $type_flag {
default "";
~*json "J";
}
# $method_flag$type_flag 조합으로 PJ, UJ 등 생성
location /api {
set $combo "${method_flag}${type_flag}";
if ($combo = "PJ") {
# POST + JSON 요청만 특별 처리
}
}
# 외부 파일 포함
map $http_user_agent $bot_detected {
include /etc/nginx/bot-list.conf;
default 0;
}
설명
이것이 하는 일: map 지시자는 설정 파싱 시 해시 테이블을 생성하고, 요청 처리 중 입력 변수의 값을 이 테이블에서 O(1) 시간에 검색하여 매칭되는 패턴의 출력 값을 결과 변수에 할당합니다. 첫 번째로, map 정의 단계에서 매핑 규칙이 최적화됩니다.
map $http_user_agent $is_mobile은 User-Agent 헤더($http_user_agent)를 입력으로 받아 $is_mobile 변수를 설정하겠다는 선언입니다. Nginx는 이 규칙들을 해시 테이블이나 정규표현식 세트로 컴파일하여 빠른 검색을 준비합니다.
그 다음으로, 요청이 들어오면 입력 변수 값이 평가됩니다. 예를 들어 User-Agent가 "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0)"라면, 패턴들을 순서대로 검사합니다.
default 0은 아무것도 매칭되지 않을 때의 기본값입니다. ~*iphone은 대소문자 무시 정규표현식 매칭이므로, 이것이 매칭되어 $is_mobile에 1이 할당됩니다.
중요한 최적화는 결과 캐싱입니다. 한 요청에서 $is_mobile을 여러 번 참조해도 map 평가는 딱 한 번만 일어납니다.
이는 복잡한 정규표현식을 사용해도 성능 걱정 없이 여러 곳에서 재사용할 수 있게 합니다. 여러 map을 조합하면 복잡한 조건도 표현할 수 있습니다.
$method_flag와 $type_flag를 각각 map으로 설정하고, set $combo "${method_flag}${type_flag}";로 결합하면 POST+JSON, PUT+XML 같은 조합을 감지할 수 있습니다. 이는 if 중첩보다 훨씬 명확하고 유지보수하기 쉽습니다.
외부 파일 포함 기능도 강력합니다. include /etc/nginx/bot-list.conf;로 수백 개의 봇 User-Agent를 별도 파일에서 관리하면, 메인 설정 파일이 깔끔해지고 봇 리스트 업데이트도 간편합니다.
여러분이 map을 활용하면 if 문 제거로 인한 성능 향상, 설정 파일 가독성 개선, 복잡한 비즈니스 로직의 명확한 표현, 그리고 재사용 가능한 매핑 규칙 관리 등의 이점을 얻을 수 있습니다. A/B 테스팅, 국가별 라우팅, 봇 감지, 캐시 정책 분기 등 거의 모든 조건부 로직을 map으로 우아하게 해결할 수 있습니다.
실전 팁
💡 map은 http 블록에만 정의하세요. server나 location에는 쓸 수 없으며, 정의 후에는 모든 곳에서 사용할 수 있습니다.
💡 정규표현식은 ~와 ~*를 앞에 붙여야 합니다. 붙이지 않으면 리터럴 문자열 매칭으로 처리됩니다. ~*\.(jpg|png)$ vs .jpg 차이를 명확히 하세요.
💡 default는 필수는 아니지만 권장됩니다. 명시하지 않으면 매칭 실패 시 빈 문자열이 할당되어 디버깅이 어려울 수 있습니다.
💡 복잡한 봇 리스트나 IP 대역은 외부 파일로 분리하세요. include를 사용하면 메인 설정을 건드리지 않고도 리스트를 업데이트할 수 있습니다.
💡 map_hash_bucket_size와 map_hash_max_size를 조정하세요. 매핑이 수천 개일 때 이 값들을 늘리면 성능이 향상됩니다(기본값은 보통 충분함).
9. Return 지시자 - 빠른 응답과 리다이렉트
시작하며
여러분이 API 헬스 체크 엔드포인트를 만들거나, 특정 경로를 간단히 차단하거나, 빠른 리다이렉트를 구현해야 하는데 굳이 백엔드 서버까지 요청을 보내고 싶지 않은 상황을 겪어본 적 있나요? 단순한 응답을 위해 애플리케이션 서버를 거치는 것은 비효율적입니다.
이런 문제는 실무에서 매우 흔합니다. 헬스 체크, 로봇 차단, 간단한 리다이렉트, 정적 JSON 응답 등은 백엔드까지 갈 필요 없이 Nginx 레벨에서 처리하는 것이 훨씬 빠르고 효율적입니다.
바로 이럴 때 필요한 것이 return 지시자입니다. Return을 사용하면 rewrite보다 빠르고 명확하게 응답을 반환하거나 리다이렉트할 수 있습니다.
개요
간단히 말해서, return 지시자는 요청 처리를 즉시 중단하고 지정된 HTTP 상태 코드와 선택적으로 응답 본문이나 리다이렉트 URL을 클라이언트에게 반환하는 지시자입니다. Rewrite와 달리 정규표현식 평가가 없으므로 훨씬 빠르며, 의도도 명확합니다.
200(성공), 301/302(리다이렉트), 403(금지), 404(없음), 500(서버 에러) 등 모든 HTTP 상태 코드를 사용할 수 있습니다. 예를 들어, /health 엔드포인트는 간단히 200 응답을 반환하고, /admin은 특정 IP가 아니면 403을 반환하고, 도메인 변경은 301로 리다이렉트할 수 있습니다.
전통적인 rewrite permanent와 비교하면, return 301은 정규표현식 컴파일이 없어 더 빠르고, 코드 의도도 훨씬 명확합니다. return의 핵심 특징은 네 가지입니다: 첫째, 모든 HTTP 상태 코드(100~599)를 지원합니다.
둘째, 3xx 리다이렉트 코드와 함께 URL을 지정하면 Location 헤더가 자동 설정됩니다. 셋째, 2xx, 4xx, 5xx와 함께 텍스트를 지정하면 응답 본문으로 사용됩니다.
넷째, 요청 처리를 즉시 중단하므로 return 이후의 지시자는 실행되지 않습니다. 이러한 특징들이 빠르고 명확한 응답 처리를 가능하게 합니다.
코드 예제
# 헬스 체크 엔드포인트 (가장 빠른 응답)
location = /health {
access_log off;
return 200 "OK\n";
add_header Content-Type text/plain;
}
# 영구 리다이렉트 (rewrite보다 빠름)
server {
server_name old-domain.com;
return 301 https://new-domain.com$request_uri;
}
# HTTP to HTTPS 강제 전환
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
# API 버전 종료 공지
location /api/v1 {
return 410 '{"error": "API v1 deprecated", "migrate_to": "/api/v2"}';
add_header Content-Type application/json;
}
# 특정 파일 차단
location ~* \.(sql|git|env)$ {
return 403 "Access Forbidden\n";
}
# 조건부 차단 (map과 함께)
map $http_user_agent $bot_blocked {
default 0;
~*bot 1;
~*spider 1;
}
location / {
if ($bot_blocked) {
return 403 "Bots not allowed\n";
}
proxy_pass http://backend;
}
# 임시 점검 페이지
location / {
return 503 '<h1>Maintenance</h1><p>Back soon!</p>';
add_header Content-Type text/html;
add_header Retry-After 3600;
}
설명
이것이 하는 일: return 지시자는 요청 처리 파이프라인을 즉시 중단하고, 지정된 HTTP 상태 코드와 함께 응답을 생성하여 클라이언트에게 전송합니다. 첫 번째로, 간단한 성공 응답을 살펴봅시다.
return 200 "OK\n";은 HTTP 200 상태 코드와 함께 "OK"라는 텍스트 본문을 반환합니다. 로드 밸런서나 모니터링 시스템의 헬스 체크는 단순히 200 응답만 필요한 경우가 많으므로, 백엔드까지 갈 필요 없이 Nginx에서 즉시 응답하여 부하를 줄일 수 있습니다.
access_log off;와 함께 사용하면 로그도 남기지 않아 더욱 효율적입니다. 그 다음으로, 리다이렉트를 살펴봅시다.
return 301 https://new-domain.com$request_uri;는 HTTP 301 영구 리다이렉트를 보냅니다. Nginx는 자동으로 Location 헤더에 지정된 URL을 설정하고, 본문에는 간단한 리다이렉트 안내 HTML을 생성합니다.
클라이언트(브라우저나 봇)는 이를 받아 새 URL로 재요청합니다. $request_uri 변수는 원래 요청 경로와 쿼리 스트링을 포함하므로, "/old-page?id=123"은 "https://new-domain.com/old-page?id=123"로 정확히 리다이렉트됩니다.
커스텀 응답 본문도 가능합니다. return 410 '{"error": "deprecated"}';는 HTTP 410 Gone 상태와 함께 JSON 본문을 반환합니다.
410은 "영구적으로 사라짐"을 의미하며, 검색엔진에게 해당 자원이 더 이상 존재하지 않음을 명확히 알립니다. API 버전 종료 공지에 이상적입니다.
return은 요청 처리를 즉시 중단합니다. return 이후에 proxy_pass, try_files 등 어떤 지시자가 있어도 실행되지 않습니다.
따라서 조건부 return(if와 함께)을 사용할 때는 순서가 중요합니다. if 블록 내의 return이 실행되면 해당 location 블록의 나머지는 모두 무시됩니다.
여러분이 return을 활용하면 헬스 체크 응답 시간을 1ms 이하로 줄이고(백엔드 호출 제거), 불필요한 봇 트래픽을 Nginx에서 차단하고(백엔드 부하 감소), 도메인 마이그레이션을 명확하고 빠르게 처리할 수 있습니다. 성능 최적화(정규표현식 없음), 가독성 향상(의도 명확), 자원 절약(백엔드 부하 감소) 등의 이점을 얻습니다.
실전 팁
💡 단순 리다이렉트는 return을 사용하세요. return 301 https://example.com;이 rewrite ^ https://example.com permanent;보다 빠르고 명확합니다.
💡 헬스 체크는 location = /health { access_log off; return 200 "OK\n"; } 패턴을 사용하세요. 로그도 남기지 않아 디스크 I/O도 절약됩니다.
💡 403 응답에 설명을 추가하세요. return 403 "IP blocked\n"; 같이 이유를 명시하면 디버깅이 쉬워집니다.
💡 JSON 응답 시 Content-Type 헤더를 명시하세요. add_header Content-Type application/json;을 함께 사용하면 클라이언트가 올바르게 파싱합니다.
💡 점검 모드는 503 + Retry-After 조합을 사용하세요. return 503; add_header Retry-After 3600;으로 클라이언트에게 1시간 후 재시도하라고 알릴 수 있습니다.
10. 정규표현식 Location - 패턴 기반 라우팅
시작하며
여러분이 모든 이미지 파일(jpg, png, gif, webp 등)에 동일한 캐싱 정책을 적용하거나, API 버전별로 다른 백엔드로 라우팅하거나, 특정 패턴의 URL에만 인증을 적용해야 하는 상황을 겪어본 적 있나요? 각 확장자나 패턴마다 개별 location 블록을 만드는 것은 비효율적입니다.
이런 문제는 URL 패턴이 다양하지만 처리 방법은 동일할 때 발생합니다. 하드코딩된 경로로는 유연성이 부족하고, 새로운 패턴이 추가될 때마다 설정을 수정해야 합니다.
바로 이럴 때 필요한 것이 정규표현식 Location입니다. 정규표현식 Location을 사용하면 하나의 블록으로 무한한 URL 패턴을 처리할 수 있습니다.
개요
간단히 말해서, 정규표현식 Location은 ~ 또는 ~* 수정자와 함께 정규표현식 패턴을 사용하여 URL을 매칭하는 Location 블록입니다. ~ 는 대소문자를 구분하고, ~*는 무시합니다.
정규표현식은 URI의 어느 부분이든 매칭할 수 있으므로(접두사 매칭과 달리), 확장자 기반, 패턴 기반, 버전 기반 등 복잡한 라우팅을 유연하게 구현할 수 있습니다. 예를 들어, .jpg, .png, .gif 확장자를 모두 하나의 location ~* \.(jpg|png|gif)$ 블록으로 처리하거나, API 버전 /api/v1/, /api/v2/ 등을 location ~ ^/api/v[0-9]+/ 패턴으로 처리할 수 있습니다.
전통적인 접두사 Location과 비교하면, 정규표현식은 훨씬 더 유연하고 강력하지만, 우선순위 규칙(설정 파일 순서)을 잘 이해해야 합니다. 정규표현식 Location의 핵심 특징은 네 가지입니다: 첫째, 설정 파일에 나타나는 순서대로 평가되며 첫 번째 매칭에서 멈춥니다.
둘째, 접두사 매칭(^~ 제외)보다 높은 우선순위를 가집니다. 셋째, 캡처 그룹을 사용할 수 없습니다(rewrite 내부에서는 가능).
넷째, PCRE 정규표현식의 모든 기능(lookahead, backreference 등)을 사용할 수 있습니다. 이러한 특징들이 복잡한 URL 패턴도 간결하게 처리할 수 있게 합니다.
코드 예제
# 이미지 파일 캐싱 (대소문자 무시)
location ~* \.(jpg|jpeg|png|gif|webp|svg|ico)$ {
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
}
# CSS/JS 번들 (버전 해시 포함)
location ~* \.(css|js)$ {
expires 1y;
gzip_static on;
# bundle.abc123.js, style.def456.css 등 모두 매칭
}
# API 버전 라우팅 (대소문자 구분)
location ~ ^/api/v1/ {
proxy_pass http://backend-v1:8001;
# /api/v1/users, /api/v1/posts 등
}
location ~ ^/api/v2/ {
proxy_pass http://backend-v2:8002;
}
# 숫자 ID 패턴
location ~ ^/user/\d+/profile$ {
# /user/123/profile, /user/456/profile 등
proxy_pass http://user-service;
}
# 보안: 숨겨진 파일 차단
location ~ /\. {
# /.git, /.env, /.htaccess 등
deny all;
return 404;
}
# 복잡한 패턴: 날짜 기반 아카이브
location ~ ^/archive/\d{4}/\d{2}/[a-z0-9-]+$ {
# /archive/2024/05/my-post-slug
root /var/www/archives;
try_files $uri /archive-handler.php;
}
# 파일 확장자 차단
location ~* \.(sql|bak|old|backup|log)$ {
deny all;
}
# 여러 정규표현식: 첫 번째 매칭 우선
location ~ ^/special {
return 200 "Special matched\n";
}
location ~ ^/spec {
# 위의 블록이 먼저 매칭되므로 /special은 여기 안 옴
return 200 "Spec matched\n";
}
설명
이것이 하는 일: 정규표현식 Location은 요청 URI를 PCRE 정규표현식으로 평가하여, 패턴과 매칭되면 해당 블록의 설정을 적용하고, 설정 파일에서 이후에 나오는 다른 정규표현식은 검사하지 않습니다. 첫 번째로, 패턴 매칭이 수행됩니다.
location ~* \.(jpg|png|gif)$는 대소문자 무시(~*)로 URI 끝($)이 .jpg, .png, .gif 중 하나로 끝나는지 검사합니다. "/images/logo.PNG"나 "/photos/cat.jpg" 모두 매칭됩니다.
확장자 앞의 .는 리터럴 점을 의미하며(이스케이프), |는 OR 연산자, $는 문자열 끝을 의미합니다. 그 다음으로, 우선순위 규칙이 적용됩니다.
완전 일치(=)와 우선순위 접두사(^~)가 없다면, 모든 정규표현식 Location이 설정 파일 순서대로 평가됩니다. location ~ ^/special이 location ~ ^/spec보다 먼저 나오면, "/special/page" 요청은 첫 번째 블록에 매칭되고 두 번째는 검사조차 되지 않습니다.
따라서 더 구체적인 패턴을 먼저 배치해야 합니다. 정규표현식은 URI 어디든 매칭할 수 있습니다.
접두사 Location은 항상 시작 부분만 매칭하지만, 정규표현식은 \.(css|js)$ 처럼 끝 부분을 매칭하거나, /\d+/ 처럼 중간 부분을 매칭할 수 있습니다. 이는 확장자 기반 처리, ID 패턴 감지, 복잡한 URL 구조 처리에 필수적입니다.
보안에도 유용합니다. location ~ /\.는 경로에 점으로 시작하는 부분이 있는지 검사합니다.
"/.git/config", "/admin/.env" 등이 모두 매칭되어 deny all;로 차단할 수 있습니다. 이는 민감한 파일 노출을 방지하는 중요한 보안 패턴입니다.
여러분이 정규표현식 Location을 마스터하면 수십 개의 개별 블록을 하나로 통합하고(유지보수 간소화), 새로운 파일 형식이 추가되어도 패턴만 수정하면 되며(유연성), 보안 규칙을 명확하게 정의할 수 있습니다(안전성). 설정 파일 크기 감소, 가독성 향상, 통일된 정책 적용 등의 이점을 얻습니다.
실전 팁
💡 확장자 매칭은 항상 대소문자 무시(~*)를 사용하세요. 사용자가 "logo.PNG"나 "Logo.Png" 같은 파일을 업로드할 수 있습니다.
💡 더 구체적인 패턴을 먼저 배치하세요. location ~ ^/api/v2/special 다음에 location ~ ^/api/v2/를 두어야 합니다.
💡 보안 패턴을 최상단에 두세요. location ~ /\. 같은 차단 규칙은 설정 파일 최상단에 두어 먼저 평가되게 합니다.
💡 정규표현식은 성능 비용이 있습니다. 수십 개의 정규표현식 Location이 있으면 각 요청마다 순서대로 평가되므로, 자주 매칭되는 패턴을 먼저 배치하세요.
💡 nginx -T로 설정 순서를 확인하세요. include 파일들이 어떤 순서로 병합되는지 보면 정규표현식 우선순위를 예측할 수 있습니다.
11. Named Location - 내부 리다이렉션 전용
시작하며
여러분이 try_files나 error_page에서 복잡한 프록시 로직을 재사용하고 싶거나, 여러 곳에서 같은 폴백 처리를 공유하고 싶은 상황을 겪어본 적 있나요? 일반 Location 블록으로는 외부 요청도 받을 수 있어 보안 우려가 있고, 코드 중복도 발생합니다.
이런 문제는 내부 로직과 외부 엔드포인트가 혼재될 때 발생합니다. 폴백 처리나 에러 핸들링 같은 내부 로직을 별도로 분리하지 않으면 설정이 복잡해지고 재사용이 어렵습니다.
바로 이럴 때 필요한 것이 Named Location입니다. @로 시작하는 Named Location은 내부 리다이렉션 전용으로, 외부에서 직접 접근할 수 없어 안전하고 명확합니다.
개요
간단히 말해서, Named Location은 @로 시작하는 이름을 가진 특별한 Location 블록으로, 외부 요청으로는 접근할 수 없고 오직 try_files, error_page, rewrite 등 내부 리다이렉션을 통해서만 호출됩니다. 클라이언트가 직접 "http://example.com/@backend" 같은 요청을 보내도 매칭되지 않으므로, 내부 로직을 안전하게 캡슐화할 수 있습니다.
복잡한 프록시 설정, 에러 핸들링, 폴백 로직 등을 Named Location으로 분리하면 코드 재사용성과 가독성이 크게 향상됩니다. 예를 들어, 정적 파일이 없을 때 백엔드로 폴백하는 로직을 @backend Named Location에 정의하고, 여러 일반 Location에서 재사용할 수 있습니다.
전통적인 일반 Location과 비교하면, Named Location은 외부 노출이 없어 보안적으로 안전하고, 의도(내부 전용)가 명확합니다. Named Location의 핵심 특징은 네 가지입니다: 첫째, 이름은 @로 시작해야 하며 정규표현식이나 수정자를 사용할 수 없습니다.
둘째, 클라이언트가 직접 접근할 수 없습니다. 셋째, try_files, error_page, rewrite, post_action 같은 내부 리다이렉션 메커니즘에서만 호출됩니다.
넷째, 일반 Location처럼 proxy_pass, fastcgi_pass 등 모든 지시자를 사용할 수 있습니다. 이러한 특징들이 깔끔한 설정 구조와 재사용 가능한 로직 분리를 가능하게 합니다.
코드 예제
# 기본 패턴: 정적 파일 우선, 없으면 백엔드
location /assets {
root /var/www/static;
try_files $uri @backend;
}
location @backend {
# 외부에서 직접 접근 불가
proxy_pass http://app-server:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# 여러 곳에서 재사용
location /images {
root /var/www/cdn;
try_files $uri @backend;
}
location /videos {
root /var/www/media;
try_files $uri @backend;
}
# 에러 페이지 처리
error_page 404 = @notfound;
location @notfound {
# 커스텀 404 처리
proxy_pass http://error-handler;
proxy_set_header X-Original-URI $request_uri;
}
# 복잡한 폴백 체인
location / {
root /var/www;
try_files $uri $uri/ @app;
}
location @app {
# 앱 서버 시도
proxy_pass http://app;
proxy_intercept_errors on;
error_page 404 = @legacy;
}
location @legacy {
# 앱에도 없으면 레거시 시스템
proxy_pass http://legacy-system;
}
# 조건부 내부 리다이렉트
location /api {
if ($http_x_debug) {
error_page 418 = @debug;
return 418;
}
proxy_pass http://prod-api;
}
location @debug {
proxy_pass http://debug-api;
add_header X-Debug-Mode "enabled";
}
설명
이것이 하는 일: Named Location은 일반 Location처럼 설정 블록을 정의하지만, 외부 URI 매칭 과정에서는 완전히 무시되며, 오직 Nginx 내부 메커니즘(try_files, error_page 등)이 명시적으로 호출할 때만 실행됩니다. 첫 번째로, 정의와 호출을 살펴봅시다.
location @backend { proxy_pass http://app-server:3000; }는 "@backend"라는 이름의 내부 블록을 정의합니다. 클라이언트가 "http://example.com/@backend"를 요청해도 이 블록은 매칭되지 않습니다.
오직 try_files $uri @backend; 같은 내부 리다이렉션이 실행될 때만 호출됩니다. 그 다음으로, try_files와의 조합을 봅시다.
try_files $uri @backend;는 먼저 $uri(요청된 파일)가 존재하는지 확인하고, 없으면 @backend Named Location으로 내부 리다이렉트합니다. 예를 들어 "/assets/logo.png"가 요청되었는데 파일이 없으면, @backend가 호출되어 http://app-server:3000/assets/logo.png로 프록시됩니다.
이는 CDN과 오리진 서버 폴백 패턴에 완벽합니다. 재사용성이 핵심 장점입니다.
여러 일반 Location(/images, /videos, /assets 등)에서 동일한 @backend Named Location을 참조할 수 있습니다. 프록시 설정(헤더, 타임아웃 등)을 한 곳에서 관리하므로, 변경 사항이 모든 곳에 자동 반영됩니다.
이는 DRY(Don't Repeat Yourself) 원칙을 완벽히 구현합니다. error_page와의 조합도 강력합니다.
error_page 404 = @notfound;는 404 에러가 발생하면 @notfound Named Location으로 처리를 위임합니다. 이곳에서 커스텀 에러 페이지를 렌더링하거나, 에러 로깅 서비스로 프록시하거나, 레거시 시스템에서 재검색할 수 있습니다.
여러분이 Named Location을 활용하면 복잡한 폴백 체인을 명확하게 구현하고(가독성), 중복 코드를 제거하며(유지보수성), 내부 로직을 외부 노출로부터 보호할 수 있습니다(보안). SPA 폴백, 마이크로서비스 폴백 체인, 커스텀 에러 핸들링 등 다양한 고급 패턴을 깔끔하게 구현할 수 있습니다.
실전 팁
💡 이름은 의미 있게 지으세요. @backend, @fallback, @error_handler 같이 목적이 명확한 이름을 사용하면 6개월 후에도 이해하기 쉽습니다.
💡 복잡한 프록시 설정은 Named Location으로 분리하세요. proxy_set_header, proxy_timeout 등 긴 설정을 재사용하면 중복이 크게 줄어듭니다.
💡 error_page와 함께 커스텀 에러 처리를 구현하세요. error_page 404 = @notfound; error_page 500 502 503 = @error; 패턴으로 모든 에러를 중앙 집중식으로 처리할 수 있습니다.
💡 Named Location 내부에서 다른 Named Location을 호출할 수 있습니다. 폴백 체인을 만들 때 유용하지만, 무한 루프를 조심하세요.
💡 외부 접근 불가를 활용하여 내부 API를 보호하세요. 관리자 기능이나 내부 도구를 Named Location으로 구현하면 실수로 외부 노출될 걱정이 없습니다.
12. 실전 예제 - SPA와 API를 함께 서빙
시작하며
여러분이 React나 Vue 같은 SPA와 백엔드 API를 하나의 도메인에서 서빙해야 하는데, /api 경로는 백엔드로, 나머지는 모두 index.html로 폴백해야 하고, 정적 파일은 캐싱하고, HTTPS도 강제해야 하는 복잡한 상황을 겪어본 적 있나요? 이런 설정을 처음 할 때는 우선순위, 폴백, 리다이렉트 등이 복잡하게 얽혀 헤매기 쉽습니다.
이런 문제는 현대 웹 애플리케이션에서 매우 흔합니다. 프론트엔드와 백엔드를 분리하면서도 하나의 도메인에서 서빙하려면, Location 우선순위, try_files, 캐싱, 보안 등 여러 개념을 종합적으로 적용해야 합니다.
바로 이럴 때 필요한 것이 지금까지 배운 모든 개념을 통합하는 실전 예제입니다. 실무에서 바로 사용할 수 있는 완전한 설정을 통해 모든 개념이 어떻게 조화롭게 동작하는지 이해할 수 있습니다.
개요
간단히 말해서, 이 예제는 SPA 프론트엔드와 RESTful API 백엔드를 하나의 Nginx 서버에서 효율적으로 서빙하는 프로덕션급 설정입니다. HTTP to HTTPS 리다이렉트, API 프록시, 정적 파일 캐싱, SPA 라우팅 폴백, 보안 헤더, 압축, 로깅 등 실무에서 필요한 거의 모든 요소를 포함합니다.
Location 우선순위(완전 일치 → 우선순위 접두사 → 정규표현식 → 일반 접두사)를 활용하여 각 경로가 올바르게 처리되도록 설계되었습니다. 예를 들어, /api는 백엔드로 프록시하고, .js/.css는 1년 캐싱하며, 나머지 모든 경로는 index.html로 폴백하여 React Router가 처리하게 합니다.
전통적인 복잡한 설정과 비교하면, 이 예제는 각 개념이 명확히 분리되어 있어 이해하기 쉽고, 확장하기도 편리합니다. 이 설정의 핵심 특징은 다섯 가지입니다: 첫째, 우선순위를 명확히 활용하여 충돌을 방지합니다(완전 일치 /api, 정규표현식 정적 파일, 일반 접두사 /).
둘째, 보안을 강화합니다(HTTPS 강제, 보안 헤더, 숨김 파일 차단). 셋째, 성능을 최적화합니다(Gzip, 정적 파일 캐싱, 헬스 체크).
넷째, 개발 편의성을 제공합니다(CORS 허용, 에러 로깅). 다섯째, 유지보수가 쉽습니다(주석, 명확한 구조).
이러한 특징들이 프로덕션 환경에 바로 배포할 수 있는 안정적인 설정을 만듭니다.
코드 예제
# HTTP to HTTPS 리다이렉트
server {
listen 80;
server_name myapp.com www.myapp.com;
return 301 https://$host$request_uri;
}
# 메인 HTTPS 서버
server {
listen 443 ssl http2;
server_name myapp.com www.myapp.com;
# SSL 설정
ssl_certificate /etc/ssl/certs/myapp.crt;
ssl_certificate_key /etc/ssl/private/myapp.key;
# 보안 헤더
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
# Gzip 압축
gzip on;
gzip_types text/css application/javascript application/json;
# 헬스 체크 (최우선 - 완전 일치)
location = /health {
access_log off;
return 200 "OK\n";
}
# API 프록시 (우선순위 접두사 - 정규표현식보다 우선)
location ^~ /api/ {
proxy_pass http://backend:3000;
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;
# CORS (개발용)
add_header Access-Control-Allow-Origin *;
}
# 정적 자산 캐싱 (정규표현식)
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
root /var/www/myapp/build;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# 숨김 파일 차단