TIL[21일차].useRef 이미지 업로드, 검색 기능

남예지·2022년 11월 28일
0

TIL

목록 보기
16/47
post-thumbnail

로컬호스트는 '나' 라고 보면 된다.
배열이나 객체는 값이 바뀌지 않고 주소가 바뀌기 때문에 스프레드를 시켜서 넣어준다. (class 15-2) 복습 요망
리렌더
업로드한 사진의 수정은 useEffect 를 사용해서 심플하게 했다.
이러면 리렌더가 한번 더 되지만 이렇게 안하면 복잡해진다.
useEffect 안에서 setState를 사용하면 리렌더링이 한 번 더 일어난다.
중요!!

현재 파일과 기존 파일을 비교하기
왜 값자기 JSON이 붙은걸까? 배열은 서로간 비교가 불가능하다. 그래서 주소 안에 값들을 비교해야되는데 주소는 똑같이 생겨도 다르다. 이때 stringify로 문자열형태로 바꿔서 비교.
주소값은 항상 다름 [7] === [7] false

const currentFiles = JSON.stringify(fileUrls);
    const defaultFiles = JSON.stringify(props.data?.fetchBoard.images);
    const isChangedFiles = currentFiles !== defaultFiles;

keyword:"점심" fetchBoard 하는데 점심이 키워드로 들어가는 걸 검색해줘

프론트는 키워드를 바꾸면 되지만 백앤드에서는 복잡하다.

DB에 게시판 테이블이 있다하면 번호도 있고, 제목도 있고 작성자도 있다.

프론트에서 키워드 "점심"을 검색하면 백앤드는 DB에 제목에 "점심"을 찾아서 돌려준다. 이때 100만개의 게시글이 있다하면 하나하나 찾아야해서 검색시간이 오래걸린다.
구글 검색 엔진은 초창기에 검색 전용 보드를 만들어 게시글을 쓰면 검색전용보드에 단어를 잘라와 인덱스를 저장해놓음. 등록/수정/조회는 기본 보드에서 하고, 검색만 검색전용보드에서 찾아서 "점심"이라는 단어가 들어간 인덱스의 게시글을 가져옴. (사전방식)
단어들을 쪼겠는데 이런걸 토큰이라고 하고 쪼게는 걸 토크나이징이라한다.

토큰 단위로 토크나이징한 걸 검색전용보드를 만들었다.
사전방식은 역인덱스 방식이라 하면 Inverted-index라고 한다.

Elasticsearch 검색전용 도구이다.
하드디스크에 저장하는 방식이라 컴퓨터가 꺼져도 저장이 유지되고 안전하다는 특징이 있지만 속도가 느리다.

ram에 저장하는 건 변수형식이고 disk에 저장하는 건 파일이다.

디스크 input/output 을 줄여 disk I/O 라고 한다.
이는 느리다.

데이터 베이스긴 한데 ram 즉 메모리에 저장하자 => 메모리 기반 DB
성능을 높이기 위해 임시저장을 해놓는다. 임시저장은 자주 검색하는 걸 잠시 저장해놓기 때문에 만약 1분 남아있다면 1분정도는 디비로 안가고 메모리디비에서 바로 가져올 수 있어서 빠르다. 이런 방식을 검색로그 캐싱이라고 한다.
이러한 메모리저장기반 방식을 Redis라고 한다.

즉, 검색이 진행될 때, 캐싱이 되어있다면 Redis , 캐싱되어 있지 않은 기록은 Elastic Search 방식이 사용된다.

백앤드에서 일어나는 일이지만 상식으로 알아두어야 소통을 할 수 있다.


Search

실습1

전에 만들었던 페이지 네이션 목록 위에 검색창과 버튼을 만들고 키워드 검색을 해서 키워드가 포함된 게시글만 불러오도록 만들어보자.
우선 목록 위에 태그를 만들어준다.

검색어입력 : <input type="text" onChange={onChangeSearch} />
<button onClick={onClickSearch}>검색하기</button>

