JavaScript URLSearchParams로 검색어와 필터를 읽고 수정하려면 어떻게 하나요?
URLSearchParams로 검색어·필터를 URL에서 읽고 수정합니다. 공유할 상태를 URL에 담으면 새로고침 뒤에도 복원할 수 있습니다. 잘못된 필터와 빈 검색어를 정리해 정상 상태로 바꿉니다.
적용 조건·범위: 실행 검증: Node.js에서 위 출력과 일치했습니다. 실제 브라우저에서 history.replaceState()로 주소만 바꾸고 새로고침 후 복원되는지는 미확인입니다.
공식 문서 참고: MDN: URLSearchParams로 쿼리 읽기·수정하기 · MDN: replaceState로 현재 방문 기록의 URL 바꾸기
혼자 실행해 보는 URLSearchParams의 읽기와 수정
JavaScript URLSearchParams 사용법: URL 파라미터 읽고 수정하기 · 독립 실습
URLSearchParams로 읽고 바꾼 쿼리 문자열을 실제 출력에서 확인하세요.
예제 크게 보기 (새 탭)독자 질문: 잘못된 필터와 빈 검색어를 가진 URL을 어떻게 정상 상태로 바꾸는가?
js-09-example.mjs로 저장하고 node js-09-example.mjs을 실행하세요. 외부 패키지는 필요하지 않습니다.
const url = new URL('https://example.test/tasks?status=wrong&q=%20%ED%95%99%EC%8A%B5%20&campaign=mail');
const valid = new Set(['all', 'completed', 'pending']);
const status = valid.has(url.searchParams.get('status')) ? url.searchParams.get('status') : 'all';
const query = (url.searchParams.get('q') ?? '').trim();
if (status === 'all') url.searchParams.delete('status');
else url.searchParams.set('status', status);
if (query) url.searchParams.set('q', query);
else url.searchParams.delete('q');
console.log('상태:', status);
console.log('검색:', query);
console.log('결과:', url.search);
console.log('유지:', url.searchParams.get('campaign'));
예상 출력:
상태: all
검색: 학습
결과: ?q=%ED%95%99%EC%8A%B5&campaign=mail
유지: mail
실행 검증: Node.js에서 위 출력과 일치했습니다. 실제 브라우저에서 history.replaceState()로 주소만 바꾸고 새로고침 후 복원되는지는 미확인입니다.
직접 풀어볼 문제
q를 공백만으로 바꾸고 다시 실행해 URL에서 q가 삭제되는지 확인하세요. campaign은 유지되어야 합니다.
현재 URL의 쿼리 문자열 읽기
주소가 다음과 같다고 가정합니다.
https://example.com/?status=pending&q=javascript
const params = new URLSearchParams(location.search);
console.log(params.get('status'));
console.log(params.get('q'));
존재하지 않는 키는 null을 반환합니다.
set과 delete
const params = new URLSearchParams();
params.set('status', 'pending');
params.set('q', 'javascript');
params.delete('q');
console.log(params.toString());
사용자가 입력한 공백·한글 등은 URL 문자열로 직렬화될 때 적절히 인코딩됩니다. 직접 encodeURIComponent()를 중복 적용하지 않습니다.
URLSearchParams의 값은 문자열입니다
const page = new URLSearchParams('?page=2').get('page');
console.log(typeof page);
숫자나 boolean이 필요하면 읽은 뒤 명시적으로 변환하고 유효성을 확인합니다.
replaceState와 pushState
필터를 한 번 바꿀 때마다 브라우저 뒤로가기 기록을 계속 쌓고 싶지 않다면 replaceState()가 단순합니다.
const url = new URL(location.href);
url.searchParams.set('status', 'completed');
history.replaceState(null, '', url);
페이지 이동 없이 주소만 바뀝니다.
pushState()는 사용자가 뒤로가기로 이전 UI 상태를 순서대로 탐색해야 하는 경우에 검토합니다.

