Node.js에서 ERR_INVALID_URL 오류를 해결하는 방법
Node.js의 ERR_INVALID_URL 오류는 URL 생성자나 url.parse()에 유효하지 않은 URL 문자열이 전달될 때 발생합니다. 주요 원인으로는 프로토콜 누락, 인코딩되지 않은 특수 문자, 잘못된 형식의 percent-encoding 등이 있습니다. 파싱하기 전에 입력값을 검증하고 특수 문자를 인코딩하여 해결할 수 있습니다.
ERR_INVALID_URL의 원인은 무엇인가?
ERR_INVALID_URL 오류(정식 명칭은 TypeError [ERR_INVALID_URL]: Invalid URL)는 URL 생성자나 레거시 url.parse() 함수에 유효한 URL로 파싱할 수 없는 문자열이 전달될 때 Node.js에서 발생합니다. 이 오류는 사용자 입력을 처리하거나, 설정 파일을 파싱하거나, 외부 소스에서 받은 URL을 다룰 때 흔히 나타납니다.
URL 생성자는 기존 RFC 기반 파싱보다 더 엄격한 WHATWG URL Standard를 따릅니다. 유효한 URL로 인정받으려면 문자열에 유효한 스킴(예: https:), 유효한 authority(호스트명), 그리고 올바른 형식의 경로 및 쿼리 구성 요소가 포함되어야 합니다.
// 이 코드는 ERR_INVALID_URL을 발생시킵니다
try {
const url = new URL('not-a-url');
} catch (err) {
console.log(err.code); // 'ERR_INVALID_URL'
console.log(err.message); // 'Invalid URL: not-a-url'
console.log(err.input); // 'not-a-url'
}
흔한 원인과 해결 방법
원인 1: 프로토콜/스킴 누락. URL 생성자는 https://나 http:// 같은 스킴을 요구합니다. example.com/path나 www.example.com 같은 문자열은 실패합니다.
// 실패: 프로토콜 없음
new URL('example.com/path'); // ERR_INVALID_URL
// 해결: 프로토콜 추가
new URL('https://example.com/path'); // 정상 동작!
// 해결: 프로토콜이 없으면 추가하기
function ensureProtocol(urlString) {
if (!/^https?:\/\//i.test(urlString)) {
return 'https://' + urlString;
}
return urlString;
}
new URL(ensureProtocol('example.com/path')); // 정상 동작!
원인 2: 인코딩되지 않은 특수 문자. 공백, 중괄호, 파이프, 특정 유니코드 문자 등은 인코딩하지 않으면 URL에서 허용되지 않습니다.
// 실패: 인코딩되지 않은 공백과 특수 문자
new URL('https://example.com/my file.pdf'); // ERR_INVALID_URL
new URL('https://example.com/path?q=a b'); // 일부 버전에서는 실패할 수 있음
// 해결: 문제가 되는 부분을 인코딩
const filename = encodeURIComponent('my file.pdf');
new URL('https://example.com/' + filename); // 정상 동작!
// 해결: 공백이 포함된 완전한 URL에는 encodeURI 사용
const rawUrl = 'https://example.com/my file.pdf';
new URL(encodeURI(rawUrl)); // 정상 동작!
원인 3: 잘못된 형식의 percent-encoding. URL에 정확히 두 자리 16진수가 뒤따르지 않는 퍼센트 기호가 포함되어 있으면 파서가 이를 거부합니다.
// 실패: 잘못된 형식의 percent encoding
new URL('https://example.com/100%done'); // ERR_INVALID_URL
new URL('https://example.com/path?q=50%'); // ERR_INVALID_URL
// 해결: 단독으로 쓰인 퍼센트 기호를 %25로 인코딩
function fixPercentSigns(urlString) {
return urlString.replace(/%(?![0-9A-Fa-f]{2})/g, '%25');
}
new URL(fixPercentSigns('https://example.com/100%done'));
// 정상 동작! URL이 https://example.com/100%25done 이 됩니다
원인 4: 비어 있거나 null인 입력값. URL 생성자에 빈 문자열, null, undefined를 전달해도 이 오류가 발생합니다.
// 실패: 비어 있거나 null인 입력값
new URL(''); // ERR_INVALID_URL
new URL(null); // ERR_INVALID_URL
new URL(undefined); // ERR_INVALID_URL
// 해결: 파싱하기 전에 입력값 검증
function parseUrl(input) {
if (!input || typeof input !== 'string') {
return null;
}
try {
return new URL(input);
} catch {
return null;
}
}
원인 5: base가 없는 상대 URL. URL 생성자는 기본적으로 첫 번째 인자를 절대 URL로 취급합니다. /path/to/page 같은 상대 URL에는 두 번째 인자로 base URL이 필요합니다.
// 실패: base가 없는 상대 URL
new URL('/api/users'); // ERR_INVALID_URL
// 해결: base URL 제공
new URL('/api/users', 'https://example.com');
// 정상 동작! → https://example.com/api/users
// API 클라이언트에 유용한 패턴
const BASE_URL = 'https://api.example.com';
const endpoint = new URL('/v2/users?page=1', BASE_URL);
console.log(endpoint.href);
// "https://api.example.com/v2/users?page=1"
파싱하기 전에 URL을 검증하는 방법
Node.js에서 URL을 검증하는 가장 확실한 방법은 try-catch 블록 안에서 URL 생성자를 사용하는 것입니다. (Node.js 22 기준) 내장 URL.isValid() 메서드가 없으므로, 오류를 잡아내는 것이 표준적인 접근 방식입니다.
// 간단한 URL 검증기
function isValidUrl(string) {
try {
new URL(string);
return true;
} catch {
return false;
}
}
console.log(isValidUrl('https://example.com')); // true
console.log(isValidUrl('not-a-url')); // false
console.log(isValidUrl('')); // false
// 허용된 프로토콜만 통과시키는 검증기
function isValidHttpUrl(string) {
try {
const url = new URL(string);
return url.protocol === 'http:' || url.protocol === 'https:';
} catch {
return false;
}
}
console.log(isValidHttpUrl('https://example.com')); // true
console.log(isValidHttpUrl('ftp://example.com')); // false
console.log(isValidHttpUrl('javascript:alert(1)')); // false
안전한 URL 파싱 패턴
다음은 흔히 발생하는 모든 오류 상황을 처리하고 파싱된 URL 객체 또는 의미 있는 오류를 반환하는 견고한 URL 파싱 패턴입니다.
class UrlParser {
static parse(input, base) {
// 입력값 검증
if (!input || typeof input !== 'string') {
return { ok: false, error: 'Input must be a non-empty string' };
}
// 공백 제거
const trimmed = input.trim();
// 흔한 문제 수정
let urlString = trimmed;
// 프로토콜이 없으면 추가
if (/^[a-zA-Z0-9]/.test(urlString) && !urlString.includes('://')) {
urlString = 'https://' + urlString;
}
// 단독으로 쓰인 퍼센트 기호 수정
urlString = urlString.replace(/%(?![0-9A-Fa-f]{2})/g, '%25');
try {
const url = base ? new URL(urlString, base) : new URL(urlString);
// 선택 사항: 안전한 프로토콜로 제한
if (!['http:', 'https:'].includes(url.protocol)) {
return { ok: false, error: 'Unsupported protocol: ' + url.protocol };
}
return { ok: true, url };
} catch (err) {
return { ok: false, error: err.message };
}
}
}
// 사용 예시
const result = UrlParser.parse('example.com/path?q=hello world');
if (result.ok) {
console.log(result.url.href);
} else {
console.error(result.error);
}
예방을 위한 모범 사례
다음 사례들을 따르면 Node.js 애플리케이션에서 ERR_INVALID_URL 오류를 피하고 더 견고한 URL 처리를 구축하는 데 도움이 됩니다.
- URL 파싱은 항상 try-catch로 감싸세요. 특히 사용자 입력, 환경 변수, 데이터베이스, 외부 API에서 온 URL 문자열이라면 절대 유효하다고 단정하지 마세요.
- 문자열 연결 대신 URL API를 사용하세요. 문자열을 이어 붙이는 대신
URL생성자와URLSearchParams를 사용해 URL을 구성하세요. 이 API는 인코딩을 자동으로 처리합니다. - 환경 변수는 시작 시점에 검증하세요. 애플리케이션이 환경 변수나 설정 파일에서 URL을 읽는다면, 처음 사용하는 시점이 아니라 애플리케이션이 시작될 때 검증하세요.
- 사용자 입력은 URL에 삽입하기 전에 인코딩하세요. 쿼리 파라미터 값에는 항상
encodeURIComponent()를, 사용자가 제공한 완전한 URL에는encodeURI()를 사용하세요. - 복잡한 경우에는 URL 빌더를 사용하세요. 동적인 부분이 많은 URL을 구성할 때는 인코딩을 일관되게 처리하는 헬퍼 함수나 클래스를 만드세요.
- 오류 발생 시 원본 입력값을 로깅하세요.
ERR_INVALID_URL오류를 잡았을 때, 문제를 디버깅하는 데 도움이 되도록 오류를 일으킨 입력값을 (민감한 데이터가 포함될 수 있다면 마스킹하여) 로깅하세요.
// 좋은 사례: 시작 시점에 설정 URL 검증하기
const requiredUrls = ['API_BASE_URL', 'AUTH_SERVER_URL', 'WEBHOOK_URL'];
for (const envVar of requiredUrls) {
const value = process.env[envVar];
if (!value) {
throw new Error(envVar + ' environment variable is required');
}
try {
new URL(value);
} catch {
throw new Error(envVar + ' is not a valid URL: ' + value);
}
}
// 좋은 사례: URL 구성에 URL API 사용하기
function buildApiUrl(endpoint, params) {
const url = new URL(endpoint, process.env.API_BASE_URL);
for (const [key, value] of Object.entries(params)) {
url.searchParams.set(key, String(value));
}
return url.toString();
}
const searchUrl = buildApiUrl('/api/search', {
q: 'Node.js & Express',
page: 1
});
// "https://api.example.com/api/search?q=Node.js+%26+Express&page=1"