회사 노트북(Windows 11 + WSL2 Ubuntu)에서 Next.js 프로젝트를 돌리다가, 아이폰 Safari로 실시간 화면을 확인하면서 개발해야 하는 상황이 생겼다. 서브 노트북에서 SSH로 붙어 작업할 때도 있고, 반응형 레이아웃을 실기기에서 바로 확인하고 싶을 때도 있었기 때문이다.
처음에는 단순하게 생각했다. next dev -H 0.0.0.0으로 띄우고 같은 Wi-Fi에 물린 아이폰에서 내부 IP로 접속하면 되겠지, 라고. 실제로는 그렇게 간단하지 않았다.
서버를 띄우면 터미널에 이렇게 뜬다.
bash
▲ Next.js
- Local: http://localhost:3000
- Network: http://0.0.0.0:3000
0.0.0.0:3000을 보고 "아, 이 주소로 접속하면 되는구나"라고 착각했는데, 이건 실제 접속 주소가 아니라 모든 인터페이스의 요청을 받겠다는 선언일 뿐이었다. 그래서 Windows의 내부 IP(192.168.0.15:3000)로 아이폰에서 접속을 시도했지만 계속 실패했다.
막히는 지점을 하나씩 따라가보니 원인이 겹쳐 있었다.
IP 대역을 맞추고 방화벽 규칙을 하나씩 뚫는 방법도 있었지만, 어차피 카페나 외부 Wi-Fi에서도 똑같은 문제를 반복해서 겪을 게 뻔했다. 그래서 방향을 아예 바꿔서 ngrok 터널링으로 우회하기로 했다. IP나 방화벽 설정에 의존하지 않고 어디서든 접속되는 URL을 하나 만드는 쪽이 훨씬 마음이 편했다.
여기서 한 가지 정해둔 원칙은, ngrok을 Windows 호스트가 아니라 WSL2(Ubuntu) 안에 직접 설치한다는 것이었다. 어차피 Next.js 서버도 WSL2 안에서 돌고 있으니 같은 위치에서 터널을 열어야 경로가 단순해진다.
bash
curl -s https://ngrok-agent.s3.amazonaws.com/ngrok.asc | sudo tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null
echo "deb https://ngrok-agent.s3.amazonaws.com buster main" | sudo tee /etc/apt/sources.list.d/ngrok.list
sudo apt update
sudo apt install ngrok
ngrok 공식 사이트에서 회원가입하고 대시보드에서 Authtoken을 복사한 뒤, WSL2 터미널에 최초 1회 등록해준다.
bash
ngrok config add-authtoken <YOUR_NGROK_AUTHTOKEN>
여기까지는 순조로웠다. 문제는 다음 단계에서 터졌다.
ngrok으로 외부 도메인(https://xxxx.ngrok-free.dev)을 열어서 아이폰으로 접속하니 화면 자체는 잘 나왔다. 그런데 컴포넌트나 스타일을 고쳐서 저장해도 아이폰 화면이 그대로였다. 수동으로 새로고침을 해야만 반영이 됐다.
아이폰 Safari 개발자 콘솔을 열어보니 이런 에러가 찍혀 있었다.
text
web-socket.ts:50 WebSocket connection to 'wss://xxxx.ngrok-free.dev/_next/webpack-hmr?id=...' failed:
원인을 찾아보니, Next.js 15+ 최신 버전부터 보안 정책이 강화되면서 localhost가 아닌 외부 도메인에서 들어오는 HMR 웹소켓(wss://.../_next/webpack-hmr) 연결을 기본적으로 막아버린다는 걸 알게 됐다. ngrok 터널 자체는 문제가 없었고, Next.js가 "낯선 도메인에서 온 웹소켓"을 신뢰하지 않은 것이었다.
next.config.ts에 allowedDevOrigins 추가허용할 도메인 패턴을 최상위 설정에 명시하면 된다.
typescript
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
reactCompiler: true,
// 💡 ngrok 외부 도메인의 HMR/Fast Refresh 웹소켓 접속 허용
allowedDevOrigins: [
"*.ngrok-free.app",
"*.ngrok-free.dev",
"localhost:3000",
],
experimental: {
viewTransition: true,
},
};
export default nextConfig;
이걸 처음 experimental 객체 안에 넣었다가 TypeScript 타입 에러(TS2353)를 만났다. allowedDevOrigins는 실험적 옵션이 아니라 NextConfig의 최상위 속성으로 옮겨야 인식됐다. 이름만 보면 experimental에 있을 것 같아서 여기서 시간을 좀 썼다.
설정을 고치고 나서 순서대로 다시 실행해봤다.
Step 1. WSL2에서 Next.js 서버 실행
bash
pnpm dev
Step 2. WSL2 터미널 하나를 더 열어서 ngrok 실행
bash
ngrok http 3000 --host-header=rewrite
text
Session Status online
Forwarding https://fling-android-appear.ngrok-free.dev -> http://localhost:3000
Step 3. 아이폰 Safari로 접속
출력된 ngrok URL을 아이폰 Safari에 입력하고, 최초 접속시 뜨는 안내 페이지에서 [Visit Site]를 눌러줬다. 그다음 VS Code에서 컴포넌트를 하나 고쳐서 저장했는데, 수동 새로고침 없이 아이폰 화면이 바로 바뀌는 걸 확인했다. 이 순간이 제일 반가웠다.
0.0.0.0은 접속 주소가 아니라 대기 상태 표시다 처음에 이걸 실제 IP처럼 취급해서 시간을 버렸다. 저 값 자체로 접속을 시도하는 게 아니라, 서버가 모든 인터페이스를 듣고 있다는 뜻으로만 이해하면 된다.
네트워크 우회보다 터널링이 더 오래 간다 방화벽 규칙이나 Wi-Fi AP Isolation은 장소가 바뀔 때마다 다시 부딪힌다. ngrok으로 한 번 세팅해두면 사내망이든 카페든 신경 쓸 일이 없어졌다.
같은 문제라도 레이어가 다를 수 있다 접속 자체가 안 되는 문제(네트워크)와, 접속은 되는데 HMR만 안 되는 문제(Next.js 보안 정책)는 원인이 완전히 달랐다. 화면이 뜬다고 안심하지 않고 콘솔 로그를 먼저 확인한 게 원인을 좁히는 데 도움이 됐다.
설정 옵션의 "위치"도 스펙의 일부다 allowedDevOrigins처럼 이름만 봐서는 실험적 기능 같아 보이는 옵션도, 실제로는 최상위 속성으로 옮겨야 하는 경우가 있다. 공식 문서나 타입 정의를 한 번 더 확인하는 습관이 필요하다는 걸 느꼈다.
정리하면 핵심은 두 가지였다.
next.config.ts 최상위에 allowedDevOrigins를 정의할 것이 두 가지만 기억해두면, 이후로는 어떤 네트워크 환경에서도 아이폰으로 실시간 HMR을 확인하면서 개발할 수 있다. 다음에 비슷한 상황이 오면 이번처럼 헤매지 않고 바로 적용할 수 있을 것 같다.