현록

URL path parameter와 query string 차이

API를 호출하거나 라우팅을 만들다 보면 path parameter와 query string을 자주 만난다.
둘 다 URL 안에 값을 담는 방법이지만, 의미는 다르다.

/users/4242는 보통 path parameter로 다룬다.
/users?role=admin&page=2role=admin&page=2는 query string으로 다룬다.

핵심 차이는 값의 역할이다.
path parameter는 주로 어떤 리소스인지 식별하는 값에 쓴다.
query string은 주로 조회 조건, 필터, 정렬, 페이지 같은 선택 조건에 쓴다.

URL의 구성

URL은 여러 부분으로 나뉜다.
아래 주소를 예로 보자.

https://example.com/users/42/posts?sort=latest&page=2#comments

브라우저의 URL 객체로 보면 중요한 부분을 나눠 읽을 수 있다.

const url = new URL('https://example.com/users/42/posts?sort=latest&page=2#comments')

console.log(url.origin) // 'https://example.com'
console.log(url.pathname) // '/users/42/posts'
console.log(url.search) // '?sort=latest&page=2'
console.log(url.hash) // '#comments'

pathname은 경로다.
search?로 시작하는 query string이다.
hash는 브라우저 안에서 특정 위치나 상태를 가리킬 때 자주 쓰는 fragment다.

이 글에서는 pathname 안에 들어가는 path parameter와 search에 들어가는 query string을 비교한다.

path parameter의 역할

path parameter는 경로의 일부로 들어가는 값이다.
서버나 프레임워크는 라우트 패턴을 정해두고, 실제 URL의 일부를 변수처럼 꺼낸다.

Route pattern: /users/:userId
Actual URL:    /users/42

여기서 42userId로 해석될 수 있다.
Next.js 같은 파일 기반 라우팅에서는 아래처럼 표현할 수도 있다.

app/users/[userId]/page.tsx

실제 URL은 여전히 /users/42처럼 보인다.
[userId]:userId 같은 표기는 라우터가 값을 꺼내기 위한 약속이다.

path parameter는 보통 리소스를 식별하는 데 쓴다.

/users/42
/posts/css-position-relative와-absolute-차이
/products/100

이 값이 빠지면 어떤 리소스를 요청하는지 알기 어렵다.
그래서 path parameter는 필수 값에 가깝게 다루는 경우가 많다.

query string의 역할

query string은 URL에서 ? 뒤에 붙는 값이다.
여러 값을 &로 이어 붙인다.

/posts?tag=css&page=2&sort=latest

이 값들은 보통 조회 조건을 표현한다.
태그 필터, 페이지 번호, 정렬 방식, 검색어 같은 값이 여기에 잘 맞는다.

/products?category=keyboard&minPrice=50000&maxPrice=150000

query string은 선택 조건을 추가하거나 바꾸기 쉽다.
같은 목록 화면에서 필터만 바꾸거나, 페이지 번호만 바꾸는 상황과 잘 맞는다.

검색 페이지도 query string과 잘 어울린다.

/search?q=javascript

이런 URL은 복사해서 공유하거나 북마크하기 쉽다.
HTTP GET 요청의 조회 조건을 URL에 드러내는 방식이라고 보면 된다.
GET의 기준이 헷갈린다면 HTTP GET과 POST 차이를 같이 보면 좋다.

선택 기준

값이 리소스의 정체성을 나타내면 path parameter를 쓴다.

/users/42
/posts/url-path-parameter-query-string

값이 리소스 목록을 어떻게 볼지 정하는 조건이면 query string을 쓴다.

/users?role=admin
/posts?tag=javascript&page=2

예를 들어 특정 사용자 상세 페이지는 path parameter가 자연스럽다.

/users/42

관리자 사용자 목록은 query string이 자연스럽다.

/users?role=admin

첫 번째 URL은 42번 사용자라는 리소스를 직접 가리킨다.
두 번째 URL은 사용자 목록을 보되 role=admin 조건으로 걸러서 보겠다는 뜻에 가깝다.

필수 값과 선택 조건

path parameter는 보통 필수 값이다.
/posts/:slug 라우트에서 slug가 없으면 특정 글을 찾을 수 없다.

/posts/쿠키와-localStorage-차이

query string은 보통 없어도 기본 동작이 가능한 선택 조건이다.

/posts
/posts?tag=javascript
/posts?tag=javascript&page=2

/posts는 전체 글 목록을 보여줄 수 있다.
tag=javascript가 붙으면 JavaScript 태그만 보여줄 수 있다.
page=2가 붙으면 두 번째 페이지를 보여줄 수 있다.

물론 모든 서비스가 이 기준을 똑같이 쓰는 것은 아니다.
하지만 초보자라면 “식별은 path, 조건은 query”로 시작하면 대부분의 API를 읽기 쉬워진다.

