11/29 검색하기

김하은·2022년 11월 28일
0

오늘은 검색기능을 구현해보았다.
검색창에 검색을하면 페이지네이션과 결과가 같이 이루어져야한다.

검색원리: 검색창에 입력,또는 검색창에 입력후 검색버튼클릭 ==> 벡엔드로 API요청 ===> 실제로 그 단어가 들어있는 fetchBoards 즉 목록이 나옴

실질적으로 검색이라는 것은 데이터 베이스에서 이루어진다.

API요청이 들어가면 벡엔드에서 DB에 요청을 하고 DB에서 테이블명.find({search:....})로 찾는 형식이다.
어떤column에서 검색할지를 결정이 가능하다.

따라서 검색이라는 것은 DB에서 이루어지는 것이다.
검색알고리즘이라는것이 존재한다고한다. DB개발하는사람들이 사용하는것, 즉 DB안에 내장되어있는 기능이다.

벡엔드에서는 어느정도는 알아야하고, DB개발자들은 아주아주 잘 알아둬야하지만, 프론트에서는 그렇게까지는 알지 않아도 된다고한다.
즉, 검색이란 벡엔드쪽에서 일어난다고 알아두면 된다.

프론트에서는 검색이 일어나는 구조 정도만 알아두면 된다

어떤 단어를 검색해 그 단어가 들어있는 것을 찾는 식인데, 전체 테이블을 다 도는 풀테이블스캔(또는 풀 스캔 테이블) 방식으로 상당히 비효율적인 방식이다.

그래서 구글에서 처음 만든게 검색용 테이블을 하나 더 만들어 단어들을 다 쪼개고 번호와 단어들의 위치를 거로 적는 방식으로 만든 역 인덱스(역 색인=inverted index)방식을 사용하는 것이었다.
요즘에는 이렇게 테이블을 또 만드는 번거로운 작업을 대신해주는 라이브러리인 ElasticSearch 라는것을 사용해 자동으로 단어를 잘려 들어가게만든다.

이렇게 단어단위로 쪼개 이것을 토큰이라고 부르는데, 토큰단위로 토크나이징한다.라고 한다.
토큰을 inverted index로 색인 즉, 역인덱스 방식으로 만드는 것이다.

테이블 풀스캔방식: 데이터가 많으면 동시 검색시 후위가 될 수록 속도가 느려질 수 밖에없어 좋은 방식은 아니다.

검색을 하면 목록에서 추리기 위해 페이지네이션을 이용한다.
목록은 검색결과로 다시 가져오기위해 refetch가 필요하다.
fetchBoard의 search라는 것이 있다. refetch({search:키워드})로 refetch를 하면된다.

그리고 검색된것도 1페이지부터 보이게 페이지 네이션이 필요하다.

페이지 클릭 시에도 검색한 키워드가 있는 페이지가 나와야 한다.

search는 입력창에 입력되는 값을 의미한다. 그 값을 변경이 가능하게 만들려면 state에 담아야한다.

const [search,setSearch] = useState("")

refetch({search:search,page:1})
객체에서 키와 값이 같으면 하나생략이 가능하므로
refetch({search,page:1})
라고 써도 같은 것이다.

==>

import { gql, useQuery } from "@apollo/client";
import { ChangeEvent, MouseEvent, useState } from "react";
import {
  IQuery,
  IQueryFetchBoardsArgs,
} from "../../src/commons/types/generated/types";

// gql query
const FETCH_BOARDS = gql`
  query fetchBoards($page: Int, $search: String) {
    fetchBoards(page: $page, search: $search) {
      _id
      writer
      title
      contents
    }
  }
`;

// fetchBoards

export default function PageNation() {
  const [search, setSearch] = useState("");
  const { data, refetch } = useQuery<
    Pick<IQuery, "fetchBoards">,
    IQueryFetchBoardsArgs
  >(FETCH_BOARDS);
  console.log(data?.fetchBoards);

  const onClickPage = async (event: MouseEvent<HTMLSpanElement>) => {
    await refetch({ page: Number(event.currentTarget.id) }); // 클릭한 페이지도 검색한 그 키워드가 있는페이지어야함
    // 검색에서 refetch할 때 사용한 search검색어가 저장되어있는 상태이므로 추가로 search 포함하지 않아도 됨.
  };

  const onClickSearch = async () => {
    await refetch({ search, page: 1 });
  };
  const onChangeSearch = (event: ChangeEvent<HTMLInputElement>) => {
    setSearch(event.target.value);
  };
  return (
    <>
      <input
        type="text"
        placeholder="검색어를 입력하세요"
        onChange={onChangeSearch}
      />
      <button onClick={onClickSearch}>검색하기</button>
      {data?.fetchBoards?.map((el) => (
        <div key={el._id}>
          <span style={{ margin: "10px" }}>{el.writer}</span>
          <span style={{ margin: "10px" }}>{el.title}</span>
        </div>
      ))}

      {new Array(10).fill(1).map((_, index) => (
        <span key={index + 1} id={String(index + 1)} onClick={onClickPage}>
          {index + 1}
        </span>
      ))}
    </>
  );
}

