이미지 로딩 중...
AI Generated
2025. 11. 8. · 3 Views
Next.js 실전 운영 3편 - Nginx 리버스 프록시와 로드 밸런싱 설정
Next.js 애플리케이션을 실제 운영 환경에서 배포할 때 필수적인 Nginx 설정 방법을 다룹니다. 리버스 프록시 설정부터 로드 밸런싱, SSL 인증서 적용, 그리고 정적 파일 최적화까지 실무에서 바로 적용할 수 있는 완벽한 가이드를 제공합니다.
목차
- Nginx 리버스 프록시 기본 설정
- 업스트림 서버 설정
- 로드 밸런싱 전략
- 헬스 체크 설정
- 정적 파일 캐싱
- Gzip 압축 설정
- SSL/HTTPS 설정
- 프록시 헤더 설정
- 커넥션 풀 최적화
- 에러 페이지 커스터마이징
1. Nginx 리버스 프록시 기본 설정
시작하며
여러분이 Next.js 애플리케이션을 개발 서버(localhost:3000)에서 실제 도메인(www.example.com)으로 배포하려고 할 때, 어떻게 해야 할지 막막했던 경험 있으신가요? 3000번 포트를 직접 노출시키는 건 보안상 위험하고, 80번 포트로 직접 실행하려니 권한 문제가 발생합니다.
이런 문제는 실무에서 Next.js를 배포할 때 가장 먼저 마주치는 과제입니다. 단순히 npm run start로 애플리케이션을 실행하는 것만으로는 부족하며, 외부 요청을 안전하게 받아서 내부 애플리케이션으로 전달하는 중간 계층이 필요합니다.
바로 이럴 때 필요한 것이 Nginx 리버스 프록시입니다. Nginx가 80/443 포트로 들어오는 모든 요청을 받아서 내부의 Next.js 애플리케이션(3000번 포트)으로 전달해주는 방식으로, 보안과 성능을 동시에 확보할 수 있습니다.
개요
간단히 말해서, 리버스 프록시는 클라이언트와 서버 사이에서 중개자 역할을 하는 서버입니다. 왜 이것이 필요한가요?
첫째, Next.js 애플리케이션을 직접 노출하지 않고 Nginx 뒤에 숨김으로써 보안을 강화할 수 있습니다. 둘째, 여러 개의 Next.js 인스턴스를 실행하고 트래픽을 분산시킬 수 있습니다.
셋째, 정적 파일 서빙, SSL 처리, 압축 등을 Nginx에서 담당하게 하여 Next.js는 비즈니스 로직에만 집중할 수 있습니다. 예를 들어, 하루 수만 명의 사용자가 접속하는 서비스에서 정적 파일까지 Next.js가 처리한다면 불필요한 리소스 낭비가 발생합니다.
기존에는 서버 애플리케이션을 80번 포트에서 직접 실행했다면, 이제는 Nginx가 80번 포트를 담당하고 애플리케이션은 안전한 내부 포트(3000, 3001 등)에서 실행됩니다. 리버스 프록시의 핵심 특징은 세 가지입니다: 1) 클라이언트는 실제 서버 주소를 알 수 없어 보안이 강화되고, 2) 하나의 도메인에서 여러 서비스를 경로별로 분기할 수 있으며, 3) 캐싱, 압축, SSL 등의 부가 기능을 중앙에서 관리할 수 있습니다.
이러한 특징들이 대규모 서비스 운영에서는 필수적인 아키텍처 패턴이 되었습니다.
코드 예제
# /etc/nginx/sites-available/nextjs-app
server {
listen 80;
server_name example.com www.example.com;
location / {
# Next.js 애플리케이션으로 프록시
proxy_pass http://localhost: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;
}
}
설명
이것이 하는 일: 이 설정은 example.com으로 들어오는 모든 HTTP 요청을 localhost:3000에서 실행 중인 Next.js 애플리케이션으로 전달합니다. 첫 번째로, server 블록은 Nginx가 80번 포트에서 요청을 받도록 설정합니다.
server_name 지시어를 통해 example.com과 www.example.com 도메인에 대한 요청만 이 설정으로 처리됩니다. 왜 이렇게 하는지?
하나의 서버에서 여러 도메인을 운영할 때 각 도메인마다 다른 애플리케이션으로 요청을 보낼 수 있기 때문입니다. 그 다음으로, location / 블록이 실행되면서 모든 경로(/)에 대한 요청을 처리합니다.
proxy_pass 지시어가 핵심인데, 이것이 실제로 요청을 localhost:3000으로 전달하는 역할을 합니다. 내부에서는 Nginx가 HTTP 클라이언트처럼 동작하여 Next.js 서버에 요청을 보내고 응답을 받아옵니다.
마지막으로, proxy_set_header 지시어들이 클라이언트의 원본 정보를 Next.js에 전달합니다. 최종적으로 Next.js 애플리케이션에서 req.headers.host로 실제 도메인을 확인하고, req.headers['x-real-ip']로 클라이언트의 진짜 IP 주소를 알 수 있게 됩니다.
이 정보가 없으면 Next.js는 모든 요청이 localhost에서 온 것으로 인식하여 로깅, 분석, 보안 기능이 제대로 작동하지 않습니다. 여러분이 이 코드를 사용하면 외부에서는 80번 포트로 접속하지만 실제로는 3000번 포트의 Next.js가 응답하는 구조를 만들 수 있습니다.
추가로 Nginx 레벨에서 접근 제어, DDoS 방어, 요청 제한 등의 보안 기능을 적용할 수 있고, Next.js 애플리케이션을 재시작해도 Nginx가 요청을 버퍼링하여 다운타임을 최소화할 수 있습니다.
실전 팁
💡 설정 파일을 수정한 후에는 반드시 sudo nginx -t로 문법을 검사하고 sudo systemctl reload nginx로 적용하세요. reload는 restart와 달리 연결을 끊지 않고 설정만 새로 고칩니다.
💡 proxy_set_header를 빠뜨리면 Next.js의 getServerSideProps에서 req.headers가 localhost 정보만 담기게 됩니다. 특히 X-Forwarded-Proto가 없으면 HTTP/HTTPS 구분이 안 되어 리다이렉트 로직이 깨질 수 있습니다.
💡 개발 환경에서 테스트할 때는 /etc/hosts 파일에 127.0.0.1 example.com을 추가하여 로컬에서 도메인 접속을 시뮬레이션할 수 있습니다.
💡 Nginx 에러 로그(/var/log/nginx/error.log)와 Next.js 로그를 함께 확인하면 프록시 연결 문제를 빠르게 디버깅할 수 있습니다. 502 Bad Gateway 에러가 나면 대부분 Next.js가 실행되지 않았거나 포트가 다른 경우입니다.
2. 업스트림 서버 설정
시작하며
여러분의 Next.js 애플리케이션이 성장하면서 단일 인스턴스로는 트래픽을 감당하기 어려워진 경험 있으신가요? CPU 사용률이 100%에 도달하고, 응답 시간이 느려지며, 심지어 서버가 다운되는 상황까지 발생합니다.
이런 문제는 트래픽이 증가하는 모든 서비스가 겪는 성장통입니다. 한 대의 서버가 아무리 강력해도 물리적 한계가 있고, 단일 장애점(Single Point of Failure)이 되어 서버 하나가 죽으면 전체 서비스가 마비됩니다.
바로 이럴 때 필요한 것이 업스트림(upstream) 서버 설정입니다. 여러 개의 Next.js 인스턴스를 다른 포트에서 실행하고, Nginx가 이들을 하나의 그룹으로 관리하여 요청을 분산시키는 방식입니다.
개요
간단히 말해서, 업스트림은 Nginx가 요청을 전달할 수 있는 백엔드 서버들의 그룹을 정의하는 것입니다. 왜 이것이 필요한가요?
첫째, 여러 인스턴스를 묶어서 하나의 이름으로 관리할 수 있어 설정이 간결해집니다. 둘째, 로드 밸런싱의 기초가 되어 트래픽을 여러 서버로 분산시킬 수 있습니다.
셋째, 한 서버가 다운되어도 다른 서버가 계속 서비스를 제공하여 가용성이 높아집니다. 예를 들어, PM2로 Next.js를 클러스터 모드로 실행하거나, 여러 VM에 배포했을 때 이 설정으로 모두 연결할 수 있습니다.
기존에는 proxy_pass에 직접 localhost:3000을 써서 단일 서버만 연결했다면, 이제는 upstream 블록에 여러 서버를 정의하고 proxy_pass에서 upstream 이름을 참조하는 방식으로 확장성을 확보합니다. 업스트림의 핵심 특징은: 1) 여러 서버를 논리적으로 그룹화하여 관리가 용이하고, 2) 서버별로 가중치, 우선순위, 활성화 상태를 개별 설정할 수 있으며, 3) 동적으로 서버를 추가/제거해도 설정 변경이 최소화됩니다.
이러한 특징들이 마이크로서비스 아키텍처나 컨테이너 환경에서 특히 강력한 이점을 발휘합니다.
코드 예제
# /etc/nginx/nginx.conf 또는 sites-available 파일 상단
upstream nextjs_backend {
# 첫 번째 인스턴스
server localhost:3000;
# 두 번째 인스턴스
server localhost:3001;
# 세 번째 인스턴스
server localhost:3002;
# 연결 유지 설정
keepalive 32;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://nextjs_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
설명
이것이 하는 일: 이 설정은 세 개의 Next.js 인스턴스를 upstream 그룹으로 정의하고, Nginx가 이 그룹으로 요청을 전달하도록 구성합니다. 첫 번째로, upstream nextjs_backend 블록에서 세 개의 서버 주소를 정의합니다.
각 server 지시어는 하나의 백엔드 인스턴스를 나타내며, localhost:3000, 3001, 3002에서 실행 중인 Next.js 애플리케이션들을 가리킵니다. 왜 이렇게 하는지?
PM2 cluster 모드나 Docker Compose로 여러 컨테이너를 띄웠을 때, 각각 다른 포트를 사용하게 되는데 이들을 모두 연결하기 위해서입니다. keepalive 32는 최대 32개의 연결을 재사용하도록 설정하여 매번 새 연결을 맺는 오버헤드를 줄입니다.
그 다음으로, server 블록의 proxy_pass에서 http://nextjs_backend를 참조합니다. 여기서 중요한 것은 직접 주소 대신 upstream 이름을 사용한다는 점입니다.
Nginx는 내부적으로 nextjs_backend에 정의된 서버 중 하나를 선택해서 요청을 전달합니다. proxy_http_version 1.1과 Connection "" 헤더는 keepalive 연결이 제대로 작동하도록 하는 필수 설정입니다.
마지막으로, 이 구조가 작동하면 클라이언트 요청이 들어올 때마다 Nginx가 라운드 로빈 방식으로 3000, 3001, 3002 포트에 번갈아가며 요청을 보냅니다. 최종적으로 트래픽이 세 인스턴스에 균등하게 분산되어 한 서버에 부하가 집중되지 않고, 하나가 다운되어도 나머지 두 개가 계속 서비스를 제공합니다.
여러분이 이 코드를 사용하면 손쉽게 수평 확장(horizontal scaling)을 구현할 수 있습니다. 트래픽이 증가하면 새 포트에 인스턴스를 추가하고 upstream 블록에 한 줄만 추가하면 됩니다.
또한 무중단 배포를 구현할 수 있는데, 한 인스턴스씩 업데이트하면서 나머지가 트래픽을 처리하게 하여 사용자는 서비스 중단을 느끼지 못합니다.
실전 팁
💡 PM2 cluster 모드로 Next.js를 실행할 때는 pm2 start npm --name "nextjs" -i 3 -- start처럼 인스턴스 개수를 지정하고, PM2가 자동으로 다른 포트를 할당하도록 설정할 수 있습니다. 이때 PM2 로그에서 각 인스턴스의 포트를 확인하세요.
💡 upstream 서버에 weight=3 옵션을 추가하면 특정 서버에 더 많은 요청을 보낼 수 있습니다. 예: server localhost:3000 weight=3; (3000번 포트가 3001, 3002보다 3배 많은 요청 처리)
💡 로컬 테스트 시에는 ss -tuln | grep -E '3000|3001|3002' 명령으로 각 포트에서 Next.js가 실제로 실행 중인지 확인하세요. LISTEN 상태가 아니면 Nginx에서 502 에러가 발생합니다.
💡 개발 환경에서는 upstream을 사용하지 말고 단일 인스턴스로 테스트하세요. 여러 인스턴스를 띄우면 메모리 사용량이 급증하고, 디버깅 시 어느 인스턴스의 로그를 봐야 할지 혼란스러워집니다.
💡 클라우드 환경에서는 내부 IP 주소를 사용할 수 있습니다: server 10.0.1.10:3000; server 10.0.1.11:3000; 이렇게 하면 여러 VM이나 컨테이너에 분산 배포할 수 있습니다.
3. 로드 밸런싱 전략
시작하며
여러분이 업스트림으로 여러 서버를 연결했는데, 특정 서버에만 요청이 몰리거나, 성능이 좋은 서버는 놀고 느린 서버만 과부하가 걸리는 상황을 겪은 적 있나요? 혹은 사용자 세션이 유지되지 않아 로그인 상태가 계속 풀리는 문제가 발생하기도 합니다.
이런 문제는 기본 로드 밸런싱 방식인 라운드 로빈만 사용할 때 흔히 발생합니다. 서버마다 성능이 다르거나, 세션 기반 애플리케이션에서는 단순 순환 분배로는 부족하며, 실제 서버 상태를 반영하지 못해 비효율이 발생합니다.
바로 이럴 때 필요한 것이 Nginx의 다양한 로드 밸런싱 전략입니다. 상황에 맞는 알고리즘을 선택하여 트래픽을 최적으로 분산시키고, 서버 자원을 효율적으로 활용하며, 사용자 경험을 향상시킬 수 있습니다.
개요
간단히 말해서, 로드 밸런싱 전략은 여러 서버 중 어떤 서버로 요청을 보낼지 결정하는 알고리즘입니다. 왜 여러 전략이 필요한가요?
첫째, 애플리케이션 특성에 따라 최적의 분배 방식이 다릅니다. 세션을 사용하는 경우 IP 해시 방식이 필요하고, 서버 성능이 다르면 가중치 기반 분배가 효율적입니다.
둘째, 서버의 실시간 상태를 반영하여 응답이 빠른 서버에 우선 배정할 수 있습니다. 셋째, 특정 패턴의 요청을 특정 서버로 라우팅하여 캐시 효율을 높일 수 있습니다.
예를 들어, 이미지 처리가 많은 요청은 GPU가 있는 서버로, API 요청은 일반 서버로 분리하는 식입니다. 기존에는 모든 서버를 동등하게 취급하여 순서대로 요청을 분배했다면, 이제는 least_conn(최소 연결), ip_hash(IP 기반), 또는 weight(가중치) 방식으로 상황에 맞게 선택할 수 있습니다.
로드 밸런싱 전략의 핵심 특징은: 1) 각 알고리즘마다 장단점이 명확하여 용도에 맞게 선택해야 하고, 2) 조합해서 사용할 수 있어(가중치 + least_conn 등) 세밀한 제어가 가능하며, 3) 서버 추가/제거 시에도 알고리즘이 자동으로 조정되어 관리가 편리합니다. 이러한 특징들이 다양한 워크로드를 처리하는 프로덕션 환경에서 필수적입니다.
코드 예제
# IP 해시 방식 - 같은 IP는 항상 같은 서버로
upstream nextjs_ip_hash {
ip_hash;
server localhost:3000;
server localhost:3001;
server localhost:3002;
}
# 최소 연결 방식 - 연결이 적은 서버로 우선 배정
upstream nextjs_least_conn {
least_conn;
server localhost:3000;
server localhost:3001;
server localhost:3002;
}
# 가중치 기반 라운드 로빈 - 서버 성능에 따라 분배
upstream nextjs_weighted {
server localhost:3000 weight=3; # 고성능 서버
server localhost:3001 weight=2; # 중간 성능
server localhost:3002 weight=1; # 저성능 또는 대기 서버
}
설명
이것이 하는 일: 이 설정들은 각기 다른 상황에 최적화된 세 가지 로드 밸런싱 방식을 보여줍니다. 첫 번째로, ip_hash 방식은 클라이언트의 IP 주소를 해싱하여 항상 같은 서버로 연결합니다.
예를 들어 IP 123.456.789.0에서 온 요청은 처음에 3000번 포트로 갔다면, 다음 요청도 계속 3000번으로 갑니다. 왜 이렇게 하는지?
Next.js에서 서버 사이드 세션을 사용하거나, 메모리 캐시를 활용하는 경우 같은 서버로 계속 연결되어야 데이터 일관성이 유지되기 때문입니다. 단, 서버가 다운되면 자동으로 다른 서버로 재분배됩니다.
그 다음으로, least_conn 방식은 현재 활성 연결 수가 가장 적은 서버로 새 요청을 보냅니다. 서버 A에 100개 연결, 서버 B에 50개 연결이 있다면 서버 B로 요청이 갑니다.
내부적으로 Nginx가 각 서버의 연결 수를 실시간으로 추적하며, 장시간 처리되는 요청(SSE, WebSocket 등)이 많은 애플리케이션에서 특히 효과적입니다. 단순 라운드 로빈보다 실제 서버 부하를 더 정확히 반영합니다.
마지막으로, weight 기반 방식은 서버별로 가중치를 부여하여 성능이 좋은 서버에 더 많은 요청을 보냅니다. 최종적으로 위 예시에서는 6개 요청이 들어오면 3000번에 3개, 3001번에 2개, 3002번에 1개가 분배됩니다.
이 방식은 서버 스펙이 다르거나, 일부 서버를 준비 상태(standby)로 두고 싶을 때 유용합니다. 여러분이 이 코드를 사용하면 애플리케이션 특성에 맞는 최적의 분산 전략을 구현할 수 있습니다.
세션 기반 앱은 ip_hash, 실시간 스트리밍은 least_conn, 이기종 서버 구성은 weight를 선택하면 됩니다. 또한 여러 전략을 조합할 수도 있는데, least_conn에 weight를 함께 사용하면 연결 수와 서버 성능을 동시에 고려한 고급 분산이 가능합니다.
실전 팁
💡 ip_hash는 세션 기반 앱에 유용하지만, Redis나 DB 세션 저장소를 사용하는 것이 더 확장성 있는 솔루션입니다. Next.js + NextAuth.js를 사용한다면 세션을 JWT나 DB에 저장하여 어느 서버든 접속 가능하게 만드세요.
💡 least_conn은 WebSocket이나 Server-Sent Events를 사용하는 Next.js API 라우트에서 매우 효과적입니다. 실시간 채팅, 알림 등의 기능이 있다면 이 방식을 추천합니다.
💡 weight 값은 CPU 코어 수나 메모리 크기에 비례하여 설정하는 것이 좋습니다. 예를 들어 4코어 서버는 weight=4, 8코어는 weight=8 식으로 설정하면 자원 활용률이 균등해집니다.
💡 프로덕션에서는 access_log에 $upstream_addr 변수를 추가하여 어느 서버가 요청을 처리했는지 로깅하세요: log_format upstreamlog '$remote_addr - $upstream_addr'; 이렇게 하면 부하 분산이 제대로 되는지 모니터링할 수 있습니다.
💡 A/B 테스트를 위해 특정 비율로 트래픽을 나눌 수도 있습니다. 예: 90%는 안정 버전(weight=9), 10%는 베타 버전(weight=1)으로 보내서 새 기능을 점진적으로 롤아웃할 수 있습니다.
4. 헬스 체크 설정
시작하며
여러분의 Next.js 인스턴스 중 하나가 메모리 부족으로 응답을 멈췄는데, Nginx가 계속 그 서버로 요청을 보내서 사용자들이 에러 페이지를 보는 상황을 상상해보세요. 자동으로 장애를 감지하고 정상 서버로만 트래픽을 보내는 기능이 없다면, 수동으로 개입할 때까지 서비스 품질이 저하됩니다.
이런 문제는 마이크로서비스나 다중 인스턴스 환경에서 피할 수 없는 현실입니다. 서버는 언제든 다운될 수 있고, 네트워크 문제나 메모리 누수, 예상치 못한 버그로 인해 특정 인스턴스가 불안정해질 수 있습니다.
이때 수동 모니터링만으로는 빠른 대응이 불가능합니다. 바로 이럴 때 필요한 것이 Nginx의 헬스 체크(health check) 기능입니다.
주기적으로 각 서버의 상태를 확인하여 장애가 발생한 서버를 자동으로 제외하고, 복구되면 다시 포함시키는 방식으로 고가용성을 확보할 수 있습니다.
개요
간단히 말해서, 헬스 체크는 백엔드 서버가 정상 작동하는지 자동으로 확인하고, 문제가 있는 서버를 트래픽에서 제외하는 기능입니다. 왜 이것이 필요한가요?
첫째, 사용자가 에러를 경험하기 전에 장애 서버를 선제적으로 차단할 수 있습니다. 둘째, 서버 재시작이나 배포 중에도 정상 서버로만 트래픽이 가서 서비스 중단이 최소화됩니다.
셋째, 일시적인 네트워크 문제나 과부하를 자동으로 감지하여 복원력 있는 시스템을 만들 수 있습니다. 예를 들어, PM2로 인스턴스를 재시작할 때 Nginx가 자동으로 그 서버를 제외했다가 시작 완료 후 다시 포함시킵니다.
기존에는 서버가 다운되어도 Nginx가 계속 요청을 보내다가 타임아웃으로 실패했다면, 이제는 실패 횟수를 추적하여 일정 횟수 이상 실패하면 자동으로 해당 서버를 비활성화합니다. 헬스 체크의 핵심 특징은: 1) 패시브(passive) 방식으로 실제 요청을 모니터링하여 오버헤드가 적고, 2) max_fails와 fail_timeout 파라미터로 민감도를 조절할 수 있으며, 3) 서버 복구 시 자동으로 다시 활성화되어 수동 개입이 불필요합니다.
이러한 특징들이 무중단 배포와 자동 복구 시스템의 기반이 됩니다.
코드 예제
upstream nextjs_backend {
server localhost:3000 max_fails=3 fail_timeout=30s;
server localhost:3001 max_fails=3 fail_timeout=30s;
server localhost:3002 max_fails=3 fail_timeout=30s;
# 백업 서버 - 모든 주 서버가 다운될 때만 사용
server localhost:3003 backup;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://nextjs_backend;
# 연결 타임아웃 설정
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# 실패 시 다음 서버로 자동 전환
proxy_next_upstream error timeout http_502 http_503;
}
}
설명
이것이 하는 일: 이 설정은 각 서버의 응답 상태를 모니터링하여 장애가 감지되면 자동으로 트래픽에서 제외하고, 일정 시간 후 재시도합니다. 첫 번째로, upstream 블록의 각 server 지시어에 max_fails=3 fail_timeout=30s가 추가되어 있습니다.
이것은 30초 이내에 3번 연속 실패하면 해당 서버를 30초 동안 "다운" 상태로 표시한다는 의미입니다. 왜 이렇게 하는지?
일시적인 네트워크 지연이나 한두 번의 우연한 실패로 서버를 제외하면 불필요한 트래픽 변동이 생기므로, 명확한 패턴(3번 연속 실패)이 있을 때만 조치를 취하는 것입니다. 30초 후에는 자동으로 다시 시도하여 서버가 복구되었는지 확인합니다.
그 다음으로, backup 옵션이 붙은 3003번 포트는 평소에는 사용하지 않다가 모든 주 서버(3000, 3001, 3002)가 다운되었을 때만 활성화됩니다. 내부적으로는 최후의 방어선 역할을 하며, 전체 장애 상황에서도 최소한의 서비스를 유지할 수 있게 합니다.
이 백업 서버는 읽기 전용 모드나 유지보수 페이지를 보여주는 용도로 사용할 수도 있습니다. 마지막으로, location 블록의 타임아웃 설정들이 헬스 체크와 연동됩니다.
proxy_connect_timeout 5s는 서버 연결 자체가 5초 내에 되지 않으면 실패로 간주하고, proxy_read_timeout 60s는 응답을 60초 내에 받지 못하면 실패로 처리합니다. 최종적으로 proxy_next_upstream 지시어가 에러, 타임아웃, 502/503 상태 코드를 받으면 즉시 다음 서버로 재시도하여 사용자는 에러를 보지 않고 정상 응답을 받게 됩니다.
여러분이 이 코드를 사용하면 99.9% 이상의 가용성을 달성할 수 있습니다. 한 서버가 다운되어도 나머지 서버가 즉시 대응하고, 배포 중에도 무중단 서비스가 가능합니다.
또한 Nginx 로그를 통해 어떤 서버가 언제 다운되었는지 추적할 수 있어 인프라 문제를 조기에 발견하고 해결할 수 있습니다.
실전 팁
💡 max_fails와 fail_timeout 값은 애플리케이션 특성에 따라 조정하세요. API 서버는 빠른 감지가 중요하므로 max_fails=2, fail_timeout=10s로 낮추고, 배치 작업은 max_fails=5, fail_timeout=60s로 여유있게 설정합니다.
💡 Next.js API 라우트에 /api/health 엔드포인트를 만들고 DB 연결, 메모리 상태 등을 체크하게 한 뒤, Nginx Plus(유료)나 써드파티 모듈로 액티브 헬스 체크를 구현할 수 있습니다. 오픈소스 Nginx는 패시브 체크만 지원합니다.
💡 proxy_next_upstream에 http_404를 추가하지 마세요. 404는 정상적인 응답이므로 다음 서버로 넘기면 같은 요청이 모든 서버에서 404를 반환하며 불필요한 부하가 발생합니다. 에러 상태(502, 503, 504)만 포함시키세요.
💡 fail_timeout 동안 서버가 완전히 제외되는 것이 아니라, 주기적으로 1개 요청을 보내서 복구 여부를 확인합니다. 따라서 서버가 복구되면 fail_timeout 이전이라도 즉시 다시 활성화될 수 있습니다.
💡 Nginx 에러 로그에서 "upstream timed out" 메시지가 반복되면 헬스 체크가 작동하는 신호입니다. tail -f /var/log/nginx/error.log로 실시간 모니터링하여 어떤 서버가 문제인지 파악하세요.
5. 정적 파일 캐싱
시작하며
여러분의 Next.js 애플리케이션에서 이미지, CSS, JS 파일 같은 정적 자원을 매번 서버에서 가져오다 보니 응답 시간이 느리고, 서버 부하가 높아지는 문제를 겪은 적 있나요? 특히 _next/static/ 폴더의 파일들은 내용이 변하지 않는데도 계속 요청이 발생합니다.
이런 문제는 Next.js 빌드 시스템의 특성을 제대로 활용하지 못해서 발생합니다. Next.js는 빌드할 때 모든 정적 파일에 해시값을 붙여서(예: main-a3f2b1c.js) 파일 내용이 변경될 때만 파일명이 바뀌도록 설계되어 있습니다.
즉, 같은 파일명은 영원히 같은 내용을 담고 있다는 것을 보장합니다. 바로 이럴 때 필요한 것이 Nginx의 정적 파일 캐싱 설정입니다.
_next/static/ 경로의 파일들을 Nginx가 직접 서빙하고, 브라우저와 CDN이 장기간 캐시하도록 설정하여 성능을 극적으로 향상시킬 수 있습니다.
개요
간단히 말해서, 정적 파일 캐싱은 변하지 않는 리소스를 Nginx가 직접 서빙하고, 브라우저에 오랫동안 저장하도록 지시하는 기능입니다. 왜 이것이 필요한가요?
첫째, Next.js 서버의 부하를 크게 줄일 수 있습니다. 정적 파일 요청이 Next.js에 도달하지 않고 Nginx에서 바로 응답하므로 Node.js 프로세스가 비즈니스 로직에 집중할 수 있습니다.
둘째, 사용자 경험이 개선됩니다. 같은 파일을 다시 다운로드하지 않고 브라우저 캐시에서 즉시 로드하므로 페이지 로딩이 빨라집니다.
셋째, 대역폭 비용을 절감할 수 있습니다. 예를 들어, 10MB짜리 번들 파일을 1000명이 재방문할 때 캐싱이 없으면 10GB 전송이지만, 캐싱이 있으면 최초 1회만 전송됩니다.
기존에는 모든 요청을 Next.js로 프록시했다면, 이제는 정적 파일 요청은 Nginx가 직접 처리하고 동적 요청만 Next.js로 전달하는 방식으로 역할을 분리합니다. 정적 파일 캐싱의 핵심 특징은: 1) immutable한 파일(해시값 포함)은 최대 1년까지 캐시 가능하여 재방문 성능이 획기적으로 향상되고, 2) Nginx가 디스크에서 직접 읽기 때문에 Node.js보다 훨씬 빠르며, 3) gzip이나 brotli 압축과 결합하여 전송 크기도 줄일 수 있습니다.
이러한 특징들이 대규모 트래픽 사이트의 필수 최적화 기법입니다.
코드 예제
server {
listen 80;
server_name example.com;
# Next.js 빌드 결과물이 있는 실제 경로
root /var/www/nextjs-app;
# _next/static/ 파일들은 Nginx가 직접 서빙
location /_next/static/ {
alias /var/www/nextjs-app/.next/static/;
# 1년간 캐시 (파일명에 해시가 있어 안전)
expires 365d;
add_header Cache-Control "public, immutable";
# 접근 로그 비활성화 (성능 향상)
access_log off;
}
# public 폴더의 정적 파일
location /static/ {
alias /var/www/nextjs-app/public/;
expires 7d;
add_header Cache-Control "public";
}
# 나머지는 Next.js로 프록시
location / {
proxy_pass http://localhost:3000;
}
}
설명
이것이 하는 일: 이 설정은 Next.js의 정적 파일을 Nginx가 직접 제공하고, 브라우저가 최대 1년간 재사용하도록 캐시 정책을 설정합니다. 첫 번째로, root 지시어가 Next.js 애플리케이션의 실제 경로를 가리킵니다.
이것은 Nginx가 파일 시스템에서 직접 파일을 읽을 수 있는 기준점이 됩니다. 왜 이렇게 하는지?
location 블록들이 이 경로를 기준으로 파일을 찾기 때문입니다. /var/www/nextjs-app은 pm2나 배포 스크립트가 Next.js 프로젝트를 클론한 디렉토리입니다.
그 다음으로, location /_next/static/ 블록이 핵심입니다. 사용자가 /_next/static/chunks/main-abc123.js를 요청하면 Next.js로 프록시하지 않고, 직접 /var/www/nextjs-app/.next/static/chunks/main-abc123.js 파일을 읽어서 응답합니다.
내부적으로 Nginx의 sendfile 시스템 콜을 사용하여 커널 레벨에서 파일을 전송하므로 Node.js보다 월등히 빠릅니다. expires 365d와 Cache-Control "public, immutable"은 브라우저에게 "이 파일은 1년간 절대 변하지 않으니 다시 요청하지 말고 캐시에서 사용하라"고 지시합니다.
마지막으로, location /static/ 블록은 public 폴더의 이미지, 폰트 등을 처리합니다. 최종적으로 이 파일들은 7일간 캐시되며, immutable 없이 "public"만 설정하여 주기적으로 재검증이 가능합니다.
access_log off는 정적 파일 요청을 로그에 남기지 않아 디스크 I/O를 줄이고 로그 분석 시 노이즈를 제거합니다. 여러분이 이 코드를 사용하면 동일한 사용자가 재방문할 때 페이지 로딩이 거의 즉각적으로 느껴집니다.
Next.js 서버는 API 요청과 서버 사이드 렌더링만 담당하게 되어 처리량이 3-5배 증가하고, 서버 비용을 크게 절감할 수 있습니다. 또한 CDN(CloudFlare, CloudFront)을 앞단에 두면 정적 파일이 전 세계 엣지에 캐시되어 글로벌 사용자에게도 빠른 속도를 제공합니다.
실전 팁
💡 Next.js 프로젝트를 빌드할 때마다 .next/static/ 폴더의 해시값이 바뀌므로, 새 배포 후에도 캐시 무효화가 자동으로 이루어집니다. 추가 작업이 필요 없습니다.
💡 public 폴더의 파일은 해시가 없으므로 expires 7d처럼 짧게 설정하고, 중요한 파일(로고, 파비콘)은 버전 쿼리 스트링(/logo.png?v=2)을 수동으로 관리하세요.
💡 alias 지시어는 경로를 완전히 대체하고, root는 추가하는 방식입니다. location /_next/static/에서는 alias를 사용해야 /var/www/nextjs-app/.next/static/으로 정확히 매핑됩니다. root를 쓰면 경로가 중복됩니다.
💡 Nginx 설정 후 curl -I https://example.com/_next/static/chunks/main.js로 응답 헤더를 확인하세요. Cache-Control: public, immutable, max-age=31536000이 보이면 성공입니다.
💡 Docker 환경에서는 Next.js 컨테이너의 .next/static/ 폴더를 Nginx 컨테이너와 볼륨 공유해야 합니다: volumes: - nextjs-static:/app/.next/static 이렇게 설정하세요.
6. Gzip 압축 설정
시작하며
여러분의 Next.js 애플리케이션에서 JavaScript 번들 파일이 2MB인데, 사용자가 모바일 네트워크로 접속하면 로딩에 10초 이상 걸리는 상황을 겪어본 적 있나요? 서버에서 파일을 그대로 전송하면 대역폭 낭비도 심하고, 느린 네트워크에서는 사용자 경험이 크게 저하됩니다.
이런 문제는 텍스트 기반 파일(HTML, CSS, JS, JSON)의 특성을 활용하지 못해서 발생합니다. 텍스트 파일은 압축률이 매우 높아서 평균 70-80%까지 크기를 줄일 수 있는데, 압축 없이 전송하면 불필요하게 큰 데이터를 보내게 됩니다.
바로 이럴 때 필요한 것이 Nginx의 Gzip 압축 설정입니다. 서버에서 응답을 전송하기 전에 자동으로 압축하여 전송 크기를 줄이고, 클라이언트 브라우저가 압축을 풀어서 사용하는 방식으로 네트워크 효율을 극대화할 수 있습니다.
개요
간단히 말해서, Gzip 압축은 HTTP 응답을 전송하기 전에 압축하여 데이터 크기를 줄이고, 브라우저가 자동으로 압축을 해제하는 기능입니다. 왜 이것이 필요한가요?
첫째, 페이지 로딩 속도가 대폭 향상됩니다. 2MB 파일이 400KB로 줄어들면 전송 시간이 1/5로 단축되며, 특히 모바일이나 느린 네트워크에서 체감 차이가 큽니다.
둘째, 대역폭 비용을 절감할 수 있습니다. CDN이나 클라우드 서비스는 전송량에 따라 과금되므로 압축으로 트래픽을 줄이면 비용이 감소합니다.
셋째, SEO에도 긍정적입니다. Google은 페이지 로딩 속도를 검색 순위 요소로 고려하므로 압축으로 성능을 높이면 검색 노출이 개선될 수 있습니다.
예를 들어, 1MB짜리 JSON API 응답을 압축하면 100KB로 줄어들어 API 호출이 10배 빨라집니다. 기존에는 서버가 파일을 그대로 전송했다면, 이제는 클라이언트가 Accept-Encoding: gzip 헤더를 보내면 서버가 압축된 버전을 전송하고 브라우저가 자동으로 압축을 풉니다.
Gzip 압축의 핵심 특징은: 1) CPU 사용량은 약간 증가하지만 네트워크 전송 시간 단축이 훨씬 크므로 전체적으로 이득이고, 2) 브라우저가 자동으로 처리하므로 클라이언트 측 코드 변경이 불필요하며, 3) 압축 레벨을 조정하여 CPU와 압축률 간 균형을 맞출 수 있습니다. 이러한 특징들이 현대 웹 서비스의 표준 설정이 되었습니다.
코드 예제
# /etc/nginx/nginx.conf의 http 블록 안에 추가
http {
# Gzip 압축 활성화
gzip on;
# 압축 레벨 (1-9, 6이 CPU와 압축률의 균형점)
gzip_comp_level 6;
# 최소 압축 파일 크기 (1KB 이하는 압축 안 함)
gzip_min_length 1000;
# 압축할 MIME 타입들
gzip_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/x-javascript
text/xml
application/xml
application/xml+rss
image/svg+xml;
# 프록시된 요청도 압축
gzip_proxied any;
# Vary 헤더 추가 (캐시 서버용)
gzip_vary on;
# IE6는 압축 비활성화 (호환성)
gzip_disable "msie6";
}
설명
이것이 하는 일: 이 설정은 Nginx가 응답을 전송하기 전에 자동으로 Gzip 압축을 적용하여 네트워크 전송량을 크게 줄입니다. 첫 번째로, gzip on이 압축 기능을 활성화하고, gzip_comp_level 6이 압축 강도를 설정합니다.
레벨 1은 가장 빠르지만 압축률이 낮고, 레벨 9는 최고 압축률이지만 CPU를 많이 사용합니다. 왜 6을 선택하는지?
실험 결과 6 이상부터는 압축률 향상이 미미한 반면 CPU 사용량은 급증하므로, 6이 최적의 균형점입니다. gzip_min_length 1000은 1KB 이하 파일은 압축하지 않는데, 작은 파일은 압축 오버헤드가 절감 효과보다 클 수 있기 때문입니다.
그 다음으로, gzip_types가 압축할 파일 종류를 명시합니다. HTML은 기본적으로 포함되므로 생략하고, CSS, JavaScript, JSON, XML, SVG 등 텍스트 기반 타입들을 나열합니다.
내부적으로 Nginx가 응답의 Content-Type 헤더를 확인하여 이 목록에 있으면 압축을 적용합니다. 중요한 점은 이미지(JPEG, PNG)나 비디오는 이미 압축되어 있으므로 재압축하면 오히려 성능이 저하됩니다.
마지막으로, gzip_proxied any는 프록시된 요청(Next.js에서 온 응답)도 압축하라는 의미입니다. 최종적으로 Next.js가 보낸 HTML이나 API JSON 응답을 Nginx가 받아서 압축한 후 클라이언트에 전달합니다.
gzip_vary on은 Vary: Accept-Encoding 헤더를 추가하여 CDN이나 프록시 서버가 압축 버전과 비압축 버전을 별도로 캐시하도록 지시합니다. 여러분이 이 코드를 사용하면 페이지 로딩 시간이 평균 40-60% 단축됩니다.
특히 모바일 사용자나 해외 사용자에게 체감 속도 향상이 크며, 서버 대역폭 비용도 절반 이하로 줄일 수 있습니다. 또한 Google PageSpeed Insights 점수가 크게 올라가서 SEO 효과도 볼 수 있습니다.
실전 팁
💡 Chrome DevTools의 Network 탭에서 파일을 선택하면 Response Headers에 Content-Encoding: gzip이 표시되고, Size 컬럼에서 2.0 MB → 400 KB 같은 압축 효과를 확인할 수 있습니다.
💡 Next.js는 기본적으로 압축을 비활성화하고 Nginx나 CDN에서 처리하도록 권장합니다. next.config.js에 compress: false로 설정되어 있는지 확인하세요. 중복 압축은 CPU 낭비입니다.
💡 Brotli 압축이 Gzip보다 15-20% 더 효율적이지만 설정이 복잡합니다. Nginx에 ngx_brotli 모듈을 설치하고 brotli on; brotli_types ...; 설정을 추가하면 됩니다. 단, 빌드 시간이 오래 걸리므로 트래픽이 매우 많은 경우에만 고려하세요.
💡 압축이 제대로 작동하는지 테스트하려면 curl -H "Accept-Encoding: gzip" -I https://example.com을 실행하고 응답 헤더에 Content-Encoding: gzip이 있는지 확인하세요.
💡 압축으로 인한 CPU 부하를 모니터링하세요. htop으로 확인했을 때 CPU 사용률이 90% 이상 지속되면 gzip_comp_level을 5나 4로 낮추는 것을 고려하세요. 대부분의 경우 6에서 문제가 없습니다.
7. SSL/HTTPS 설정
시작하며
여러분의 Next.js 애플리케이션을 HTTP로만 서비스하다 보니 브라우저에서 "안전하지 않음" 경고가 뜨고, 검색 엔진 순위가 낮아지며, 심지어 일부 API(Geolocation, Camera 등)가 작동하지 않는 문제를 겪은 적 있나요? 현대 웹에서 HTTPS는 선택이 아닌 필수입니다.
이런 문제는 보안 인증서 없이 평문 HTTP로 통신할 때 발생합니다. 중간자 공격에 취약하고, 사용자 데이터가 암호화되지 않으며, 브라우저와 검색 엔진이 신뢰하지 않는 사이트로 분류됩니다.
또한 Service Worker, HTTP/2 같은 최신 기술을 사용할 수 없습니다. 바로 이럴 때 필요한 것이 Nginx에서 SSL/HTTPS를 설정하는 것입니다.
Let's Encrypt로 무료 인증서를 발급받고, Nginx에 적용하여 모든 트래픽을 암호화하며, HTTP 요청은 자동으로 HTTPS로 리다이렉트하는 완벽한 보안 환경을 구축할 수 있습니다.
개요
간단히 말해서, SSL/HTTPS 설정은 서버와 클라이언트 간 통신을 암호화하여 데이터를 안전하게 전송하고, 브라우저에 녹색 자물쇠 아이콘을 표시하는 기능입니다. 왜 이것이 필요한가요?
첫째, 사용자 데이터(로그인 정보, 개인정보)를 보호할 수 있습니다. 평문 HTTP는 와이파이 같은 공용 네트워크에서 쉽게 도청당하지만, HTTPS는 TLS 암호화로 안전합니다.
둘째, SEO와 신뢰도가 향상됩니다. Google은 HTTPS 사이트를 검색 순위에서 우대하고, 브라우저는 HTTP 사이트에 "안전하지 않음" 경고를 표시합니다.
셋째, 최신 웹 기술을 사용할 수 있습니다. 예를 들어, PWA, HTTP/2, SameSite 쿠키 등은 HTTPS 환경에서만 작동합니다.
기존에는 비싼 인증서를 구매하고 복잡한 설정을 해야 했다면, 이제는 Let's Encrypt와 Certbot으로 무료로 자동화된 인증서 발급과 갱신이 가능합니다. SSL/HTTPS의 핵심 특징은: 1) Let's Encrypt가 90일마다 자동 갱신되어 수동 관리가 불필요하고, 2) HTTP/2와 함께 사용하면 성능도 HTTP/1.1보다 빨라지며, 3) HSTS, OCSP Stapling 같은 고급 보안 기능을 추가할 수 있습니다.
이러한 특징들이 프로덕션 환경의 기본 요구사항이 되었습니다.
코드 예제
# Certbot으로 인증서 발급 (최초 1회)
# sudo apt install certbot python3-certbot-nginx
# sudo certbot --nginx -d example.com -d www.example.com
# /etc/nginx/sites-available/nextjs-app
# HTTP를 HTTPS로 리다이렉트
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$server_name$request_uri;
}
# HTTPS 서버
server {
listen 443 ssl http2;
server_name example.com www.example.com;
# Let's Encrypt 인증서 경로 (certbot이 자동 설정)
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# SSL 프로토콜 및 암호화 설정
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# HSTS (브라우저가 항상 HTTPS로 접속)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
proxy_pass http://localhost:3000;
}
}
설명
이것이 하는 일: 이 설정은 무료 SSL 인증서를 적용하여 모든 통신을 암호화하고, HTTP 요청을 HTTPS로 자동 전환합니다. 첫 번째로, Certbot 명령어가 Let's Encrypt에서 인증서를 발급받습니다.
--nginx 옵션을 사용하면 Certbot이 자동으로 Nginx 설정을 수정하고 인증서 경로를 추가합니다. -d 옵션으로 여러 도메인(example.com, www.example.com)을 한 인증서에 포함시킬 수 있습니다.
왜 이렇게 하는지? 사용자가 www 유무에 관계없이 접속할 수 있도록 양쪽 모두 인증서에 포함시키는 것이 좋습니다.
Certbot은 90일마다 자동 갱신 cron job도 설치합니다. 그 다음으로, 80번 포트 server 블록이 HTTP 요청을 받아서 return 301로 HTTPS로 영구 리다이렉트합니다.
사용자가 http://example.com/about을 입력하면 자동으로 https://example.com/about으로 전환됩니다. 내부적으로 301 상태 코드는 검색 엔진에게 "영구적으로 이동했다"고 알려서 SEO 점수를 유지합니다.
302(임시 이동)를 쓰면 검색 엔진이 HTTP 주소를 계속 인덱싱할 수 있습니다. 마지막으로, 443번 포트 server 블록이 HTTPS 요청을 처리합니다.
최종적으로 ssl_certificate와 ssl_certificate_key가 인증서 파일을 가리키고, ssl_protocols에서 취약한 TLSv1.0/1.1을 제외하여 보안을 강화합니다. http2를 추가하면 HTTP/2 프로토콜이 활성화되어 다중 요청을 병렬 처리하여 성능이 향상됩니다.
HSTS 헤더는 브라우저에게 1년간 이 사이트는 항상 HTTPS로만 접속하라고 지시하여, 이후에는 사용자가 http://를 입력해도 브라우저가 자동으로 https://로 바꿉니다. 여러분이 이 코드를 사용하면 완벽한 A+ 등급의 SSL 보안을 무료로 구현할 수 있습니다.
사용자 데이터가 암호화되어 안전하고, Google 검색 순위가 올라가며, 브라우저 경고가 사라져서 신뢰도가 높아집니다. 또한 HTTP/2로 페이지 로딩도 빨라집니다.
실전 팁
💡 Certbot 설치 후 sudo certbot renew --dry-run으로 자동 갱신이 제대로 설정되었는지 테스트하세요. 실패하면 90일 후 인증서가 만료되어 사이트가 접속 불가능해집니다.
💡 인증서 발급 전에 DNS가 제대로 설정되어 있어야 합니다. dig example.com으로 A 레코드가 서버 IP를 가리키는지 확인하세요. DNS 전파 안 되면 Certbot이 도메인 소유권을 확인할 수 없어 실패합니다.
💡 SSL Labs(https://www.ssllabs.com/ssltest/)에서 도메인을 테스트하여 보안 등급을 확인하세요. A+ 등급을 받으려면 위 설정에 ssl_session_cache shared:SSL:10m;와 OCSP Stapling을 추가하면 됩니다.
💡 로컬 개발 환경에서는 mkcert로 자체 서명 인증서를 만들어 테스트할 수 있습니다. mkcert localhost를 실행하고 생성된 파일을 Nginx에 적용하면 로컬에서도 HTTPS를 사용할 수 있습니다.
💡 Next.js에서 x-forwarded-proto 헤더를 확인하여 HTTPS 여부를 판단하는 로직이 있다면, Nginx의 proxy_set_header에 proxy_set_header X-Forwarded-Proto $scheme;이 포함되어 있는지 확인하세요. 없으면 Next.js가 HTTP로 인식할 수 있습니다.
8. 프록시 헤더 설정
시작하며
여러분의 Next.js 애플리케이션에서 사용자 IP 주소를 로깅하려고 하는데 모든 요청이 127.0.0.1(localhost)에서 온 것으로 나오거나, HTTPS로 접속했는데 req.protocol이 http로 나와서 리다이렉트 로직이 무한 루프에 빠지는 문제를 겪은 적 있나요? 이런 문제는 Nginx가 프록시 역할을 하면서 원본 클라이언트 정보가 소실되기 때문에 발생합니다.
Next.js 입장에서는 모든 요청이 localhost에서 온 것처럼 보이고, 실제 클라이언트 IP, 원본 도메인, 프로토콜(HTTP/HTTPS) 정보를 알 수 없게 됩니다. 이는 로깅, 보안, IP 기반 제한 등의 기능을 무용지물로 만듭니다.
바로 이럴 때 필요한 것이 프록시 헤더 설정입니다. Nginx가 요청을 Next.js로 전달할 때 원본 클라이언트 정보를 HTTP 헤더에 추가하여, Next.js가 실제 클라이언트를 정확히 파악할 수 있도록 하는 것입니다.
개요
간단히 말해서, 프록시 헤더는 Nginx가 원본 클라이언트의 IP, 호스트, 프로토콜 정보를 HTTP 헤더로 Next.js에 전달하는 설정입니다. 왜 이것이 필요한가요?
첫째, 정확한 사용자 분석과 로깅이 가능해집니다. Google Analytics, 로그 시스템, 보안 모니터링 등이 실제 사용자 IP를 기준으로 작동합니다.
둘째, IP 기반 기능이 제대로 작동합니다. 지역별 콘텐츠 제공, Rate Limiting, DDoS 방어 등이 클라이언트 IP를 필요로 합니다.
셋째, HTTPS 리다이렉트나 절대 URL 생성 로직이 정상 작동합니다. 예를 들어, Next.js에서 이메일에 포함할 링크를 생성할 때 req.protocol과 req.headers.host를 사용하는데, 이 정보가 없으면 잘못된 URL이 만들어집니다.
기존에는 Next.js가 프록시 서버(Nginx)의 정보만 받았다면, 이제는 X-Real-IP, X-Forwarded-For, X-Forwarded-Proto 같은 표준 헤더를 통해 실제 클라이언트 정보를 받을 수 있습니다. 프록시 헤더의 핵심 특징은: 1) 업계 표준 헤더(X-Forwarded-*)를 사용하여 대부분의 프레임워크가 자동으로 인식하고, 2) 다중 프록시 환경(CDN → Nginx → Next.js)에서도 체인 형태로 정보를 전달할 수 있으며, 3) Next.js에서 코드 변경 없이 req.headers에서 바로 접근 가능합니다.
이러한 특징들이 마이크로서비스나 컨테이너 환경에서 필수적입니다.
코드 예제
server {
listen 443 ssl http2;
server_name example.com;
location / {
proxy_pass http://localhost:3000;
# 원본 호스트 정보 전달
proxy_set_header Host $host;
# 실제 클라이언트 IP 전달
proxy_set_header X-Real-IP $remote_addr;
# 프록시 체인의 모든 IP 전달
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 원본 프로토콜 (http 또는 https) 전달
proxy_set_header X-Forwarded-Proto $scheme;
# 원본 요청 URI 전달
proxy_set_header X-Forwarded-Uri $request_uri;
# WebSocket 지원을 위한 헤더
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# HTTP 버전 설정
proxy_http_version 1.1;
}
}
설명
이것이 하는 일: 이 설정은 Nginx가 요청을 프록시할 때 원본 클라이언트 정보를 손실하지 않도록 표준 HTTP 헤더들을 추가합니다. 첫 번째로, Host 헤더는 사용자가 입력한 도메인(example.com)을 그대로 전달합니다.
Nginx 없이 직접 localhost:3000에 접속하면 Host가 localhost:3000이지만, 프록시를 거치면 example.com이 유지됩니다. 왜 이렇게 하는지?
Next.js의 getServerSideProps나 API 라우트에서 req.headers.host로 현재 도메인을 확인하여 절대 URL을 생성하거나, 멀티 도메인 앱에서 도메인별 로직을 분기할 때 필요하기 때문입니다. 그 다음으로, X-Real-IP와 X-Forwarded-For가 클라이언트 IP 정보를 전달합니다.
$remote_addr은 Nginx에 직접 연결한 클라이언트의 IP이고, $proxy_add_x_forwarded_for는 기존 X-Forwarded-For 값에 $remote_addr을 추가한 것입니다. 내부적으로 CDN → Nginx → Next.js 같은 다중 프록시 환경에서는 X-Forwarded-For가 "CDN_IP, Nginx_IP" 형태로 전체 경로를 담게 됩니다.
Next.js에서는 req.headers['x-forwarded-for'].split(',')[0]으로 최초 클라이언트 IP를 추출합니다. 마지막으로, X-Forwarded-Proto는 사용자가 HTTP로 접속했는지 HTTPS로 접속했는지 알려줍니다.
최종적으로 Next.js에서 if (req.headers['x-forwarded-proto'] === 'https') 같은 조건문으로 프로토콜을 확인할 수 있습니다. Upgrade와 Connection 헤더는 WebSocket 연결을 지원하며, Next.js API 라우트에서 실시간 통신을 구현할 때 필수적입니다.
proxy_http_version 1.1은 Keep-Alive와 WebSocket을 위해 HTTP/1.1을 사용하도록 강제합니다. 여러분이 이 코드를 사용하면 Next.js 애플리케이션이 프록시 뒤에 있어도 마치 직접 연결된 것처럼 모든 정보를 정확히 받을 수 있습니다.
IP 기반 Rate Limiting, 지역별 콘텐츠 제공, 보안 로깅, HTTPS 감지 등 프로덕션 환경의 필수 기능들이 정상 작동합니다. 또한 Next.js가 제공하는 req.ip, req.protocol 같은 편의 속성도 올바른 값을 반환합니다.
실전 팁
💡 Next.js에서 클라이언트 IP를 가져올 때는 req.headers['x-forwarded-for']?.split(',')[0] || req.socket.remoteAddress처럼 폴백 로직을 작성하세요. 개발 환경에서는 X-Forwarded-For가 없을 수 있습니다.
💡 CloudFlare나 AWS CloudFront 같은 CDN을 사용하면 CF-Connecting-IP나 CloudFront-Viewer-Address 같은 CDN 전용 헤더도 확인하세요. 이것들이 X-Forwarded-For보다 더 정확할 수 있습니다.
💡 보안상 X-Forwarded-For는 조작 가능하므로, IP 기반 인증이나 과금에 사용하지 마세요. 신뢰할 수 있는 프록시 체인에서만 사용하고, 중요한 로직은 서버 측 $remote_addr을 기준으로 하세요.
💡 Next.js에서 trustProxy: true 옵션을 설정하면 자동으로 X-Forwarded-* 헤더를 신뢰합니다. server.js에서 Express를 사용한다면 app.set('trust proxy', 1)을 추가하세요.
💡 WebSocket이나 Server-Sent Events를 사용하는 경우 proxy_read_timeout을 충분히 길게 설정하세요: proxy_read_timeout 3600s; 그렇지 않으면 장시간 연결이 끊어집니다.
9. 커넥션 풀 최적화
시작하며
여러분의 Next.js 애플리케이션에서 트래픽이 급증할 때 Nginx가 매번 새로운 TCP 연결을 맺고 끊으면서 응답 시간이 느려지고, 서버 리소스가 불필요하게 낭비되는 문제를 겪은 적 있나요? 초당 수천 건의 요청이 들어오는데 연결 설정에만 수백 밀리초가 소요되는 상황입니다.
이런 문제는 HTTP 기본 동작 방식에서 발생합니다. 기본적으로 각 요청마다 새 TCP 연결을 맺고(3-way handshake), 응답 후 연결을 닫는 과정이 반복되어 불필요한 오버헤드가 발생합니다.
특히 Nginx와 Next.js 간은 같은 서버 또는 같은 네트워크 내부이므로 연결을 재사용할 수 있는데도 매번 새로 만드는 것은 비효율적입니다. 바로 이럴 때 필요한 것이 커넥션 풀(connection pool) 최적화입니다.
Nginx와 백엔드 서버 간 연결을 재사용하고, 동시 연결 수를 적절히 제한하며, 타임아웃을 조정하여 리소스 효율과 성능을 동시에 높일 수 있습니다.
개요
간단히 말해서, 커넥션 풀 최적화는 Nginx와 Next.js 간 연결을 재사용하고 효율적으로 관리하여 불필요한 연결 생성/해제 오버헤드를 줄이는 기능입니다. 왜 이것이 필요한가요?
첫째, 응답 시간이 크게 단축됩니다. TCP 3-way handshake를 매번 하지 않고 기존 연결을 재사용하면 수십 밀리초를 절약할 수 있습니다.
초당 1000 요청이라면 수십 초의 누적 절감 효과가 발생합니다. 둘째, 서버 리소스 사용이 최적화됩니다.
파일 디스크립터, 메모리, CPU 사용량이 줄어들어 같은 하드웨어로 더 많은 트래픽을 처리할 수 있습니다. 셋째, Next.js 서버의 부담이 감소합니다.
예를 들어, Next.js의 Node.js 이벤트 루프가 새 연결 처리에 소모하는 시간이 줄어 실제 비즈니스 로직 처리에 집중할 수 있습니다. 기존에는 각 요청마다 새 연결을 만들고 끊었다면, 이제는 keepalive로 연결을 풀에 보관했다가 다음 요청에 재사용하는 방식으로 전환합니다.
커넥션 풀의 핵심 특징은: 1) keepalive 연결 수를 upstream 서버별로 관리하여 메모리 사용을 예측 가능하게 하고, 2) keepalive_timeout과 keepalive_requests로 연결의 수명을 제어하여 메모리 누수를 방지하며, 3) HTTP/1.1을 강제하여 Keep-Alive 기능을 활성화합니다. 이러한 특징들이 고성능 프로덕션 환경의 필수 설정입니다.
코드 예제
upstream nextjs_backend {
server localhost:3000;
server localhost:3001;
server localhost:3002;
# 백엔드당 최대 32개의 keepalive 연결 유지
keepalive 32;
# keepalive 연결 타임아웃 (기본 60초)
keepalive_timeout 60s;
# 하나의 연결로 처리할 최대 요청 수 (기본 100)
keepalive_requests 100;
}
server {
listen 443 ssl http2;
server_name example.com;
location / {
proxy_pass http://nextjs_backend;
# HTTP/1.1 강제 (keepalive 필수)
proxy_http_version 1.1;
# Connection 헤더 초기화 (keepalive 필수)
proxy_set_header Connection "";
# 백엔드 연결 타임아웃 설정
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# 버퍼 설정 최적화
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
}
}
설명
이것이 하는 일: 이 설정은 Nginx와 백엔드 서버 간 연결을 재사용하도록 구성하여 TCP 연결 오버헤드를 크게 줄입니다. 첫 번째로, upstream 블록의 keepalive 32는 각 워커 프로세스가 백엔드 서버마다 최대 32개의 유휴 연결을 캐시에 보관한다는 의미입니다.
Nginx 워커가 4개, 백엔드 서버가 3개라면 최대 4 × 3 × 32 = 384개의 연결이 풀에 존재할 수 있습니다. 왜 32인가?
일반적인 워크로드에서 충분한 값이며, 너무 크면 메모리 낭비, 너무 작으면 재사용 효과가 떨어집니다. 트래픽이 매우 많으면 64나 128로 늘릴 수 있습니다.
keepalive_timeout 60s는 유휴 연결을 60초간 유지한 후 닫으며, keepalive_requests 100은 한 연결로 100개 요청을 처리한 후 갱신합니다. 그 다음으로, location 블록의 proxy_http_version 1.1이 핵심입니다.
HTTP/1.0은 keepalive를 지원하지 않으므로 반드시 1.1로 설정해야 합니다. 내부적으로 Nginx와 Next.js 간 통신이 HTTP/1.1 프로토콜을 사용하게 되어 Connection: keep-alive 헤더가 자동으로 처리됩니다.
proxy_set_header Connection ""은 클라이언트가 보낸 Connection 헤더를 지우는 역할인데, 이것이 없으면 클라이언트의 Connection: close가 그대로 전달되어 keepalive가 작동하지 않습니다. 마지막으로, 타임아웃 설정들이 연결 관리를 세밀하게 제어합니다.
최종적으로 proxy_connect_timeout 5s는 백엔드 연결 자체가 5초 내에 성공해야 하고, proxy_read_timeout 60s는 백엔드가 60초 내에 응답해야 하며, 이를 초과하면 연결을 닫고 새로 시도합니다. proxy_buffering과 버퍼 설정은 백엔드 응답을 메모리에 버퍼링하여 느린 클라이언트가 백엔드 연결을 오래 점유하지 않도록 합니다.
여러분이 이 코드를 사용하면 같은 하드웨어로 처리량이 30-50% 증가합니다. 벤치마크 결과 keepalive 없이는 초당 1000 RPS, keepalive로는 1500 RPS를 처리할 수 있는 경우가 많습니다.
또한 레이턴시가 낮아져서 p95(95 퍼센타일) 응답 시간이 200ms에서 100ms로 단축되는 효과를 볼 수 있습니다.
실전 팁
💡 keepalive 값은 worker_processes * 백엔드_서버_수 * keepalive가 총 메모리에 미치는 영향을 고려하여 설정하세요. 각 연결이 약 4-8KB 메모리를 사용하므로, 384개 연결이면 약 3MB 정도입니다.
💡 Next.js가 PM2 cluster 모드로 실행 중이라면, PM2 인스턴스 수보다 keepalive 값이 작으면 일부 인스턴스가 연결을 받지 못할 수 있습니다. PM2 인스턴스가 4개면 keepalive를 최소 8 이상으로 설정하세요.
💡 ss -tn | grep :3000 | grep ESTAB | wc -l 명령으로 실제 활성 연결 수를 모니터링할 수 있습니다. keepalive가 작동하면 트래픽이 없을 때도 일정 수의 ESTABLISHED 연결이 유지됩니다.
💡 proxy_buffering을 off로 하면 스트리밍 응답(SSE, 파일 다운로드)에 유리하지만 keepalive 효율이 떨어집니다. 대부분의 경우 on으로 유지하고, 특정 location에만 off를 적용하세요.
💡 Nginx 에러 로그에서 "upstream prematurely closed connection" 메시지가 자주 보이면 keepalive_timeout과 Next.js의 서버 타임아웃이 불일치하는 경우입니다. Next.js의 server.keepAliveTimeout을 Nginx보다 5초 정도 길게 설정하세요.
10. 에러 페이지 커스터마이징
시작하며
여러분의 Next.js 애플리케이션에서 서버가 다운되거나 502 Bad Gateway 에러가 발생할 때, 사용자에게 보여지는 기본 Nginx 에러 페이지가 너무 투박하고 브랜드 이미지와 맞지 않아 당황한 적 있나요? 심지어 사용자는 무슨 문제인지 이해하지 못하고 그냥 이탈하게 됩니다.
이런 문제는 Nginx의 기본 에러 페이지가 기술적인 정보만 담고 있고, 사용자 친화적이지 않기 때문에 발생합니다. 사용자는 "502 Bad Gateway"가 무엇을 의미하는지 모르고, 어떻게 해야 할지 안내가 없으며, 서비스와 전혀 어울리지 않는 디자인 때문에 불안감을 느낍니다.
바로 이럴 때 필요한 것이 Nginx의 커스텀 에러 페이지 설정입니다. 브랜드에 맞는 디자인의 에러 페이지를 만들고, 사용자에게 명확한 안내를 제공하며, 자동으로 재시도하거나 대체 페이지로 안내하는 등 사용자 경험을 개선할 수 있습니다.
개요
간단히 말해서, 커스텀 에러 페이지는 Nginx에서 발생하는 에러(502, 503, 504 등)를 사용자 친화적이고 브랜드에 맞는 HTML 페이지로 대체하는 기능입니다. 왜 이것이 필요한가요?
첫째, 사용자 경험이 크게 개선됩니다. 기술 용어 대신 "일시적인 문제가 발생했습니다.
잠시 후 다시 시도해주세요" 같은 친절한 안내를 제공할 수 있습니다. 둘째, 브랜드 일관성을 유지할 수 있습니다.
에러 페이지에도 로고, 색상, 폰트를 적용하여 전문적인 이미지를 유지합니다. 셋째, 이탈을 방지할 수 있습니다.
예를 들어, 503 에러 페이지에 "서버 점검 중입니다. 10분 후 재방문해주세요"라고 안내하고, 자동 새로고침 기능을 추가하여 사용자가 직접 새로고침할 필요가 없게 만들 수 있습니다.
기존에는 Nginx가 제공하는 기본 에러 페이지가 표시되었다면, 이제는 error_page 지시어로 커스텀 HTML 파일을 연결하여 브랜드화된 경험을 제공합니다. 커스텀 에러 페이지의 핵심 특징은: 1) 각 에러 코드(502, 503, 504)별로 다른 메시지를 제공할 수 있어 상황에 맞는 안내가 가능하고, 2) 정적 HTML이므로 백엔드가 다운되어도 항상 표시되며, 3) JavaScript로 자동 재시도, 로깅, 리다이렉트 등을 구현할 수 있습니다.
이러한 특징들이 프로페셔널한 서비스 운영의 디테일을 보여줍니다.
코드 예제
# /etc/nginx/sites-available/nextjs-app
server {
listen 443 ssl http2;
server_name example.com;
# 커스텀 에러 페이지 경로 설정
error_page 502 503 504 /errors/maintenance.html;
error_page 404 /errors/404.html;
error_page 500 /errors/500.html;
# 에러 페이지 파일 제공
location ^~ /errors/ {
internal;
root /var/www/nextjs-app/public;
}
location / {
proxy_pass http://localhost:3000;
proxy_intercept_errors on; # Nginx가 에러 응답 가로채기
}
}
설명
이것이 하는 일: 이 설정은 Nginx나 백엔드에서 발생하는 에러를 사용자 친화적인 커스텀 페이지로 교체합니다. 첫 번째로, error_page 지시어들이 각 에러 코드를 특정 HTML 파일에 매핑합니다.
502, 503, 504(서버 관련 에러)는 모두 같은 maintenance.html로, 404(페이지 없음)는 404.html로, 500(서버 내부 에러)은 500.html로 연결됩니다. 왜 이렇게 하는지?
502/503/504는 모두 "서버가 일시적으로 응답할 수 없음"을 의미하므로 같은 메시지(점검 중, 잠시 후 재시도)를 보여주는 것이 적합하고, 404는 "페이지를 찾을 수 없음", 500은 "서버 오류"로 각각 다른 안내가 필요하기 때문입니다. 그 다음으로, location ^~ /errors/ 블록이 실제 HTML 파일을 제공합니다.
internal 지시어가 핵심인데, 이것은 외부에서 직접 /errors/maintenance.html에 접근하는 것을 차단하고 오직 Nginx 내부 리다이렉트만 허용합니다. 내부적으로 사용자가 브라우저에 https://example.com/errors/maintenance.html을 직접 입력하면 404가 나지만, 502 에러 발생 시 Nginx가 내부적으로 이 파일을 제공합니다.
보안상 에러 페이지를 외부에 노출하지 않는 것이 좋습니다. 마지막으로, proxy_intercept_errors on이 백엔드에서 발생한 에러 응답(4xx, 5xx)을 Nginx가 가로채서 커스텀 에러 페이지로 대체하도록 합니다.
최종적으로 Next.js가 500 에러를 반환하더라도 사용자에게는 Nginx의 커스텀 500.html이 표시되어 일관된 경험을 제공합니다. 이 옵션이 없으면 Next.js의 기본 에러 페이지가 그대로 노출됩니다.
여러분이 이 코드를 사용하면 배포 중이나 장애 상황에서도 전문적인 이미지를 유지할 수 있습니다. 사용자에게 투명하게 상황을 알리고, 언제 다시 방문해야 하는지 안내하며, 자동 재시도 기능으로 복구 즉시 서비스를 제공할 수 있습니다.
또한 에러 페이지에 고객센터 링크, FAQ, 대체 서비스 안내를 추가하여 이탈을 최소화할 수 있습니다.
실전 팁
💡 maintenance.html에 JavaScript로 10초마다 자동 새로고침을 추가하세요: <script>setTimeout(() => location.reload(), 10000);</script> 이렇게 하면 배포가 완료되는 즉시 사용자가 자동으로 정상 페이지를 보게 됩니다.
💡 에러 페이지는 외부 리소스(CDN의 CSS/JS)에 의존하지 않고 모든 스타일을 인라인으로 작성하세요. 네트워크 장애 시 외부 리소스를 불러올 수 없어 페이지가 깨질 수 있습니다.
💡 404 페이지에 검색 기능이나 인기 페이지 링크를 추가하면 사용자를 다른 콘텐츠로 유도할 수 있습니다. "찾으시는 페이지가 없습니다. 대신 이런 콘텐츠는 어떠세요?"
💡 에러 페이지에서도 Google Analytics나 Sentry에 이벤트를 전송하여 얼마나 많은 사용자가 에러를 겪는지 모니터링하세요. 단, 무한 루프를 방지하기 위해 localStorage에 플래그를 저장하여 중복 전송을 막으세요.
💡 배포 시 계획된 다운타임이라면 503 상태 코드와 Retry-After 헤더를 반환하는 것이 좋습니다: add_header Retry-After 600; (10분 후 재시도) 검색 엔진 크롤러가 이를 인식하여 SEO에 부정적 영향을 최소화합니다.