URLSearchParams 사용

query string은 문자열로 직접 붙일 수 있다.

const url = `/posts?tag=javascript&page=2`

하지만 값이 동적으로 바뀐다면 URLSearchParams를 쓰는 편이 안전하다.

const params = new URLSearchParams()

params.set('tag', 'javascript')
params.set('page', '2')

const url = `/posts?${params.toString()}`

URLSearchParams는 query string을 다루기 위한 Web API다.
값을 추가하고, 읽고, 삭제하고, 문자열로 바꿀 수 있다.

const params = new URLSearchParams('tag=javascript&page=2')

console.log(params.get('tag')) // 'javascript'
console.log(params.get('page')) // '2'

현재 페이지 URL에서 query string을 읽을 때는 URL 객체와 함께 쓰면 좋다.

const url = new URL(window.location.href)
const tag = url.searchParams.get('tag')

주의할 점도 있다.
new URLSearchParams()는 전체 URL을 파싱하는 도구가 아니다.
전체 URL을 다룰 때는 먼저 new URL()을 만들고 searchParams를 사용한다.

const url = new URL('https://example.com/posts?tag=css')
console.log(url.searchParams.get('tag')) // 'css'

여러 값 처리

같은 key를 여러 번 보낼 수도 있다.

/posts?tag=css&tag=javascript

이런 경우 get()은 첫 번째 값만 가져온다.

const params = new URLSearchParams('tag=css&tag=javascript')

console.log(params.get('tag')) // 'css'

모든 값을 가져오려면 getAll()을 사용한다.

console.log(params.getAll('tag')) // ['css', 'javascript']

필터 UI에서 여러 태그를 선택할 수 있다면 이 방식이 자연스러울 수 있다.
다만 서버와 클라이언트가 같은 규칙을 써야 한다.
어떤 서비스는 tag=css,javascript처럼 하나의 값에 쉼표로 묶기도 하고, 어떤 서비스는 같은 key를 반복하기도 한다.

인코딩 기준

URL에는 공백, 한글, &, ?, / 같은 문자가 그대로 들어가면 의미가 깨질 수 있다.
그래서 URL에 값을 넣을 때는 인코딩을 신경 써야 한다.

const keyword = 'CSS & HTML'
const url = `/search?q=${encodeURIComponent(keyword)}`

console.log(url) // '/search?q=CSS%20%26%20HTML'

encodeURIComponent()는 URL의 일부로 들어갈 값을 인코딩할 때 사용한다.
특히 사용자가 입력한 값을 query string에 직접 붙일 때 중요하다.

가능하면 문자열을 직접 조립하기보다 URLSearchParams를 쓰는 편이 실수를 줄인다.

const params = new URLSearchParams()
params.set('q', 'CSS & HTML')

console.log(`/search?${params.toString()}`)

path parameter도 인코딩이 필요할 수 있다.
slug처럼 URL에 들어가는 값에는 공백이나 특수문자가 섞이지 않도록 미리 규칙을 정하는 편이 좋다.

민감한 값의 위치

path parameter와 query string은 모두 URL에 드러난다.
브라우저 주소창, 방문 기록, 서버 로그, 분석 도구, 공유된 링크에 남을 수 있다.

그래서 비밀번호, access token, 개인 식별 정보처럼 노출되면 안 되는 값은 URL에 넣지 않는 편이 좋다.

/login?password=secret

이런 URL은 피해야 한다.
민감한 값을 보내야 한다면 HTTPS를 사용하고, request body나 안전한 쿠키 설계를 검토해야 한다.
브라우저 저장소와 쿠키 기준이 헷갈리면 쿠키와 localStorage 차이를 같이 보면 좋다.

흔한 실수

첫 번째 실수는 모든 값을 query string에 넣는 것이다.

/posts?slug=url-path-parameter-query-string

특정 글 상세 페이지라면 아래처럼 path에 두는 편이 더 자연스럽다.

/posts/url-path-parameter-query-string

두 번째 실수는 필터 값을 path에 계속 쌓는 것이다.

/posts/tag/javascript/page/2/sort/latest

동작할 수는 있지만 조건이 늘어날수록 URL 구조가 딱딱해진다.
필터와 정렬, 페이지처럼 조합이 많은 값은 query string이 더 다루기 쉽다.

/posts?tag=javascript&page=2&sort=latest

세 번째 실수는 URL 조립을 문자열 더하기로만 처리하는 것이다.
값에 공백이나 &가 들어오면 의도하지 않은 query string이 될 수 있다.
동적인 값은 URL, URLSearchParams, encodeURIComponent()를 사용해 조립하는 편이 안전하다.

선택 기준 정리

