개발자
2026.09.15 · 2분
이 블로그를 Vercel 에 처음 올렸을 때 빌드가 세 번 연달아 죽었다. 세 번 다 에러 메시지가 원인을 가리키지 않았다. 기록해 둔다.
?? 는 빈 문자열을 걸러주지 않는다첫 배포 로그가 이랬다.
Error: Failed to collect configuration for /_not-found
[cause]: TypeError: Invalid URL
at module evaluation (src/app/layout.tsx:13:17)
code: 'ERR_INVALID_URL',
input: ''
input: '' 이 답이었다. 코드는 이랬다.
export const SITE_URL = (
process.env.NEXT_PUBLIC_SITE_URL ?? 'https://blog.devs-jun.com'
).replace(/\/$/, '')
Next 는 설정되지 않은 NEXT_PUBLIC_* 를 빈 문자열로 치환한다. 그런데 ?? 는
null 과 undefined 에만 듣는다. 그래서 대체값이 쓰이지 않고 '' 가 그대로
내려갔고, new URL('') 이 던졌다.
/sitemap.xml·/admin·OG 이미지까지 여러 라우트가 한꺼번에 무너져서 처음에는
라우팅 문제인 줄 알았다. 실제로는 한 줄이었다.
환경변수를 읽을 때는 ?? 가 아니라 trim() 과 || 를 쓴다.
다음 실패는 더 헷갈렸다. Turso 접속 정보를 분명히 넣어 뒀는데 빌드는 "비어 있다" 고 했다.
vercel env ls 로 보니 값이 있긴 한데 타입이 달랐다.
NEXT_PUBLIC_SITE_URL eyJ2IjoidjIiLCJjIj… Config ← 값이 보임
TURSO_DATABASE_URL Hidden Secret ← 값이 숨겨짐
vercel env pull 로 받아 보면 Sensitive 로 표시된 변수는 실제 값 대신 짧은
자리표시자가 온다. 대시보드에서 Sensitive 를 켜 두면 값을 되읽을 수 없고, 빌드
단계에도 넘어오지 않는다.
지우고 CLI 로 다시 넣으니 해결됐다. CLI 로 넣으면 기본이 일반 타입이다.
그리고 배포는 생성 시점의 환경변수 집합에 묶인다. 변수를 추가한 뒤에는 기존 배포를 승격해도 안 잡히고 새로 빌드해야 한다. 이것 때문에 한 번 더 헤맸다.
마지막이 제일 고약했다.
Error: When using Cache Components, all `generateStaticParams`
functions must return at least one result.
글 목록과 태그 목록을 DB 에서 가져와 generateStaticParams 에 넘기고 있었는데,
글이 0건이면 빈 배열이 된다. Cache Components 는 그걸 허용하지 않는다.
교착이었다. 글을 쓰려면 관리자 화면에 들어가야 하고, 들어가려면 배포가 돼 있어야 한다. 그런데 글이 없으면 배포가 안 된다.
해법은 문서에 있었다. generateStaticParams 를 빼고 loading.tsx 를 두면 그
파일이 세그먼트를 감싸는 Suspense 경계가 되어 폴백 셸이 프리렌더된다. 덤으로 새
글이 나와도 다시 빌드할 필요가 없어졌다.
대가도 있다. 그 라우트는 스트리밍이 되므로 없는 주소가 404 가 아니라 200 을
낸다. Next 가 그 응답에 noindex 를 넣어 색인은 막아 준다.
고치고 나서 레이아웃 이동을 재보니 글 화면이 390px 에서 0.1064 였다. 기준이 0.1 이니 넘긴 것이다.
광고를 하나씩 빼도 값이 그대로였고, 전부 빼도 같았다. 광고가 아니었다.
layout-shift 항목의 sources 를 찍어 보고서야 알았다.
0.096 <FOOTER> y 759 -> 0
골격이 200px 남짓이라 그 아래 푸터가 화면 안에 들어왔고, 본문이 도착하는 순간 크게 밀린 것이었다. 골격을 한 화면 높이로 잡으니 푸터가 처음부터 접힌 부분 아래로 내려갔고 0.0006 이 됐다. 화면 밖 요소의 이동은 CLS 에 세지 않는다.
CLS 는 총합만 보면 원인을 못 찾는다. sources 를 찍어야 한다.
세 번 다 같은 모양이었다. 에러 메시지가 원인에서 한두 단계 떨어진 곳을 가리켰고, 그 사이를 좁히는 데 시간이 걸렸다. 그래서 지금은 실패할 때 무엇을 확인하면 되는지까지 말하도록 고쳐 뒀다.