“#nginx가 node 서버 앞단에 없다면, 아마 잘못하고 있는 겁니다.”– Bryan Hughes의 Twitter 게시물
Node.js는 세계에서 가장 인기 있는 프로그래밍 언어인 JavaScript로 서버 애플리케이션을 개발하는 데 가장 널리 사용되는 도구입니다. 웹 서버와 애플리케이션 서버의 기능을 모두 제공하는 Node.js는 이제 모든 종류의 마이크로서비스 기반 개발 및 배포에 필수적인 도구로 여겨집니다. (Forrester의 무료 Node.js 및 NGINX 보고서를 다운로드하세요.)
Node.js는 백엔드 애플리케이션 개발에서 Java 또는 .NET을 대체하거나 보완할 수 있습니다.
Node.js는 단일 스레드 방식으로 작동하며 논블로킹 I/O를 사용하므로 확장성이 뛰어나고 수만 건의 동시 작업을 지원할 수 있습니다. 이러한 아키텍처 특성은 NGINX와 동일하며, NGINX 역시 10,000개 이상의 동시 연결을 지원하는 C10K 문제를 해결하기 위해 개발되었습니다。 Node.js는 높은 성능과 개발자 생산성으로 잘 알려져 있습니다.
그렇다면, 대체 무엇이 잘못될 수 있을까요?
Node.js에는 몇 가지 약점과 취약점이 있어 Node.js 기반 시스템의 성능이 저하되거나 심지어 충돌할 수도 있습니다. 특히 Node.js 기반 웹 애플리케이션의 트래픽이 급증할 때 이러한 문제가 더 자주 발생합니다.
또한 Node.js는 웹 페이지의 핵심적인 동적 콘텐츠를 생성하는 애플리케이션 로직을 개발하고 실행하는 데 매우 유용한 도구입니다. 하지만 이미지나 JavaScript 파일 같은 정적 콘텐츠를 제공하거나 여러 서버에 걸쳐 부하를 분산하는 데는 그다지 적합하지 않습니다.
Node.js를 최대한 활용하려면 정적 콘텐츠를 캐시하고, 여러 애플리케이션 서버를 대상으로 프록시 및 부하 분산을 수행하며, 클라이언트와 Node.js, 그리고 Socket.IO를 실행하는 서버와 같은 보조 구성 요소 간의 포트 경합을 관리해야 합니다. NGINX는 이러한 모든 용도로 사용할 수 있으므로 Node.js 성능 튜닝에 매우 유용한 도구입니다.
다음 팁을 활용하여 Node.js 애플리케이션의 성능을 개선하세요:
참고: Node.js 애플리케이션의 성능을 빠르게 개선하려면 최신 멀티 코어 서버를 활용할 수 있도록 Node.js 구성을 변경하세요. 자세한 내용은 Node.js 문서에서 Node.js가 웹 서버의 CPU 수와 동일한 개수의 개별 자식 프로세스를 생성하도록 설정하는 방법을 확인하세요. 그러면 각 프로세스가 마치 마법처럼 각각 하나의 CPU에만 할당되어 성능이 크게 향상됩니다.
팁 1 – 리버스 프록시 서버 구현
NGINX, Inc.는 고성능 사이트의 핵심에서 사용되는 애플리케이션 서버가 인터넷에서 유입되는 트래픽에 직접 노출된 모습을 볼 때마다 다소 경악하곤 합니다. 여기에는 많은 WordPress 기반 사이트와 Node.js 사이트 등이 포함됩니다.
Node.js는 대부분의 애플리케이션 서버보다 확장성을 더욱 중점적으로 고려해 설계되었으며, 웹 서버 측에서도 상당한 양의 인터넷 트래픽을 무리 없이 처리할 수 있습니다. 하지만 웹 서비스는 Node.js의 존재 이유가 아니며, 본래의 주된 개발 목적도 아닙니다.
트래픽이 많은 사이트를 운영하는 경우, 애플리케이션 성능을 높이기 위한 첫 번째 단계는 Node.js 서버 앞에 리버스 프록시 서버를 배치하는 것입니다. 이렇게 하면 Node.js 서버가 인터넷 트래픽에 직접 노출되지 않도록 보호할 수 있으며, 여러 애플리케이션 서버 사용, 서버 간 부하 분산, 콘텐츠 캐싱을 매우 유연하게 구성할 수 있습니다.

