[React] 쿼리 스트링

JH Cho·2022년 9월 24일
13

React

목록 보기
14/27

쿼리스트링

1. 쿼리스트링의 정의와 필요성

쿼리스트링은 URL의 한 부분이며 요청하고자 하는 URL에 부가적인 정보를 포함하고 싶을 때 사용한다.

  • 기존 URL 요청 예시)

    프론트: /list -> 리스트 요청
       /detail/ ->상세 페이지 요청
    백엔드: /signin -> 로그인에 대한 응답
       /products -> 상품들 응답
       /product -> 단일 상품 응답

만약 상품의 종류가 어마무시하게 많아진다면 /list 페이지에서 모든 상품을 보여주는 것은 비효율적이다.

왜냐하면 1억개의 상품 정보를 모두 불러오는 것도, 또한 유저는 판매량이나 최신순 같은 기준을 두고 정렬된 데이터를 보고싶어 하기 때문이다.

이런 상황에서 '상품리스트 보여줘 + 최신순 10개' 와 같이 구체적인 요청을 한다면 효율적일 것이다.

2. 쿼리스트링의 형태

[사진 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의 개수에 제한은 없고 구분할 때는 "&"을 기입한다.

3.react-router-dom에서 쿼리 스트링 사용하기

3.1 쿼리스트링을 포함해서 Routing 하기

Link나 navigate로 사용하면 된다.

<Link to="/list?sort=popular" />
 
navigate("/list?sort=popular")

3.2 컴포넌트에서 쿼리 스트링 값 가져오기

필요 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 경로 접속 시 리스트 컴포넌트에서 쿼리스트링의 값을 가져와서 활용해야하는 상황.

3.2.1 useLoaction hook

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;


렌더링 결과.

3.2.2 useSearchParams hook

useLocation으로 가져온 쿼리 스트리은 활용하기엔 불편한 점이 있다.

?sort=popular 는 전체 쿼리 스트링을 하나의 문자열로 표현해 주기 때문에 원하는 값만 가져오기 위해서는 ?sort=popular를 자바스크립을 통해 parsing하는 과정을 거쳐야 한다.

  • 과정: "?"를 제거 -> sort문자열 찾기 -> "=" 뒤에 위치한 값 꺼내오기 -> popular 획득

이러한 복잡한 과정을 해결해주는 useSearchParams

const [searchParams, setSearchParams] = useSearchParams();
  1. 값 읽어오는 메서드
  • searchParams.get(key) : 특정 key의 value 가져오기
  • searchParams.getAll(key) : 특정 key의 모든 value 가져오기

    예시) ?sort=popular&sort=latest의 경우
    searchParams.get("sort")의 리턴값: "popular"
    searchParams.getAll("sort")의 리턴값: ["popular","latest"]

  • searchParams.toString(): 쿼리스트링을 string형태로 리턴

    예시) ?sort=popular&sort=latest의 경우
    searchParams.toString()의 리턴값: "?sort=popular&sort=latest"

  1. 값을 변경하는 메서드
  • 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;

3.3 쿼리스트링 활용 예시

구글 검색

페이지네이션

  • 페이지네이션?
    : 전체 데이터를 페이지 별로 분리해서 보여주는 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

3.4페이지네이션 구현

🧨🧨🧨 순서

접속 URL : "list?offset=10&limit=10"
-> URL의 정보를 useSearchParams() 훅 이용해서 가져오고
-> offset과 limit 변수에 값을 저장해줌
-> fetch에 백엔드 API 호출하는 _start와 _limit의 값으로 위 두 변수 값을 넣어주고
-> 불러온 값을 posts state에 저장한다.
-> posts 데이터를 이용해서 map 리렌더링!!

-> 버튼 함수에 offset만 변경해주면 됨 (왜? 리밋은 10으로 두면 됨)

3.4.1 기본 구성

  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/posts?_start=0&_limit=10')
      .then((response) => response.json())
      .then((data) => console.log(data));
  }, []);

위와 같은 경우 100개 데이터 중 0부터 10개의 데이터를 보여주고 있음.

3.4.2 10개씩 보여주는 UI 구현

  • 페이지 별로 10개씩 포스트를 나눠서 보여주는 UI 구현
// 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;
  1. useSearchParams hook 이용해 쿼리 스트링을 searchParams 형태로 가져옴
  2. searchParams.get() 메서드를 통해 쿼리 스트링에서의 offset과 limit 값을 각각 변수에 할당

