위코드에서 공부하며 정리한 내용입니다.
쿼리 스트링은 URL 의 한 부분으로, 요청하는 URL 에 부가정보를 포함할 때 사용합니다. 기존 URL 은 단순한 형태의 요청과 응답을 주고 받았지만 쿼리 스트링을 사용하면 조건에 맞게 정렬된 특정 형태의 정보를 요청하고 받을 수 있습니다.
예를 들어 규모가 크고 복잡한 애플리케이션의 상품 종류가 1000개라면, 상품 리스트 페이지에서 1억개의 상품 정보를 한번에 불러와 보여주는 것은 매우 비효율적입니다. 1억개의 데이터를 불러오는 시간도 문제지만, 실제 유저는 판매량 순 상위 10개, 최신순 10개, 리뷰 평점 순 10개 처럼 특정 기준으로 편집된 정보를 보길 원합니다.
이런 상황에서 '상품 리스트 보여줘' 가 아닌 '상품 리스트를 최신순으로 상위 10개만 보여줘'라고 구체적으로 요청하기 위해 쿼리 스트링을 사용합니다.
쿼리 스트링은 문자열의 형태를 띄며 key=value 로 표현합니다. URL 의 일부이므로 ? 를 통해 여기부터 시작이라고 표시해야 하고, 각 페어의 구분은 & 로 합니다.
// 인기순으로 정렬된 정보
https://www.example.com/products?sort=popular
// 인기순으로 정렬된 정보를 내림차순으로 보고 싶다면
https://www.example.com/products?sort=popular&direction=desc
쿼리 스트링은 URL에 부가적인 정보를 포함하는 것이므로 라우터 컴포넌트에도 특별한 설정이 필요 없습니다. 아래처럼 링크 역할을 하는 컴포넌트에 쿼리스트링이 포함된 주소를 전달하면 됩니다.
// Link 컴포넌트
<Link to="/list?sort=popular" />
// useNavigate
navigate("/list?sort=popular")
react-router-dom 에서 쿼리 스트링 값을 가져올 수 있는 hook 으로는 useLocation, useSearchParams 두개가 있습니다. useLocation 훅은 현재의 Location 객체를 반환합니다. (현재 URL 에 포함된 여러 가지 정보를 담은
// 해당 훅을 호출하고
import { useLocation } from "react-router-dom"
// 컴포넌트 안에서 데이터를 변수에 담고 확인
const location = useLocation();
console.log(location);
이렇게 확인해보면, 콘솔 창에 여러 객체가 나오고 개중 search 프로퍼티가 ㅋ퀴리 스트링 값을 담고 있는 것을 볼 수 있습니다. 이를 활용해 쿼리 스트링 값을 가져와 사용할 수 있습니다.
console.log(location.search); // => ?sort=popular
이렇게 가져온 값에서 popular 만 뽑아서 사용하려면 별도의 작업을 해야 하므로 복잡합니다. (여러개의 페어가 존재한다면 더더욱) 이럴 때 다양한 메서드를 제공해 원하는 값을 가져올 수 있게 하는 것이 URLSearchParams 이라는 객체입니다. react-router-dom 은 이 객체를 반환해주는 useSearchParams 라는 훅을 제공하고 아래처럼 useState 와 유사한 방식으로 선언하고 사용할 수 있습니다.
const [serchParams, setSearchParams] = useSearchParams();
searchParams 는 URLSearchParams 객체이면서 쿼리 스트링을 다루기 위한 여러 메서드를 제공합니다. setSearchParams 는 인자에 객체, 문자열을 넣어주면 현재 url 의 쿼리스트링을 변경하는 기능을 제공합니다.
자주 사용하는 메서드를 살펴보면
searchParams.get(key)
- 특정한 key의 value를 가져오는 메서드, 해당 key 의 value 가 두개라면 제일 먼저 나온 value 만 리턴
searchParams.getAll(key)
- 특정 key 에 해당하는 모든 value 를 가져오는 메서드
searchParams.toString()
- 쿼리 스트링을 string 형태로 리턴
searchParams.set(key, value)
- 인자로 전달한 key 값을 value 로 설정, 기존에 값이 존재했다면 그 값은 삭제됨
searchParams.append(key, value)
- 기존 값을 변경하거나 삭제하지 않고 추가하는 방식
serchParams 을 변경하는 메서드로 값을 변경해도 실제 url 의 쿼리 스트링은 변경되지 않습니다. 이를 변경하려면 setSearchParams 에 searchParams 를 인자로 전달해야 합니다.
해당 훅을 호출하고
import { useSearchParams } from 'react-router-dom';
useSearchParams 를 선언하고 setSortParams 함수를 만듭니다. set 메서드를 사용해 sort 라는 키에 clear 라는 밸류를 설정한 후 현재 url 의 쿼리 스트링을 변경합니다.
const [searchParams, setSearchParams] = useSearchParams();
const setSortParams = () => {
searchParams.set('sort', 'clear');
setSearchParams(searchParams);
};
여기서 append 메서드를 사용하면 기존 값을 유지하며 sort 라는 키에 hello-world 라는 밸류를 추가할 수 있습니다.
const appendSortParams = () => {
searchParams.append("sort", "hello-world");
setSearchParams(searchParams);
};
쿼리 스트링은 검색, 필터링, 페이지네이션 등에 다양하게 활용됩니다. 페이지네이션을 위해선 offset 과 limit 라는 기준이 필요한데, offset 은 몇 번째 아이템부터 보여줄 것인가에 대한 정보를 limit 는 몇개를 보여줄지에 대한 정보입니다. 0번째 이후로 10개를 보여줘를 쿼리 스트링으로 표현하면,
"?offset=0&limit=10"
offset, limit 는 start, size 등의 다른 이름으로 표현되기도 하지만 전체 아이템 중 보고자 하는 범위를 표현한다는 개념은 같습니다. 페이지네이션을 구현한 실제 코드를 살펴보면,
// src/List.js
// 처음 유저가 /list?offset=10&limit=10 이렇게 접속한다고 가정
import React, { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom'; // 훅을 호출하고
const List = () => {
const [searchParams, setSearchParams] = useSearchParams(); // 쿼리 스트링을 searchParams 형태로 가져오고
const offset = searchParams.get('offset'); // offset 값 변수에 저장
const limit = searchParams.get('limit'); // limit 값 변수에 저장
return (
<section>
<h1>This is Posts</h1>
</section>
);
};
export default List;
이제 api 호출로 가져온 데이터을 담을 state 를 생성하고 useEffect, fetch 로 전체 포스트의 데이터를 가져옵니다. 이때 사용하는 api 주소에 위에서 저장한 offset 과 limit 값을 넣습니다. 의존성 배열에 offset 과 limit 를 넣어서 해당 값이 변경될 때마다 (url 주소의 쿼리스트링이 바뀔 때마다) 매번 해당 쿼리스트링를 포함하는 데이터를 받아옵니다.
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch(`https://jsonplaceholder.typicode.com/posts_start=${offset}&_limit=${limit}`)
.then((response) => response.json())
.then((result) => setPosts(result));
}, [offset, limit]);
이제 페이지 번호를 표시하고, 해당 페이지마다 10개씩 포스트를 보여주는 함수를 만듭니다.
(1번 누르면 1-10번 포스트, 2번은 11-20번 포스트, 3번은 21-30번 포스트) 이 함수는 searchParams 를 변경하고(set 활용) setSearchParams 를 통해 쿼리 스트링을 변경합니다. 이때 limit 는 기존 값 10을 유지하면 되므로 offset 값만 0, 10, 20으로 바뀌면 됩니다.
const movePage = (pageNumber) => {
searchParams.set('offset', (pageNumber - 1) * 10);
setSearchParams(searchParams);
};
이제 생성한 버튼 태그에서 페이지 넘버를 인자로 넣어서 위 함수를 실행합니다.
<div>
<button onClick={() => movePage(1)}>1</button>
<button onClick={() => movePage(2)}>2</button>
<button onClick={() => movePage(3)}>3</button>
</div>
알아둘 내용으로 프론트엔드에서 사용하는 쿼리 스트링과 백엔드에 보내는 쿼리 스트링의 key 가 동일할 필요는 없습니다. 서로 다른 별개의 리소스를 관리하는 path parameter 와 쿼리 스트링은 다릅니다. 쿼리스트링은 같은 리소스를 표현하되 추가적인 정보를 포함해서 요청하는 것입니다. offset, limit 등의 값을 state 가 아닌 쿼리 스트링으로 관리하는 이유는 url 에 포함시킬 수 있기 때문입니다. state 는 페이지네이션으로 이동할 때 초기화되지만 쿼리 스트링은 url 에 정보가 담겨 있어서 해당 페이지를 그대로 유지합니다. 필터링, 검색 결과 등 해당 정보가 지속적으로 유지되어야 하는 경우에 쿼리 스트링을 활용하는 것이 좋습니다.
전체 코드 보기
// src/List.js
import React, { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import './List.css';
const List = () => {
const [searchParams, setSearchParams] = useSearchParams();
const offset = searchParams.get('offset');
const limit = searchParams.get('limit');
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch(
`https://jsonplaceholder.typicode.com/posts?_limit=${limit}&_start=${offset}`
)
.then((response) => response.json())
.then((result) => setPosts(result));
}, [offset, limit]);
const movePage = (pageNumber) => {
// 1
searchParams.set('offset', (pageNumber - 1) * 10);
setSearchParams(searchParams);
};
return (
<section>
<h1>This is Posts</h1>
{posts.map(({ id, title }) => (
<article key={id}>
<p>
<div>id:{id}</div>
<div>title:{title}</div>
</p>
</article>
))}
<div>
<button onClick={() => movePage(1)}>1</button>
<button onClick={() => movePage(2)}>2</button>
<button onClick={() => movePage(3)}>3</button>
</div>
</section>
);
};
export default List;