그리고 버튼을 클릭 시 실행할 함수onClickSearch와 인풋창에 체인지가 일어날 때 실행할 함수onChangeSearch 2개가 필요하다.
또 이 검색결과를 저장할 수 있는 state도 만들어준다.

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

// 검색하기 클릭시 실행할 함수
const onClickSearch = () => {
    // 패치 된게 있는데 이를 다시 리패치 해줘야함
    // 우리가 패치한 걸 점심이라는 키워드로 리패치한다는 뜻 , 1page를 암시
	void refetch({ search, page: 1 });
  };

// 체인지 일어날때 실행할 함수
// target과 currentTarget은 버블링이 일어날때는 구분해서 해주고 
   지금은 둘 다 상관없다.
const onChangeSearch = (event: ChangeEvent<HTMLInputElement>) => {
    setSearch(event.target.value);
  };

결과가 잘 나왔다.

실습2

그런데 요즘에는 검색 버튼이 없고 검색창에 입력만 하면 검색이 되는 페이지가 많다. 버튼 없이 리패치 하는 것도 만들어보자.

위에서 버튼을 없애고 버튼함수도 없애준다. 대신 클릭 시 refetch 하던걸 onChange 할 때 refetch하는 걸로 바꿔준다. 그럼 아까 useState도 필요없어지니 없애준다.

// 체인지 일어날 때 실행할 함수
const onChangeSearch = (event: ChangeEvent<HTMLInputElement>) => {
    // setSearch(event.target.value);
    void refetch({ search: event.currentTarget.value, page: 1 });
};

이러면 검색할 때마다 자동으로 refetch가 되어 검색된다.
하지만 이러면 체인지 될 때마다 리렌더링해서 굉장히 비효율적이다.

실습3

마지막 검색 이후 1초, 0.5초 등 일정 시간이 지난 이후에도 검색이 안들어오면 그때 패치하도록 하면 딱 1번만 불러올 수 있어 효율을 높여줄 수 있다.

setTimeout(() => {
    도전해보자~ debouncing
    }, 1000)

setTimeout으로 구현할 수도 있지만 이런 검색기능을 쉽게 할 수 있는 라이브러리 lodash 가 있다.
lodash에서 debounce 기능을 활용해보자.

디바운스(싱) : 특정 시간 이내, 추가 입력 없을 시 마지막 1회 실행
쓰로틀(링) : 특정 시간 이내, 추가 입력 있어도 처음 1회만 실행

우선 lodash 라이브러리를 사용하려면 설치를 해줘야한다.

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

터미널에 설치를 다 해줬다면 아래와 같이 코드를 추가해준다.
간단히 말하자면 "짱구"를 검색했을때 검색한 짱구라는 키워드 양 옆을 일반인은 알 수 없는 코드로 채워 split으로 잘라주고, 이를 span태그로 묶었다.
이는 키워드만 다른 색으로 지정하기 위한 절차이다.

import { v4 as uuidv4 } from "uuid";

const [keyword, setKeyword] = useState("");

const getDebounce = _.debounce((value) => {
    // 마지막에 리패치 한 번 요청
    void refetch({ search: value, page: 1 });
    setKeyword(value);
  }, 500);
 
 
const onChangeSearch = (event: ChangeEvent<HTMLInputElement>) => {
    // setSearch(event.target.value);
    getDebounce(event.currentTarget.value);
  };
  
// 더 안전한 시크릿코드 (너무 길어지면 성능이 느려지기 때문에 고려해야한다.)
  const mySecretCode = uuidv4();
  
