Query String이란 무엇이고 언제 어떻게 써야 할까
프론트엔드 개발을 하다 보면 언젠가 이런 고민을 하게 됩니다.
- 검색 조건을 새로고침 뒤에도 유지하고 싶을 때
- 필터와 정렬 상태를 링크로 공유하고 싶을 때
- 페이지네이션 상태를 URL로 표현하고 싶을 때
- 반대로 어떤 값은 URL에 두면 안 될 것 같을 때
이때 자주 등장하는 것이 Query String입니다.
겉보기에는 단순합니다. URL 뒤에 ?page=2&sort=latest 같은 값을 붙이는 것이니까요. 하지만 실무에서는 단순히 "파라미터를 붙이는 문법" 정도로 이해하면 금방 한계에 부딪힙니다. 중요한 것은 어떤 상태를 URL에 드러내야 하고, 어떤 상태는 URL 밖에 둬야 하는지 판단하는 기준입니다.
이 글에서는 Query String을 아래 흐름으로 정리해보겠습니다.
- 무엇인지
- path, hash와는 무엇이 다른지
- 왜 실무에서 중요한지
- 어떤 상태를 넣으면 좋고 어떤 상태는 피해야 하는지
URLSearchParams로 어떻게 다루는지- React와 Next.js에서는 어떤 패턴으로 연결하는지
한눈에 보면
먼저 짧게 정리하면 이렇습니다.
Query String은 URL에서?뒤에 오는key=value형태의 파라미터 집합입니다.- 보통 검색, 필터, 정렬, 페이지네이션처럼 공유 가능하고 복원 가능해야 하는 상태를 표현할 때 잘 맞습니다.
- 반대로 민감한 정보, 너무 큰 데이터, 일시적인 UI 상태는 넣지 않는 편이 좋습니다.
- 실무에서는 문자열 결합보다
URLSearchParams를 사용하는 편이 안전합니다. - 핵심은 "URL에 넣을 수 있는가?"보다 "URL에 드러나야 하는 상태인가?" 입니다.
즉, Query String은 단순 문법이 아니라 브라우저 주소창을 상태 저장소처럼 활용하는 방식에 가깝습니다.
Query String은 정확히 어디를 말할까?
가장 단순한 URL 예제로 보면:
/posts?category=frontend&sort=latest&page=2여기서:
/posts는 path?category=frontend&sort=latest&page=2는 query string
조금 더 쪼개면 각각의 항목은 보통 query parameter라고 부릅니다.
category=frontendsort=latestpage=2
즉:
- 전체:
query string - 개별 항목:
query parameter
실무에서는 둘을 섞어 말하는 경우도 많지만, 개념상으로는 이렇게 나누어 생각하면 정리가 쉽습니다.
path parameter, query string, hash는 무엇이 다를까?
이 부분이 생각보다 중요합니다. 같은 상태라도 URL 어디에 두느냐에 따라 의미가 달라지기 때문입니다.
예를 들어 아래 URL을 보겠습니다.
/posts/123?tab=comments#latest여기에는 세 종류의 정보가 같이 들어 있습니다.
1. path
/posts/123이 부분은 보통 "어떤 리소스인가"를 나타냅니다.
- 게시글 목록인지
- 게시글 상세인지
- 특정 id의 자원인지
즉, path는 대개 페이지의 정체성에 가깝습니다.
2. query string
?tab=comments이 부분은 같은 리소스를 어떤 조건으로 보고 있는지에 더 가깝습니다.
- 필터
- 정렬
- 검색어
- 페이지 번호
- 현재 탭
즉, query string은 보통 보기 방식이나 조회 조건을 표현합니다.
3. hash
#latest이 부분은 보통 같은 문서 안에서 특정 위치로 이동할 때 씁니다.
- 문서 목차 이동
- 특정 섹션 강조
- SPA 내부 앵커 이동
즉, hash는 서버에 보내는 데이터라기보다 문서 내부 위치 정보에 더 가깝습니다.
짧게 비교하면 아래처럼 볼 수 있습니다.
| 구분 | 주로 의미하는 것 | 예시 |
|---|---|---|
| path | 리소스 자체 | /posts/123 |
| query string | 조회 조건, 보기 방식, URL 상태 | ?sort=latest&page=2 |
| hash | 문서 내부 위치, 앵커 | #comments |
즉, "게시글 123"은 path에 가깝고, "댓글 탭으로 보기"는 query string에 가깝고, "댓글 영역으로 스크롤"은 hash에 가깝습니다.
왜 Query String이 실무에서 중요할까?
실무에서 Query String이 중요한 이유는 단순합니다. 상태를 링크로 만들 수 있기 때문입니다.
예를 들어 상품 목록 화면을 생각해보겠습니다.
- 카테고리:
shoes - 정렬:
price_asc - 브랜드:
nike - 페이지:
3
이 상태를 URL로 표현하면:
/products?category=shoes&brand=nike&sort=price_asc&page=3이렇게 됩니다.
이 표현이 좋은 이유는 아래와 같습니다.
- 새로고침해도 상태를 복원할 수 있고
- 뒤로 가기/앞으로 가기 흐름이 자연스럽고
- 링크를 공유하면 같은 조건을 다른 사람도 바로 볼 수 있고
- 서버 로그나 분석 기준에서도 어떤 조회 조건이었는지 파악하기 쉽습니다
즉, Query String은 화면 상태를 브라우저 히스토리와 공유 가능한 형태로 외부화하는 도구입니다.
이 지점이 중요합니다. 모든 상태를 React state나 전역 상태 안에만 두면 구현은 쉬울 수 있어도:
- 새로고침 시 초기화되고
- URL만 봐서는 현재 맥락을 알 수 없고
- 링크 공유가 어려워질 수 있습니다
그래서 검색 조건, 필터, 정렬처럼 "사용자 입장에서 지금 이 화면을 다시 열었을 때 그대로 복원되길 기대하는 상태"는 Query String이 특히 잘 맞습니다.
어떤 값이 Query String에 잘 맞을까?
이 질문이 실무에서 가장 중요합니다.
보통 아래 조건에 맞으면 URL로 드러내기 좋습니다.
1. 공유 가능한 상태
예를 들어 검색 결과를 동료에게 그대로 보여주고 싶다면:
/search?q=nextjs+cache&category=frontend같은 URL 하나로 맥락이 전달됩니다.
즉, 링크 공유가 자연스러운 상태는 Query String 후보입니다.
2. 새로고침 뒤에도 유지되어야 하는 상태
예를 들어 목록 필터와 정렬은 새로고침 후에도 유지되길 기대하는 경우가 많습니다.
- 카테고리
- 정렬 기준
- 현재 페이지
- 검색어
이런 값은 메모리 state보다 URL에 둘 때 사용성이 더 좋아질 수 있습니다.
3. 서버 조회 조건으로 그대로 이어지는 상태
예를 들어:
/posts?tag=react&page=2&sort=popular이 값들이 결국 API 요청 조건으로 이어진다면 URL에 표현하는 것이 자연스럽습니다.
즉, 서버가 "무엇을 어떻게 조회할지"와 직접 연결되는 값은 query string과 잘 맞습니다.
4. 문자열로 직렬화하기 쉬운 상태
Query String은 결국 문자열입니다. 그래서 아래처럼 단순한 값이 잘 맞습니다.
- 문자열
- 숫자
- boolean
- 짧은 enum 값
- 작은 배열
반대로 중첩 객체나 아주 큰 상태는 URL로 다루기 불편해집니다.
어떤 값은 Query String에 넣지 않는 편이 좋을까?
반대로 아래와 같은 값은 주의가 필요합니다.
1. 민감한 정보
대표적으로 이런 값들입니다.
- access token
- refresh token
- 주민등록번호, 이메일 같은 민감 식별 정보
- 내부 관리자 전용 민감 값
이유는 단순합니다. URL은 생각보다 노출 범위가 넓습니다.
- 브라우저 히스토리에 남고
- 복사/공유되기 쉽고
- 로그나 분석 도구에 기록될 수 있고
- 레퍼러를 통해 다른 곳으로 전달될 수도 있습니다
즉, 보안상 민감한 값은 Query String에 두지 않는 편이 맞습니다.
2. 너무 크거나 복잡한 상태
예를 들어 이런 식입니다.
?filters=%7B%22category%22%3A%5B...%5D%2C%22price%22%3A...이론적으로는 가능하지만, 실무에서는 금방 문제가 됩니다.
- URL이 지나치게 길어지고
- 디버깅이 어려워지고
- 사람이 읽기 어려워지고
- 직렬화/역직렬화 비용이 커집니다
이런 상태는 URL보다 로컬 상태나 서버 저장 방식이 더 나을 수 있습니다.
3. 아주 일시적인 UI 상태
예를 들어:
- 모달 열림 여부
- hover 상태
- 드롭다운 펼침 여부
- 임시 입력 중인 폼 값 전체
이런 값은 보통 공유하거나 복원할 필요가 없습니다. 오히려 URL에 넣으면 과하게 복잡해질 수 있습니다.
물론 예외는 있습니다. 예를 들어 특정 모달을 링크로 바로 열어야 한다면 ?modal=login 같은 형태가 의미 있을 수 있습니다. 중요한 것은 "이 상태를 URL로 드러낼 이유가 있는가"입니다.
4. 순서와 구조가 지나치게 중요한 복합 상태
예를 들어 복잡한 드래그 앤 드롭 빌더의 중간 편집 상태 전체를 URL에 담으려 하면 유지보수가 급격히 어려워집니다.
이런 경우는:
- 서버에 draft를 저장하거나
- localStorage/sessionStorage를 쓰거나
- 앱 내부 상태로만 유지하는 편이 더 적절할 수 있습니다
즉, Query String은 만능 저장소가 아니라 공개 가능하고 복원 가치가 있는 얕은 상태에 더 적합합니다.
기본 문법은 어떻게 생겼을까?
가장 기본 형태는 아래와 같습니다.
?key=value여러 개가 되면 &로 연결합니다.
?page=2&sort=latest&category=frontend여기서 자주 나오는 포인트는 두 가지입니다.
1. 값은 문자열로 표현된다
Query String은 결국 문자열입니다.
예를 들어:
page=2isDraft=true
라고 써도 실제로는 문자열로 들어갑니다. 그래서 읽을 때는 숫자나 boolean으로 다시 해석해야 합니다.
2. 인코딩이 필요할 수 있다
공백이나 한글, 특수문자가 있으면 URL 인코딩이 들어갑니다.
예를 들어:
?q=react query는 실제로는 이런 식으로 변환될 수 있습니다.
?q=react+query또는:
?q=react%20query한글도 마찬가지입니다.
?keyword=프론트엔드는 전송 과정에서 인코딩된 문자열로 바뀔 수 있습니다.
즉, query string은 눈에 보이는 문자열 그대로만 다루기보다 인코딩과 디코딩이 포함된 데이터 표현 방식으로 이해하는 편이 맞습니다.
문자열 결합보다 URLSearchParams가 중요한 이유
실무에서는 직접 문자열을 이어붙이는 코드가 종종 보입니다.
const url = `/posts?page=${page}&sort=${sort}&keyword=${keyword}`;짧을 때는 괜찮아 보여도, 조건부 파라미터가 늘어나면 금방 복잡해집니다.
- 어떤 값이 없을 때는 어떻게 뺄지
- 인코딩은 누가 처리할지
- 기존 파라미터를 어떻게 유지할지
- 같은 key를 여러 개 넣을지
그래서 보통은 URLSearchParams를 쓰는 편이 더 안전합니다.
const searchParams = new URLSearchParams();
searchParams.set('page', String(page));
searchParams.set('sort', sort);
if (keyword) {
searchParams.set('keyword', keyword);
}
const url = `/posts?${searchParams.toString()}`;이 방식의 장점은 명확합니다.
- 인코딩을 비교적 안전하게 처리하고
- 조건부 추가/삭제가 쉽고
- 읽는 쪽도
get,has,getAll로 다루기 쉽습니다
즉, Query String은 문자열처럼 보이지만, 실무에서는 URLSearchParams 같은 도구로 다루는 편이 유지보수에 훨씬 유리합니다.
읽을 때는 어떻게 해석할까?
예를 들어 현재 URL이 아래와 같다고 해보겠습니다.
/posts?tag=react&page=2&draft=true브라우저에서는 이렇게 읽을 수 있습니다.
const searchParams = new URLSearchParams(window.location.search);
const tag = searchParams.get('tag');
const page = Number(searchParams.get('page') ?? '1');
const isDraft = searchParams.get('draft') === 'true';여기서 중요한 포인트는 파싱 규칙을 팀 안에서 일관되게 가져가는 것입니다.
예를 들어 boolean을 다룰 때도:
true | false1 | 0yes | no
중 하나로 섞이면 읽는 쪽 코드가 지저분해집니다.
숫자도 마찬가지입니다.
const rawPage = searchParams.get('page');
const page = Math.max(1, Number(rawPage ?? '1') || 1);즉, 읽는 로직에서는 단순히 get()만 하는 것이 아니라:
- 기본값 처리
- 타입 변환
- 유효성 보정
까지 같이 생각해야 합니다.
배열과 다중 선택 필터는 어떻게 표현할까?
실무에서 자주 나오는 고민입니다.
예를 들어 브랜드를 여러 개 선택할 수 있다면 표현 방식이 여러 가지입니다.
1. 같은 key를 반복
?brand=nike&brand=adidas&brand=puma이 방식은 URLSearchParams.getAll()과 잘 맞습니다.
const brands = searchParams.getAll('brand');2. 쉼표로 합치기
?brand=nike,adidas,puma이 방식은 읽을 때 split이 필요합니다.
const brands = (searchParams.get('brand') ?? '').split(',').filter(Boolean);둘 다 가능하지만, 중요한 것은 팀 안에서 하나의 규칙으로 맞추는 것입니다.
개인적으로는 아래 기준으로 많이 나뉩니다.
- 서버가 같은 key 반복을 자연스럽게 지원하면
brand=nike&brand=adidas - 사람이 읽기 쉬운 짧은 표현이 중요하면
brand=nike,adidas
중요한 것은 문법 그 자체보다, 백엔드와 프론트가 동일한 파싱 규칙을 공유하는 상태입니다.
실무에서는 언제 특히 잘 맞을까?
Query String이 특히 빛나는 대표 사례는 아래와 같습니다.
1. 검색
/search?q=react+query검색어는 공유 가치가 높고, 새로고침 복원 가치도 높습니다.
2. 필터
/products?category=shoes&brand=nike필터 상태는 URL과 가장 잘 맞는 축 중 하나입니다.
3. 정렬
/posts?sort=latest정렬 기준도 "현재 목록을 어떤 기준으로 보고 있는가"를 표현하므로 자연스럽습니다.
4. 페이지네이션
/posts?page=3목록 탐색 흐름에서는 뒤로 가기와 새로고침 복원이 중요해서 URL 상태로 두는 경우가 많습니다.
5. 탭이나 보기 모드
/settings?tab=profile
/dashboard?view=table같은 페이지 안에서도 어떤 섹션을 보고 있는지 공유 가치가 있다면 잘 맞습니다.
즉, Query String은 대부분 "조회 조건"과 "보기 방식"을 표현할 때 가장 힘을 발휘합니다.
반대로 언제 URL보다 다른 상태 저장 방식이 나을까?
반대로 아래와 같은 경우는 다른 방식을 먼저 검토하는 편이 낫습니다.
1. 인증/보안 정보
이건 거의 항상 URL보다:
- 쿠키
- 헤더
- 메모리 상태
같은 방식이 우선입니다.
2. 큰 draft 데이터
긴 폼 작성 중간 상태 전체를 URL에 넣는 것은 거의 항상 무리입니다.
이런 값은:
- 서버 draft 저장
- localStorage
- sessionStorage
같은 방식이 더 적합할 수 있습니다.
3. 완전히 일시적인 인터랙션 상태
예를 들어 마우스를 어디에 올렸는지, 드롭다운이 잠깐 열렸는지 같은 값은 URL까지 갈 필요가 없는 경우가 많습니다.
즉, 중요한 질문은 "이 상태를 브라우저 주소창에 남길 이유가 있는가?"입니다. 이유가 약하면 굳이 Query String으로 끌어오지 않는 편이 낫습니다.
React에서는 어떤 식으로 연결할까?
React에서 자주 하는 패턴은 필터 state와 URL을 동기화하는 것입니다.
예를 들어:
type PostFilter = {
keyword: string;
sort: 'latest' | 'popular';
};
function buildPostListUrl(filter: PostFilter) {
const searchParams = new URLSearchParams();
if (filter.keyword) {
searchParams.set('keyword', filter.keyword);
}
searchParams.set('sort', filter.sort);
return `/posts?${searchParams.toString()}`;
}이런 구조를 가져가면 좋은 점은:
- UI state를 URL 표현으로 분리할 수 있고
- 링크 생성 규칙이 한 곳에 모이고
- 테스트하기 쉬워진다는 점입니다
실무에서는 여기서 한 단계 더 들어가, "초기값은 URL에서 읽고, 사용자 조작이 발생하면 URL을 갱신"하는 흐름으로 많이 갑니다.
즉, React 상태를 진실의 원천으로 둘지, URL을 진실의 원천으로 둘지는 화면 성격에 따라 달라집니다. 검색/목록 화면이라면 URL을 기준으로 보는 쪽이 더 자연스러운 경우가 많습니다.
Next.js에서는 왜 더 자주 중요해질까?
Next.js에서는 URL이 라우팅과 데이터 요청 흐름에 더 직접적으로 연결되기 때문에 Query String이 더 중요하게 느껴질 수 있습니다.
예를 들어 목록 페이지에서:
/posts?keyword=react&sort=latest&page=2이 값이 그대로:
- 서버 컴포넌트의 조회 조건이 되거나
- 캐시 키 구분에 영향을 주거나
- 클라이언트 컴포넌트의 필터 초기값이 될 수 있습니다
즉, Query String은 단순 UI 상태가 아니라 데이터 패칭 경계의 일부가 되기도 합니다.
특히 App Router 기준으로 보면, 검색 조건과 페이지 번호를 URL에 드러내는 설계는:
- 새로고침 복원
- 링크 공유
- 서버/클라이언트 역할 분리
를 더 자연스럽게 만들어줍니다.
물론 여기서도 원칙은 같습니다. URL에 있는 값은 공개 가능하고, 비교적 얕고, 조회 맥락을 설명하는 상태일 때 더 적합합니다.
자주 하는 실수
정리하면 아래 실수가 정말 자주 나옵니다.
- 민감한 값을
Query String에 넣는다 - 숫자와 boolean도 자동으로 타입이 맞을 것이라 생각한다
- 조건부 파라미터를 문자열 결합으로 만들다가 버그가 생긴다
- 배열 파라미터 규칙을 프론트와 백엔드가 다르게 해석한다
- URL에 넣을 이유가 없는 UI 상태까지 전부 올린다
- 반대로 공유 가치가 큰 필터 상태를 메모리 state에만 둔다
즉, 문제는 Query String 자체보다도 URL에 어떤 상태를 올릴지에 대한 기준이 없는 것에서 자주 시작됩니다.
실무 체크리스트
실제로 적용할 때는 아래 질문으로 빠르게 점검하면 도움이 됩니다.
- 이 상태는 링크로 공유할 가치가 있는가?
- 새로고침 뒤에도 복원되어야 하는가?
- 서버 조회 조건이나 화면 보기 방식과 직접 연결되는가?
- 공개 가능한 값인가?
- 문자열로 단순하게 직렬화할 수 있는가?
- 프론트와 백엔드가 같은 파싱 규칙을 공유하고 있는가?
이 질문에 대부분 "예"라고 답할 수 있다면 Query String이 꽤 좋은 후보일 가능성이 높습니다.
정리하면
Query String을 한 줄로 줄이면, URL에서 조회 조건과 보기 상태를 표현하는 문자열 기반 파라미터 영역입니다.
실무 기준으로 기억할 핵심은 이렇습니다.
- 검색, 필터, 정렬, 페이지네이션처럼 공유와 복원이 중요한 상태에 잘 맞고
- 민감 정보나 큰 복합 상태에는 잘 맞지 않으며
- 문자열 결합보다
URLSearchParams로 다루는 편이 안전하고 - 결국 중요한 것은 "URL에 넣을 수 있는가"보다 "URL에 있어야 하는 상태인가" 입니다
프론트엔드에서 상태를 다룬다는 것은 단순히 state 훅을 어디에 둘지 정하는 문제가 아닙니다. 어떤 상태는 컴포넌트 안에 있어야 하고, 어떤 상태는 전역 저장소에 있어야 하며, 어떤 상태는 아예 URL로 올라가야 합니다. Query String은 그중에서도 사용자와 브라우저가 함께 이해할 수 있는 상태 표현이라는 점에서 꽤 강력한 도구입니다.