기존 서버 구성 앞단에 NGINX를 리버스 프록시 서버로 배치한 후 추가 용도로 활용하는 것은 NGINX의 핵심 사용 사례로, 전 세계 수천만 개의 웹사이트에서 구현되고 있습니다.
다음과 같은 구체적인 이점이 있습니다: NGINX를 Node.js 리버스 프록시 서버로 사용하면, including:
- 권한 처리 및 포트 할당 간소화
- 정적 이미지를 더 효율적으로 제공하기(참고: 다음 팁)
- Node.js 충돌을 성공적으로 관리하기
- DoS 공격 완화
참고: 이 튜토리얼에서는 Ubuntu 14.04 또는 CentOS 환경에서 NGINX를 리버스 프록시 서버로 사용하는 방법을 설명하며, Node.js 앞단에 NGINX를 배치하려는 모든 사용자에게 유용한 개요를 제공합니다.
팁 2 – 정적 파일 캐싱
Node.js 기반 사이트의 사용량이 증가하면 서버에 부하가 나타나기 시작합니다. 이 시점에서 해야 할 일은 두 가지입니다:
- Node.js 서버를 최대한 활용하기
- 애플리케이션 서버를 쉽게 추가하고 서버 간에 부하를 분산할 수 있도록 합니다
사실 이 작업은 쉽게 수행할 수 있습니다. 먼저 이전 팁에서 설명한 대로 NGINX를 리버스 프록시 서버로 구현하세요. 그러면 캐싱, 부하 분산(Node.js 서버가 여러 대인 경우) 등을 쉽게 구현할 수 있습니다.
애플리케이션 컨테이너 플랫폼인 Modulus의 웹사이트에는 NGINX를 사용해 Node.js 애플리케이션의 성능을 대폭 향상하는 방법을 다룬 유용한 글이 있습니다. Node.js가 모든 작업을 단독으로 처리했을 때 작성자의 사이트는 초당 평균 약 900개의 요청을 처리할 수 있었습니다. NGINX를 리버스 프록시 서버로 사용해 정적 콘텐츠를 제공하자 동일한 사이트에서 초당 1,600개가 넘는 요청을 처리해 성능이 거의 2배 향상되었습니다.
성능을 두 배로 높이면 사이트 디자인을 검토하고 필요에 따라 개선하거나, 애플리케이션 코드를 최적화하고, 애플리케이션 서버를 추가로 배포하는 등 향후 성장에 대비하기 위한 추가 조치를 취할 시간을 확보할 수 있습니다.
다음은 Modulus에서 실행되는 웹사이트에 적용할 수 있는 구성 코드입니다:
server { listen 80;
server_name static-test-47242.onmodulus.net;
root /mnt/app;
index index.html index.htm;
location /static/ {
try_files $uri $uri/ =404;
}
location /api/ {
proxy_pass http://node-test-45750.onmodulus.net;
}
}NGINX, Inc.의 Patrick Nommensen이 작성한 이 상세한 글에서는 Node.js 애플리케이션인 오픈 소스 블로깅 플랫폼 Ghost에서 운영되는 개인 블로그의 정적 콘텐츠를 캐싱하는 방법을 설명합니다. 일부 내용은 Ghost에만 해당하지만, 코드의 상당 부분은 다른 Node.js 애플리케이션에도 재사용할 수 있습니다.
예를 들어 NGINX의 location 블록에서는 일부 콘텐츠가 캐시되지 않도록 제외하는 것이 좋습니다. 일반적으로 블로그 플랫폼의 관리 인터페이스는 캐시하지 않는 것이 좋습니다. 다음은 Ghost 관리 인터페이스의 캐싱을 비활성화하거나 캐시 대상에서 제외하는 구성 코드입니다:
location ~ ^/(?:ghost|signout) { proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_pass http://ghost_upstream;
add_header Cache-Control "no-cache, private, no-store,
must-revalidate, max-stale=0, post-check=0, pre-check=0";
}정적 콘텐츠 제공에 대한 일반적인 정보는 NGINX Plus 관리자 가이드를 참조하세요. 관리자 가이드에는 구성 지침, 파일 검색 성공 또는 실패 시 대응할 수 있는 다양한 옵션, 그리고 성능을 더욱 향상하기 위한 최적화 방법이 포함되어 있습니다.
NGINX 서버에서 정적 파일을 캐싱하면 Node.js 애플리케이션 서버의 작업 부하가 크게 줄어들어 훨씬 더 높은 성능을 달성할 수 있습니다.
팁 3 – Node.js 로드 밸런서 구현
Node.js 애플리케이션에서 높은, 즉 거의 무제한에 가까운 성능을 구현하는 진정한 핵심은 여러 애플리케이션 서버를 실행하고 모든 서버에 부하를 분산하는 것입니다.
Node.js는 웹 브라우저에서 실행되는 JavaScript 코드와 Node.js 애플리케이션 서버에서 실행되는 JavaScript 코드 간에 높은 수준의 상호 작용을 지원하고 JSON 객체를 데이터 교환 매개체로 사용하므로, Node.js의 부하 분산은 특히 까다로울 수 있습니다. 이는 특정 클라이언트 세션이 하나의 특정 애플리케이션 서버에서 계속 실행된다는 의미이며, 여러 애플리케이션 서버를 사용할 경우 세션 지속성을 유지하기가 본질적으로 어렵습니다.
인터넷과 웹의 주요 장점 중 하나는 높은 수준의 무상태성입니다. 즉, 요청된 파일에 액세스할 수 있는 서버라면 어느 서버든 클라이언트 요청을 처리할 수 있습니다. Node.js는 이러한 무상태성에 반하는 방식으로 작동하며, 특정 클라이언트의 요청에 항상 동일한 서버가 응답하는 상태 유지 환경에서 가장 효과적으로 작동합니다.
이 요구 사항은 NGINX Open Source보다 NGINX Plus를 사용할 때 가장 효과적으로 충족할 수 있습니다. 두 NGINX 버전은 매우 유사하지만, 지원하는 부하 분산 알고리즘이 다르다는 중요한 차이점이 있습니다.
NGINX는 상태 비저장 부하 분산 방식을 지원합니다:
- 라운드 로빈 – 새 요청이 목록의 다음 서버로 전달됩니다.
- 최소 연결 – 새 요청은 활성 연결 수가 가장 적은 서버로 전달됩니다.
- IP 해시 – 새 요청은 클라이언트의 IP 주소 해시값에 따라 할당된 서버로 전달됩니다.
이러한 방식 중 IP Hash만이 특정 클라이언트의 요청을 동일한 서버로 안정적으로 전송하므로 Node.js 애플리케이션에 유리합니다. 그러나 이 블로그 게시물에서 설명하는 부하 분산 기법처럼, IP Hash를 사용하면 다른 서버에 할당될 요청까지 특정 서버 하나에 지나치게 집중되기 쉽습니다. 이 방식은 상태 유지를 지원하지만, 서버 리소스 간 요청 분배가 최적화되지 않을 수 있습니다.
NGINX와 달리 NGINX Plus는 세션 지속성을 지원합니다. 세션 지속성을 사용하면 특정 클라이언트의 모든 요청이 항상 동일한 서버로 전달됩니다. 클라이언트와 서버 간의 상태 저장 통신을 지원하는 Node.js의 장점과 고급 부하 분산 기능을 제공하는 NGINX Plus의 장점을 모두 극대화할 수 있습니다.
따라서 NGINX 또는 NGINX Plus를 사용하여 여러 Node.js 서버 간의 부하 분산을 지원할 수 있습니다. 하지만 최고의 부하 분산 성능과 Node.js에 적합한 상태 유지 기능을 모두 구현하려면 NGINX Plus를 사용하는 것이 좋습니다. NGINX Plus에 기본 제공되는 애플리케이션 상태 검사 및 모니터링 기능도 이 경우에 유용합니다.
NGINX Plus는 세션 드레이닝도 지원합니다. 이 기능을 사용하면 애플리케이션 서버가 서비스 중단을 요청한 후에도 현재 세션을 정상적으로 완료할 수 있습니다.
팁 4 – WebSocket 연결 프록시
모든 버전의 HTTP는 클라이언트가 서버에 파일을 요청하는 “풀(pull)” 통신을 위해 설계되었습니다. WebSocket은 서버가 클라이언트가 요청하지 않은 파일도 선제적으로 전송할 수 있는 “푸시(push)” 및 “푸시/풀(push/pull)” 통신을 지원하는 도구입니다.
The WebSocket 프로토콜을 사용하면 전송되는 데이터의 양과 지연 시간을 최소화하면서 클라이언트와 서버 간의 더욱 안정적인 상호 작용을 쉽게 지원할 수 있습니다. 필요한 경우 클라이언트와 서버가 모두 필요에 따라 요청을 시작하고 수신하는 전이중 연결을 구현할 수 있습니다.
WebSocket 프로토콜은 강력한 JavaScript 인터페이스를 제공하므로 Node.js를 애플리케이션 서버로 사용하는 데 적합하며, 트랜잭션 규모가 중간 정도인 웹 애플리케이션에서는 웹 서버로도 적합합니다. 트랜잭션 규모가 증가하면 클라이언트와 Node.js 웹 서버 사이에 NGINX를 배치하고, NGINX 또는 NGINX Plus를 사용하여 정적 파일을 캐시하고 여러 애플리케이션 서버 간에 부하를 분산하는 것이 좋습니다.
Node.js는 Node.js 애플리케이션과 함께 널리 사용되는 WebSocket API인 Socket.IO와 함께 사용되는 경우가 많습니다. 이로 인해 포트 80(HTTP용) 또는 포트 443(HTTPS용)이 상당히 혼잡해질 수 있으며, 해결 방법은 Socket.IO 서버로 프록시하는 것입니다. 위에서 설명한 대로 NGINX를 프록시 서버로 사용할 수 있으며, 정적 파일 캐싱, 로드 밸런싱 등의 추가 기능도 활용할 수 있습니다.