리소스를 식별하는 값이면 path parameter를 쓴다.
사용자 ID, 게시글 slug, 상품 ID처럼 없으면 대상이 정해지지 않는 값이 여기에 가깝다.

조회 조건을 표현하는 값이면 query string을 쓴다.
검색어, 필터, 정렬, 페이지, 탭 상태처럼 없어도 기본 화면을 보여줄 수 있는 값이 여기에 가깝다.

URL에 넣은 값은 사용자와 시스템 여러 곳에 드러날 수 있다.
민감한 값은 path에도 query에도 넣지 않는 편이 좋다.

정리하면 기준은 단순하다.
대상을 고르면 path parameter다.
보는 방식을 바꾸면 query string이다.
동적인 query string은 URLSearchParams로 다룬다.

참고 자료

관련 포스트
쿠키와 localStorage 차이 thumbnail
쿠키와 localStorage 차이
쿠키와 localStorage의 차이를 초보자 기준으로 정리합니다. 서버 자동 전송, JavaScript 접근, 만료 시간, HttpOnly, Secure, SameSite, 로그인 상태 저장 기준을 함께 봅니다.
script async와 defer 차이 thumbnail
script async와 defer 차이
HTML script 태그의 기본 동작, async와 defer의 다운로드 방식, 실행 순서, DOMContentLoaded와의 관계를 초보자 기준으로 정리합니다.
CORS 에러와 해결 기준 thumbnail
CORS 에러와 해결 기준
브라우저의 Same-Origin Policy, CORS 응답 헤더, preflight, 서버에서 해결해야 하는 이유를 초보자 기준으로 정리합니다.
fetch와 axios 차이 thumbnail
fetch와 axios 차이
fetch와 axios의 차이를 초보자 기준으로 정리합니다. 설치 여부, JSON 처리, HTTP 에러 처리, timeout, interceptor, 언제 어떤 방식을 쓰면 좋은지 함께 봅니다.
Vue 3 입문기 thumbnail
Vue 3 입문기
Vue의 메인 버전이 3가 된지 꽤 됐다. 작성 당시에는 Nuxt 3가 RC 단계였기 때문에 실서비스에 바로 적용하기에는 조심스러웠다. 그래서 궁금하고 심심하던 참에 간단한 Todo App을 만들어봤다.
marked.js renderer custom 하기 thumbnail
marked.js renderer custom 하기
marked.js renderer custom 방법을 정리합니다. Renderer의 역할, image와 link 렌더러 커스텀, alt와 rel 처리, sanitize-html 같은 HTML 정화 주의점까지 함께 봅니다.
가독성 있게 상수 넣기 thumbnail
가독성 있게 상수 넣기
오늘 회사 동료의 PR 리뷰 과정에서 좋은 기능을 공유해주셔서 TIL로 남겨봅니다. 아래와 같이 상수에 언더바(_)로 콤마처럼 구분을 시켜줄 수 있습니다. 앞으로 깔끔하고 좋은 코드를 작성하기 위해 자주 사용해야겠습니다 :)
TS에서 generic optional 하게 설정하기 thumbnail
TS에서 generic optional 하게 설정하기
오늘 next에서 `getStaticProps`와 `getLayout` 패턴을 함께 사용할 때, typescript generic을 넘겨주는 작업을 하고 있었는데, 기본값이 없다보니 기존 코드에 에러가 발생했었다. 이를 해결하기 위해 찾아보니 단순히 아래 예시처럼 `= {}`을 추가해주면 해결된다고 한다.
Backend에서 API Response가 snake_case인 경우엔? thumbnail
Backend에서 API Response가 snake_case인 경우엔?
안녕하세요. 프론트엔드 개발자의 경우, 가끔 백엔드의 API Response 값이 snake_case일 경우 어떻게 관리할지에 대해 고민에 빠지게 됩니다. 저도 오늘 같은 상황을 겪게 되었는데,  이번엔 네이밍 컨벤션을 맞춰주기로 했습니다. 컨벤션을 맞추는 데에는 여러가지 방법이 있겠지만, 고민 끝에 저는 axios의 interceptors를 통해 해결을 해보았습니다.
Promise 다루기 (feat. 병렬실행, 순차실행) thumbnail
Promise 다루기 (feat. 병렬실행, 순차실행)
오늘은 Promise를 통해 구문을 동기 처리 할 때, 여러 Promise들을 다루는 법을 소개해보겠습니다. Javscript를 작성하다 보면, 가끔 여러 Promise들을 다룰 때가 있습니다. 필자도 Nodejs 서버에서 동시에 여러 쿼리를 실행할 때 자주 마주쳤었는데요. 오늘은 어떻게 하면 Promise들을 유연하게 다룰 수 있는지 알아보겠습니다. 시작하기 앞서, 네 가지 Promise를 선언하고, 그들을 하나의 Array에 묶어보겠습니다.