Next.js 빌드 중 NextAuth TypeError: Invalid URL 분석
환경: Next.js 16(Turbopack), NextAuth v4.24, GitHub Actions CI
증상
CI의 next build에서 빌드가 실패했다.
Error occurred prerendering page "/_not-found".
TypeError: Invalid URL
at module evaluation (.next/server/chunks/ssr/[root-of-the-server]__xxx.js:1:1779)
{
code: 'ERR_INVALID_URL',
input: '',
}
Export encountered an error on /_not-found/page: /_not-found, exiting the build.
핵심 단서는 input: ''다. 빈 문자열로 new URL()을 호출했다.
원인 추적
1. 어디서 불렸나
/_not-found도 root layout을 쓰고, root layout은 next-auth/react의 SessionProvider를 감싼 AuthProvider를 포함한다.
/_not-found
└─ app/layout.tsx
└─ AuthProvider
└─ SessionProvider (next-auth/react)
2. next-auth/react는 모듈을 불러오는 순간 URL을 만든다
next-auth/react는 모듈 최상위에서 바로 URL을 파싱한다(4.24.15 기준).
// next-auth/react/index.js
var __NEXTAUTH = {
baseUrl: parseUrl(process.env.NEXTAUTH_URL ?? process.env.VERCEL_URL).origin,
basePath: parseUrl(process.env.NEXTAUTH_URL).path,
// ...
};
3. parseUrl은 빈 문자열을 걸러내지 못한다
// next-auth/utils/parse-url.js
function parseUrl(url) {
const defaultUrl = new URL('http://localhost:3000/api/auth');
if (url && !url.startsWith('http')) {
url = `https://${url}`;
}
const _url = new URL(url ?? defaultUrl); // ''는 null·undefined가 아니므로 그대로 전달
// ...
}
url 값 | url ?? defaultUrl | 결과 |
|---|---|---|
undefined | defaultUrl | 정상 (localhost 기본값) |
'' | '' | new URL('') → TypeError |
'https://...' | 그대로 | 정상 |
환경 변수가 아예 없으면 기본값으로 넘어가지만, 빈 문자열로 설정되어 있으면 빌드가 깨진다.
4. 왜 빈 문자열이 되었나
GitHub Actions에서 ${{ secrets.NEXTAUTH_URL }}은 해당 secret을 읽을 수 없을 때 undefined가 아니라 빈 문자열로 치환된다. 다음 경우가 대표적이다.
- secret 이름 오타, 또는 아직 등록하지 않음
- secret이 특정 environment에만 등록되어 있는데 job에
environment:를 지정하지 않음 - fork에서 온 PR 워크플로(보안상 secret이 전달되지 않음)
당시에는 Turbopack이 node_modules 안의 process.env 참조를 감지하지 못해 값이 비었다고 추정했다. 비슷한 증상의 Turbopack 이슈도 있다. 다만 확실하게 확인된 것은 빌드 중 process.env.NEXTAUTH_URL이 빈 문자열이었다는 사실이다. 같은 증상이라면 먼저 CI 로그에서 해당 변수가 실제로 채워지는지(예: 길이만 출력) 확인한다.
해결
next.config.ts의 env에 선언한 값은 빌드할 때 번들 안의 process.env.NEXTAUTH_URL 참조를 문자열로 치환한다. 실행 시점에 환경 변수를 다시 읽지 않으므로 런타임 값이 비어 있어도 영향을 받지 않는다.
// next.config.ts
const nextConfig: NextConfig = {
env: {
// ''도 기본값으로 대체하기 위해 ?? 대신 || 사용
NEXTAUTH_URL: process.env.NEXTAUTH_URL || 'http://localhost:3000',
},
};
?? vs ||
'' ?? 'http://localhost:3000'; // '' (null·undefined만 대체)
'' || 'http://localhost:3000'; // 'http://localhost:3000' (falsy 값 모두 대체)
빈 문자열이 들어올 수 있는 환경 변수 기본값에는 ||가 안전하다.
주의할 점
env로 선언한 값은 빌드 시점에 고정된다. 같은 빌드 결과물을 여러 도메인에 배포한다면 도메인마다 다시 빌드해야 한다.- 이 방식은 서버 번들뿐 아니라 클라이언트 번들에서도 치환되므로, 비밀값은
env에 넣지 않는다.NEXTAUTH_URL은 공개되어도 되는 값이라 괜찮지만NEXTAUTH_SECRET은 절대 넣으면 안 된다. - 근본적으로는 CI에서 secret이 비어 있으면 빌드 전에 실패하도록 검사하는 단계를 두는 편이 낫다. 기본값으로 조용히 넘어가면 잘못된 URL로 배포될 수 있다.
- name: Check env
run: test -n "$NEXTAUTH_URL" || (echo "NEXTAUTH_URL is empty" && exit 1)
env:
NEXTAUTH_URL: ${{ secrets.NEXTAUTH_URL }}
교훈
TypeError: Invalid URL의input값을 먼저 본다.''라면 환경 변수가 "없음"이 아니라 "빈 문자열"이다.- 서드파티 라이브러리가 모듈 최상위에서 환경 변수를 읽으면, 그 페이지를 빌드(프리렌더)하는 순간 바로 실패한다.
- GitHub Actions의
${{ secrets.X }}는 값이 없을 때 빈 문자열이 된다. 기본값 처리에는||를, 필수 값에는 사전 검사를 쓴다.