개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

공부 › 네트워크 › 주소와 이름

URL의 구조

검색어 "A&B"를 주소에 붙였더니 서버에는 "A"만 도착했다

URL은 정해진 기호(://, :, /, ?, #, &, =)를 기준으로 스킴, 호스트, 포트, 경로, 쿼리, 프래그먼트로 나뉩니다. 이 기호들은 주소 안에서 구분하는 역할을 맡고 있어서, 값 안에 같은 글자를 그대로 넣으면 주소가 엉뚱한 곳에서 잘립니다.

실행으로 대조 · Node.js 24의 URL, URLSearchParams, HTTP 클라이언트(127.0.0.1의 시험 서버로 받음)로 삼백 가지 조합을 실행해 대조

이 실습에 나오는 개념

URI와 URL
URI는 자원을 식별하는 문자열이고, URL은 그중 자원의 위치(접근 방법과 주소)로 식별하는 것이다. 웹에서 쓰는 주소는 대부분 URL이며 URL은 URI에 포함되는 개념이다.
URL의 구성 요소
URL은 정해진 기호를 기준으로 여러 부분으로 나뉜다. https://shop.example:8443/shop/list?q=a#review 에서 https가 스킴, shop.example이 호스트, 8443이 포트, /shop/list가 경로, q=a가 쿼리, review가 프래그먼트다. 포트를 적지 않으면 스킴의 기본 포트(http는 80, https는 443)로 접속한다. 호스트와 포트는 어디에 연결할지를 정하고, 경로와 쿼리는 요청 줄에 실려 서버로 간다. 프래그먼트는 브라우저가 받은 문서 안의 위치를 찾는 데만 쓰며 서버로 보내지 않는다.
퍼센트 인코딩
주소에서 특별한 뜻이 있거나 주소에 쓸 수 없는 글자를 % 뒤에 16진수 두 자리를 붙인 모양으로 바꿔 적는 방법이다. 글자를 UTF-8 바이트로 바꾼 뒤 바이트마다 %XX로 적으므로 &는 %26이 되고 한글 한 글자는 %XX 세 개가 된다. 쿼리의 값에 &, =, #, + 같은 글자를 그대로 넣으면 구분 기호로 읽혀 값이 잘리거나 바뀌므로, 값은 인코딩해서 넣어야 한다. 쿼리에서는 공백을 +로 적기도 하며, 그래서 + 글자 자체는 %2B로 적어야 한다.
쿼리 스트링과 폼 본문
쿼리 스트링은 주소에서 ? 뒤에 이름=값을 &로 이어 붙인 부분이고, 폼 본문은 같은 모양의 글자를 요청 본문에 실어 보낸 것이다. 폼 본문을 보낼 때는 Content-Type 헤더를 application/x-www-form-urlencoded로 적는다. HTML의 form을 GET으로 보내면 값이 쿼리 스트링에, POST로 보내면 폼 본문에 실린다. 서블릿 컨테이너는 이 둘을 합쳐 "요청 파라미터"로 제공하므로, 서버 코드는 어느 쪽으로 왔는지 구분하지 않고 같은 방법으로 꺼낸다. JSON 본문은 요청 파라미터에 포함되지 않는다.
상대 URL
스킴과 호스트를 생략하고 주소의 일부만 적은 URL이다. 브라우저는 지금 문서의 주소를 기준으로 빠진 부분을 채워 완전한 주소로 만든다. /로 시작하면 경로 전체를 바꾸고, 그렇지 않으면 기준 경로의 마지막 조각을 바꾸며, ..은 한 단계 위를 뜻한다. 기준 경로가 /로 끝나는지에 따라 결과가 달라진다. /shop/list를 기준으로 한 detail은 /shop/detail이지만, /shop/list/를 기준으로 하면 /shop/list/detail이다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. 주소 만들기: 검색어("A&B")를 글자 그대로 주소 뒤에 이어 붙였습니다. 만들어진 글자는 https://shop.example/shop/list?q=A&B&sort=price#review 입니다.
  2. 스킴: 맨 앞에서 :// 앞까지인 https입니다. 어떤 프로토콜로 접속할지를 정합니다.
  3. 호스트와 포트: :// 다음부터 첫 번째 / 앞까지가 접속할 곳입니다. 호스트는 shop.example입니다. 포트는 적지 않았으므로 https의 기본 포트인 443번으로 접속합니다.
  4. 경로: 호스트와 포트 다음의 /shop/list입니다. 서버 안에서 무엇을 달라는 것인지를 가리키며, 첫 번째 ? 앞에서 끝납니다.
  5. 쿼리: ? 다음부터 첫 번째 # 앞까지인 q=A&B&sort=price입니다. 서버에 넘길 이름=값 쌍을 &로 이어 적는 자리입니다.
  6. 프래그먼트: 첫 번째 # 뒤 전부인 review입니다. 받은 문서 안의 어느 위치인지를 가리키는 표시이고 브라우저만 씁니다. 서버로는 보내지 않습니다.
  7. 서버로 가는 것: 브라우저는 shop.example의 443번 포트에 연결해, 요청 줄(GET /shop/list?q=A&B&sort=price HTTP/1.1)과 Host 헤더(Host: shop.example)를 보냅니다. 요청 줄에는 경로와 쿼리만 들어가고, 프래그먼트는 어디에도 실리지 않습니다.
  8. 서버의 쿼리 풀이: 쿼리를 &에서 나누고, 조각마다 첫 번째 =에서 이름과 값으로 나눈 다음, +는 공백으로 %XX는 원래 글자로 되돌립니다. 결과는 q="A", B="", sort="price"입니다. 검색어 안의 &가 쌍을 나누는 기호로 읽혔습니다. q의 값은 "A"에서 잘렸고, B라는 엉뚱한 이름이 하나 더 생겼습니다.
  9. 상대 주소 풀이: 이 페이지 안의 링크(href="detail")는 주소의 일부만 적은 것이라서, 브라우저가 지금 주소를 기준으로 빠진 부분을 채웁니다. /로 시작하지 않으므로, 지금 경로(/shop/list)의 마지막 조각을 detail로 바꿉니다. 쿼리와 프래그먼트는 물려받지 않습니다. 결과는 https://shop.example/shop/detail 입니다.

실행 결과

코드

// 자바스크립트입니다. 브라우저 개발자 도구의 콘솔에 붙여 넣으면 실행됩니다.
const keyword = 'A&B';

// 검색어를 글자 그대로 이어 붙인다
const url = new URL('https://shop.example/shop/list?q=' + keyword + '&sort=price#review');

console.log(url.href);                                   // 브라우저가 정리한 주소
console.log(url.protocol, url.hostname, url.port, url.pathname, url.search, url.hash);
console.log([...url.searchParams]);                      // 쿼리를 이름과 값으로 풀이한 결과 (서버가 하는 풀이와 같은 규칙)
console.log(new URL('detail', url).href);                // 링크(href="detail")가 가리키는 주소

이 모형의 전제

삼백 가지 조합을 모두 Node.js 24에서 실행해 대조했습니다. 주소를 나눈 결과와 정리된 주소, 상대 주소의 풀이는 Node.js의 URL(브라우저와 같은 WHATWG URL 표준의 구현)이 낸 값과 대조했습니다. 접속 포트, 요청 줄, Host 헤더는 Node.js의 HTTP 클라이언트로 그 주소에 요청을 보내고, 연결만 127.0.0.1의 시험 서버로 돌려 실제로 나간 글자를 받아 대조했습니다(shop.example에 대한 DNS 조회나 외부 접속은 하지 않았고, https 주소도 암호화하지 않은 채 받았습니다). 서버가 받은 값은 그 요청 줄의 쿼리를 URLSearchParams로 풀이한 것입니다. 톰캣 같은 다른 서버도 &와 =로 나누고 +와 %XX를 되돌리는 같은 규칙(application/x-www-form-urlencoded)을 쓰지만, 이 실습에서 따로 실행해 보지는 않았습니다. 잘못된 %XX가 왔을 때 오류로 처리하는지처럼 서버마다 다른 부분은 다루지 않았습니다. encodeURIComponent는 공백을 +가 아닌 %20으로 바꾸며, 쿼리에서는 둘 다 공백으로 풀립니다(이 차이도 실행으로 확인했습니다). 주소가 나뉘는 규칙은 RFC 3986 3절, 프래그먼트를 떼어 내고 요청한다는 것은 3.5절, 상대 주소의 풀이는 5.2절, 요청 줄과 Host 헤더는 RFC 9112 3.2절과 RFC 9110 7.2절에 있습니다. #으로 시작하는 링크를 눌렀을 때 서버에 새 요청이 가지 않는다는 것은 브라우저의 동작(HTML 표준의 프래그먼트 이동)을 따라 적은 것이고 실행해 보지는 않았습니다. 사용자 이름이 든 주소, IPv6 주소, 한글 도메인은 다루지 않았습니다.

앞 실습: 계층 구조와 캡슐화 · 다음 실습: DNS

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요