URL 파싱이란?
URL 파싱은 Uniform Resource Locator를 개별 구성 요소인 프로토콜(스킴), 호스트명, 포트, 경로명, 쿼리 파라미터, 프래그먼트 식별자로 분해하는 과정입니다. 모든 URL은 RFC 3986과 WHATWG URL 표준에서 정의한 구조를 따릅니다. URL 파서는 원시 문자열을 읽고 구분 문자(://, :, /, ?, #, &, =)를 기준으로 각 세그먼트를 식별하여 별도의 접근 가능한 필드로 반환합니다.
브라우저는 주소를 입력하거나 링크를 클릭할 때마다 URL 파싱을 수행합니다. JavaScript의 URL 생성자, Python의 urllib.parse 모듈, Go의 net/url 패키지는 모두 동일한 구조 규칙을 따르는 파서를 구현합니다. URL 파싱은 URL 인코딩의 역과정입니다. 안전한 전송을 위해 문자를 변환하는 대신, 이미 구성된 URL을 구성하는 부분들로 분해합니다.
https://api.example.com:8080/v1/users?page=2&limit=10#section과 같은 일반적인 URL에는 여섯 가지 구성 요소가 있습니다. 구분 문자인 ://, :, /, ?, &, =, # 가 파싱을 결정론적으로 만듭니다. 각 문자는 경계를 나타내어 파서가 모호함 없이 필드를 추출할 수 있도록 합니다.
온라인 URL 파서를 사용하는 이유
눈으로 URL을 수동으로 분리하는 것은 오류가 발생하기 쉽습니다. 특히 인코딩된 문자, 여러 쿼리 파라미터, 비표준 포트가 포함된 경우 더욱 그렇습니다. 이 도구는 브라우저가 사용하는 것과 동일한 WHATWG 호환 알고리즘을 사용하여 URL을 파싱하고 모든 구성 요소를 명확하게 복사 가능한 테이블로 표시합니다.
URL 파서 사용 사례
URL 구성 요소 참조
아래 표는 URL을 파싱할 때 JavaScript URL 생성자가 반환하는 모든 속성을 보여줍니다. Python의 urlparse 결과, Go의 url.URL 구조체, PHP의 parse_url 출력에도 동일한 구성 요소가 존재하지만 언어마다 속성 이름이 다릅니다.
| 속성 | 예시 | 설명 |
|---|---|---|
| protocol | https: | Scheme including the trailing colon |
| hostname | api.example.com | Domain name or IP address |
| port | 8080 | Port number (empty string if default) |
| pathname | /v1/users | Path starting with / |
| search | ?page=2&limit=10 | Query string including the leading ? |
| hash | #section | Fragment identifier including the leading # |
| origin | https://api.example.com:8080 | protocol + hostname + port |
| host | api.example.com:8080 | hostname + port |
| username | admin | Credentials before @ (rarely used in practice) |
| password | secret | Credentials before @ (avoid in production URLs) |
| href | (full URL) | The complete, serialized URL string |
WHATWG URL 표준 vs RFC 3986
URL 파싱 방법을 정의하는 두 가지 사양이 있습니다. 기본 구조에는 동의하지만 엣지 케이스에서 차이가 있습니다. 이 차이가 브라우저와 서버가 URL을 다르게 처리할 때의 원인이 되는 경우가 많습니다.
실제로 대부분의 차이는 국제 도메인 이름(IDN), 누락된 스킴, 비정상적인 문자가 포함된 URL을 파싱할 때 나타납니다. WHATWG 파서는 IDN 호스트명을 자동으로 Punycode로 변환하는 반면, 엄격한 RFC 3986 파서는 이를 거부할 수 있습니다. URL을 이 도구에 붙여넣었을 때 서버 측 코드와 다른 결과가 나온다면, WHATWG vs RFC 차이가 가장 가능성 있는 원인입니다.
코드 예시
주요 언어에는 모두 내장 URL 파서가 있습니다. 아래 예시는 동일한 URL을 파싱하여 구성 요소를 추출합니다. 언어마다 이름 차이가 있습니다: Python은 protocol 대신 scheme을 사용하고, Go는 search 대신 RawQuery를 노출합니다.
const url = new URL('https://api.example.com:8080/v1/users?page=2&limit=10#section')
url.protocol // → "https:"
url.hostname // → "api.example.com"
url.port // → "8080"
url.pathname // → "/v1/users"
url.search // → "?page=2&limit=10"
url.hash // → "#section"
// Iterate over query parameters
for (const [key, value] of url.searchParams) {
console.log(`${key} = ${value}`)
}
// → "page = 2"
// → "limit = 10"
// Modify and re-serialize
url.searchParams.set('page', '3')
url.toString()
// → "https://api.example.com:8080/v1/users?page=3&limit=10#section"from urllib.parse import urlparse, parse_qs
result = urlparse('https://api.example.com:8080/v1/users?page=2&limit=10#section')
result.scheme # → 'https'
result.hostname # → 'api.example.com'
result.port # → 8080
result.path # → '/v1/users'
result.query # → 'page=2&limit=10'
result.fragment # → 'section'
# Parse query string into a dict
params = parse_qs(result.query)
params['page'] # → ['2']
params['limit'] # → ['10']
# Reconstruct with modifications
from urllib.parse import urlencode, urlunparse
new_query = urlencode({'page': '3', 'limit': '10'})
urlunparse(result._replace(query=new_query))
# → 'https://api.example.com:8080/v1/users?page=3&limit=10#section'package main
import (
"fmt"
"net/url"
)
func main() {
u, err := url.Parse("https://api.example.com:8080/v1/users?page=2&limit=10#section")
if err != nil {
panic(err)
}
fmt.Println(u.Scheme) // → "https"
fmt.Println(u.Hostname()) // → "api.example.com"
fmt.Println(u.Port()) // → "8080"
fmt.Println(u.Path) // → "/v1/users"
fmt.Println(u.RawQuery) // → "page=2&limit=10"
fmt.Println(u.Fragment) // → "section"
// Query params as map
q := u.Query()
fmt.Println(q.Get("page")) // → "2"
fmt.Println(q.Get("limit")) // → "10"
}<?php $url = 'https://api.example.com:8080/v1/users?page=2&limit=10#section'; $parts = parse_url($url); $parts['scheme']; // → "https" $parts['host']; // → "api.example.com" $parts['port']; // → 8080 $parts['path']; // → "/v1/users" $parts['query']; // → "page=2&limit=10" $parts['fragment']; // → "section" // Parse query string into an array parse_str($parts['query'], $params); $params['page']; // → "2" $params['limit']; // → "10"