URL에 모든 상태를 넣지 않습니다
Task 배열 전체를 쿼리 문자열에 저장하는 것은 적절하지 않습니다.
이번 과정에서는 다음 두 값만 URL 상태로 사용합니다.
status
q
작업 데이터는 store가 소유합니다.
잘못된 값은 기본값으로 복구
const VALID_STATUS = new Set(['all', 'pending', 'completed']);
function normalizeStatus(value) {
return VALID_STATUS.has(value) ? value : 'all';
}
?status=unknown처럼 앱이 모르는 값이 들어와도 화면이 깨지지 않습니다.
자주 발생하는 오류
URL을 문자열 이어붙이기로 만듭니다
공백, 한글, &, ? 등의 인코딩 문제를 직접 처리해야 합니다. URL과 URLSearchParams를 사용합니다.
빈 검색어도 q=로 계속 남깁니다
값이 비었다면 파라미터를 삭제하면 URL이 더 단순해집니다.
URL을 유일한 앱 상태로 만듭니다
URL은 공유·복원할 가치가 있는 상태만 저장하고 실제 데이터 상태와 역할을 분리합니다.
학습 완료 체크리스트
- 쿼리 파라미터를 읽을 수 있습니다.
- 값을 추가·수정·삭제할 수 있습니다.
- 잘못된 파라미터를 기본값으로 처리할 수 있습니다.
replaceState()로 페이지 이동 없이 URL을 바꿀 수 있습니다.- UI 상태와 데이터 상태를 구분할 수 있습니다.
독립 실습은 여기서 마칠 수 있습니다. 아래 선택 실습은 TaskFlow 프로젝트 파일로 진행합니다.
선택 실습: 할 일 앱에 적용하기
TaskFlow는 작업을 추가·완료·삭제하는 할 일 앱입니다. 이 선택 실습에서는 JavaScript로 화면에 동작을 붙입니다.
필터와 검색어를 URL에 기록하고 새로고침 시 화면 상태를 복원합니다.
선택: 연재 프로젝트와 전체 자료
연재 프로젝트를 이어 만드는 분을 위한 자료입니다. 이 글의 개념과 예제만 찾으셨다면 필요한 파일과 실행 방법만 펼쳐보세요.
TaskFlow 프로젝트 적용
JavaScript URLSearchParams 사용법: URL 파라미터 읽고 수정하기 · 단계별 TaskFlow
작업 추가·완료 전환·삭제와 현재 단계의 필터/출력을 확인하세요. 검색 조건을 바꿔 가상 URL을 확인하세요.
예제 크게 보기 (새 탭)변경 설명과 부분 예제를 읽은 뒤, 시작·완료 파일과 적용 순서에서 필요한 선언·import·화면 연결을 포함한 전체 파일을 확인하세요. 이번 단계의 변경 이유와 적용 위치도 함께 확인합니다.
시작본과 이번 변경
JavaScript 투두리스트 만들기: 상태와 이벤트 위임으로 완성하기의 완료 체크포인트를 시작본으로 사용합니다.
js/url-state.js를 추가하고 task-view.js에 setTaskControls를 추가합니다. app.js 초기 render 전에 readUrlState→setTaskControls를 호출하고 필터 이벤트에서 writeUrlState 뒤 render를 호출합니다. ?status=wrong과 한글 검색어로 새로고침 복원을 확인하세요.
연속 프로젝트 선수 조건
- TaskFlow의 상태 필터와 검색 기능
- 브라우저 주소와 쿼리 문자열의 기본 구조
javascript-step-08-todo-core체크포인트
이전 체크포인트
javascript-step-08-todo-core
이번 회차 변경 코드와 전체본 연결
기존 store와 view는 유지하고 URL 상태 모듈 및 app 연결을 추가합니다.
전체 파일·코드 보기
js/url-state.js
const VALID_STATUS = new Set(['all', 'pending', 'completed']);
function normalizeStatus(value) {
return VALID_STATUS.has(value) ? value : 'all';
}
export function readUrlState() {
const params = new URLSearchParams(location.search);
return {
status: normalizeStatus(params.get('status')),
query: params.get('q') ?? '',
};
}
export function writeUrlState({ status, query }) {
const url = new URL(location.href);
if (status === 'all') {
url.searchParams.delete('status');
} else {
url.searchParams.set('status', status);
}
const normalizedQuery = query.trim();
if (normalizedQuery) {
url.searchParams.set('q', normalizedQuery);
} else {
url.searchParams.delete('q');
}
history.replaceState(null, '', url);
}
task-view.js에 초기값 설정 함수 추가
export function setTaskControls({ status, query }) {
document.querySelector('#task-status').value = status;
document.querySelector('#task-search').value = query;
}
js/app.js 전체 흐름
import {
addTask,
deleteTask,
filterTasks,
getTaskSummary,
toggleTask,
} from './task-store.js';
import {
bindTaskControls,
bindTaskForm,
bindTaskList,
getTaskControls,
renderTasks,
setTaskControls,
} from './task-view.js';
import { readUrlState, writeUrlState } from './url-state.js';
function render() {
const controls = getTaskControls();
const tasks = filterTasks(controls.status, controls.query);
renderTasks(tasks, getTaskSummary());
}
const initialUrlState = readUrlState();
setTaskControls(initialUrlState);
bindTaskForm((title) => {
addTask(title);
render();
});
bindTaskControls(() => {
const controls = getTaskControls();
writeUrlState(controls);
render();
});
bindTaskList(({ id, action }) => {
if (action === 'toggle') toggleTask(id);
if (action === 'delete') deleteTask(id);
render();
});
render();
검증
- 상태를 미완료로 변경
- 검색어 입력
- 주소에
status와q가 반영되는지 확인 - 새로고침
- 같은 필터와 검색어가 복원되는지 확인
?status=wrong으로 직접 접속해 전체 상태로 복구되는지 확인
중간부터 시작하는 전체 파일과 적용 순서
시작 ZIP은 직전 프로젝트 단계 javascript-step-08-todo-core의 실제 완료 파일입니다. 프로젝트 순서를 이어가는 기준이며 사이트의 기술 읽기 순서와는 구분합니다.
- 시작 ZIP을 새 폴더에 풉니다. 이 폴더가 작업 루트이며 기존 체크포인트 파일을 별도로 찾을 필요가 없습니다.
- url-state.js를 추가하고 view의 setTaskControls 및 app의 복원/갱신 호출을 연결합니다.
- 아래 변경 파일을 같은 상대 경로에 추가하거나 전체 교체합니다. 표시하지 않은 파일과 images는 시작본 그대로 유지합니다. package-lock.json은 완료 ZIP에 있는 파일을 함께 적용합니다.
- 작업 루트에서 명령을 실행하고 예상 결과를 확인합니다. 완료 ZIP은 직접 적용한 결과를 비교하는 기준입니다.
추가·교체 파일
CHANGES.md— 전체 교체README.md— 전체 교체js/app.js— 전체 교체js/task-view.js— 전체 교체js/url-state.js— 추가
실행과 확인
npm start
npm test
개발 서버는 실행 상태로 두고 다른 터미널에서 검사 명령을 실행하세요. Node.js 24.18.0에서 검증했습니다. JavaScript는 http://127.0.0.1:4173, TypeScript는 Vite 실행 출력의 주소로 접속합니다.
예상 결과: ?status=completed&q=TaskFlow에서 완료 항목 1개, 잘못된 status는 all, 공백 검색은 URL에서 q가 제거됩니다.
실제 검증: JavaScript 모듈·DOM·경계값 실행 검사 통과. 시작 ZIP에 이 글의 변경 파일만 적용한 결과와 완료 ZIP의 모든 파일 내용이 일치했습니다. 브라우저 화면 배치·실제 기기·외부 CDN 가용성은 이 검사에 포함하지 않았습니다.
이 절의 전체 파일은 ZIP과 일치합니다. 위쪽 원문의 짧은 예제나 생략 표시가 있는 부분 코드는 개념·변경 위치 설명용입니다. 서버 저장과 인증은 제공하지 않으며 JSON fixture는 로컬 읽기 실습용입니다.
복사할 변경 파일 전체 코드
js/app.js — 전체 파일
import {
addTask,
deleteTask,
filterTasks,
getTaskSummary,
toggleTask,
} from './task-store.js';
import {
bindTaskControls,
bindTaskForm,
bindTaskList,
getTaskControls,
renderTasks,
setTaskControls,
} from './task-view.js';
import { readUrlState, writeUrlState } from './url-state.js';
function render() {
const controls = getTaskControls();
const tasks = filterTasks(controls.status, controls.query);
renderTasks(tasks, getTaskSummary());
}
const initialUrlState = readUrlState();
setTaskControls(initialUrlState);
bindTaskForm((title) => {
addTask(title);
render();
});
bindTaskControls(() => {
const controls = getTaskControls();
writeUrlState(controls);
render();
});
bindTaskList(({ id, action }) => {
if (action === 'toggle') toggleTask(id);
if (action === 'delete') deleteTask(id);
render();
});
render();
js/task-view.js — 전체 파일
export function bindTaskForm(onAdd) {
const form = document.querySelector('#task-form');
const titleInput = document.querySelector('#task-title');
const message = document.querySelector('#task-message');
if (!form || !titleInput || !message) {
throw new Error('TaskFlow 작업 폼 요소를 찾을 수 없습니다.');
}
form.addEventListener('submit', (event) => {
event.preventDefault();
const title = titleInput.value.trim();
if (!title) {
titleInput.setAttribute('aria-invalid', 'true');
message.textContent = '작업 이름을 입력해 주세요.';
titleInput.focus();
return;
}
onAdd(title);
titleInput.value = '';
titleInput.setAttribute('aria-invalid', 'false');
message.textContent = '작업을 추가했습니다.';
titleInput.focus();
});
}
export function bindTaskControls(onChange) {
const status = document.querySelector('#task-status');
const search = document.querySelector('#task-search');
if (!status || !search) {
throw new Error('TaskFlow 필터 요소를 찾을 수 없습니다.');
}
status.addEventListener('change', onChange);
search.addEventListener('input', onChange);
}
export function getTaskControls() {
return {
status: document.querySelector('#task-status').value,
query: document.querySelector('#task-search').value,
};
}
export function bindTaskList(onAction) {
const list = document.querySelector('#task-list');
if (!list) {
throw new Error('task-list를 찾을 수 없습니다.');
}
list.addEventListener('click', (event) => {
const button = event.target.closest('button[data-action]');
const item = button?.closest('[data-task-id]');
if (!button || !item) return;
onAction({
id: item.dataset.taskId,
action: button.dataset.action,
});
});
}
export function renderTasks(tasks, summary) {
const list = document.querySelector('#task-list');
const summaryElement = document.querySelector('#task-summary');
list.replaceChildren();
summaryElement.textContent =
`전체 ${summary.total} · 완료 ${summary.completed} · 미완료 ${summary.pending}`;
if (tasks.length === 0) {
const empty = document.createElement('li');
empty.textContent = '조건에 맞는 작업이 없습니다.';
list.append(empty);
return;
}
tasks.forEach((task) => {
const item = document.createElement('li');
item.dataset.taskId = String(task.id);
const title = document.createElement('span');
title.textContent = task.title;
const toggleButton = document.createElement('button');
toggleButton.type = 'button';
toggleButton.dataset.action = 'toggle';
toggleButton.textContent = task.completed ? '미완료로 변경' : '완료';
const deleteButton = document.createElement('button');
deleteButton.type = 'button';
deleteButton.dataset.action = 'delete';
deleteButton.textContent = '삭제';
item.append(title, toggleButton, deleteButton);
list.append(item);
});
}
export function setTaskControls({ status, query }) {
document.querySelector('#task-status').value = status;
document.querySelector('#task-search').value = query;
}
js/url-state.js — 전체 파일
const VALID_STATUS = new Set(['all', 'pending', 'completed']);
function normalizeStatus(value) {
return VALID_STATUS.has(value) ? value : 'all';
}
export function readUrlState() {
const params = new URLSearchParams(location.search);
return {
status: normalizeStatus(params.get('status')),
query: params.get('q') ?? '',
};
}
export function writeUrlState({ status, query }) {
const url = new URL(location.href);
if (status === 'all') {
url.searchParams.delete('status');
} else {
url.searchParams.set('status', status);
}
const normalizedQuery = query.trim();
if (normalizedQuery) {
url.searchParams.set('q', normalizedQuery);
} else {
url.searchParams.delete('q');
}
history.replaceState(null, '', url);
}
완료 체크포인트
javascript-step-09-url-state
다음 학습
다음 글에서는 Task 데이터에 생성 시각과 선택적 마감 시각을 추가하고 UTC 저장값과 KST 표시값을 분리합니다.
다음 글: JavaScript Date UTC KST 차이: 시간대 변환 기준 잡기
글 소개·학습 준비·전체 목차

JavaScript · 9 / 15 · 필수 학습 · URLSearchParams
검색어나 필터를 선택했는데 새로고침하자마자 모두 초기화되면 사용자가 같은 화면을 다시 만들기 어렵습니다.
URLSearchParams를 사용하면 화면 상태 중 공유할 가치가 있는 일부를 URL에 저장할 수 있습니다.
이번 글에서 배우는 내용
location.searchURLSearchParamsget,set,delete,has- 문자열 값과 타입 변환
- URL 인코딩
history.replaceState()- 잘못된 파라미터 기본값
- TaskFlow 필터·검색 상태 복원
먼저 알아야 할 내용
문자열·객체와 URL의 ? 뒤 쿼리 문자열을 알면 됩니다. 상단 URL 예제는 실제 페이지나 TaskFlow 없이 실행합니다.
목차
이 글이 도움이 되었나요?
읽음 기록
읽음 — / 전체 15개 · 필수 14개
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.