쿼리스트링은 URL의 한 부분이며 요청하고자 하는 URL에 부가적인 정보를 포함하고 싶을 때 사용한다.
기존 URL 요청 예시)
프론트: /list -> 리스트 요청
/detail/ ->상세 페이지 요청
백엔드: /signin -> 로그인에 대한 응답
/products -> 상품들 응답
/product -> 단일 상품 응답
만약 상품의 종류가 어마무시하게 많아진다면 /list 페이지에서 모든 상품을 보여주는 것은 비효율적이다.
왜냐하면 1억개의 상품 정보를 모두 불러오는 것도, 또한 유저는 판매량이나 최신순 같은 기준을 두고 정렬된 데이터를 보고싶어 하기 때문이다.
이런 상황에서 '상품리스트 보여줘 + 최신순 10개' 와 같이 구체적인 요청을 한다면 효율적일 것이다.
[사진 2-1] Query String의 형태(출처: https://www.semrush.com/)
쿼리스트링은 이름 그대로 문자열 타입이며 key = value로 표현된다. 또한 URL의 일부로 쿼리스트링의 시작점은 "?" 으로 표시된다.
예시)
https://www.example.com/products?sort=popular
&로 시작 key=value
https://www.example.com/products?sort=popular&direction=desc
key=value의 개수에 제한은 없고 구분할 때는 "&"을 기입한다.
Link나 navigate로 사용하면 된다.
<Link to="/list?sort=popular" />
navigate("/list?sort=popular")
필요 hook : useLocation, useSearchParams
쿼리스트링 값 가져오고 쿼리스트링 값 변경 시 리렌더링
// src/Router.js
import React from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import List from './List';
const Router = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/list" element={<List />} />
</Routes>
</BrowserRouter>
);
};
export default Router;
--------------------------------------------
// src/List.js
import React from 'react';
const List = () => {
return (
<section>
<h1>This is List Page</h1>
</section>
);
};
export default List;
위 상황에서 list?sort=popular
경로 접속 시 리스트 컴포넌트에서 쿼리스트링의 값을 가져와서 활용해야하는 상황.
useLoaction은 Location 객체를 리턴한다. Location 객체는 현재 위치(URL)에 포함된 여러 정보를 포함한다.
정보 프로퍼티 - pathname, search, hash, state, key
(Location 객체의 state는 React state와 다름)
쿼리스트링 정보 = search 프로퍼티
// src/List.js
import React from 'react';
import { useLocation } from 'react-router-dom';
const List = () => {
const location = useLocation();
const queryString = location.search;
return (
<section>
<h1>This is List Page</h1>
<p>
쿼리 스트링: <b>{queryString}</b>
</p>
</section>
);
};
export default List;
렌더링 결과.
useLocation으로 가져온 쿼리 스트리은 활용하기엔 불편한 점이 있다.
?sort=popular 는 전체 쿼리 스트링을 하나의 문자열로 표현해 주기 때문에 원하는 값만 가져오기 위해서는 ?sort=popular를 자바스크립을 통해 parsing하는 과정을 거쳐야 한다.
이러한 복잡한 과정을 해결해주는 useSearchParams
const [searchParams, setSearchParams] = useSearchParams();
예시) ?sort=popular&sort=latest의 경우
searchParams.get("sort")의 리턴값: "popular"
searchParams.getAll("sort")의 리턴값: ["popular","latest"]
예시) ?sort=popular&sort=latest의 경우
searchParams.toString()의 리턴값: "?sort=popular&sort=latest"
searchParams.set(key, value): 인자로 전달한 key값을 value로 설정. 만약 동일 key에 여러 value가 이미 존재하면 set 메서드를 호출하면서 설정한 값 외에는 삭제됨.
?sort=popular&sort=latest 의 경우
searchParams.set("sort", "clear") 호출
searchParams.toString()의 리턴값: "?sort=clear"
searchParams.append(key,value): 메서드를 호출하면서 인자로 전달한 key 값을 value로 추가. 기존 value를 건들지 않음.
?sort=popular&sort=latest 의 경우
searchParams.append("sort", "clear") 호출
searchParams.toString()의 리턴 값
: "?sort=popular&sort=latest&sort=clear"
searchParams를 변경하는 메서드를 이용해서 값을 변경해도 실제 URL의 쿼리 스트링은 변하지 않기 때문에 이 때 setSearchParams(searchParams)를 인자로 전달해줘야 한다.
// src/List.js
// URL: /list?sort=popular&sort=latest
import React from 'react';
import { useSearchParams } from 'react-router-dom';
const List = () => {
const [searchParams, setSearchParams] = useSearchParams();
const setSortParams = () => {
searchParams.set('sort', 'clear');
setSearchParams(searchParams);
};
const appendSortParams = () => {
searchParams.append('sort', 'hello-world');
setSearchParams(searchParams);
};
return (
<section>
<h1>This is List Page</h1>
<p>
toString: <b>{searchParams.toString()}</b>
</p>
<p>
get("sort"): <b>{searchParams.get('sort')}</b>
</p>
<p>
getAll("sort"):
{searchParams.getAll('sort').map((value) => (
<b key={value}>{value} </b>
))}
</p>
<button onClick={setSortParams}>setSortParams</button>
<button onClick={appendSortParams}>appendSortParams</button>
</section>
);
};
export default List;
페이지네이션?
: 전체 데이터를 페이지 별로 분리해서 보여주는 UI
기본 원리
페이지제이션 구현하기 위해서 offset, limit이라는 두 가지 기준이 필요함.
offset: 몇번째 아이템부터 보여줄 것인가.
limit: 한 번에 몇개를 보여줄 것인가
예 ) 페이지당 10개의 아이템 보여주는 UI 구현
1페이지 - 0번째 이후 10개 아이템 보여줘
2페이지 - 10번째 이후 10개의 아이템 보여줘
n번째 이후 = offset
n개의 아이템 = limit
?offset=0&limit=10
프로젝트마다 용어는 달라질 수 있다
offset -> start
limit -> size
접속 URL : "list?offset=10&limit=10"
-> URL의 정보를 useSearchParams() 훅 이용해서 가져오고
-> offset과 limit 변수에 값을 저장해줌
-> fetch에 백엔드 API 호출하는 _start와 _limit의 값으로 위 두 변수 값을 넣어주고
-> 불러온 값을 posts state에 저장한다.
-> posts 데이터를 이용해서 map 리렌더링!!
-> 버튼 함수에 offset만 변경해주면 됨 (왜? 리밋은 10으로 두면 됨)
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/posts?_start=0&_limit=10')
.then((response) => response.json())
.then((data) => console.log(data));
}, []);
위와 같은 경우 100개 데이터 중 0부터 10개의 데이터를 보여주고 있음.
// src/List.js
import React, { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
const List = () => {
const [searchParams, setSearchParams] = useSearchParams(); // 1
const offset = searchParams.get('offset'); // 2
const limit = searchParams.get('limit'); // 2
//처음 접속 URL : '/list?offset=10&limit=10"
return (
<section>
<h1>This is Posts</h1>
</section>
);
};
export default List;
다음으로 API호출을 통해 가져온 데이터 담을 state 만들고
state에 데이터 저장하기.
const [posts, setPosts] = useState([]); // 1
useEffect(() => { // 2
fetch(
`https://jsonplaceholder.typicode.com/posts?_start=${offset}&_limit=${limit}` // 2-a
)
.then((response) => response.json())
.then((result) => setPosts(result));
}, [offset, limit]); // 2-b
포스트에 담긴 데이터 이용 map 돌리기
{posts.map(
(
{ id, title } // 3
) => (
<article key={id}>
<p>
<div>id:{id}</div>
<div>title:{title}</div>
</p>
</article>
)
)}
<div>
{/* 버튼 추가 */}
<button>1</button>
<button>2</button>
<button>3</button>
</div>
1번 버튼 -> 1~10번 포스트
2번 버튼 -> 11~20번 포스트
3번 버튼 -> 21~30번 포스트
3.4.1~3에서 offset, limit 변경 시 데이터 호출하는 로직이 완성돼어있으니 버튼에는 offset과 limit을 적절히 변경하는 이벤트를 넣어주면 된다.
const movePage = (pageNumber) => {
// 1
searchParams.set('offset', (pageNumber - 1) * 10);
setSearchParams(searchParams);
};
<div>
<button onClick={() => movePage(1)}>1</button> {/* 2 */}
<button onClick={() => movePage(2)}>2</button> {/* 2 */}
<button onClick={() => movePage(3)}>3</button> {/* 2 */}
</div>
1.프론트엔드에서 쿼리스트링을 통해서 보여주고자 하는 범위 설정
2. 이 값을 통해서 백엔드 API에 쿼리스트링 포함해서 호출하기
3. 보여주고자 하는 범위 변경하기 위해서 프론트엔드에서 쿼리 스트링 변경 -> 다시 백엔드 API 호출하기
Q) 프론트엔드와 백엔드가 각각 보내는 쿼리스트링의 key가 동일해야 하는가? ㄴㄴ
Q) 쿼리스트링과 패스 파라미터의 차이점
path parameter는 완전히 다른 별개의 리소스를 표현할 때 사용한다.
예 ) /detail/1 /detail/2 는 path가 다르며 서로 다른 별개의 소스
쿼리스트링
예) /list, /list?offset=0&limit=10 , /list?offset=10&limit=10
얘는 똑같은 소스를 통해 가져오는 기준을 정해서 필요 정보만 뽑아오는 것!
Q. offset, limit, 검색어 등 쿼리 스트링으로 관리하는 값은 state로도 관리할 수 있을 것 같은데 왜 쿼리 스트링을 사용하나요?
A. URL에 정보를 포함시킬 수 있기 때문입니다. state는 컴포넌트가 최초로 화면에 나타날 때 초기화됩니다. 따라서 컴포넌트가 화면에서 사라졌다가 다시 화면에 나타날 때도 state는 초기화됩니다.
페이지네이션 상황을 예시로 들어보겠습니다. 리스트 페이지에서 10페이지를 보고 있다가 특정 제품을 선택해서 상세 페이지로 이동한 상황을 가정해 봅시다. 상세페이지에서 뒤로 가기를 눌러서 다시 리스트 페이지로 진입한다면, 리스트 컴포넌트는 화면에서 사라졌다가 다시 화면에 나타났기 때문에 state가 초기화되어서 10페이지가 아닌 초기 페이지를 다시 보여주고 있을 것입니다.
하지만 쿼리 스트링을 이용해서 페이지네이션을 했다면 URL에 몇 페이지를 보고 있는지에 대한 정보가 있기에 상세페이지에서 뒤로 가기를 해서 돌아왔을 때에도 URL에 있는 쿼리 스트링을 해석해서 10페이지를 보여줄 수 있을 것입니다.
실제 구글도 검색어를 쿼리 스트링으로 관리하고 있습니다. 그래서 https://www.google.com/search?q=위코드 이 URL을 브라우저에 붙여넣기하면 위코드가 검색된 결과를 볼 수 있습니다. 만약 검색어가 state로 관리되고 있었다면, 이렇게 URL을 통해서 특정한 검색어를 검색한 결과 페이지를 링크하는 등의 동작을 할 수 없었을 것입니다. 따라서, 필터링, 검색 결과 등 해당 정보가 지속적으로 유지되어야 하는 경우에는 쿼리 스트링을 활용하는 것이 좋습니다.