다음은 포트 5000에서 수신 대기하는 Node 애플리케이션 파일 server.js의 코드입니다. 이 애플리케이션은 웹 서버가 아닌 프록시 서버로 작동하며 요청을 적절한 포트로 라우팅합니다:
var io = require('socket.io').listen(5000);
io.sockets.on('connection', function (socket) {
socket.on('set nickname', function (name) {
socket.set('nickname', name, function () {
socket.emit('ready');
});
});
socket.on('msg', function () {
socket.get('nickname', function (err, name) {
console.log('Chat message by ', name);
});
});
});Within your index.html 파일에 다음 코드를 추가하여 서버 애플리케이션에 연결하고 애플리케이션과 사용자의 브라우저 간에 WebSocket을 생성합니다:
<script src="/socket.io/socket.io.js"></script><script>// <![CDATA[
var socket = io(); // your initialization code here.
// ]]>
</script>NGINX 구성을 포함한 전체 지침은 NGINX 및 NGINX Plus를 Node.js 및 Socket.IO와 함께 사용하는 방법에 관한 블로그 게시물을 참조하세요. 이러한 종류의 웹 애플리케이션에서 발생할 수 있는 아키텍처 및 인프라 문제에 대해 더 자세히 알아보려면 실시간 웹 애플리케이션 및 WebSocket에 관한 블로그 게시물을 참조하세요.
팁 5 – SSL/TLS 및 HTTP/2 구현
점점 더 많은 사이트에서 SSL/TLS를 사용하여 사이트 내 모든 사용자 상호작용을 보호하고 있습니다. 이러한 전환 여부와 시점은 여러분이 결정할 사항이지만, 전환하기로 했다면 NGINX는 다음 두 가지 방식으로 이를 지원합니다:
- NGINX를 리버스 프록시로 설정한 후에는 NGINX에서 클라이언트와의 SSL/TLS 연결을 종료할 수 있습니다. Node.js 서버는 NGINX 리버스 프록시 서버와 암호화되지 않은 요청 및 콘텐츠를 주고받습니다.
- 초기 징후에 따르면 새로운 버전의 HTTP 프로토콜인 HTTP/2를 사용하면 SSL/TLS 사용으로 인해 발생하는 성능 저하를 상당 부분 또는 완전히 상쇄할 수 있습니다. NGINX는 HTTP/2를 지원하며 SSL/TLS와 함께 HTTP/2 연결도 종료할 수 있으므로, Node.js 애플리케이션 서버를 변경할 필요가 없습니다.
구현을 위해 수행해야 할 단계에는 Node.js 구성 파일의 URL 업데이트, NGINX 구성에서 보안 연결 설정 및 최적화, 필요한 경우 SPDY 또는 HTTP/2 사용 등이 있습니다. HTTP/2 지원을 추가하면 HTTP/2를 지원하는 브라우저 버전은 새 프로토콜을 사용하여 애플리케이션과 통신하고, 이전 브라우저 버전은 계속 HTTP/1.x를 사용합니다.