검색버튼없이 입력한것으로 리페치하기. 문제점: onChange될때마다 API요청이 들어간다.

이때 사용하는것. debounce. 완성부분만 요청을 보내는 방법이다.

debounce(debouncing): 입력이 들어가고 특정 시간을 준다. 특정 시간이 지나고도 입력이 없다면 그것으로 요청이 들어가는 방식이다.
반대의 경우로 throttle 이라는것이 있다. 먼저 한번 요청이가고 특정시간이내 발생한 추가입력은 무시하는 방식이다.

우리가 사용할 것은 debounce이다.

라이브러리로 Lodash를 사용하였다.

yarn add lodash
yarn add --dev @types/lodash

일단 검색어 onChange시 refetch를 사용하는대신 debounce를 사용한다.
Lodash는 특이하게 import 모양이 이다.
import
from "lodash"

 const getDebounce = _.debounce((value) => {
    // 전달 받은것을 매개변수로 받음
    void refetch({ search: value, page: 1 });
  }, 1000); // 1초 이내에 재입력된것은 무시. 1초 이내에 무언가 입력이 안되면 마지막에 한번 실행.
  
  const onChangeSearch = (event: ChangeEvent<HTMLInputElement>) => {
    getDebounce(event.target.value); // 여기에서의 event.target.value니까 함수에 전달
  };

_.debounce라는 함수를 만들어 그 안에서 리페치를 일어나게 하고 해당함수를 변수에 담아 onChange함수 안에서 실행시킨다.
debounce의 맨 뒤쪽에는 특정 시간이 들어가는데 저 시간이 지나면 리페치되고 요청이 들어가게된다.

그러면 검색한 키워드만 색상을 바꾸는 법은 ?
우선 무엇이 검색되는 지를 보면 제목부분에서 search되어지는 것같다. 그럼 해당 fetchboards의 title의 부분을 건드리면 된다는 의미인데..

이부분을 이해하려면 문자열을 쪼개어 사용한다는 것에서 시작한다.
해당 키워드부분만떼어 쪼개서 그것만 span태그에 담아 출력한다면 그 span태그에 색을 준다면 원하는 결과가 나올것이다.

.split()사용.
split은 특정한무언가로 쪼개는 방법이다.
"우리집고양이는뚱뚱해".split("")
이렇게하면 하나하나 ""로쪼개져 배열로 들어오게된다.
그러면 단어로 쪼개려면
특정단어뒤나 앞에 기호가 있어야하는데, replace를 사용해 입력창에 입력되는 단어의 앞과 뒤에 임의의 혹시나 유저가 사용할 가능성을 제외하기위해 임의의 여러 기호를 혼합한 시크릿코드 라는 것을 사용한다.

왜 앞과 뒤에 불여야하나?
특정단어의 앞에도 뒤에도 문자가 있을 수 있기 때문이다.

시크릿코드?
&, %, #.. 등 유저가 얼마든지 사용할 수 있는 기호이기에 그것을 방지하여 @#$%^%&^ 이런식으로 겹칠 가능성이 없는 기호를 말한다.

그리고 시크릿 코드를 사용해 split을 진행한다.

{el.title
              .replaceAll(keyword, `#%$#^%^${keyword}#%$#^%^`)
              .split("#%$#^%^")
              .map((el) => (
                <span
                  key={uuidv4()}
                  style={{ color: el === keyword ? "red" : "black" }}
                >
                  {el}
                </span>
              ))}
              
  const [keyword, setKeyword] = useState("");
  
const getDebounce = _.debounce((value) => {
    // 전달 받은것을 매개변수로 받음
    void refetch({ search: value, page: 1 });
    setKeyword(value);
  }, 1000);

state에 담아 state값을 replaceAll을 사용해 앞뒤에 시크릿 코드를 붙여주고 그 코드로 잘라 그 요소를 맵으로뿌린뒤 그것이 state라면 즉, 검색창에 적은 것이라면 색을 빨간색으로 바꾸어나오게한다.

오늘 수업은 어렵지 않았으나 될거라 생각했던 오류가 계속 날 힘들게한다... 어디가 잘못된건지 도저히 찾을 수가 없다. ..
컴포넌트 분리해 작업하는것도 너무어렵고..
뭘 분리해야될지는 알것같은데 어떻게 분리해야할지도 모르겠다.
한번에 다쓰면 편하지만 가독성이 안좋기는 하고...
하... 복잡하다..

오늘도 같은오류..

왜지..?

0개의 댓글