다음으로 API호출을 통해 가져온 데이터 담을 state 만들고
state에 데이터 저장하기.

3.4.3 state 이용 10개씩 보여주는 기능 세팅

  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>
        )
      )}
  1. posts state 생성
  2. 응답 데이터 state에 저장
  • 쿼리스트링 _start key에 offset값을 _limit key에 limit 값을 담아서 요청 보냄.
  • API 호출 후 post state에 저장하는 과정이 offset 또는 limit 값이 변경될 때 마다 매번 발생해야하므로 의존성 배열에 offset과 limit을 넣어줌.
  1. posts state 값을 map으로 렌더링

3.4.4 페이지 버튼 만들기

<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. movePage 함수.
  • limit 값은 기존 10 그대로 나둬도 ㅇㅋ
  • offset은 1, 2, 3일때 10 20 30 되게 ㄱㄱ
  1. 버튼에 온클릭 주고 각 버튼 누를 때 함수 실행 ㄱㄱ

요약

1.프론트엔드에서 쿼리스트링을 통해서 보여주고자 하는 범위 설정
2. 이 값을 통해서 백엔드 API에 쿼리스트링 포함해서 호출하기
3. 보여주고자 하는 범위 변경하기 위해서 프론트엔드에서 쿼리 스트링 변경 -> 다시 백엔드 API 호출하기

Q&A

프론/ 백 쿼리스트링 key 같아야하냐?

Q) 프론트엔드와 백엔드가 각각 보내는 쿼리스트링의 key가 동일해야 하는가? ㄴㄴ

쿼리스트링과 패스파라미터 차이점.

Q) 쿼리스트링과 패스 파라미터의 차이점
path parameter는 완전히 다른 별개의 리소스를 표현할 때 사용한다.
예 ) /detail/1 /detail/2 는 path가 다르며 서로 다른 별개의 소스

쿼리스트링
예) /list, /list?offset=0&limit=10 , /list?offset=10&limit=10
얘는 똑같은 소스를 통해 가져오는 기준을 정해서 필요 정보만 뽑아오는 것!

state로도 충분할 것 같은데 왜 씀?

Q. offset, limit, 검색어 등 쿼리 스트링으로 관리하는 값은 state로도 관리할 수 있을 것 같은데 왜 쿼리 스트링을 사용하나요?

A. URL에 정보를 포함시킬 수 있기 때문입니다. state는 컴포넌트가 최초로 화면에 나타날 때 초기화됩니다. 따라서 컴포넌트가 화면에서 사라졌다가 다시 화면에 나타날 때도 state는 초기화됩니다.

페이지네이션 상황을 예시로 들어보겠습니다. 리스트 페이지에서 10페이지를 보고 있다가 특정 제품을 선택해서 상세 페이지로 이동한 상황을 가정해 봅시다. 상세페이지에서 뒤로 가기를 눌러서 다시 리스트 페이지로 진입한다면, 리스트 컴포넌트는 화면에서 사라졌다가 다시 화면에 나타났기 때문에 state가 초기화되어서 10페이지가 아닌 초기 페이지를 다시 보여주고 있을 것입니다.

하지만 쿼리 스트링을 이용해서 페이지네이션을 했다면 URL에 몇 페이지를 보고 있는지에 대한 정보가 있기에 상세페이지에서 뒤로 가기를 해서 돌아왔을 때에도 URL에 있는 쿼리 스트링을 해석해서 10페이지를 보여줄 수 있을 것입니다.

실제 구글도 검색어를 쿼리 스트링으로 관리하고 있습니다. 그래서 https://www.google.com/search?q=위코드 이 URL을 브라우저에 붙여넣기하면 위코드가 검색된 결과를 볼 수 있습니다. 만약 검색어가 state로 관리되고 있었다면, 이렇게 URL을 통해서 특정한 검색어를 검색한 결과 페이지를 링크하는 등의 동작을 할 수 없었을 것입니다. 따라서, 필터링, 검색 결과 등 해당 정보가 지속적으로 유지되어야 하는 경우에는 쿼리 스트링을 활용하는 것이 좋습니다.

profile
주먹구구식은 버리고 Why & How를 고민하며 프로그래밍 하는 개발자가 되자!

0개의 댓글