다음 구성 코드는 여기에 설명된 대로 SPDY를 사용하는 Ghost 블로그를 위한 것입니다. 여기에는 OCSP 스테이플링과 같은 고급 기능이 포함되어 있습니다. OCSP 스테이플링 옵션을 비롯해 SSL 종료에 NGINX를 사용할 때의 고려 사항은 여기를 참조하세요. 동일한 주제에 대한 전반적인 개요는 여기를 참조하세요.
Node.js 애플리케이션을 구성하고 SPDY에서 HTTP/2로 업그레이드하려면 지금 또는 SPDY 지원이 종료되는 2016년 초에 몇 가지 사소한 변경만 하면 됩니다.
server { server_name domain.com;
listen 443 ssl spdy;
spdy_headers_comp 6;
spdy_keepalive_timeout 300;
keepalive_timeout 300;
ssl_certificate_key /etc/nginx/ssl/domain.key;
ssl_certificate /etc/nginx/ssl/domain.crt;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 24h;
ssl_buffer_size 1400;
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/ssl/trust.crt;
resolver 8.8.8.8 8.8.4.4 valid=300s;
add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains';
add_header X-Cache $upstream_cache_status;
location / {
proxy_cache STATIC;
proxy_cache_valid 200 30m;
proxy_cache_valid 404 1m;
proxy_pass http://ghost_upstream;
proxy_ignore_headers X-Accel-Expires Expires Cache-Control;
proxy_ignore_headers Set-Cookie;
proxy_hide_header Set-Cookie;
proxy_hide_header X-powered-by;
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 https;
proxy_set_header Host $http_host;
expires 10m;
}
location /content/images {
alias /path/to/ghost/content/images;
access_log off;
expires max;
}
location /assets {
alias /path/to/ghost/themes/uno-master/assets;
access_log off;
expires max;
}
location /public {
alias /path/to/ghost/built/public;
access_log off;
expires max;
}
location /ghost/scripts {
alias /path/to/ghost/core/built/scripts;
access_log off;
expires max;
}
location ~ ^/(?:ghost|signout) {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_pass http://ghost_upstream;
add_header Cache-Control "no-cache, private, no-store,
must-revalidate, max-stale=0, post-check=0, pre-check=0";
proxy_set_header X-Forwarded-Proto https;
}
}
결론
이 블로그 게시물에서는 Node.js 애플리케이션에 적용할 수 있는 가장 중요한 성능 개선 방법 몇 가지를 설명합니다. 특히 NGINX를 역방향 프록시 서버로 사용하고, 정적 파일을 캐싱하며, 부하를 분산하고, WebSocket 연결을 프록시하고, SSL/TLS 및 HTTP/2 프로토콜을 종단 처리하는 등 Node.js와 함께 애플리케이션 구성에 NGINX를 추가하는 방법을 중점적으로 다룹니다.
NGINX와 Node.js의 조합은 널리 인정받고 있으며 새로운 마이크로서비스 친화적 애플리케이션을 만들거나 Java 또는 Microsoft .NET을 사용하는 기존 SOA 기반 애플리케이션에 유연성과 기능을 더하는 방법으로 활용됩니다. 이 글에서는 Node.js 애플리케이션을 최적화하고, 원한다면 Node.js와 NGINX의 조합을 실제로 구현하는 방법을 알아봅니다.
작성자 소개

