리액트 기초, 쿼리 스트링, useSearchParams

라용·2022년 9월 15일
25

위코드 - 스터디로그

목록 보기
44/100
post-custom-banner

위코드에서 공부하며 정리한 내용입니다.

쿼리 스트링 정의와 필요성

쿼리 스트링은 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;
profile
Today I Learned
post-custom-banner

0개의 댓글