자바스크립트의 URL API로 웹 주소 다루기
웹 개발에서 URL을 다루는 일은 참 빈번하게 발생하죠? 프론트엔드에서는 URL로 다른 웹페이지로 링크를 걸고, CSS와 자바스크립트 코드를 불러오며, 이미지, 오디오, 비디오와 같은 멀티미디어도 제공할 수 있습니다. 반면에 백엔드에서는 요청 URL의 경로에 따라서 DB에 저장되어 있는 데이터를 조회하고 URL의 쿼리 스트링으로 넘어온 매개변수에 따라서 다른 데이터 처리를 해줄 수 있습니다.
이번 글에서는 웹 주소를 좀 더 안전하게 다룰 수 있도록 도와주는 자바스크립트의 URL API에 대해 알아보겠습니다.
URL이란?
우리는 거의 매일 URL을 사용하고 있으면서도, URL이 정작 무엇인지 진지하게 생각해볼 기회는 많지 않은 것 같죠?
URL은 Uniform Resource Locator의 약자로 쉽게 말해 웹에서 사용되는 주소를 의미하는데요. 현실 세계에서도 모든 건물의 세대마다 유일한 주소가 등록되어 있어서 우체부나 택배 기사가 배달을 할 수 있는 것처럼, 인터넷에도 모든 자원(resource)에 유일한 주소가 부여되어 있어야지 브라우저가 웹사이트를 방문하고 이미지나 오디오, 비디오를 불러올 수 있습니다.
현실 세계에도 국가마다 다른 주소 시스템이 있는 것처럼 URL에도 미리 약속된 규약이 있고 문서로 표준화가 되어 있는데요. 웹 개발자로서 URL을 다룰 때 관련 웹 표준에 대해서 잘 이해하고 있으면 큰 도움이 됩니다.
URL의 구성 요소
자바스크립트의 URL API에 대해서 본격적으로 배우기 전에 먼저 URL이 어떻게 구성되어 있는지 대해서 짚고 넘어가는 게 좋을 것 같아요.
그럼 아래와 같은 전형적인 URL을 가지고 함께 생각해볼까요?
https://example.org:8080/foo/bar?q=baz#bang
맨 앞 부분인 http:
는 스키마(scheme) 또는 프로토콜(protocol)을 나타내는데요.
즉, 브라우저가 웹 서버와 접속할 때 사용해야 할 통신 프로토콜을 의미합니다.
다양한 프로토콜이 있지만 웹에서는 보통 웹사이트에 접근할 때는 http:
또는 좀 더 안전한 https:
가 많이 사용됩니다.
다음으로 example.org
부분은 호스트 네임(hostname)이라고 하는데 이와 같이 사람이 읽기 쉬운 도메인 네임(domain name)이 될 수도 있고, 127.0.0.1
처럼 컴퓨터가 바로 이해할 수 있는 형태인 IP 주소가 될 수도 있습니다.
호스트 네임 뒤에 :
기호가 나오고 다음에 나오는 숫자는 포트(port)를 의미하는데요.
현실 세계에 비유하지만 호스트 네임이 아파트 한 동이고 포트를 각 호라고 볼 수 있을 것 같아요.
하나의 서버에는 여러 개의 출입구가 있고 각 출입구에 이렇게 번호가 붙어있다고 상상할 수 있을 것 같습니다.
그런데 기본 포트로 접근하는 경우에는 URL에서 포트 부분을 생략할 수 있는데요.
예를 들어, http:
프로토콜을 사용할 때는 80 포트, https:
프로토콜을 사용할 때는 443 포트가 기본 포트입니다.
따라서 만약에 URL 주소로 https://example.org/
을 사용하다면 https://example.org:443/
을 사용하는 것과 동일한 효과가 납니다.
지금까지 살펴본 프로토콜, 호스트 네임, 포트를 묶어서 출처(origin)이라고도 하는데요. 브라우저에서 보안 상의 이유로 다른 출처로의 HTTP 요청을 제한하는 CORS(Cross-Origin Resource Sharing)의 토대가 되는 개념이니 참고 바랍니다.
그 다음에 나오는 /foo/bar
는 경로명(pathname)라고 하며 서버에서 해당 리소스가 어디에 위치하는지를 나타냅니다.
과거에는 실제로 서버의 하드 드라이브 내에서 물리적인 파일의 경로를 사용하고, 그래서 경로 맨 뒤에 파일 확장자까지 붙는 경우가 많았는데요.
요즘에는 웹 서버의 자체적인 라우팅(routing)을 통해서 논리적인 위치를 나타내는 경우가 더 많은 것 같습니다.
?q=baz
부분은 보통 쿼리 스트링(query string)라고 하는데 검색 매개변수(search parameter)라고도 부르기도 합니다.
웹 서버에서는 URL의 쿼리 스트링을 분석하여 요청한 리소스를 응답하기 전에 다양한 추가 작업을 수행할 수 있는데요.
비단 검색 뿐만 아니라 필터링(filtering), 페이지네이션(pagination), 정렬(sorting) 등 다양한 용도로 사용됩니다.
?key1=value1&key2=value2&...
형태로 여러 개의 키와 값의 쌍을 매개변수로 명시할 수 있으며, ?key=value1&key=value2
와 같이 하나의 키에 여러 개의 값을 사용할 수도 있습니다.
URL의 제일 마지막 부분인 #bang
은 앵커(anchor), 해시(hash), 조각(fragment) 등 다소 다양한 이름으로 불리는데요.
리소스 내에서 특정 부분을 나타내는데 사용하며 긴 컨텐츠의 목차를 구현할 때 유용하게 사용됩니다.
예를 들어, 목차 내의 해시가 붙어 있는 링크를 클릭하면 브라우저가 웹 페이지의 특정 섹션으로 자동으로 스크롤되게 할 수 있습니다.
단, 해시는 클라이언트에서만 의미가 있는 부분이기 때문에 웹 서버로는 전송되지 않는다는 점에 주의하시기 바랍니다.
URL API 소개
예전에는 자바스크립트에서 URL을 일반 문자열로 다루는 경우가 많았는데요. 위에서 살펴본 것처럼 URL 문자열은 엄격한 규격을 따르기 때문에 일반 문자열을 다루듯이 다루면 버그가 생기기 쉽죠. 따라서 URL에서 특정 요소를 추출하거나 조작하는데 적지 않은 노력과 주의가 필요했었습니다.
이러한 문제점을 보완하기 위해서 현재 대부분의 브라우저와 Node.js, Bun과 같은 자바스크립트 런타임에서는 URL API를 지원하고 있는데요. 이 웹 표준 API를 이용하면 웹 주소를 보다 쉽고 안전하게 자바스크립트로 다룰 수 있게 됩니다.
URL API는 크게 웹주소를 다루기 위한 URL
과 쿼리 스트링을 다루기 위한 URLSearchParams
로 나누어지는데요.
본 포스팅에서는 URL
에 대해서만 살펴보고 URLSearchParams
는 별도의 포스팅에서 다루도록 하겠습니다.
URL 객체의 생성
자바스크립트에서 URL API를 사용하려면 우선 URL 객체를 생성해야 하는데요.
URL()
생성자에 문자열 형태의 웹 주소를 전달하면 URL 객체가 만들어 집니다.
new URL("https://example.org:8080/foo/bar?q=baz#bang");
URL()
생성자는 자동으로 웹 주소도 해주기 때문에 URL에 포함되어 있는 다국어나 특수 문자를 처리하는 것에 대해서 걱정하지 않아도 됩니다.
new URL("https://example.org:8080/파일 이름.pdf");
// 'https://example.org:8080/%ED%8C%8C%EC%9D%BC%20%EC%9D%B4%EB%A6%84.pdf'
뿐만 아니라, URL()
생성자는 인자로 넘어온 URL 문자열의 유효성을 검사하여 유효하지 않은 경우에는 에러를 발생시킵니다.
그러므로 우리는 더욱 안전하게 자바스크립트로 URL을 다룰 수 있게 됩니다.
new URL("user@test.com"); // Failed to construct 'URL': Invalid URL
URL 객체의 속성
URL 객체에는 URL을 구성하고 있는 모든 요소들이 속성으로 저장되어 있어서 우리는 각 요소에 쉽게 접근할 수 있습니다.
예를 들어, https://example.org:8080/foo/bar?q=baz#bang
라는 URL 문자열을 가지고 URL 속성을 만들고 여러 속성에 접근해보겠습니다.
const url = new URL("https://example.org:8080/foo/bar?q=baz#bang");
url.protocol; // 'https:'
url.hostname; // 'example.org'
url.port; // '8080'
url.pathname; // '/foo/bar'
url.search; // '?q=baz'
url.hash; // '#bang'
url.host; // example.org:8080
url.origin; // 'https://example.org:8080'
url.href; // 'https://example.org:8080/foo/bar?q=baz#bang'
URL 객체의 속성은 읽을 수 있을 뿐만 아니라 쓸 수도 있는데요. 특정 속성을 갱신함으로써 아주 쉽게 새로운 URL을 얻을 수 있습니다.
url.pathname = "/new";
url.href; // 'https://example.org:8080/new?q=baz#bang'
url.hash = "#bangbangbang";
url.href; // 'https://example.org:8080/new?q=baz#bangbangbang'
예외적으로 origin
속성은 읽기 전용이라서 주의가 필요한데요.
다른 속성처럼 새로운 값을 할당하더라도 URL 객체에 반영이 되지 않기 때문입니다.
url.origin = "http://127.0.0.1:4321";
url.origin; // 'https://example.org:8080'
url.href; // 'https://example.org:8080/new?q=baz#bangbangbang'
origin
속성을 편의 상 protocol
, hostname
, port
속성을 엮어 놓은 지름길이라고 생각하시면 기억이 쉬우실 것 같습니다.
`${url.protocol}//${url.hostname}:${url.port}`; // 'https://example.org:8080'
URL 객체의 메서드
URL 객체에는 toString()
이라는 메서드도 있는데요.
href
속성 대신에 쓸 수 있으며 차이점이라고 하면 toString()
으로는 값을 읽을 수만 있으며 쓸 수는 없습니다.
url.toString(); // 'https://example.org:8080/foo/bar?q=baz#bang'
URL 객체가 toString()
메서드를 제공하기 때문에 자바스크립트의 fetch()
함수의 인자로 바로 넘기는 것도 가능해집니다.
fetch(new URL("https://example.org:8080/foo/bar?q=baz#bang"));
자바스크립트에서 원격 API 호출할 때 사용하는 fetch() 함수에 대해서는 관련 포스팅에서 자세히 다루었으니 참고 바랍니다.
Base URL 활용
URL()
생성자에 두 번째 인자로 기본(base) URL을 넘기면 첫 번째 인자로 절대 또는 상대 경로를 사용할 수도 있습니다.
const url1 = new URL("/xx", "https://example.org:8080/foo/bar?q=baz#bang");
url1.href; // 'https://example.org:8080/xx'
const url2 = new URL("xx", "https://example.org:8080/foo/bar?q=baz#bang");
url2.href; // 'https://example.org:8080/foo/xx'
const url3 = new URL("../xx", "https://example.org:8080/foo/bar?q=baz#bang");
url3.href; // 'https://example.org:8080/xx'
이 기능은 특히 현재 브라우저에서 열린 URL을 기준으로 다른 URL을 만들 때 유용합니다.
const newUrl = new URL("/new", window.location);
nweUrl.href; // <현재 URL>/new
location.assign(nweUrl.href); // 새로운 페이지로 이동
브라우저의 location 전역 객체를 사용하여 자바스크립트로 웹 페이지를 이동하는 방법에 대해서는 관련 포스팅을 참고 바랍니다.
마치면서
지금까지 URL에 대한 핵심 개념을 짚어보고 자바스크립트에서 URL 객체를 통해 안전하게 웹 주소를 다를 수 있는 방법에 대해서 살펴보았습니다.
URL
객체를 통해서 URL를 구성하는 대부분의 요소를 안전하게 다룰 수 있지만 여러 개의 키와 값의 쌍으로 이루어진 search
부분은 여전히 다루기가 까다로운데요.
다음 포스팅에서는 이러한 문제를 해결해주는 URL API의 다른 부분인 URLSearchParams
에 대해서 알아보겠습니다.