1. Nginx는 왜 빠른가: 이벤트 기반 아키텍처
Nginx가 등장한 배경에는 C10K 문제(동시 접속 1만 개를 어떻게 처리할까)가 있습니다. 당시 주류였던 Apache(prefork/worker MPM)는 연결 하나당 프로세스나 스레드 하나를 배정했습니다. 동시 접속이 늘면 메모리와 컨텍스트 스위칭 비용이 폭증합니다.
Nginx는 다른 길을 택했습니다.
┌──────────────────┐
│ master process │ ← root 권한, 설정 로드, 포트 바인딩, 워커 관리
└────────┬─────────┘
┌──────────────┼──────────────┐
┌──────▼─────┐ ┌──────▼─────┐ ┌──────▼─────┐
│ worker 1 │ │ worker 2 │ │ worker N │ ← 일반 권한, 실제 요청 처리
│ (event loop)│ │ (event loop)│ │ (event loop)│
└────────────┘ └────────────┘ └────────────┘
수천 개 연결 수천 개 연결 수천 개 연결- 마스터 프로세스: 설정 파일을 읽고, 80/443 포트를 열고, 워커를 생성·관리합니다. 요청은 직접 처리하지 않습니다.
- 워커 프로세스: 보통 CPU 코어 수만큼 띄웁니다. 각 워커는 싱글 스레드 이벤트 루프로
epoll(Linux)/kqueue(BSD, macOS) 같은 OS의 I/O 멀티플렉싱을 이용해 수천 개의 연결을 동시에 처리합니다. - 논블로킹 I/O: 어떤 연결이 데이터를 기다리는 동안 워커는 멈추지 않고 다른 연결을 처리합니다.
Node.js의 이벤트 루프를 아는 분이라면 익숙한 모델입니다. 차이는 Nginx가 이것을 C로, 여러 프로세스에 걸쳐 수행한다는 점입니다.
이 구조 덕분에 가능한 기능이 하나 더 있습니다. 무중단 설정 리로드입니다. nginx -s reload를 하면 마스터가 새 설정으로 새 워커를 띄우고, 기존 워커는 처리 중인 요청을 마무리한 뒤 종료(graceful shutdown)합니다.
그 외에도 캐시를 관리하는
cache manager,cache loader프로세스가 있습니다. 4편 캐싱에서 다시 만납니다.
2. 설치
Ubuntu/Debian
배포판 기본 저장소의 패키지는 버전이 다소 오래될 수 있습니다. 최신 버전이 필요하면 nginx.org 공식 저장소를 등록하는 것을 권장합니다.
# 간단하게: 배포판 패키지
sudo apt update
sudo apt install nginx
# 공식 저장소 (stable/mainline 선택 가능)
sudo apt install curl gnupg2 ca-certificates lsb-release ubuntu-keyring
curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor \
| sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
http://nginx.org/packages/ubuntu $(lsb_release -cs) nginx" \
| sudo tee /etc/apt/sources.list.d/nginx.list
sudo apt update && sudo apt install nginxstable vs mainline: Nginx는 짝수 마이너 버전(예: 1.28.x)이 stable, 홀수(1.29.x)가 mainline입니다. 이름과 달리 mainline도 충분히 안정적이며, 공식적으로는 새 기능과 버그 수정이 먼저 들어가는 mainline 사용을 권장합니다.
macOS
brew install nginx
# 설정 파일: /opt/homebrew/etc/nginx/nginx.conf (Apple Silicon 기준)Docker
docker run -d --name web -p 8080:80 \
-v $(pwd)/html:/usr/share/nginx/html:ro \
nginx:stable-alpine설치 확인
nginx -v # 버전
nginx -V # 버전 + 컴파일 옵션 + 포함된 모듈 (중요!)
systemctl status nginx
curl -I localhostnginx -V 출력의 --with-http_v2_module, --with-http_ssl_module 같은 항목은 어떤 기능이 컴파일되어 있는지 알려줍니다. Nginx는 대부분의 기능이 빌드 시점에 결정된다는 점을 기억해 두세요.
3. 디렉터리 구조
배포판마다 조금 다르지만, Debian/Ubuntu 계열 기준으로는 다음과 같습니다.
/etc/nginx/
├── nginx.conf # 메인 설정 파일 (진입점)
├── mime.types # 확장자 → Content-Type 매핑
├── conf.d/ # *.conf 파일이 자동 include 됨 (공식 패키지 방식)
│ └── default.conf
├── sites-available/ # 사이트별 설정 보관 (Debian 계열 관례)
├── sites-enabled/ # 활성화할 사이트의 심볼릭 링크
└── snippets/ # 재사용 조각
/var/log/nginx/
├── access.log
└── error.log
/var/www/html/ # 기본 문서 루트 (공식 패키지는 /usr/share/nginx/html)sites-available / sites-enabled는 Nginx 자체 기능이 아니라 Debian의 관례입니다. nginx.conf 안에 include /etc/nginx/sites-enabled/*;가 있기 때문에 동작합니다. 공식 nginx.org 패키지는 conf.d/*.conf만 씁니다. 어느 쪽이든 한 가지 방식으로 통일하는 게 좋습니다.
# Debian 방식으로 사이트 활성화
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx4. 설정 문법: 지시어와 컨텍스트
Nginx 설정은 딱 두 가지 요소로 이루어집니다.
- 단순 지시어(simple directive):
이름 값;— 세미콜론으로 끝납니다. - 블록 지시어(block directive):
이름 { ... }— 중괄호 안에 다른 지시어를 담습니다. 블록이 다른 지시어를 담으면 컨텍스트(context)라고 부릅니다.
# ─── main 컨텍스트 (최상위) ───
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /run/nginx.pid;
events { # ─── events 컨텍스트 ───
worker_connections 1024;
}
http { # ─── http 컨텍스트 ───
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
server { # ─── server 컨텍스트 (가상 호스트) ───
listen 80;
server_name localhost;
location / { # ─── location 컨텍스트 ───
root /usr/share/nginx/html;
index index.html index.htm;
}
}
include /etc/nginx/conf.d/*.conf;
}컨텍스트 계층을 그림으로 보면 이렇습니다.
main
├── events
├── http
│ ├── upstream
│ └── server
│ └── location
│ └── location (중첩 가능)
└── stream (L4 프록시, 4편)
└── server상속 규칙: 바깥에서 안으로
대부분의 지시어는 바깥 컨텍스트에서 선언하면 안쪽이 물려받고, 안쪽에서 다시 선언하면 덮어씁니다.
http {
gzip on; # 모든 server가 상속
server {
root /var/www/site; # 이 server의 모든 location이 상속
location /legacy/ {
gzip off; # 여기서만 덮어씀
}
}
}⚠️ 단, 배열형 지시어(add_header, proxy_set_header 등)는 함정이 있습니다. 안쪽에서 하나라도 선언하면 바깥 것을 전부 상속받지 않습니다. 4편 보안 헤더 파트에서 자세히 다룹니다.
각 지시어는 정해진 컨텍스트에서만 쓸 수 있다
공식 문서의 각 지시어 항목에는 Context: http, server, location 같은 표시가 있습니다. 엉뚱한 곳에 쓰면 nginx -t에서 "xxx" directive is not allowed here 에러가 납니다. 처음엔 이 에러를 자주 보게 되는데, 문서의 Context 항목만 확인하면 바로 해결됩니다.
변수
Nginx에는 $로 시작하는 내장 변수가 많습니다. 자주 쓰는 것만 추려보면:
| 변수 | 의미 |
|---|---|
$host | 요청의 Host 헤더 (없으면 server_name) |
$uri | 정규화된 현재 URI (쿼리 스트링 제외) |
$request_uri | 원본 URI 전체 (쿼리 스트링 포함, 디코딩 안 됨) |
$args / $arg_name | 쿼리 스트링 전체 / 특정 파라미터 |
$remote_addr | 클라이언트 IP |
$scheme | http 또는 https |
$http_헤더이름 | 임의의 요청 헤더 (예: $http_user_agent) |
$status | 응답 상태 코드 (로그에서 사용) |
$request_time | 요청 처리 시간 (초, ms 정밀도) |
5. 첫 번째 사이트: 정적 파일 서빙
/etc/nginx/conf.d/mysite.conf를 만들어 봅시다.
server {
listen 80;
server_name mysite.local;
root /var/www/mysite;
index index.html;
access_log /var/log/nginx/mysite.access.log;
error_log /var/log/nginx/mysite.error.log warn;
location / {
try_files $uri $uri/ =404;
}
# 정적 자산은 브라우저 캐시를 길게
location ~* \.(css|js|png|jpg|jpeg|gif|svg|ico|woff2?)$ {
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
}
error_page 404 /404.html;
}sudo mkdir -p /var/www/mysite
echo '<h1>Hello Nginx</h1>' | sudo tee /var/www/mysite/index.html
sudo nginx -t && sudo systemctl reload nginx
curl -H "Host: mysite.local" http://localhostroot vs alias
초보자가 가장 많이 헷갈리는 부분입니다.
# root: location 경로를 "붙여서" 찾는다
location /images/ {
root /data;
}
# /images/cat.png → /data/images/cat.png
# alias: location 경로를 "대체해서" 찾는다
location /images/ {
alias /data/pictures/;
}
# /images/cat.png → /data/pictures/cat.pngalias를 쓸 때는 location과 alias 양쪽의 끝 슬래시를 맞추는 것이 중요합니다. location /images + alias /data/pictures/ 같은 불일치는 경로 조작(path traversal) 취약점의 원인이 되기도 합니다.
try_files: 순서대로 시도하기
try_files $uri $uri/ /index.html;$uri파일이 있으면 그것을 응답- 없으면
$uri/디렉터리(그 안의 index)를 시도 - 그래도 없으면 마지막 인자로 내부 리다이렉트
마지막 인자는 URI(/index.html)나 상태 코드(=404), 또는 이름 붙은 location(@backend)이 될 수 있습니다. React/Vue 같은 SPA는 위처럼 모든 경로를 index.html로 보내야 클라이언트 라우팅이 동작합니다.
6. 반드시 알아야 할 명령어
sudo nginx -t # 설정 문법 검사 (리로드 전에 무조건!)
sudo nginx -T # 검사 + include된 것까지 합친 전체 설정 출력
sudo nginx -s reload # 무중단 설정 리로드 (= systemctl reload nginx)
sudo nginx -s quit # 처리 중인 요청을 마치고 종료 (graceful)
sudo nginx -s stop # 즉시 종료
sudo nginx -s reopen # 로그 파일 다시 열기 (logrotate 후)습관처럼 쓰세요.
sudo nginx -t && sudo systemctl reload nginxnginx -t가 실패하면 리로드되지 않으므로, 오타 하나로 서비스 전체가 내려가는 사고를 막을 수 있습니다. 참고로 reload 자체도 새 설정이 잘못되면 기존 워커를 유지하지만, restart는 그렇지 않습니다. 운영 중에는 restart보다 reload입니다.
nginx -T는 트러블슈팅의 일등 공신입니다. include가 여러 겹이면 "도대체 어떤 설정이 적용되는 거지?"가 헷갈리는데, 이 명령 하나로 최종 설정을 한 화면에서 볼 수 있습니다.
7. 로그 읽기
sudo tail -f /var/log/nginx/access.log
sudo tail -f /var/log/nginx/error.log기본 combined 포맷의 access log 한 줄:
203.0.113.7 - - [06/Oct/2026:10:15:32 +0900] "GET /index.html HTTP/1.1" 200 612 "-" "curl/8.5.0"클라이언트IP - 사용자 [시간] "요청라인" 상태코드 바이트 "리퍼러" "UA" 순서입니다.
error log의 레벨은 debug, info, notice, warn, error, crit, alert, emerg 순으로, 지정한 레벨 이상만 기록됩니다. 문제를 찾을 때는 잠시 info로 낮춰보면 도움이 됩니다.
자주 만나는 에러:
| 에러 | 원인 |
|---|---|
bind() to 0.0.0.0:80 failed (98: Address already in use) | 이미 다른 프로세스(Apache, 다른 nginx)가 포트 사용 중 → sudo ss -tlnp | grep :80 |
open() "/var/www/..." failed (13: Permission denied) | 워커 실행 유저(nginx/www-data)가 파일 또는 상위 디렉터리에 접근 권한이 없음. SELinux 환경이면 컨텍스트도 확인 |
directive is not allowed here | 지시어를 허용되지 않는 컨텍스트에 작성 |
unknown directive "xxx" | 오타거나 해당 모듈이 컴파일되지 않음 → nginx -V 확인 |
정리
- Nginx는 마스터/워커 + 이벤트 루프 구조로 적은 자원으로 많은 연결을 처리하고, 무중단 리로드를 지원한다.
- 설정은 "지시어"와 "컨텍스트"의 계층 구조이며, 바깥에서 안쪽으로 상속된다.
root/alias,try_files는 정적 파일 서빙의 핵심이다.nginx -t && reload는 몸에 익혀야 할 습관이고,nginx -T는 최고의 디버깅 도구다.
다음 편에서는 Nginx의 진짜 주력 기능인 location 매칭 규칙, 리버스 프록시, 로드 밸런싱, HTTPS 설정을 다룹니다.