return(
	검색어입력 : <input type="text" onChange={onChangeSearch} />
	{data?.fetchBoards.map((el) => (
        <div key={el._id}>
          {/* 백앤드에서 타이틀로만 검색되게 만들어놔서 title에서 키워드를 찾아야함 / 이러면 강아지를 검색 시 강아지를 기준으로 양쪽을 자르고 다 span을 줘야한다. */}
          <span style={{ margin: "10px" }}>
            {el.title
              .replaceAll(keyword, `${mySecretCode}${keyword}${mySecretCode}`)
              .split(mySecretCode)
              .map((el) => (
                <span
                  key={uuidv4()}
                  style={{ color: el === keyword ? "red" : "black" }}
                >
                  {el}
                </span>
              ))}
          </span>
          <span style={{ margin: "10px" }}>{el.contents}</span>
        </div>
      ))}
)

추가된 코드만 쓴거에요

uuid는 key값을 넣어줄 때 사용이 가능하다. 별로 좋은 선택지는 아니라고 한다.

yarn add uuid
yarn add --dev @types/uuid

시크릿코드도 만들어서 더욱 안전하게 만들었다.

마지막으로 키워드만 빨간색으로 표시되게끔 style을 주면 끝!

-전체코드-

// 키워드 입력하기  빨강색으로 키워드 표시
import { gql, useQuery } from "@apollo/client";
// import { debounce } from "lodash" 이렇게 쓰면 _.debounce로 바로 할수도 있다.
import _ from "lodash";
import React, { ChangeEvent, MouseEvent, useState } from "react";
import {
  IQuery,
  IQueryFetchBoardsArgs,
} from "../../src/commons/types/generated/types";
// docs에 나와있음. uuidv4라고 이름을 바꿔서 씀
import { v4 as uuidv4 } from "uuid";

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

export default function StaticRoutedPage() {
  const [keyword, setKeyword] = useState("");

  const { data, refetch } = useQuery<
    Pick<IQuery, "fetchBoards">,
    IQueryFetchBoardsArgs
  >(FETCH_BOARDS);

  const onClickPage = (event: MouseEvent<HTMLSpanElement>) => {
    void refetch({ page: Number(event.currentTarget.id) });
    //  검색에서 리패치할 때, 사용한 서치 검색어가 저장되어 있는 상태이므로 추가로 서치 포함하지 않아도 됨.
  };

  const getDebounce = _.debounce((value) => {
    // 마지막에 리패치 한 번 요청
    void refetch({ search: value, page: 1 });
    setKeyword(value);
  }, 500);

  // 체인지 일어날때 실행할 함수
  const onChangeSearch = (event: ChangeEvent<HTMLInputElement>) => {
    // setSearch(event.target.value);
    getDebounce(event.currentTarget.value);
  };

  // 더 안전한 시크릿코드 (너무 길어지면 성능이 느려지기 때문에 고려해야한다.)
  const mySecretCode = uuidv4();

  return (
    <div>
      검색어입력 : <input type="text" onChange={onChangeSearch} />
      {/* <button onClick={onClickSearch}>검색하기</button> */}
      {data?.fetchBoards.map((el) => (
        <div key={el._id}>
          {/* 백앤드에서 타이틀로만 검색되게 만들어놔서 title에서 키워드를 찾아야함 / 이러면 강아지를 검색 시 강아지를 기준으로 양쪽을 자르고 다 span을 줘야한다. */}
          <span style={{ margin: "10px" }}>
            {el.title
              .replaceAll(keyword, `${mySecretCode}${keyword}${mySecretCode}`)
              .split(mySecretCode)
              .map((el) => (
                <span
                  key={uuidv4()}
                  style={{ color: el === keyword ? "red" : "black" }}
                >
                  {el}
                </span>
              ))}
          </span>
          <span style={{ margin: "10px" }}>{el.contents}</span>
        </div>
      ))}
      {new Array(10).fill("철수").map((_, index) => (
        <span key={index + 1} id={String(index + 1)} onClick={onClickPage}>
          {index + 1}
        </span>
      ))}
    </div>
  );
}

오늘은 집중했다 아주 뿌듯!!
포트폴리오에 얼른 적용해봐야겠다.

profile
총총

1개의 댓글

comment-user-thumbnail
2022년 11월 28일

쵝오

답글 달기