출발 — Next.js 도입을 결정한 후 진짜 막막한 것
Laravel + Blade로 10년 살았다. Next.js를 도입하자고 결정한 다음, 정작 막힌 건 코드가 아니라 어디에 띄울까였다.
기존 머릿속 모델:
브라우저 → nginx → PHP-FPM → Laravel → 화면 렌더 → 응답
Next.js를 추가하면 이 그림이 어떻게 바뀌나? “Laravel 안에 Next 폴더 하나 두면 되는 거 아닌가?” 까지 생각했다가 멈췄다.
SSR 시대의 진짜 그림은 다르다.
브라우저 → nginx → Next/Nuxt 서버 → Laravel API → 응답
Laravel은 화면을 렌더링하지 않는다. 웹 사용자가 보는 웹 프론트 서버는 Next/Nuxt다. Laravel은 API만 제공한다.
이 패러다임 전환 자체가 PHP 시니어에게 가장 어렵다. Laravel이 다 처리하는 줄 알았는데가 깨지는 순간이다.
이 글의 범위
- 단일 nginx vs dual nginx vs Caddy vs FrankenPHP 4가지 옵션 비교
- try_files / proxy_pass / fastcgi_pass — PHP 개발자가 헷갈리는 nginx 3분기
- 상황별 결정 매트릭스
- 추천 진화 경로
4가지 옵션 매트릭스
옵션 1 — 단일 nginx (gateway + Laravel FastCGI 통합)
브라우저
↓
nginx (단일)
├─ / → Next/Nuxt:3000 (proxy_pass)
└─ /api/* → php-fpm:9000 (fastcgi_pass)
같은 nginx 하나가 reverse proxy도 하고 Laravel FastCGI도 처리한다.
장점: 컨테이너 수 적음, 자원 경제. 단점: 설정 복잡. 같은 nginx가 Next proxy와 Laravel FastCGI를 동시에 알아야 함. Laravel public 디렉터리도 nginx 컨테이너에 마운트되어야 함. 디버깅 어려움.
옵션 2 — Dual nginx (gateway + Laravel nginx + php-fpm)
브라우저
↓
gateway nginx
├─ / → frontend (Next/Nuxt)
└─ /api/* → laravel-nginx
↓
php-fpm:9000 → Laravel
장점: 책임 분리 명확. gateway가 외부 라우팅만, laravel-nginx가 Laravel 처리만. 설정 단순. 디버깅 쉬움. 전환기에 가장 안전. 단점: nginx 인스턴스 2개 (Docker 컨테이너 2개).
옵션 3 — Caddy gateway + Laravel nginx
브라우저
↓
Caddy (gateway)
├─ / → frontend
└─ /api/* → laravel-nginx → php-fpm
Caddy로 외부 진입 + HTTPS 자동 + reverse proxy. Laravel은 기존 nginx + php-fpm 유지.
장점: HTTPS 자동 발급/갱신, 설정 짧음, HTTP/3 대응 편함. 단점: 팀에 Nginx 운영 경험 많으면 Caddy가 낯섦. 고트래픽 튜닝은 Nginx 자료가 더 많음.
도메인 분리하면 더 단순해진다:
www.example.com → Caddy → frontend
api.example.com → Caddy → laravel-nginx → php-fpm
옵션 4 — FrankenPHP (Octane worker mode)
브라우저
↓
Caddy (gateway)
├─ / → frontend
└─ /api/* → FrankenPHP (Laravel + Octane)
FrankenPHP가 Caddy 기반 + PHP 실행기 통합. Laravel을 Octane worker mode로 실행.
장점: Laravel 한 번 boot 후 메모리 유지 → API latency·처리량 큰 폭 개선. nginx + php-fpm 두 단계가 한 단계로. 단점: Worker mode 위험 — static 변수 누적, singleton에 Request 주입, 전역 상태, 메모리 누수, 일부 PHP extension 호환성 문제. 코드 검증 안 된 상태에서 도입은 위험.
Laravel 공식 문서도 Octane에서 stale request/container, global state, memory leak을 조심하라고 명시한다. (Laravel Octane 문서)
헷갈리는 nginx 3분기 — try_files / proxy_pass / fastcgi_pass
PHP 개발자가 가장 자주 혼동하는 셋. 같아 보이는데 다 다르다.
try_files
같은 nginx 안에서 로컬 파일/내부 경로 확인.
location / {
try_files $uri $uri/ /index.php?$query_string;
}
$uri파일이 있나? → /css/app.css$uri/디렉터리가 있나? → /images/- 없으면 내부적으로 /index.php로 넘김 (Laravel 진입점)
proxy_pass
다른 HTTP 서버로 요청 전달. Reverse proxy.
location / {
proxy_pass http://127.0.0.1:3000;
}
Next/Nuxt 같은 Node 서버, 다른 컨테이너로 보낼 때.
fastcgi_pass
FastCGI 프로토콜로 php-fpm에 PHP 실행 요청 전달.
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
| 분기 | 어디로 | 프로토콜 |
|---|---|---|
| try_files | 같은 nginx 안 (파일 또는 내부 location) | 내부 |
| proxy_pass | 다른 HTTP 서버 (Node, 다른 nginx) | HTTP |
| fastcgi_pass | php-fpm (또는 다른 FastCGI server) | FastCGI |
Laravel + Next 구조에서는 셋 다 등장한다:
# Next로 프록시
location / {
proxy_pass http://frontend:3000;
}
# Laravel API — try_files로 index.php에 보냄
location /api/ {
try_files $uri /index.php?$query_string;
}
# index.php는 fastcgi_pass로 php-fpm에 넘김
location ~ \.php$ {
fastcgi_pass app:9000;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
상황별 결정 매트릭스
| 상황 | 추천 |
|---|---|
| 마이그레이션 전환기, 팀 Nginx 익숙 | 옵션 2 (Dual nginx) |
| 신규 프로젝트, HTTPS 자동화 원함 | 옵션 3 (Caddy gateway) |
| 고트래픽 API, Octane worker 검증 끝남 | 옵션 4 (FrankenPHP) |
| 컨테이너 수 최소화 우선 | 옵션 1 (단일 nginx) |
| Bare metal, 한 서버 | 단일 nginx, server block 분리 |
추천 진화 경로 — 처음엔 단순하게, 안정 후 통합
옵션 2 (Dual nginx) → 옵션 3 (Caddy) → 옵션 4 (FrankenPHP + Octane)
바로 최고 성능 모드로 가지 않는다. 단계별로 안정화 후 다음 layer로 통합하는 게 안전하다. 특히 worker mode(옵션 4)는 static 변수·singleton·전역 상태 같은 코드 함정이 남아 있을 가능성이 있어, 코드베이스 검증이 끝난 후에 도입한다.
Bare metal — 한 서버에서는 어떻게
Docker 없이 한 서버에서 띄우면 nginx 인스턴스를 두 개 못 띄운다 (같은 80 포트). server block 분리로 충분하다:
server {
server_name www.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
}
}
server {
server_name api.example.com;
root /var/www/laravel/public;
location / {
try_files $uri /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
}
이 경우 nginx 프로세스는 하나, server block만 둘.
결론
Laravel + Next SSR 배포의 핵심은 Laravel이 더 이상 화면을 렌더링하지 않는다는 패러다임 전환이다. 그 전환이 박히면 4가지 옵션 중 하나는 자연스럽게 결정된다.
전환기에는 Dual nginx가 안전. 안정 후 Caddy 또는 FrankenPHP로 통합 가능. 바로 최고 모드로 가지 마라 — Laravel 마이그레이션 프로젝트에서 worker mode 위험은 코드 검증 후에만 감수할 일이다.