본문 바로가기
TYLER SONGBlog
← Blog index
CS학습5 min read

URL: 리소스의 위치와 접근 방법을 문자열 하나로 표현하는 구조

URL(Uniform Resource Locator)은 스킴, 권한(authority), 경로, 쿼리, 프래그먼트로 구성되어 웹 리소스의 위치와 접근 방식을 나타내는 문자열이다. 이 글은 RFC 3986 기준의 구조, 퍼센트 인코딩(percent-encoding), 상대 경로 해석 원리를 설명하고, TypeScript의 `URL`, `URLSearchPar

By 송민성

1. 개념

URL(Uniform Resource Locator)은 네트워크 상의 리소스가 어디에 있고 어떤 방법으로 접근할지를 나타내는 문자열이다. URI(Uniform Resource Identifier)의 하위 개념이며, 식별만 하는 URI 중 "위치"를 알려주는 형태가 URL이다. 문법은 RFC 3986에 정의되어 있다.

text
https://user:pw@example.com:8080/docs/intro?lang=ko&page=2#section-3
└─┬─┘   └──┬──┘ └────┬────┘└─┬─┘└────┬────┘└─────┬─────┘└────┬────┘
스킴    userinfo    호스트  포트    경로        쿼리      프래그먼트
        └────────── 권한(authority) ──────────┘
구성 요소예시역할
스킴(scheme)https사용할 프로토콜
권한(authority)user:pw@example.com:8080대상 서버 (userinfo, 호스트, 포트)
경로(path)/docs/intro서버 내 리소스 위치
쿼리(query)lang=ko&page=2리소스에 전달하는 추가 파라미터
프래그먼트(fragment)section-3문서 내부 위치. 서버로 전송되지 않음

2. 왜 사용하는가

  • 리소스를 전 세계에서 유일하게 가리키는 표준 주소 체계가 필요하다.
  • 프로토콜, 서버, 리소스 위치를 한 문자열에 담아 링크, 북마크, API 호출에 같은 형식을 쓸 수 있다.
  • REST API에서는 경로로 리소스를 식별하고 쿼리로 필터·정렬·페이지네이션을 표현한다.

3. 동작 원리

3.1 브라우저가 URL을 처리하는 흐름

  1. URL을 파싱해 스킴, 호스트, 포트, 경로, 쿼리, 프래그먼트로 분리한다.
  2. 호스트를 DNS로 조회해 IP 주소를 얻는다.
  3. 스킴과 포트(생략 시 https는 443, http는 80)로 연결을 맺는다. https면 TLS 핸드셰이크가 추가된다.
  4. 요청 라인에 경로와 쿼리를 담아 보낸다. 예: GET /docs/intro?lang=ko HTTP/1.1, 호스트는 Host 헤더로 전달된다.
  5. 프래그먼트는 서버로 전송되지 않고 클라이언트가 문서 내 위치를 찾는 데만 쓴다.

3.2 퍼센트 인코딩(percent-encoding)

URL에는 예약 문자(reserved)와 비예약 문자(unreserved)가 있다. 비예약 문자는 영문자, 숫자, -, ., _, ~이다. 그 외 문자나 구분자와 충돌하는 문자는 UTF-8 바이트를 %XX 형태로 바꿔 표현한다.

  • 한 → UTF-8 바이트 ED 95 9C → %ED%95%9C
  • 공백 → %20 (폼 데이터 application/x-www-form-urlencoded에서는 +)
  • & → %26 (쿼리 값 안에 들어갈 때)

3.3 상대 URL 해석

기준 URL(base)에 대해 상대 참조(relative reference)를 합쳐 절대 URL을 만든다. ., .. 세그먼트는 정규화 과정에서 제거된다.

4. 코드 예제

Node.js 18 이상 또는 최신 브라우저에서 실행된다.

typescript
// url-demo.ts const raw = "https://user:pw@example.com:8080/docs/intro?lang=ko&page=2#section-3"; const url = new URL(raw); console.log(url.protocol); // "https:" console.log(url.username); // "user" console.log(url.hostname); // "example.com" console.log(url.port); // "8080" console.log(url.pathname); // "/docs/intro" console.log(url.search); // "?lang=ko&page=2" console.log(url.hash); // "#section-3" console.log(url.origin); // "https://example.com:8080" // 쿼리 파라미터는 문자열 이어붙이기 대신 URLSearchParams로 만든다 const api = new URL("https://api.example.com/search"); api.searchParams.set("q", "자바 & 코틀린"); api.searchParams.set("page", "1"); console.log(api.toString()); // https://api.example.com/search?q=%EC%9E%90%EB%B0%94+%26+%EC%BD%94%ED%8B%80%EB%A6%B0&page=1 // 상대 URL 해석 const base = "https://example.com/a/b/c.html"; console.log(new URL("d.html", base).href); // https://example.com/a/b/d.html console.log(new URL("../d.html", base).href); // https://example.com/a/d.html console.log(new URL("/d.html", base).href); // https://example.com/d.html console.log(new URL("//cdn.example.com/x.js", base).href); // https://cdn.example.com/x.js // 경로 세그먼트 인코딩: encodeURIComponent 사용 const filename = "보고서 2026/최종.pdf"; const bad = `https://example.com/files/${filename}`; const good = `https://example.com/files/${encodeURIComponent(filename)}`; console.log(good); // https://example.com/files/%EB%B3%B4%EA%B3%A0%EC%84%9C%202026%2F%EC%B5%9C%EC%A2%85.pdf // 잘못된 URL은 예외가 발생한다 try { new URL("not a url"); } catch (e) { console.log("파싱 실패:", (e as Error).name); // TypeError }

