이미지 로딩 중...
AI Generated
2025. 11. 14. · 4 Views
Nginx 정적 파일 서빙 완벽 가이드
웹 서버에서 이미지, CSS, JavaScript 등 정적 파일을 효율적으로 제공하는 방법을 배웁니다. Nginx를 활용한 정적 파일 서빙 설정부터 성능 최적화까지 실무에 바로 적용할 수 있는 가이드입니다.
목차
- 기본 정적 파일 서빙 설정
- 파일 타입별 경로 분리
- MIME 타입 설정과 charset
- Gzip 압축으로 전송 속도 최적화
- 캐시 헤더로 재방문 속도 개선
- 보안 헤더 추가
- 대용량 파일 업로드 설정
- 정적 파일 디렉토리 리스팅
1. 기본 정적 파일 서빙 설정
시작하며
여러분이 웹사이트를 만들고 이미지나 CSS 파일을 올렸는데, 사용자가 페이지를 열 때마다 로딩이 느리거나 파일이 제대로 보이지 않는 경험을 해보신 적 있나요? 이는 단순히 인터넷 속도 문제가 아니라 서버 설정의 문제일 수 있습니다.
실제로 많은 초보 개발자들이 Node.js나 Python 같은 애플리케이션 서버로 정적 파일까지 처리하려다가 성능 저하를 겪습니다. 애플리케이션 서버는 동적 콘텐츠 처리에 최적화되어 있어서, 단순한 파일 전송에는 비효율적입니다.
바로 이럴 때 필요한 것이 Nginx의 정적 파일 서빙 설정입니다. Nginx는 정적 파일을 매우 빠르고 효율적으로 제공하도록 설계되어 있어, 올바르게 설정하면 웹사이트 성능이 눈에 띄게 향상됩니다.
개요
간단히 말해서, 정적 파일 서빙은 변하지 않는 파일(이미지, CSS, JavaScript, 폰트 등)을 사용자에게 빠르게 전달하는 과정입니다. 웹사이트를 운영할 때 전체 트래픽의 70-80%가 정적 파일 요청입니다.
사용자가 페이지를 열 때마다 수십 개의 이미지, 스타일시트, 스크립트가 다운로드되기 때문이죠. 이러한 파일들을 효율적으로 서빙하지 못하면 서버 자원이 낭비되고 사용자 경험이 나빠집니다.
예를 들어, 이커머스 사이트에서 상품 이미지가 느리게 로드되면 매출에 직접적인 영향을 미칩니다. 기존에는 Apache나 애플리케이션 서버가 모든 요청을 처리했다면, 이제는 Nginx가 정적 파일을 직접 제공하고 애플리케이션 서버는 동적 처리만 담당하도록 분리할 수 있습니다.
Nginx의 정적 파일 서빙은 첫째, 매우 빠른 속도로 파일을 전송하고, 둘째, 적은 메모리로 수천 개의 동시 연결을 처리하며, 셋째, 간단한 설정만으로 캐싱과 압축을 적용할 수 있습니다. 이러한 특징들이 대규모 트래픽을 처리하는 프로덕션 환경에서 필수적인 이유입니다.
코드 예제
# /etc/nginx/sites-available/mysite.conf
server {
listen 80;
server_name example.com;
# 정적 파일이 위치한 루트 디렉토리 지정
root /var/www/html;
# 인덱스 파일 우선순위 설정
index index.html index.htm;
# 모든 요청을 처리하는 location 블록
location / {
# 파일이나 디렉토리가 존재하면 제공하고, 없으면 404 반환
try_files $uri $uri/ =404;
}
}
설명
이것이 하는 일: 이 설정은 Nginx가 특정 디렉토리에서 정적 파일을 찾아 사용자에게 제공하도록 만듭니다. 사용자가 http://example.com/images/logo.png를 요청하면, Nginx는 /var/www/html/images/logo.png 파일을 찾아서 응답합니다.
첫 번째로, server 블록은 하나의 웹사이트 설정을 정의합니다. listen 80은 HTTP 기본 포트에서 요청을 받고, server_name은 이 설정이 적용될 도메인을 지정합니다.
여기서는 example.com으로 들어오는 모든 요청이 이 설정을 따르게 됩니다. 만약 여러 도메인을 운영한다면 각각 별도의 server 블록을 만들어야 합니다.
그 다음으로, root 지시어가 실행되면서 정적 파일의 기본 경로를 설정합니다. 이 경로는 절대 경로여야 하며, Nginx 프로세스가 읽기 권한을 가져야 합니다.
index 지시어는 디렉토리 요청 시 자동으로 제공할 파일을 지정합니다. 예를 들어 /about/ 요청이 오면 /var/www/html/about/index.html을 찾습니다.
마지막으로, location / 블록이 모든 URI 요청을 처리합니다. try_files $uri $uri/ =404는 세 단계로 동작합니다: 먼저 요청된 경로에 파일이 있는지 확인하고($uri), 없으면 디렉토리인지 확인하며($uri/), 둘 다 아니면 404 에러를 반환합니다(=404).
이 메커니즘 덕분에 존재하지 않는 파일 요청을 빠르게 처리할 수 있습니다. 여러분이 이 설정을 사용하면 별도의 애플리케이션 서버 없이도 HTML, CSS, JavaScript, 이미지 등을 즉시 제공할 수 있습니다.
또한 Nginx의 비동기 처리 덕분에 수천 명의 동시 사용자가 접속해도 안정적으로 서비스를 유지할 수 있으며, 서버 자원 사용량도 크게 줄일 수 있습니다.
실전 팁
💡 root 경로를 설정할 때는 반드시 절대 경로를 사용하고, 해당 디렉토리에 nginx 사용자(보통 www-data)의 읽기 권한이 있는지 확인하세요. chmod 755로 권한을 설정하는 것이 일반적입니다.
💡 try_files에서 $uri/ 옵션을 빼먹으면 디렉토리 요청 시 index 파일을 찾지 못합니다. 항상 $uri $uri/ =404 순서를 지켜주세요.
💡 설정 파일을 수정한 후에는 반드시 nginx -t로 문법 검사를 하고, 문제가 없으면 sudo systemctl reload nginx로 적용하세요. restart 대신 reload를 사용하면 서비스 중단 없이 설정을 반영할 수 있습니다.
💡 개발 환경에서는 /usr/share/nginx/html이 기본 root이지만, 프로덕션에서는 /var/www/html이나 프로젝트별 경로(/var/www/myproject/public)를 사용하는 것이 보안과 관리 측면에서 좋습니다.
💡 index 지시어에 여러 파일을 지정할 수 있습니다(index index.html index.htm index.php). Nginx는 왼쪽부터 순서대로 찾아서 첫 번째로 발견된 파일을 제공합니다.
2. 파일 타입별 경로 분리
시작하며
여러분의 프로젝트가 커지면서 이미지, CSS, JavaScript, 폰트 파일들이 뒤섞여 관리가 어려워진 경험 있으신가요? 모든 파일을 한 곳에 두면 나중에 특정 파일을 찾기도 어렵고, 캐싱이나 보안 설정을 적용할 때도 불편합니다.
실무에서는 파일 타입에 따라 다른 캐싱 정책이나 압축 설정을 적용해야 합니다. 예를 들어, 이미지는 오래 캐싱해도 되지만 HTML은 자주 업데이트될 수 있으니 캐싱 기간을 짧게 가져가야 하죠.
이를 위해서는 파일을 타입별로 구분해서 관리해야 합니다. 바로 이럴 때 필요한 것이 location 블록을 활용한 파일 타입별 경로 분리입니다.
각 파일 타입에 맞는 최적의 설정을 적용하여 성능과 관리 효율성을 동시에 높일 수 있습니다.
개요
간단히 말해서, 파일 타입별 경로 분리는 이미지는 /images, CSS는 /css, JavaScript는 /js처럼 파일 종류에 따라 다른 디렉토리와 설정을 적용하는 것입니다. 실제 프로젝트에서는 수백에서 수천 개의 정적 파일을 다루게 됩니다.
이 파일들을 타입별로 분리하면 첫째, 각 타입에 최적화된 캐싱 정책을 적용할 수 있고, 둘째, 특정 파일 타입에만 접근 제한을 걸 수 있으며, 셋째, 파일 관리와 배포가 훨씬 쉬워집니다. 예를 들어, CDN에 이미지만 올리고 싶을 때 /images 디렉토리만 동기화하면 되는 것처럼 말이죠.
기존에는 모든 파일을 한 곳에 두고 애플리케이션 레벨에서 처리했다면, 이제는 웹 서버 레벨에서 파일 타입을 구분하여 각각에 최적화된 전략을 적용할 수 있습니다. location 블록을 사용한 경로 분리는 첫째, URI 패턴 매칭으로 정확한 제어가 가능하고, 둘째, 정규표현식으로 다양한 파일 확장자를 그룹화할 수 있으며, 셋째, 각 location마다 독립적인 설정을 적용할 수 있습니다.
이는 대규모 웹사이트에서 성능 최적화의 핵심 전략입니다.
코드 예제
server {
listen 80;
server_name example.com;
root /var/www/html;
# 이미지 파일 처리
location /images/ {
alias /var/www/html/static/images/;
# 이미지는 오래 캐싱 (30일)
expires 30d;
add_header Cache-Control "public, immutable";
}
# CSS와 JavaScript 처리
location ~* \.(css|js)$ {
expires 7d;
add_header Cache-Control "public";
# 압축 전송 활성화
gzip_static on;
}
# 폰트 파일 처리 (CORS 헤더 추가)
location ~* \.(woff|woff2|ttf|otf|eot)$ {
expires 365d;
add_header Cache-Control "public, immutable";
add_header Access-Control-Allow-Origin "*";
}
}
설명
이것이 하는 일: 이 설정은 요청된 파일의 경로나 확장자에 따라 서로 다른 처리 규칙을 적용합니다. 이미지는 긴 캐싱, CSS/JS는 중간 캐싱, 폰트는 매우 긴 캐싱과 CORS 헤더를 자동으로 적용합니다.
첫 번째로, location /images/ 블록은 /images/로 시작하는 모든 요청을 처리합니다. 여기서 alias 지시어는 root와 다르게 location 경로를 완전히 대체합니다.
예를 들어 /images/logo.png 요청이 오면 /var/www/html/static/images/logo.png를 제공합니다. expires 30d는 브라우저에게 이 파일을 30일간 캐싱하라고 알려주고, immutable 플래그는 파일이 절대 변하지 않음을 보장하여 재검증 요청조차 막습니다.
그 다음으로, location ~* .(css|js)$ 블록이 CSS와 JavaScript 파일을 처리합니다. ~*는 대소문자 구분 없는 정규표현식 매칭을 의미하고, $는 파일명 끝을 나타냅니다.
따라서 style.css, app.CSS, main.js 모두 매칭됩니다. gzip_static on은 사전 압축된 .gz 파일이 있으면 그것을 우선 제공하여 CPU 사용량을 줄입니다.
이 파일들은 가끔 업데이트될 수 있으므로 7일로 캐싱 기간을 설정했습니다. 마지막으로, 폰트 파일 location 블록은 웹폰트 서빙의 특수한 요구사항을 처리합니다.
웹폰트는 CSS에서 다른 도메인의 폰트를 로드할 때 CORS 에러가 발생할 수 있어서, Access-Control-Allow-Origin "*" 헤더로 모든 도메인의 접근을 허용합니다. 폰트는 거의 변하지 않으므로 365일 캐싱을 적용합니다.
여러분이 이 설정을 사용하면 각 파일 타입에 맞는 최적의 전송 전략이 자동으로 적용됩니다. 사용자는 빠른 로딩 속도를 경험하고, 서버는 불필요한 재전송을 줄여 대역폭과 자원을 절약할 수 있습니다.
특히 모바일 사용자에게는 데이터 절약 효과도 있습니다.
실전 팁
💡 alias와 root의 차이를 명확히 이해하세요. root는 location 경로에 추가되고, alias는 완전히 대체됩니다. location /images/에 root /var/www를 쓰면 /var/www/images/가 되지만, alias /var/www/static/을 쓰면 /var/www/static/이 됩니다.
💡 정규표현식 location의 우선순위는 일반 location보다 높습니다. 구체적인 순서는: = (정확한 매칭) > ^~ (우선 prefix) > ~ (정규표현식) > (일반 prefix) 순입니다.
💡 expires 설정 시 시간 단위를 잘 선택하세요: s(초), m(분), h(시간), d(일). 이미지나 폰트는 30d365d, CSS/JS는 7d30d, HTML은 1h 이하가 일반적입니다.
💡 gzip_static을 사용하려면 배포 시 .gz 파일을 미리 생성해두어야 합니다. webpack이나 gulp로 빌드할 때 compression-webpack-plugin 같은 도구를 사용하세요.
💡 Cache-Control의 public/private 차이를 알아두세요. public은 CDN이나 프록시도 캐싱할 수 있고, private은 브라우저만 캐싱합니다. 정적 파일은 보통 public이 적합합니다.
3. MIME 타입 설정과 charset
시작하며
여러분이 UTF-8로 작성한 한글 텍스트 파일을 웹에서 열었는데 깨진 글자가 보이거나, 다운로드해야 할 PDF가 브라우저에서 이상하게 표시되는 경험을 하신 적 있나요? 이는 파일의 실제 내용과 브라우저가 인식하는 타입이 일치하지 않아서 발생합니다.
브라우저는 파일 확장자가 아니라 서버가 보내는 Content-Type 헤더를 보고 파일을 어떻게 처리할지 결정합니다. 잘못된 MIME 타입은 보안 문제를 일으킬 수도 있습니다.
예를 들어, JavaScript 파일이 text/plain으로 전송되면 실행되지 않아 웹사이트가 제대로 작동하지 않습니다. 바로 이럴 때 필요한 것이 올바른 MIME 타입과 charset 설정입니다.
각 파일 형식에 맞는 정확한 타입을 지정하여 브라우저가 콘텐츠를 올바르게 해석하고 표시하도록 만들 수 있습니다.
개요
간단히 말해서, MIME 타입은 파일의 종류를 브라우저에게 알려주는 표준화된 레이블이고, charset은 텍스트 파일의 문자 인코딩 방식을 지정합니다. 웹에서 파일을 전송할 때마다 Content-Type 헤더가 함께 전송됩니다.
이 헤더가 없거나 잘못되면 브라우저가 파일을 잘못 해석할 수 있습니다. 예를 들어, JSON API 응답이 text/html로 전송되면 JavaScript에서 파싱 에러가 발생하고, SVG 이미지가 text/plain으로 전송되면 다운로드되어 버립니다.
특히 국제화된 웹사이트에서 charset을 명시하지 않으면 한글, 일본어, 중국어 등이 깨져 보입니다. 기존에는 .htaccess나 애플리케이션 코드에서 개별적으로 Content-Type을 설정했다면, 이제는 Nginx의 mime.types 파일과 default_type으로 모든 파일 타입을 중앙에서 일관되게 관리할 수 있습니다.
MIME 타입 설정의 핵심은 첫째, Nginx가 제공하는 기본 mime.types를 활용하여 대부분의 표준 파일 형식을 자동으로 처리하고, 둘째, charset=utf-8을 추가하여 텍스트 기반 파일의 인코딩을 명확히 하며, 셋째, 커스텀 파일 타입이 있다면 types 블록에 추가하는 것입니다. 이렇게 하면 모든 정적 파일이 올바르게 전송됩니다.
코드 예제
server {
listen 80;
server_name example.com;
root /var/www/html;
# Nginx 기본 MIME 타입 파일 포함
include /etc/nginx/mime.types;
# 알 수 없는 확장자의 기본 타입 설정
default_type application/octet-stream;
# UTF-8 charset을 기본으로 설정
charset utf-8;
# 커스텀 MIME 타입 추가
types {
application/manifest+json webmanifest;
text/markdown md;
}
# 텍스트 기반 파일에 charset 명시
location ~* \.(html|css|js|json|xml)$ {
charset utf-8;
add_header Content-Type "$content_type; charset=utf-8";
}
}
설명
이것이 하는 일: 이 설정은 모든 파일에 올바른 Content-Type 헤더를 자동으로 추가하고, 텍스트 파일에는 UTF-8 인코딩을 명시하여 브라우저가 정확하게 파일을 해석하도록 합니다. 첫 번째로, include /etc/nginx/mime.types는 Nginx가 제공하는 수백 개의 MIME 타입 매핑을 불러옵니다.
이 파일에는 .html → text/html, .jpg → image/jpeg, .pdf → application/pdf 같은 매핑이 이미 정의되어 있습니다. 한 번 include하면 모든 표준 파일 형식이 자동으로 올바른 타입으로 전송됩니다.
이 파일은 Nginx 업데이트 시 최신 MIME 타입이 추가되므로 계속 최신 상태를 유지합니다. 그 다음으로, default_type application/octet-stream은 매핑되지 않은 확장자의 기본 동작을 설정합니다.
application/octet-stream은 "이진 데이터"를 의미하며, 브라우저는 이 타입을 받으면 파일을 다운로드합니다. charset utf-8은 서버 전체의 기본 문자 인코딩을 UTF-8로 설정하여, 한글을 포함한 모든 유니코드 문자가 올바르게 표시되도록 합니다.
types 블록에서는 커스텀 MIME 타입을 추가할 수 있습니다. 예를 들어, PWA(Progressive Web App)의 manifest 파일(.webmanifest)이나 마크다운 파일(.md)은 비교적 최근에 등장한 형식이라 오래된 mime.types에 없을 수 있습니다.
이런 경우 직접 추가하면 됩니다. 마지막으로, location 블록에서 텍스트 기반 파일에만 charset을 명시적으로 추가합니다.
HTML, CSS, JavaScript, JSON, XML 같은 파일은 모두 텍스트 기반이므로 인코딩 정보가 중요합니다. add_header로 Content-Type에 charset=utf-8을 붙여서 전송하면, 브라우저가 파일을 UTF-8로 해석하여 한글이나 특수문자가 깨지지 않습니다.
여러분이 이 설정을 사용하면 파일 확장자만으로도 자동으로 올바른 Content-Type이 설정되어, 브라우저 호환성 문제나 문자 인코딩 이슈를 미연에 방지할 수 있습니다. 특히 다국어 웹사이트를 운영할 때 문자 깨짐 문제를 완전히 해결할 수 있으며, 개발자는 매번 헤더를 수동으로 설정할 필요가 없어집니다.
실전 팁
💡 /etc/nginx/mime.types 파일을 직접 수정하지 마세요. Nginx 업데이트 시 덮어써질 수 있습니다. 대신 server나 http 블록 안에 types {}를 추가하여 확장하세요.
💡 default_type을 text/plain으로 설정하면 보안 위험이 있습니다. 알 수 없는 파일이 텍스트로 해석되어 악의적인 스크립트가 실행될 수 있으니 application/octet-stream을 사용하세요.
💡 charset은 server 블록에 한 번만 설정하면 모든 하위 location에 상속됩니다. 특정 location에서 다른 charset이 필요한 경우에만 개별 설정하세요.
💡 Content-Type 헤더가 중복 설정되지 않도록 주의하세요. add_header는 상속되지 않고 덮어쓰므로, charset을 location에서 설정했다면 상위 블록의 다른 헤더들도 함께 다시 선언해야 합니다.
💡 개발 도구(F12)의 Network 탭에서 Response Headers를 확인하여 Content-Type이 올바르게 전송되는지 검증하세요. 문제가 있다면 nginx -T로 실제 적용된 설정을 확인할 수 있습니다.
4. Gzip 압축으로 전송 속도 최적화
시작하며
여러분의 웹사이트가 1MB짜리 JavaScript 파일을 로드하느라 모바일 사용자들이 몇 초씩 기다리는 모습을 본 적 있나요? 파일 크기가 클수록 다운로드 시간이 길어지고, 특히 느린 네트워크에서는 사용자 이탈로 직결됩니다.
실제로 구글은 페이지 로드 시간이 3초를 넘으면 사용자의 53%가 이탈한다고 발표했습니다. 파일 크기를 줄이는 것은 성능 최적화의 가장 직접적인 방법이지만, 코드를 수정하거나 이미지를 재가공하는 것은 시간이 오래 걸립니다.
바로 이럴 때 필요한 것이 Gzip 압축입니다. 서버에서 파일을 압축해서 보내면 전송 크기가 70-90%까지 줄어들어, 같은 파일을 훨씬 빠르게 전달할 수 있습니다.
브라우저는 자동으로 압축을 풀어서 사용하므로 사용자 경험은 그대로 유지됩니다.
개요
간단히 말해서, Gzip 압축은 텍스트 기반 파일(HTML, CSS, JavaScript, JSON 등)을 서버에서 압축하여 전송하고, 브라우저가 받아서 압축을 푸는 과정입니다. 텍스트 파일은 반복되는 패턴이 많아서 압축률이 매우 높습니다.
예를 들어, 500KB의 JavaScript 파일은 Gzip으로 압축하면 평균 100-150KB로 줄어듭니다. 이는 네트워크 대역폭을 70-80% 절약하는 것이며, 특히 모바일이나 저속 인터넷 환경에서 체감 속도 향상이 큽니다.
예를 들어, 전자상거래 사이트에서 상품 페이지 로드 시간을 2초에서 0.5초로 단축할 수 있습니다. 기존에는 파일을 압축하지 않고 원본 그대로 전송하거나, 애플리케이션 레벨에서 압축을 처리했다면, 이제는 Nginx가 자동으로 압축하여 전송하므로 애플리케이션 부담이 없습니다.
Gzip 압축의 핵심 특징은 첫째, CPU 사용량이 적으면서도 압축률이 높고(특히 텍스트), 둘째, 모든 주요 브라우저가 지원하며(IE6 이후), 셋째, 설정만으로 즉시 적용되어 코드 수정이 필요 없다는 점입니다. 이는 가장 쉽고 효과적인 성능 최적화 방법 중 하나입니다.
코드 예제
server {
listen 80;
server_name example.com;
root /var/www/html;
# Gzip 압축 활성화
gzip on;
# 압축 레벨 설정 (1-9, 6이 균형잡힌 수준)
gzip_comp_level 6;
# 압축할 MIME 타입 지정
gzip_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/xml
text/xml
image/svg+xml;
# 최소 압축 파일 크기 (1KB 미만은 압축 안 함)
gzip_min_length 1024;
# 프록시된 요청도 압축
gzip_proxied any;
# Vary: Accept-Encoding 헤더 추가
gzip_vary on;
}
설명
이것이 하는 일: 이 설정은 텍스트 기반 파일을 요청할 때 Nginx가 자동으로 Gzip으로 압축하여 전송합니다. 브라우저가 Accept-Encoding: gzip 헤더를 보내면, Nginx는 파일을 압축해서 Content-Encoding: gzip 헤더와 함께 응답합니다.
첫 번째로, gzip on은 Gzip 압축 기능을 활성화합니다. 이것만으로도 기본적인 압축이 시작되지만, 세부 설정 없이는 최적의 효과를 얻기 어렵습니다.
gzip_comp_level 6은 압축 강도를 설정하는데, 1은 가장 빠르지만 압축률이 낮고, 9는 최대 압축이지만 CPU를 많이 씁니다. 6은 압축률과 CPU 사용량의 균형점으로, 대부분의 프로덕션 환경에서 권장됩니다.
실제 테스트 결과 5-6 사이에서 압축률 차이는 1-2%에 불과하지만 CPU 사용량은 크게 차이납니다. 그 다음으로, gzip_types는 어떤 파일을 압축할지 지정합니다.
기본적으로 text/html만 압축되므로, CSS, JavaScript, JSON 등을 추가해야 합니다. 주의할 점은 이미지(JPEG, PNG, GIF)나 이미 압축된 파일(ZIP, GZIP)은 압축 효과가 거의 없고 오히려 CPU만 낭비하므로 포함하지 않습니다.
SVG는 텍스트 기반 XML이라 압축 효과가 크므로 꼭 포함하세요. gzip_min_length 1024는 1KB 미만의 작은 파일은 압축하지 않습니다.
작은 파일은 압축해도 크기 절감이 미미하고, 압축/해제 오버헤드가 더 클 수 있기 때문입니다. gzip_proxied any는 프록시나 CDN을 거친 요청도 압축하도록 하며, gzip_vary on은 Vary: Accept-Encoding 헤더를 추가하여 캐시 서버가 압축 버전과 비압축 버전을 구분하여 저장하도록 합니다.
여러분이 이 설정을 사용하면 즉시 대역폭 사용량이 크게 줄어듭니다. 500KB JavaScript 번들이 100KB로 줄어들면 네트워크 전송 시간이 5분의 1이 되고, 이는 특히 모바일 사용자에게 큰 차이를 만듭니다.
또한 서버의 송신 대역폭 비용도 절감되어 트래픽이 많은 사이트에서는 비용 절감 효과도 있습니다.
실전 팁
💡 gzip_comp_level을 9로 설정하면 압축률은 조금 좋아지지만 CPU 사용량이 급증합니다. 실제 서비스에서는 5-6이 최적이며, 테스트해보면 6과 9의 압축률 차이는 1-2%에 불과합니다.
💡 gzip_static on을 추가하면 .gz 파일을 미리 만들어두고 실시간 압축 없이 바로 전송할 수 있습니다. 빌드 타임에 webpack-compression-plugin으로 생성하면 서버 CPU를 아낄 수 있습니다.
💡 gzip_types에 image/jpeg, image/png를 추가하지 마세요. 이미 압축된 포맷이라 효과가 없고 CPU만 낭비됩니다. 대신 image/svg+xml은 반드시 포함하세요.
💡 브라우저 개발자 도구(F12)의 Network 탭에서 Size와 Transferred를 비교하면 압축 효과를 확인할 수 있습니다. Transferred가 Size보다 훨씬 작으면 압축이 잘 적용된 것입니다.
💡 CDN을 사용한다면 gzip_vary on이 필수입니다. 이 설정이 없으면 압축을 지원하지 않는 구형 브라우저가 압축된 파일을 받아서 깨진 페이지를 보게 될 수 있습니다.
5. 캐시 헤더로 재방문 속도 개선
시작하며
여러분의 웹사이트를 방문한 사용자가 로고 이미지나 CSS 파일을 페이지를 열 때마다 다시 다운로드하고 있다면, 그것은 엄청난 낭비입니다. 특히 변하지 않는 파일을 매번 새로 받는 것은 사용자와 서버 모두에게 부담입니다.
실제로 한 번 방문한 사용자가 재방문할 때 모든 정적 파일을 브라우저 캐시에서 불러온다면 페이지 로드 시간이 70-90% 단축됩니다. 하지만 잘못된 캐싱 설정은 파일이 업데이트되어도 사용자가 옛날 버전을 보는 문제를 일으킬 수 있습니다.
바로 이럴 때 필요한 것이 올바른 캐시 헤더 설정입니다. 파일 종류에 따라 적절한 캐싱 기간을 설정하고, 업데이트 전략을 함께 고려하면 성능과 신선도를 모두 확보할 수 있습니다.
개요
간단히 말해서, 캐시 헤더는 브라우저에게 "이 파일을 얼마나 오래 저장해두고 재사용해도 되는지"를 알려주는 HTTP 헤더입니다. 브라우저 캐싱은 웹 성능 최적화의 핵심입니다.
사용자가 페이지를 두 번째 방문할 때 캐시된 파일을 사용하면 네트워크 요청 자체가 사라지므로 로딩이 거의 즉시 완료됩니다. 예를 들어, 뉴스 사이트의 로고나 공통 CSS는 매일 바뀌지 않으므로 한 달간 캐싱해도 문제없지만, 기사 내용 HTML은 자주 업데이트되므로 짧게 캐싱하거나 아예 캐싱하지 않아야 합니다.
올바른 캐싱 전략은 서버 부하를 50% 이상 줄일 수 있습니다. 기존에는 Expires 헤더로 절대 시간을 지정했다면, 이제는 Cache-Control로 상대 시간과 세밀한 캐싱 정책을 함께 설정할 수 있습니다.
캐시 헤더 설정의 핵심은 첫째, 파일의 변경 빈도에 따라 캐싱 기간을 차등 적용하고(이미지 30일, CSS/JS 7일, HTML 1시간), 둘째, immutable 플래그로 불필요한 재검증을 방지하며, 셋째, ETag나 Last-Modified로 파일 변경 감지를 자동화하는 것입니다. 이 세 가지가 조화를 이루면 최적의 캐싱 전략이 완성됩니다.
코드 예제
server {
listen 80;
server_name example.com;
root /var/www/html;
# 이미지, 폰트 등 거의 변하지 않는 파일 - 장기 캐싱
location ~* \.(jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
# CSS, JavaScript - 중기 캐싱 (버전관리 권장)
location ~* \.(css|js)$ {
expires 7d;
add_header Cache-Control "public";
# ETag 활성화로 파일 변경 감지
etag on;
}
# HTML - 짧은 캐싱 또는 무효화
location ~* \.html$ {
expires 1h;
add_header Cache-Control "public, must-revalidate";
}
# API 응답이나 동적 콘텐츠 - 캐싱 안 함
location /api/ {
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate";
add_header Pragma "no-cache";
}
}
설명
이것이 하는 일: 이 설정은 파일 타입별로 브라우저 캐싱 기간을 다르게 지정하여, 변하지 않는 파일은 오래 저장하고 자주 바뀌는 파일은 최신 상태를 유지하도록 합니다. 첫 번째로, 이미지와 폰트 파일에 대한 location 블록은 30일간 캐싱을 설정합니다.
expires 30d는 현재 시점부터 30일 후를 만료 시간으로 설정하며, Cache-Control: public은 CDN이나 프록시도 캐싱할 수 있도록 하고, immutable은 "이 파일은 절대 변하지 않으니 만료 전에는 재검증하지 마라"는 의미입니다. 이렇게 하면 사용자가 페이지를 새로고침해도 서버에 재검증 요청(304 Not Modified)조차 보내지 않습니다.
그 다음으로, CSS와 JavaScript 블록은 7일 캐싱을 적용합니다. 이 파일들은 업데이트가 좀 더 빈번하므로 기간을 짧게 잡았습니다.
immutable 대신 일반 public만 사용하여, 만료 전이라도 사용자가 강제 새로고침(Ctrl+F5)하면 새 파일을 받을 수 있게 합니다. etag on은 파일의 해시값을 ETag 헤더로 전송하여, 브라우저가 "이 파일 바뀌었나요?" 요청을 보낼 때 서버가 빠르게 판단할 수 있게 합니다.
HTML 파일은 1시간만 캐싱하고 must-revalidate를 추가합니다. 이는 만료 후 반드시 서버에 재검증하라는 의미입니다.
HTML은 콘텐츠의 진입점이므로 최신 상태를 유지하는 것이 중요합니다. 너무 길게 캐싱하면 업데이트된 내용을 사용자가 보지 못할 수 있습니다.
마지막으로, API 엔드포인트는 캐싱을 완전히 비활성화합니다. expires -1은 과거 시간을 의미하여 즉시 만료시키고, no-store는 아예 저장하지 말라는 뜻이며, no-cache는 저장해도 되지만 매번 재검증하라는 의미입니다.
Pragma: no-cache는 HTTP/1.0 호환을 위한 헤더입니다. 동적 데이터는 실시간성이 중요하므로 캐싱하면 안 됩니다.
여러분이 이 설정을 사용하면 재방문 사용자의 페이지 로드 속도가 극적으로 빨라집니다. 1MB의 리소스가 캐시에서 로드되면 네트워크 시간이 0이 되어, 3G 환경에서도 거의 즉시 페이지가 열립니다.
또한 서버는 정적 파일 요청의 80-90%를 처리하지 않아도 되어 더 많은 사용자를 동시에 서비스할 수 있습니다.
실전 팁
💡 CSS/JS 파일명에 버전이나 해시를 포함하세요(app.a3f2b1.js). 그러면 파일명이 바뀌므로 캐시를 무시하고 새 파일을 받습니다. Webpack의 [contenthash]를 사용하면 자동으로 생성됩니다.
💡 immutable은 비교적 최신 기능(2017년)이라 오래된 브라우저는 무시하지만, Chrome 65+, Firefox 49+에서는 불필요한 재검증을 완전히 제거하여 성능이 크게 향상됩니다.
💡 no-cache와 no-store의 차이를 혼동하지 마세요. no-cache는 캐시하되 매번 검증하고, no-store는 아예 저장하지 않습니다. 민감한 정보는 no-store를 사용하세요.
💡 CDN을 사용한다면 Cache-Control: public이 필수입니다. private으로 설정하면 CDN이 캐싱하지 못해 CDN의 이점을 전혀 누리지 못합니다.
💡 개발 중에는 캐싱 때문에 변경사항이 반영되지 않는 것처럼 보일 수 있습니다. 브라우저에서 "캐시 비우기 및 강력 새로고침"(Ctrl+Shift+R)을 사용하거나, 개발 환경에서는 expires를 짧게 설정하세요.
6. 보안 헤더 추가
시작하며
여러분이 만든 웹사이트가 XSS 공격이나 클릭재킹 같은 보안 위협에 노출되어 있다면, 사용자 정보가 유출되거나 악의적인 스크립트가 실행될 수 있습니다. 특히 사용자가 입력한 데이터를 표시하는 웹사이트는 더욱 취약합니다.
실무에서는 OWASP Top 10에 포함된 보안 취약점들을 방어하기 위해 여러 계층의 보안 장치가 필요합니다. 애플리케이션 코드만으로는 모든 공격을 막기 어렵고, 특히 브라우저가 제공하는 보안 메커니즘을 활용하지 않으면 쉽게 뚫릴 수 있습니다.
바로 이럴 때 필요한 것이 보안 HTTP 헤더입니다. Nginx에서 몇 가지 헤더만 추가하면 브라우저가 자동으로 다양한 공격을 차단하고, 보안 정책을 강제할 수 있습니다.
개요
간단히 말해서, 보안 헤더는 브라우저에게 "이 사이트에서는 이런 보안 정책을 따르라"고 지시하는 HTTP 헤더들입니다. 현대 브라우저는 XSS(Cross-Site Scripting), 클릭재킹(Clickjacking), MIME 스니핑 공격 등을 방어할 수 있는 기능을 내장하고 있습니다.
하지만 이 기능들은 서버가 적절한 헤더를 보내야만 활성화됩니다. 예를 들어, Content-Security-Policy 헤더로 허용된 스크립트 출처를 제한하면 해커가 악의적인 스크립트를 삽입해도 브라우저가 실행을 거부합니다.
금융이나 전자상거래 사이트에서는 이러한 보안 헤더가 필수적이며, 없으면 규제 요구사항을 충족하지 못할 수도 있습니다. 기존에는 보안이 애플리케이션 코드에만 의존했다면, 이제는 웹 서버 레벨에서 브라우저 보안 기능을 활성화하여 다중 방어선을 구축할 수 있습니다.
보안 헤더의 핵심은 첫째, X-Frame-Options로 클릭재킹을 방지하고, 둘째, X-Content-Type-Options로 MIME 스니핑을 차단하며, 셋째, Content-Security-Policy로 허용된 리소스만 로드하도록 제한하고, 넷째, Strict-Transport-Security로 HTTPS를 강제하는 것입니다. 이 네 가지만 잘 설정해도 대부분의 일반적인 웹 공격을 막을 수 있습니다.
코드 예제
server {
listen 443 ssl;
server_name example.com;
root /var/www/html;
# 클릭재킹 방지 (iframe 삽입 차단)
add_header X-Frame-Options "SAMEORIGIN" always;
# MIME 타입 스니핑 방지
add_header X-Content-Type-Options "nosniff" always;
# XSS 필터 활성화 (구형 브라우저용)
add_header X-XSS-Protection "1; mode=block" always;
# Content Security Policy - 스크립트 출처 제한
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://trusted-cdn.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;" always;
# HTTPS 강제 (HSTS)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Referrer 정보 제어
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# 권한 정책 (기능 접근 제어)
add_header Permissions-Policy "geolocation=(self), microphone=(), camera=()" always;
}
설명
이것이 하는 일: 이 설정은 다양한 보안 헤더를 추가하여 브라우저가 악의적인 공격을 자동으로 차단하도록 만듭니다. 각 헤더는 특정 유형의 공격을 방어하는 역할을 합니다.
첫 번째로, X-Frame-Options: SAMEORIGIN은 클릭재킹 공격을 방지합니다. 클릭재킹은 공격자가 여러분의 웹사이트를 투명한 iframe으로 악성 사이트에 삽입하여, 사용자가 의도하지 않은 행동을 하도록 속이는 공격입니다.
SAMEORIGIN은 같은 도메인의 페이지만 iframe으로 삽입할 수 있게 제한합니다. 만약 iframe을 전혀 허용하지 않으려면 DENY를 사용하세요.
X-Content-Type-Options: nosniff는 브라우저가 Content-Type 헤더를 무시하고 파일 내용을 추측하는 것을 막습니다. 예를 들어, 공격자가 이미지 파일로 위장한 HTML을 업로드하면, 오래된 IE는 이것을 HTML로 해석하여 스크립트를 실행할 수 있습니다.
nosniff는 이를 차단합니다. X-XSS-Protection은 구형 브라우저(IE, Safari)의 XSS 필터를 활성화하지만, 최신 브라우저는 CSP를 우선하므로 보조적 역할입니다.
그 다음으로, Content-Security-Policy(CSP)는 가장 강력한 보안 헤더입니다. default-src 'self'는 모든 리소스를 같은 도메인에서만 로드하도록 제한하고, script-src는 JavaScript를 어디서 로드할지 지정합니다.
'self'는 자기 도메인, https://trusted-cdn.com은 신뢰하는 CDN입니다. style-src에서 'unsafe-inline'은 인라인 스타일을 허용하는데, 보안상 좋지 않지만 기존 코드와의 호환성 때문에 자주 사용됩니다.
img-src는 이미지 출처를 제한하며, data:는 Base64 인코딩 이미지를 허용합니다. Strict-Transport-Security(HSTS)는 브라우저가 항상 HTTPS로만 접속하도록 강제합니다.
max-age=31536000은 1년간 이 정책을 기억하고, includeSubDomains는 모든 서브도메인에도 적용하며, preload는 브라우저의 HSTS 사전로드 목록에 등록할 수 있게 합니다. 이렇게 하면 중간자 공격(MITM)으로 HTTP로 다운그레이드하는 공격을 원천 차단합니다.
마지막으로, Referrer-Policy는 다른 사이트로 이동할 때 Referer 헤더에 어떤 정보를 포함할지 제어합니다. strict-origin-when-cross-origin은 같은 도메인 내에서는 전체 URL을, 다른 도메인으로는 도메인만 보냅니다.
Permissions-Policy는 지리정보, 마이크, 카메라 등 브라우저 기능 접근을 제어합니다. 여러분이 이 설정을 사용하면 웹사이트가 OWASP Top 10의 여러 취약점에 대해 방어력을 갖추게 됩니다.
특히 사용자 입력을 받는 사이트에서는 XSS 공격의 위험을 크게 줄일 수 있으며, 금융이나 개인정보를 다루는 서비스에서는 규제 요구사항을 충족하는 데 도움이 됩니다.
실전 팁
💡 CSP는 처음에는 Content-Security-Policy-Report-Only 헤더로 테스트하세요. 위반 사항을 보고만 하고 차단하지 않아서, 기존 사이트에 적용 시 문제를 미리 파악할 수 있습니다.
💡 HSTS를 활성화하기 전에 HTTPS 설정이 완벽한지 확인하세요. HSTS 활성화 후에는 인증서 오류가 있어도 사용자가 우회할 수 없어서, 잘못 설정하면 사이트 접속이 완전히 차단됩니다.
💡 add_header의 always 플래그는 매우 중요합니다. 없으면 4xx/5xx 에러 응답에는 헤더가 포함되지 않아, 에러 페이지에서 보안이 약해질 수 있습니다.
💡 CSP 위반 로그를 수집하려면 report-uri 디렉티브를 추가하세요. 공격 시도나 잘못된 설정을 실시간으로 모니터링할 수 있습니다.
💡 SecurityHeaders.com에서 여러분의 사이트를 테스트하면 누락된 보안 헤더와 개선 방안을 확인할 수 있습니다. A+ 등급을 목표로 하세요.
7. 대용량 파일 업로드 설정
시작하며
여러분이 파일 공유 서비스나 이미지 업로드 기능을 만들었는데, 사용자가 5MB 이상의 파일을 올리려고 하면 "413 Request Entity Too Large" 에러가 나는 경험을 해보셨나요? 이는 Nginx의 기본 업로드 크기 제한 때문입니다.
실제 서비스에서는 고화질 이미지, 동영상, PDF 문서 등 큰 파일을 업로드받아야 하는 경우가 많습니다. 하지만 기본 설정의 1MB 제한으로는 대부분의 실무 요구사항을 충족할 수 없고, 사용자들은 업로드가 실패하는 좌절감을 겪게 됩니다.
바로 이럴 때 필요한 것이 대용량 파일 업로드 설정입니다. 업로드 크기 제한을 늘리고, 타임아웃을 조정하며, 임시 파일 저장 경로를 최적화하여 안정적인 파일 업로드를 구현할 수 있습니다.
개요
간단히 말해서, 대용량 파일 업로드 설정은 Nginx가 받을 수 있는 요청 본문의 최대 크기를 늘리고, 업로드 중 타임아웃이 발생하지 않도록 조정하는 것입니다. 파일 업로드는 일반적인 GET 요청과 달리 POST 요청의 본문에 대량의 데이터를 포함합니다.
Nginx는 기본적으로 1MB 이상의 요청을 거부하는데, 이는 DoS 공격을 방지하기 위한 안전 장치입니다. 하지만 합법적인 파일 업로드를 처리하려면 이 제한을 늘려야 합니다.
예를 들어, 클라우드 스토리지 서비스에서는 100MB~2GB까지도 허용해야 하고, 동영상 플랫폼은 수 GB급 파일도 받아야 합니다. 제한을 늘릴 때는 보안과 디스크 용량도 함께 고려해야 합니다.
기존에는 애플리케이션 서버가 직접 업로드를 처리했다면, 이제는 Nginx가 먼저 파일을 받아서 임시 저장하고, 완전히 받은 후 애플리케이션에 전달하여 효율성을 높일 수 있습니다. 대용량 업로드 설정의 핵심은 첫째, client_max_body_size로 최대 크기를 설정하고, 둘째, client_body_timeout으로 타임아웃을 늘리며, 셋째, client_body_temp_path로 임시 파일 경로를 최적화하고, 넷째, 프록시 타임아웃도 함께 조정하는 것입니다.
이 네 가지가 균형을 이루어야 대용량 파일을 안정적으로 받을 수 있습니다.
코드 예제
http {
# 업로드 파일 최대 크기 (전역 설정)
client_max_body_size 100M;
# 클라이언트 본문 수신 타임아웃 (60초)
client_body_timeout 60s;
# 임시 파일 저장 경로 및 레벨
client_body_temp_path /var/nginx/client_body_temp 1 2;
# 버퍼 크기 설정
client_body_buffer_size 128k;
}
server {
listen 80;
server_name example.com;
# 특정 경로만 더 큰 파일 허용
location /upload {
client_max_body_size 500M;
client_body_timeout 120s;
# 애플리케이션 서버로 프록시
proxy_pass http://localhost:3000;
proxy_request_buffering off; # 스트리밍 업로드
proxy_read_timeout 120s;
proxy_send_timeout 120s;
}
# 일반 페이지는 기본 제한 유지
location / {
root /var/www/html;
}
}
설명
이것이 하는 일: 이 설정은 Nginx가 큰 파일을 업로드받을 때 거부하지 않고, 충분한 시간과 공간을 할당하여 안정적으로 처리하도록 만듭니다. 첫 번째로, http 블록에서 전역 설정을 합니다.
client_max_body_size 100M은 모든 가상 호스트에서 기본적으로 100MB까지 업로드를 허용합니다. 이 값은 서비스 요구사항에 맞춰 조정해야 하며, 너무 크게 설정하면 악의적인 사용자가 큰 파일로 서버를 공격할 수 있으니 주의하세요.
client_body_timeout 60s는 클라이언트가 요청 본문을 보내는 동안 60초 이상 멈추면 연결을 끊습니다. 느린 네트워크에서 큰 파일을 올릴 때는 이 값을 늘려야 합니다.
client_body_temp_path는 업로드 중인 파일을 임시 저장할 디렉토리를 지정합니다. 1 2는 디렉토리 레벨을 의미하는데, 파일이 많을 때 한 디렉토리에 수천 개가 쌓이는 것을 방지하기 위해 해시 기반 서브디렉토리를 만듭니다.
예를 들어 temp_file_12345는 /var/nginx/client_body_temp/5/34/temp_file_12345에 저장됩니다. client_body_buffer_size 128k는 메모리에 버퍼링할 크기로, 이보다 큰 파일은 디스크에 씁니다.
그 다음으로, server 블록에서 경로별로 다른 제한을 적용합니다. /upload 경로는 파일 업로드 전용이므로 500MB까지 허용하고 타임아웃도 120초로 늘립니다.
반면 일반 페이지는 기본 100MB 제한을 유지하여 불필요한 위험을 줄입니다. 이처럼 경로별로 차등 적용하는 것이 보안과 유연성 측면에서 좋습니다.
proxy_request_buffering off는 매우 중요한 설정입니다. 기본적으로 Nginx는 전체 요청을 받을 때까지 기다린 후 백엔드로 전달하는데, 큰 파일은 이렇게 하면 메모리나 디스크를 많이 사용합니다.
off로 설정하면 받는 즉시 백엔드로 스트리밍하여 자원을 절약합니다. proxy_read_timeout과 proxy_send_timeout도 업로드에 맞게 늘려서, 백엔드 처리 중 타임아웃이 발생하지 않도록 합니다.
여러분이 이 설정을 사용하면 사용자가 대용량 파일을 업로드할 때 에러 없이 안정적으로 완료할 수 있습니다. 특히 느린 네트워크 환경이나 모바일 사용자도 타임아웃 없이 업로드할 수 있으며, 서버는 메모리 효율적으로 파일을 처리할 수 있습니다.
이는 파일 공유, 클라우드 스토리지, 미디어 플랫폼 등에서 필수적인 설정입니다.
실전 팁
💡 client_max_body_size를 무제한으로 하려면 0을 설정하세요. 하지만 보안상 매우 위험하므로 특별한 이유가 없다면 절대 권장하지 않습니다. 합리적인 제한을 두세요.
💡 client_body_temp_path 디렉토리는 충분한 디스크 공간이 있는 곳을 선택하세요. 기본 /var는 작은 파티션일 수 있으니, df -h로 확인하고 큰 파티션(예: /data)을 사용하세요.
💡 proxy_request_buffering off를 사용할 때는 백엔드 애플리케이션이 스트리밍 업로드를 지원하는지 확인하세요. Node.js의 Express나 Python의 Flask는 기본 지원하지만, 일부 프레임워크는 추가 설정이 필요합니다.
💡 대용량 업로드를 지원할 때는 진행률 표시 UI를 추가하세요. JavaScript의 XMLHttpRequest나 Fetch API의 progress 이벤트로 사용자에게 업로드 상태를 보여주면 이탈을 줄일 수 있습니다.
💡 업로드된 파일의 바이러스 검사를 고려하세요. ClamAV 같은 도구를 백엔드에 통합하여 악성 파일 업로드를 차단할 수 있습니다. 특히 공개 서비스라면 필수입니다.
8. 정적 파일 디렉토리 리스팅
시작하며
여러분이 파일 공유 서버나 미디어 라이브러리를 만들 때, 특정 디렉토리의 파일 목록을 사용자에게 보여주고 싶은 경우가 있습니다. 예를 들어, 다운로드 센터나 문서 아카이브처럼 파일 브라우징이 필요한 경우죠.
기본적으로 Nginx는 디렉토리 요청 시 index 파일을 찾고, 없으면 403 Forbidden을 반환합니다. 하지만 때로는 Apache처럼 디렉토리 내용을 자동으로 HTML 목록으로 보여주는 것이 편리할 때가 있습니다.
물론 보안에 주의해야 합니다. 바로 이럴 때 필요한 것이 autoindex 기능입니다.
특정 디렉토리에서만 선택적으로 활성화하여 파일 브라우징을 제공하면서도, 민감한 경로는 보호할 수 있습니다.
개요
간단히 말해서, 디렉토리 리스팅은 사용자가 디렉토리 URL을 요청할 때 그 안의 파일과 하위 디렉토리 목록을 HTML 페이지로 보여주는 기능입니다. 파일 서버나 내부 문서 공유 시스템에서는 사용자가 파일을 직접 탐색할 수 있어야 합니다.
index.html을 일일이 만들어서 링크를 관리하는 것은 비효율적이고, 파일이 추가될 때마다 업데이트해야 하는 번거로움이 있습니다. autoindex를 사용하면 Nginx가 자동으로 디렉토리 내용을 스캔하여 보기 좋은 목록을 생성합니다.
예를 들어, 소프트웨어 다운로드 미러 사이트나 회사 내부 자료실에서 유용하게 쓰입니다. 하지만 잘못 설정하면 시스템 파일이나 소스 코드가 노출될 수 있으니 반드시 특정 경로에만 제한해야 합니다.
기존에는 별도의 파일 브라우저 애플리케이션을 설치했다면, 이제는 Nginx 내장 기능으로 간단하게 구현할 수 있습니다. autoindex 기능의 핵심은 첫째, autoindex on으로 기능을 활성화하고, 둘째, autoindex_exact_size와 autoindex_localtime으로 표시 형식을 사용자 친화적으로 만들며, 셋째, 접근 제어로 공개 범위를 제한하는 것입니다.
이 세 가지를 잘 조합하면 안전하고 편리한 파일 브라우저를 만들 수 있습니다.
코드 예제
server {
listen 80;
server_name files.example.com;
# 공개 다운로드 디렉토리
location /downloads/ {
alias /var/www/downloads/;
# 디렉토리 리스팅 활성화
autoindex on;
# 파일 크기를 읽기 쉬운 형식으로 (MB, KB 등)
autoindex_exact_size off;
# 서버의 로컬 시간대로 표시
autoindex_localtime on;
# 목록 형식 (html, json, xml, jsonp)
autoindex_format html;
# index 파일이 없어도 에러 안 남
index index.html;
}
# 내부 문서 - IP 제한
location /docs/ {
alias /var/www/internal_docs/;
autoindex on;
autoindex_exact_size off;
# 회사 내부 IP만 허용
allow 192.168.1.0/24;
allow 10.0.0.0/8;
deny all;
}
# 민감한 디렉토리는 리스팅 비활성화
location /config/ {
alias /var/www/config/;
autoindex off; # 명시적으로 비활성화
deny all;
}
}
설명
이것이 하는 일: 이 설정은 사용자가 디렉토리 URL을 방문할 때 그 안의 파일 목록을 자동으로 생성하여 보여주되, 보안이 필요한 경로는 접근을 제한합니다. 첫 번째로, /downloads/ location은 공개 파일 저장소입니다.
autoindex on이 핵심 설정으로, 이것만으로도 기본 디렉토리 리스팅이 활성화됩니다. autoindex_exact_size off는 파일 크기를 바이트 단위 대신 1.5M, 234K처럼 읽기 쉬운 단위로 표시합니다.
on이면 1574328 같은 정확한 바이트 수를 보여주지만 사용자에게는 불편합니다. autoindex_localtime on은 파일 수정 시간을 GMT 대신 서버의 로컬 시간대로 보여줍니다.
autoindex_format html은 목록을 어떤 형식으로 출력할지 지정합니다. html은 브라우저에서 보기 좋은 기본 형식이고, json은 API로 사용하거나 JavaScript로 커스텀 UI를 만들 때 유용합니다.
xml과 jsonp도 지원합니다. index index.html은 index.html 파일이 있으면 그것을 우선 보여주고, 없으면 autoindex가 작동하도록 합니다.
그 다음으로, /docs/ location은 내부 문서 디렉토리로, IP 기반 접근 제어를 추가했습니다. allow 192.168.1.0/24는 192.168.1.0~192.168.1.255 범위의 사설 IP만 허용하고, allow 10.0.0.0/8은 10.x.x.x 대역(회사 VPN)을 허용합니다.
마지막 deny all은 이외의 모든 IP를 차단합니다. allow/deny는 순서대로 평가되며, 매칭되는 첫 규칙이 적용됩니다.
마지막으로, /config/ location은 설정 파일 디렉토리로, autoindex off로 명시적으로 리스팅을 비활성화하고 deny all로 모든 접근을 차단합니다. 설정 파일에 데이터베이스 비밀번호나 API 키가 있을 수 있으므로 절대 공개하면 안 됩니다.
autoindex의 기본값은 off이므로 생략해도 되지만, 명시적으로 쓰는 것이 의도를 분명히 합니다. 여러분이 이 설정을 사용하면 파일 공유 서비스를 쉽게 만들 수 있습니다.
사용자는 웹 브라우저로 파일을 탐색하고 다운로드할 수 있으며, 관리자는 파일을 디렉토리에 추가하기만 하면 자동으로 목록에 나타납니다. 특히 소프트웨어 릴리즈, 문서 아카이브, 미디어 라이브러리 같은 용도에 적합합니다.
하지만 접근 제어를 반드시 함께 설정하여 민감한 파일이 노출되지 않도록 주의하세요.
실전 팁
💡 autoindex로 생성된 HTML 디자인이 마음에 들지 않으면, autoindex_format json으로 설정하고 JavaScript로 커스텀 UI를 만드세요. Fetch API로 목록을 가져와 예쁜 테이블이나 카드 UI로 표시할 수 있습니다.
💡 대용량 디렉토리(수천 개 파일)에서 autoindex를 사용하면 목록 생성이 느릴 수 있습니다. 이런 경우 별도의 파일 브라우저 애플리케이션(예: Filebrowser)을 고려하세요.
💡 allow/deny 규칙은 위에서 아래로 평가되며, 첫 번째 매칭이 적용됩니다. 순서가 중요하므로 더 구체적인 규칙을 위에 쓰세요. deny all을 맨 위에 쓰면 아래 allow가 무시됩니다.
💡 autoindex가 활성화된 경로에 .htaccess, .git, .env 같은 숨김 파일이 있는지 확인하세요. location ~ /. { deny all; } 규칙을 추가하여 점으로 시작하는 모든 파일 접근을 차단하는 것이 안전합니다.
💡 nginx -V로 컴파일 옵션을 확인하면 --with-http_autoindex_module이 포함되어 있는지 볼 수 있습니다. 대부분의 배포판에는 기본 포함되지만, 커스텀 빌드에서는 확인이 필요합니다.