관련 블로그 게시물
시청하기: KubeCon North America 2023에서의 NGINX Gateway Fabric
이 영상에서는 KubeCon의 NGINX 부스를 찾아가, 저희가 공개한 Kubernetes Gateway API 구현을 살펴봅니다.
F5 NGINX로 대규모 WAF 플릿을 확인하고 관리하는 2가지 방법
앱 보안을 강화하는 두 가지 방법을 소개합니다. NGINX Management Suite를 통해 WAF 운영에 대한 가시성을 높이고, NGINX App Protect WAF를 활용해 코드형 보안을 구현하는 것입니다.
Ingress 컨트롤러 선택 가이드, 4부: NGINX Ingress Controller 옵션
개발 주체, 개발 철학, 프로덕션 환경 준비도, 보안 및 지원을 기준으로 어떤 NGINX Ingress 컨트롤러가 가장 적합한지 알아보세요.
진화하는 공격 환경에 NGINX App Protect Denial of Service가 적응하는 방법
NGINX App Protect Denial of Service(DoS)가 계층 7 DoS 공격을 방어하기 위해 완화 방법을 실시간으로 조정하는 방식을 설명합니다. 정상적인 사용자 행동과 서버 상태에 대한 모델을 생성하여 공격을 나타내는 이상 징후를 감지하고 맞춤형 완화 시그니처를 적용할 수 있습니다.
NGINX Instance Manager를 활용한 대규모 구성 업데이트 및 인증서 관리
NGINX Instance Manager 버전 1.0에는 두 가지 새로운 기능이 도입되었습니다. NGINX 인스턴스에 태그와 사용자 역할을 지정하면 전체 그룹에 구성을 한 번에 적용할 수 있습니다. 인증서 검사는 만료 예정인 인증서를 감지하여 서비스 중단을 방지할 수 있도록 인증서를 교체할 수 있게 해줍니다.
Netflix가 자사 CDN의 핵심으로 NGINX를 선택한 이유
Netflix가 글로벌 콘텐츠 전송 네트워크인 Open Connect의 스트리밍 미디어 기술로 NGINX를 선택한 이유를 NGINX 개발자 Gleb Smirnoff에게서 알아보세요.