Next.js Custom Dev Server
Next.js는 next dev를 실행하면 내장 개발 서버가 뜬다. 대부분의 경우 이걸로 충분하지만, 특수한 상황에서는 서버를 직접 커스터마이징해야 할 때가 있다. 예를 들어 로컬 HTTPS가 필요하거나, 같은 네트워크의 모바일 기기에서 접속해야 하거나, WebSocket 서버를 개발 서버에 붙여야 하는 경우다.
이런 요구사항은 next dev의 CLI 옵션만으로는 해결하기 어렵다. Next.js가 제공하는 해결책이 Custom Dev Server다. Node.js의 http 또는 https 모듈로 서버를 직접 만들고, Next.js 앱을 그 위에 올리는 방식이다.
기본 구조
Custom Dev Server의 핵심은 next() 함수로 Next.js 앱 인스턴스를 만들고, app.getRequestHandler()로 요청 핸들러를 가져와서 Node.js 서버에 연결하는 것이다.
const { createServer } = require('http');
const { parse } = require('url');
const next = require('next');
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
app.prepare().then(() => {
createServer((req, res) => {
const parsedUrl = parse(req.url, true);
handle(req, res, parsedUrl);
}).listen(3000, (err) => {
if (err) throw err;
console.log('> Ready on http://localhost:3000');
});
});
동작 흐름을 보면:
next({ dev: true })로 개발 모드의 Next.js 앱을 초기화한다app.prepare()가 내부적으로 라우트 컴파일, 웹팩/Turbopack 설정 등을 준비한다- 준비가 끝나면 Node.js HTTP 서버를 생성하고, 들어오는 모든 요청을
handle()에 위임한다 handle()은 Next.js의 라우팅 시스템에 따라 페이지를 렌더링하거나 정적 파일을 서빙한다
parse(req.url, true)에서 두 번째 인자 true는 쿼리스트링을 파싱하라는 의미다. 이걸 handle에 전달해야 Next.js가 쿼리 파라미터를 올바르게 처리한다.
HTTPS 개발 서버
Custom Dev Server가 가장 많이 필요한 상황이 로컬 HTTPS다. OAuth 콜백이나 Service Worker, Secure Cookie 등은 HTTPS에서만 동작하기 때문에 개발 환경에서도 HTTPS가 필수인 경우가 있다.
const { createServer } = require('https');
const fs = require('fs');
const path = require('path');
const { parse } = require('url');
const next = require('next');
const app = next({ dev: true });
const handle = app.getRequestHandler();
const httpsOptions = {
key: fs.readFileSync(path.join(__dirname, 'certificates/localhost-key.pem')),
cert: fs.readFileSync(path.join(__dirname, 'certificates/localhost.pem')),
};
app.prepare().then(() => {
createServer(httpsOptions, (req, res) => {
const parsedUrl = parse(req.url, true);
handle(req, res, parsedUrl);
}).listen(3000, (err) => {
if (err) throw err;
console.log('> Ready on https://localhost:3000');
});
});
http.createServer 대신 https.createServer를 사용하고, SSL 인증서 파일 경로를 옵션으로 넘기면 된다. 로컬 인증서는 mkcert로 생성하는 것이 일반적이다.
# mkcert 설치 후
mkcert -install
mkcert localhost 127.0.0.1 youvico.dev
이렇게 하면 로컬 CA가 설치되면서 브라우저에서 신뢰하는 인증서가 만들어진다. 커스텀 도메인(youvico.dev 같은)을 사용하려면 /etc/hosts에 도메인을 127.0.0.1로 매핑하면 된다.
HTTP/HTTPS 모드 전환
개발 중에는 상황에 따라 HTTP와 HTTPS를 오갈 필요가 있다. HTTPS가 기본이지만, 모바일 기기에서 테스트할 때는 자체 서명 인증서 문제로 HTTP가 더 편할 수 있다. 환경 변수로 모드를 전환하는 패턴이 실용적이다.
const { createServer: createHttpsServer } = require('https');
const { createServer: createHttpServer } = require('http');
const next = require('next');
const fs = require('fs');
const path = require('path');
const { parse } = require('url');
const useHttp = process.env.USE_HTTP === 'true';
const app = next({ dev: true });
const handle = app.getRequestHandler();
app.prepare().then(() => {
const handler = (req, res) => {
const parsedUrl = parse(req.url, true);
handle(req, res, parsedUrl);
};
if (useHttp) {
createHttpServer(handler).listen(3000, '0.0.0.0', () => {
console.log('> HTTP mode: http://localhost:3000');
});
} else {
const httpsOptions = {
key: fs.readFileSync(path.join(__dirname, 'certificates/key.pem')),
cert: fs.readFileSync(path.join(__dirname, 'certificates/cert.pem')),
};
createHttpsServer(httpsOptions, handler).listen(3000, '0.0.0.0', () => {
console.log('> HTTPS mode: https://localhost:3000');
});
}
});
# HTTPS 모드 (기본)
node server.dev.js
# HTTP 모드
USE_HTTP=true node server.dev.js
package.json의 스크립트에 등록해두면 더 편하다.
{
"scripts": {
"dev": "node server.dev.js",
"dev:http": "USE_HTTP=true node server.dev.js"
}
}
LAN IP 자동 감지
모바일 기기에서 개발 서버에 접속하려면 같은 네트워크에서 PC의 IP 주소로 접근해야 한다. 매번 ifconfig를 치는 건 번거로우니까, 서버 시작 시 LAN IP를 자동으로 감지해서 출력하면 편리하다.
const os = require('os');
function getLocalIp() {
const networkInterfaces = os.networkInterfaces();
// macOS는 주로 en0, Linux는 eth0이나 wlan0
const candidates = ['en0', 'en1', 'eth0', 'wlan0'];
for (const name of candidates) {
const interfaces = networkInterfaces[name];
if (!interfaces) continue;
for (const iface of interfaces) {
if (iface.family === 'IPv4' && !iface.internal) {
return iface.address;
}
}
}
return 'localhost';
}
os.networkInterfaces()는 시스템의 모든 네트워크 인터페이스 정보를 반환한다. 여기서 IPv4이면서 internal이 아닌(루프백이 아닌) 주소를 찾으면 그게 LAN IP다.
인터페이스 이름은 OS마다 다르다:
- macOS:
en0(Wi-Fi),en1(유선) - Linux:
eth0(유선),wlan0(Wi-Fi) - Windows WSL:
eth0
서버 시작 시 이 IP를 출력해주면 모바일에서 바로 접속할 수 있다.
app.prepare().then(() => {
const localIp = getLocalIp();
createServer(handler).listen(3000, '0.0.0.0', () => {
console.log(`> Local: https://localhost:3000`);
console.log(`> Network: https://${localIp}:3000`);
});
});
'0.0.0.0'으로 바인딩하는 것이 중요하다. localhost나 127.0.0.1로 바인딩하면 외부 기기에서 접근할 수 없다. 0.0.0.0은 모든 네트워크 인터페이스에서 들어오는 연결을 수락하라는 의미다.
next() 옵션
next() 함수에 전달할 수 있는 주요 옵션들이 있다.
const app = next({
dev: true, // 개발 모드 여부
dir: './src', // Next.js 앱 디렉토리 (기본: '.')
hostname: 'localhost', // 호스트네임
port: 3000, // 포트
quiet: false, // 에러/경고 출력 여부
});
| 옵션 | 기본값 | 설명 |
|---|---|---|
dev | false | 개발 모드 활성화. HMR, 에러 오버레이 등 |
dir | '.' | pages/ 또는 app/ 디렉토리가 있는 루트 경로 |
hostname | 'localhost' | 서버 호스트 |
port | 3000 | 서버 포트 |
quiet | false | true면 서버 관련 에러/경고를 숨김 |
dev 옵션은 process.env.NODE_ENV와 연동하는 것이 좋다. 프로덕션 빌드에서 dev: true로 실행하면 성능이 크게 떨어진다.
커스텀 라우팅 추가
Custom Dev Server의 장점 중 하나는 Next.js 라우팅 이전에 자체 로직을 끼워넣을 수 있다는 것이다. 특정 경로에 대해 커스텀 처리를 하거나, 미들웨어를 추가하거나, 요청을 변환할 수 있다.
createServer((req, res) => {
const parsedUrl = parse(req.url, true);
const { pathname } = parsedUrl;
// 헬스 체크 엔드포인트
if (pathname === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok' }));
return;
}
// CORS 헤더 추가
res.setHeader('Access-Control-Allow-Origin', '*');
// 나머지는 Next.js에 위임
handle(req, res, parsedUrl);
}).listen(3000);
다만 이 방식은 개발 환경에서만 사용해야 한다. 프로덕션에서는 Next.js의 middleware나 API Route를 사용하는 것이 올바른 접근이다.
Next.js 13+ experimental HTTPS
Next.js 13.5부터 --experimental-https 플래그가 추가되어, 간단한 HTTPS 용도라면 Custom Dev Server 없이도 가능하다.
next dev --experimental-https
이 명령은 내부적으로 mkcert를 자동 설치하고 인증서를 생성해서 HTTPS 서버를 띄운다. 하지만 커스텀 도메인 지원이 제한적이고, HTTP/HTTPS 모드 전환이나 LAN IP 감지 같은 기능은 없기 때문에, 복잡한 요구사항이 있다면 여전히 Custom Dev Server가 필요하다.
Turbopack과의 호환성
Next.js 13부터 도입된 Turbopack(next dev --turbopack)을 Custom Dev Server와 함께 사용할 때 주의할 점이 있다. Turbopack은 자체 개발 서버를 사용하기 때문에, Custom Dev Server와 조합하면 HMR WebSocket 연결이 꼬일 수 있다.
이 경우 next() 옵션에 Turbopack 관련 설정을 명시하거나, next.config.js에서 turbopack 옵션을 조정해야 한다. 아직 Turbopack이 안정화 단계가 아니기 때문에, Custom Dev Server를 사용한다면 Webpack 기반 개발 서버를 유지하는 것이 안전하다.
정리
Custom Dev Server는 결국 "Next.js의 요청 핸들러를 내가 만든 서버 위에 올린다"는 단순한 개념이다. 핵심 API는 next(), app.prepare(), app.getRequestHandler() 세 가지뿐이다. 이 세 가지만 알면 나머지는 순수 Node.js 서버 프로그래밍이다.
사용이 적절한 경우:
- 로컬 HTTPS + 커스텀 도메인이 필요할 때
- 모바일 테스트를 위한 LAN 접근이 필요할 때
- 개발 서버에 WebSocket이나 커스텀 미들웨어를 붙여야 할 때
- HTTP/HTTPS 모드를 유연하게 전환해야 할 때
대부분의 프로젝트에서는 next dev만으로 충분하다. Custom Dev Server는 특수한 요구사항이 있을 때만 사용하고, 프로덕션 서버 로직은 Next.js의 공식 메커니즘(middleware, API Route, next start)을 활용하는 것이 올바른 방향이다.