5. 시간 복잡도 또는 성능 특성

  • URL 파싱과 인코딩은 문자열 길이를 n이라 할 때 한 번 훑는 선형 작업이므로 O(n)이다.
  • 표준으로 정해진 최대 길이는 없다. 다만 브라우저, 프록시, 웹 서버마다 구현상 한계가 달라서, 실무에서는 대략 2,000자 이내로 유지하는 것이 안전하다고 알려져 있다. 서버 설정(예: 요청 라인 크기 제한)에 따라 더 짧게 거부될 수도 있다.
  • 긴 데이터는 쿼리에 넣지 말고 요청 본문(body)으로 보내는 것이 낫다.

6. 실무 사용 사례

  • REST API 설계: /users/42/orders?status=paid&sort=-created_at처럼 경로는 리소스 계층, 쿼리는 조건을 표현한다.
  • SPA 라우팅: History API로 경로를 바꾸고, 프래그먼트(#)는 서버 요청 없이 클라이언트 상태를 구분하는 데 쓴다.
  • OAuth 리디렉션: redirect_uri는 쿼리 값이므로 인코딩해서 전달해야 한다.
  • 서명된 URL: 쿼리에 만료 시각과 서명을 포함해 임시 접근 권한을 부여한다.
  • 오리진(origin) 판단: 스킴, 호스트, 포트 세 가지가 모두 같아야 동일 출처(same-origin)다. CORS와 쿠키 정책의 기준이 된다.

7. 주의할 점

  • 이중 인코딩: 이미 인코딩된 문자열을 다시 인코딩하면 %20이 %2520이 된다. 인코딩은 값을 조립하는 시점에 한 번만 적용한다.
  • `encodeURI`와 `encodeURIComponent`의 차이: encodeURI는 /, ?, & 등 구분자를 그대로 두므로 값 하나를 인코딩하는 데는 encodeURIComponent를 써야 한다.
  • 공백 표기: URL 경로에서는 %20, 폼 인코딩에서는 +다. URLSearchParams는 +로 출력한다.
  • 문자열 이어붙이기 금지: 사용자 입력을 직접 이어붙이면 파라미터 주입(&admin=true 등)이 가능해진다.
  • 오픈 리디렉트(open redirect): ?next= 같은 값을 검증 없이 리디렉션에 쓰면 피싱에 악용된다. 허용 목록(allowlist)으로 호스트를 검증한다.
  • 민감 정보 노출: 토큰이나 비밀번호를 쿼리에 넣으면 서버 로그, 브라우저 기록, Referer 헤더에 남을 수 있다.
  • userinfo 사용: https://user:pw@host 형태는 보안상 권장되지 않으며 피싱에도 악용되므로 쓰지 않는다.
  • 국제화 도메인: 한글 도메인은 내부적으로 퓨니코드(Punycode, xn--...)로 변환된다.

8. 핵심 정리

  • URL은 스킴://권한/경로?쿼리#프래그먼트 구조이며 RFC 3986에 정의된다.
  • 프래그먼트는 서버로 전송되지 않는다.
  • 예약 문자와 비ASCII 문자는 UTF-8 바이트 기준 퍼센트 인코딩으로 표현한다.
  • 코드에서는 new URL()과 URLSearchParams로 파싱·조립하고, 문자열 이어붙이기는 피한다.
  • 이중 인코딩, 오픈 리디렉트, 쿼리 내 민감 정보 노출이 대표적인 실무 함정이다.
  • 동일 출처는 스킴, 호스트, 포트가 모두 같을 때 성립한다.