출발 — 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;
}
  1. $uri 파일이 있나? → /css/app.css
  2. $uri/ 디렉터리가 있나? → /images/
  3. 없으면 내부적으로 /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_passphp-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 위험은 코드 검증 후에만 감수할 일이다.

참고