ERR_INVALID_URL, 잘못된 주소, new URL | 환경 변수가 비었는데 빌드가 주소에서 죽으면?
ERR_INVALID_URL은 new URL이 빈 문자열이나 상대 경로를 절대 주소로 읽지 못할 때 노드가 던지는 타입 오류입니다. 빌드 전에 URL.canParse로 거르고, 카카오 콜백은 베이스를 붙여 만듭니다. 클라이언트 번들에만 값이 없는 NEXT_PUBLIC과는 다른 줄입니다. Next.js, 버셀 프리뷰, 환경 변수, 한국 1인 개발. 2026년 10월 문서.
WHATWG URL 파서는 절대 주소만 단독으로 받습니다. 스키마와 호스트가 없으면 파싱이 실패하고, 노드는 그걸 TypeError [ERR_INVALID_URL]: Invalid URL로 올립니다.
빈 문자열 ''은 실패합니다. 변수가 아예 없으면 자바스크립트가 undefined를 글자 "undefined"로 바꾼 뒤 생성자에 넣고, 그 글자도 주소가 아니라 같은 코드가 납니다. /kakao/callback처럼 슬래시로 시작하는 경로는 베이스가 없으면 실패합니다. localhost:3000도 스키마가 없으면 실패합니다.
베이스를 두 번째 인자로 주면 상대 경로를 붙일 수 있습니다. new URL('/kakao/callback', 'https://preview.example.com')은 통과합니다. 베이스 자체가 비어 있으면 두 번째 인자에서 다시 같은 오류가 납니다. 노드 18.17과 20부터는 URL.canParse가 있어, 던지기 전에 false만 받을 수 있습니다.
로그에 찍히는 입력 | 오류 메시지 기본문은 Invalid URL 한 줄입니다. 어떤 변수가 비었는지는 생성자가 알려 주지 않으니, 호출 전에 변수 이름을 포함한 오류를 던지세요.
빈 문자열과 스키마 없는 경로는 new URL이 절대 주소로 받지 않는다
빌드 때 죽는 자리와 요청 때 죽는 자리
모듈 최상단에서 new URL(process.env.APP_URL)을 실행하면, 그 파일을 처음 평가하는 순간에 터집니다. 넥스트 빌드가 페이지를 수집할 때 이미 죽으면 프리뷰 URL이 아예 안 나옵니다.
호출 위치
언제 죽나
화면
모듈 최상단
빌드 또는 서버 기동
프리뷰 주소가 안 열림
서버 함수 안
그 요청만
로그인 버튼만 500
클라이언트 클릭
브라우저 콘솔
버튼은 있는데 이동이 없음
next.config 리다이렉트
빌드가 설정을 읽을 때
설정 로드 실패
카카오 로그인처럼 주소가 요청마다 필요하면 함수 안으로 내리세요. 최상단에 두면 프리뷰 환경 변수를 빼먹은 배포 전체가 막힙니다. 함수 안이면 그 버튼만 실패하고, 로그에 변수 이름을 남길 수 있습니다.
로컬은 .env가 채워져 통과하는데 프리뷰만 죽는 이유는, 버셀에서 Production에만 값을 넣고 Preview는 비워 둔 경우가 많습니다. 빈 칸으로 저장한 변수는 없는 게 아니라 빈 문자열이라, new URL('')과 같은 길이 됩니다.
절대 주소일 때만 생성자를 부른다
function appOrigin() {
const raw = process.env.APP_URL;
if (!raw || !URL.canParse(raw)) {
throw new Error('APP_URL이 절대 주소가 아닙니다');
}
return new URL(raw);
}
export function kakaoCallback() {
return new URL('/kakao/callback', appOrigin());
}
통과하는 문자열과 실패하는 문자열
생성자에 넣기 전에 문자열 모양을 나누면, 콘솔의 Invalid URL 한 줄에서 헤매지 않습니다.
입력
단독 new URL
이유
https://app.example.com
통과
스키마와 호스트가 있음
빈 문자열
ERR_INVALID_URL
파싱할 문자가 없음
undefined 변수
글자 undefined로 실패
문자열이 강제 변환됨
/kakao/callback
베이스 없으면 실패
상대 경로
localhost:3000
실패
스키마가 없음
끝에 슬래시가 있든 없든 절대 주소면 생성자는 통과합니다. 카카오 콘솔에 등록한 리다이렉트와 글자가 같은지는 그 다음 문제입니다. 생성자가 성공해도 콘솔 주소와 한 글자만 달라도 로그인은 거절됩니다. 비교는 href를 찍어서 하세요. origin만 보면 경로가 잘립니다.
쿼리를 붙일 때는 문자열 이어 붙이기보다 searchParams.set을 쓰세요. ?를 두 번 붙이면 생성자는 통과하는데 카카오가 code를 이상한 키로 받습니다.
프리뷰 환경 변수는 빈 칸도 값이다
버셀은 Production, Preview, Development를 따로 저장합니다. 로컬 .env.local은 배포 빌드에 올라가지 않습니다. 프리뷰 빌드가 읽는 값은 대시보드의 Preview 칸입니다.
체크할 이름은 코드가 읽는 이름과 같아야 합니다. APP_URL을 읽는데 대시보드에는 NEXT_PUBLIC_APP_URL만 있으면, 서버 함수 쪽 변수는 여전히 비어 있습니다. 반대로 서버 전용 주소를 NEXT_PUBLIC_ 없이 클라이언트에서 읽으면 번들에는 빈 값이 들어갑니다. 그 경우도 new URL까지 가면 같은 코드가 나지만, 원인은 접두사입니다.
프리뷰 도메인이 배포마다 바뀌면 고정 APP_URL 대신 VERCEL_URL에 스키마를 붙여 베이스를 만듭니다. VERCEL_URL에는 https://가 없습니다. 그대로 생성자에 넣으면 스키마 없음으로 실패합니다. https://를 붙인 뒤에 canParse를 통과시키세요.
한국에서 카카오 로그인을 프리뷰에 붙여 쓰는 1인 프로젝트는, 콘솔의 리다이렉트 목록과 이 베이스가 같은 호스트인지까지 봐야 합니다. 생성자가 통과해도 콘솔에 그 호스트가 없으면 로그인 창만 거절합니다. 빌드 로그의 Invalid URL이 사라졌는지를 먼저 가르세요.
VERCEL_URL에는 스키마가 없다
function previewOrigin() {
if (process.env.APP_URL && URL.canParse(process.env.APP_URL)) {
return process.env.APP_URL;
}
const host = process.env.VERCEL_URL;
const withScheme = host ? 'https://' + host : '';
if (!URL.canParse(withScheme)) {
throw new Error('APP_URL 또는 VERCEL_URL이 없습니다');
}
return withScheme;
}
Production에만 주소를 넣으면 프리뷰 빌드의 new URL은 빈 문자열을 만난다
NEXT_PUBLIC 미주입과 다른 줄
증상이 비슷해 보여도 로그 한 줄이 다릅니다. NEXT_PUBLIC을 클라이언트에서 읽었는데 빌드에 값이 없으면, 대개 undefined인 채로 화면은 뜨고 버튼만 이상합니다. 서버 빌드가 ERR_INVALID_URL로 중단되면 생성자가 먼저 던진 것입니다.
쿠키를 동기처럼 읽어 넥스트 15가 경고하는 줄, 정적 생성 중에 cookies()를 써서 Dynamic server usage가 나는 줄도 따로입니다. 그 둘은 주소 파서가 아니라 요청 API입니다. 스택에 new URL 또는 ERR_INVALID_URL이 있는지만 보면 됩니다.
한국 서비스에서 카카오 로그인을 프리뷰에 붙이면, 콘솔에 등록하는 주소와 서버가 만드는 주소가 한 글자만 달라도 로그인이 거절됩니다. 그 거절은 빌드가 끝난 뒤의 일이고, 이번 오류는 그 주소를 만들기도 전에 납니다. 프리뷰 환경의 값에 스키마가 있는지, 빈 칸으로 저장하지는 않았는지부터 보세요. 로컬 파일에만 주소가 있고 배포 칸이 비어 있으면 집 맥에서는 버튼이 되고 프리뷰에서는 빌드가 죽습니다.
값을 로그에 전부 찍지 마세요. 주소 안에 토큰이나 질의 문자열이 섞여 있으면 빌드 로그에 남습니다. 길이와 앞 여덟 글자만 남기면 빈 문자열인지, 스키마가 빠졌는지, 호스트가 잘렸는지를 구분할 수 있습니다. 그 한 줄이 있으면 대시보드를 열어 어느 칸이 비었는지 바로 찾을 수 있습니다.
인용한 생성자 동작은 2026년 10월 Node.js와 넥스트 공개 문서 기준입니다. URL.canParse는 노드 18.17, 19.9, 20 이상입니다.
자주 묻는 질문
localhost:3000 만 넣어도 되나요?
스키마가 없으면 실패합니다. http://localhost:3000 처럼 스키마와 호스트가 같이 있어야 단독 new URL이 통과합니다. 포트만 있는 문자열은 상대 참조로 거절됩니다.
변수는 있는데도 Invalid URL이 납니다.
빈 칸으로 저장된 값은 있는 변수이지만 빈 문자열입니다. 앞뒤 공백, 따옴표가 값에 포함된 경우도 파서가 거절합니다. 로그에는 길이만 찍고 값 전체는 찍지 마세요.
try로 감싸면 빌드가 통과하나요?
최상단 try는 오류를 삼키고 주소가 빈 채로 남습니다. 로그인 주소가 조용히 깨집니다. 함수 안에서 변수 이름이 있는 오류로 실패시키는 편이 다음 배포에서 찾습니다.
카카오 콘솔까지 고쳐야 하나요?
빌드가 Invalid URL에서 끝나면 콘솔에 도달하기 전입니다. 생성자가 통과한 뒤 로그인만 거절되면, 그때 리다이렉트 문자열이 콘솔 목록과 같은지 봅니다.
URL.canParse가 없다고 나옵니다.
노드 18.17 미만에는 그 정적 메서드가 없습니다. 노드 20으로 올리거나, try 안에서 new URL을 부르고 실패하면 변수 이름을 다시 던지세요. 빈 catch로 끝내면 안 됩니다.
NEXT_PUBLIC을 붙이면 해결되나요?
접두사는 클라이언트 번들에 넣을지만 정합니다. 값이 빈 문자열이면 생성자는 그대로 실패합니다. 서버 콜백 주소는 접두사 없이 두고, 프리뷰 칸에 절대 주소를 채우세요.
ERR_INVALID_URL은 new URL이 절대 주소를 받지 못했을 때의 타입 오류입니다. 빈 프리뷰 변수와 스키마 없는 VERCEL_URL을 먼저 고치고, 생성자는 URL.canParse 뒤에만 부르세요. 번들에 값이 없는 증상은 NEXT_PUBLIC 글을 보시면 됩니다.