URL 쿼리로 필터 초기화 문제 해결하기
현재 진행중인 프로젝트에는 프로필, 이력서, 프로젝트 등등을 탐색할 수 있는 기능이 있다. 사용자는 프로필을 탐색하고싶을 때 포지션 / 기수 / 대학 / 학년 같은 조건으로 필터링해서 보고 싶은 프로필만 모아서 탐색할 수 있다.
URL 쿼리로 필터 초기화 문제 해결하기

현재 진행중인 프로젝트에는 프로필, 이력서, 프로젝트 등등을 탐색할 수 있는 기능이 있다.
사용자는 프로필을 탐색하고싶을 때 포지션 / 기수 / 대학 / 학년 같은 조건으로 필터링해서 보고 싶은 프로필만 모아서 탐색할 수 있다.
근데 큰 문제가 하나 있었다. 프론트엔드 포지션을 필터링해서 프로필을 클릭 → 뒤로가기 했더니… 필터가 전부 초기화되는 것이다.
문제 상황
사용자는 보통 이렇게 행동한다.
- 프로필 목록 →
포지션=FRONTEND필터 적용 - 특정 유저 프로필 클릭
- 뒤로가기 눌러서 다시 목록으로
- 당연히 프론트엔드만 남아있길 기대하지만, 실제로는 전체 목록으로 초기화돼 버렸다.
탐색 흐름이 끊기는 UX였다.
다른 서비스들은 어떻게 할까?
비슷한 문제를 검색하다 보니, 무신사, 마켓컬리, 네이버 쇼핑 같은 서비스는 이미 이 문제를 해결해두었다.
방법은 바로 아래처럼 필터 상태를 URL 쿼리에 싣는 것이다.
/profiles?position=FRONTEND&year=2&sortBy=이름순

위는 마켓컬리의 예시인데 page(현재 몇 페이지인지)값과 선택한 필터의 값등을 url에 저장하고 있다.
이렇게 하면 필터값 유지와 뒤로가기, 앞으로가기 뿐만 아니라 다른 사람에게 공유할 때도 필터값을 유지할 수 있다!
이렇게 해두면 장점이 많다.
- 뒤로가기/앞으로가기 자연스럽게 유지
- 새로고침해도 그대로 유지
- URL 공유 시 동일한 화면 재현 가능
결국 “URL이 곧 상태”가 된다.
커스텀 훅 만들기
이 방식을 적용하기 위해 **useUrlQueryFilters** 라는 훅을 만들었다.
전체 코드는 길지만, 핵심 아이디어만 정리하면 이렇다.
문제 해결 핵심 아이디어
- 필터 → URL 동기화: 상태를 URLSearchParams로 변환
- URL → 필터 파싱: 페이지 진입/뒤로가기 시 URL에서 복원
- 기본값은 URL에 저장하지 않음: 주소를 깔끔하게 유지
1) URL → Filters 파싱
function parseFromParams(params: URLSearchParams) {
const toArr = (k: string) => params.getAll(k).filter(Boolean)
const filters = {
position: toArr('position'),
year: toArr('year'),
university: toArr('university'),
grade: toArr('grade'),
sortBy: toArr('sortBy'),
}
// sortBy 기본값 보정
if (filters.sortBy.length === 0) filters.sortBy = ['기수순']
return filters
}
2) Filters → URL 머지
function mergeParams(base: URLSearchParams, filters: Filters) {
const next = new URLSearchParams(base.toString())
;['position','year','university','grade','sortBy'].forEach(k => next.delete(k))
const addAll = (key: string, values: string[]) => {
[...new Set(values)].sort().forEach(v => next.append(key, v))
}
if (filters.position.length) addAll('position', filters.position)
if (filters.year.length) addAll('year', filters.year)
if (filters.university.length) addAll('university', filters.university)
if (filters.grade.length) addAll('grade', filters.grade)
// 기본값('기수순')이면 URL에 안 씀
const sort = filters.sortBy[0]
if (sort && sort !== '기수순') next.append('sortBy', sort)
return next
}
3) 훅 사용법
const { filters, set, remove } = useUrlQueryFilters()
// 드롭다운 선택
<Dropdown
title="포지션"
options={['FRONTEND','BACKEND']}
selectedOptions={filters.position}
setSelectedOptions={(v) => set('position', v)}
/>
// 필터 제거
{filters.position.map((p) => (
<FilterBtn key={p} title={p} onClick={() => remove('position', p)} />
))}
적용 결과
- 뒤로가기/앞으로가기: 히스토리에 쿼리가 남아 UX 자연스러움
- 새로고침: 필터 초기화 안 됨
- 공유: URL만 주면 상대방도 같은 조건으로 접근 가능
배운 점
- 상태를 URL에 담는 건 단순히 편의를 위한 게 아니라, 사용자 기대와 흐름을 지키는 것이었다.
- 기본값은 URL에서 제거해 주소를 깔끔하게 유지하는 것도 중요했다.
- replace를 기본으로 해서 필터 변경이 히스토리를 더럽히지 않도록 했다.
결국, 우리 프로젝트도 무신사/마켓컬리 같은 방식으로 동작하게 되었다. 필터링 UX가 훨씬 자연스러워졌고, 탐색의 연속성을 지켜낼 수 있었다! 필터링을 사용하는 다른 페이지에도 훅을 재사용해서 적용해주었다.
메타데이터
- post_id
- 233a9c75d3fc
- slug
- url-쿼리로-필터-초기화-문제-해결하기-233a9c75d3fc
- url
- https://medium.com/@qkrb8019/url-%EC%BF%BC%EB%A6%AC%EB%A1%9C-%ED%95%84%ED%84%B0-%EC%B4%88%EA%B8%B0%ED%99%94-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0-233a9c75d3fc
- canonical_url
- https://medium.com/@qkrb8019/url-%EC%BF%BC%EB%A6%AC%EB%A1%9C-%ED%95%84%ED%84%B0-%EC%B4%88%EA%B8%B0%ED%99%94-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0-233a9c75d3fc
- author_url
- https://medium.com/@qkrb8019
- status
- ok
- fetched_at
- 2026-06-26 03:39:16