URL.parse() 메서드를 활용하여 URL 객체를 생성하고 처리하는 방법
URL.parse(url)메서드는 주어진 URL에 따라 새 URL 객체를 생성- 유효하지 않은 URL 값이 주어질 경우
null을 반환 - 두 번째 파라미터 base는 상대 URL을 해석하기 위한 기준 URL로 사용되며, 이를 통해 URL의 경로가 올바르게 조정됨
- URL 객체나 다른 문자열을 파라미터로 사용할 수 있으며, 내부에서 문자열로 변환됨
describe('null 입력에 대한 URL API 동작 테스트', () => {
test('new URL(null)은 TypeError를 발생시킨다', () => {
expect(() => new URL(null)).toThrow(TypeError)
})
test('URL.canParse(null)은 false를 반환한다', () => {
expect(URL.canParse(null)).toBe(false)
})
test('URL.parse(null)은 null을 반환한다', () => {
expect(() => URL.parse(null)).toBe(null)
})
})URL의 공백이 %2520으로 깨져 들어온다. 원인은 iOS 17.0/17.1 WebKit pasteboard가 복사할 때 URL을 다시 인코딩하는 버그(WebKit Bug 261936)였다. Claude Code라면 깨진 값 해독 → 경로 좁히기 → 우리 코드 배제 → 방어 → 방어 확인 순으로 간다.
1. 깨진 값을 해독한다
decodeURIComponent('%2520')을 한 번 돌리면 %20이 남는다 — %25(= %) 뒤에 원래의 20, 두 번 인코딩된 것이다. 같은 URL의 한글(%EC%88%98)은 한 번 디코딩으로 풀리니 공백만 다시 인코딩됐다. URL 인코딩은 멱등이 아니다.
2. 어느 경로에서 깨지나
서버 액세스 로그에서 %2520이 든 요청만 모아 User-Agent·Referer를 센다. iOS에 몰려 있고 Referer가 비어 있다 — 링크 클릭(JS redirect)이 아니라 주소를 복사해 붙여넣은 경로다. 클릭 경로의 같은 URL은 정상이다.
3. 우리 코드인가, 밖인가
링크를 만드는 코드에서 encodeURIComponent 호출을 찾아 출력이 %20 한 번인지 본다. 한 번이면 이중 인코딩은 우리 밖에서 일어난다. iOS 버전으로 좁혀 검색하면 WebKit Bug 261936이 나온다.
- iOS 16↓:
NSURL URLWithString:이 invalid char에nil - iOS 17.0/17.1: invalid char를 자동 percent-encode (regression)
- iOS 17.2: 수정
4. 고칠 수 없으니 막는다
원인이 OS 버그면 추적보다 방어가 싸다.
- 화이트리스트 라우트 패턴에서만 한 번 더 디코딩
- 디코딩 후 위험 패턴(
..,//,\) 차단 - segment별
encodeURIComponent후 301 redirect
무조건 이중 디코딩 금지 — ..%252f..%252f → ../../ path traversal 우회 벡터.
5. 방어를 확인한다
%2520이 든 URL과 ..%252f..%252f가 든 URL을 curl -I로 보낸다. 앞의 것은 301로 정상 경로에 가고, 뒤의 것은 막혀야 한다.
교훈
- URL은 디코딩된 원본으로 보관하고, 인코딩은 출력 직전 한 번 (single source of truth)
- “방어적으로 한 번 더”가 함정 — 멱등이 아닌 연산엔 통하지 않는다