✍🏻 [Code Camp_TIL] 21일차: 검색 ν”„λ‘œμ„ΈμŠ€(Elasticsearch, Redis λ“±), λ””λ°”μš΄μ‹± & μ“°λ‘œν‹€λ§, lodash

code_JΒ·2023λ…„ 4μ›” 22일
1

TIL

λͺ©λ‘ 보기
26/41
post-thumbnail

검색

검색과 λ°μ΄ν„°λ² μ΄μŠ€ 이해


μš°λ¦¬κ°€ ν‰μ†Œμ— μ›Ήμ—μ„œ 검색을 ν•  λ•Œ, 컴퓨터 λ‚΄λΆ€μ—μ„œλŠ” μ–΄λ–€ ν”„λ‘œμ„ΈμŠ€λ‘œ μ›€μ§μΌκΉŒ?


기본적으둜 λΈŒλΌμš°μ €μ—μ„œ 검색을 ν•˜λ©΄, λ°±μ—”λ“œμ—μ„œλŠ” λ°μ΄ν„°λ² μ΄μŠ€ μ•ˆμ˜ 데이터듀을 μŠ€μΊ”ν•œλ‹€. 이런 방식은 데이터가 많으면 λ§Žμ„μˆ˜λ‘ 처리 속도가 λŠλ €μ§€κ²Œ λœλ‹€.

μ—­μΈλ±μŠ€ 방식

이λ₯Ό λ³΄μ™„ν•˜κ³ μž λ‚˜μ˜¨ 것이 μ—­μΈλ±μŠ€ 방식(역색인 방식= inverted index)이닀.

λ°μ΄ν„°λ² μ΄μŠ€μ—μ„œ 데이터듀을 띄어쓰기λ₯Ό κΈ°μ€€μœΌλ‘œ 자λ₯Έ λ‹€μŒμ—(토큰화 = ν† ν¬λ‚˜μ΄μ§•) 검색 μ „μš© ν…Œμ΄λΈ”μ„ λ§Œλ“€μ–΄μ„œ 자λ₯Έ 토큰듀을 λΆ„λ₯˜ν•˜κ³ , κ²€μƒ‰ν•œ ν‚€μ›Œλ“œκ°€ λ“€μ–΄κ°„ λ°μ΄ν„°λ“€λ§Œ λͺ¨μ•„μ„œ λŒλ €μ£ΌλŠ” 방식을 λ§ν•œλ‹€.


Elasticsearch

ν•˜μ§€λ§Œ μœ„μ˜ 방식은 검색을 ν•  λ•Œλ§ˆλ‹€ 이루어져야 ν•˜κΈ° λ•Œλ¬Έμ— κ½€ λ²ˆκ±°λ‘­λ‹€λŠ” 단점이 μžˆμ—ˆλ‹€. λ”°λΌμ„œ 이 과정을 μ‰½κ²Œ μˆ˜ν–‰ν•˜κΈ° μœ„ν•΄ μ—˜λΌμŠ€ν‹± μ„œμΉ˜(Elasticsearch)λΌλŠ” 도ꡬλ₯Ό μ‚¬μš©ν•˜κΈ° μ‹œμž‘ν–ˆλ‹€.

μ—˜λΌμŠ€ν‹± μ„œμΉ˜λŠ” 데이터λ₯Ό Disk에 μ €μž₯ν•œλ‹€. Disk에 μ €μž₯ν•˜λ©΄ μ˜κ΅¬μ €μž₯은 κ°€λŠ₯ν•˜μ§€λ§Œ Disk IO(Input/Output)κ°€ λŠλ¦¬λ‹€λŠ” 단점이 μžˆλ‹€.


Redis

반면, Redis에 μ €μž₯ν•˜λŠ” 방법도 μžˆλ‹€. Redisλ₯Ό ν™œμš©ν•˜λŠ” 방법은 μΊμ‹œ-μ–΄μ‚¬μ΄λ“œ νŒ¨ν„΄ 쀑 ν•˜λ‚˜μ΄λ‹€. RedisλŠ” RAM μ €μž₯ 방식을 μ‚¬μš©ν•œλ‹€. μž„μ‹œ μ €μž₯ 방식이기 λ•Œλ¬Έμ— Disk μ €μž₯보닀 μ•ˆμ •μ„±μ€ λ–¨μ–΄μ§€μ§€λ§Œ 속도가 λΉ λ₯΄λ‹€λŠ” μž₯점이 μžˆλ‹€.

μ„œλΉ„μŠ€κ°€ λ‚˜μ˜€κ³  μ‹œκ°„μ΄ 흐λ₯΄λ©΄, 검색에 μ–΄λŠμ •λ„ μΌμ •ν•œ νŒ¨ν„΄μ΄ μƒκΈ°κ²Œ λœλ‹€. μ‚¬λžŒλ“€μ΄ 자주 κ²€μƒ‰ν•˜λŠ” 것듀은 κ²€μƒ‰λ§ˆλ‹€ Diskμ—μ„œ κΊΌλ‚΄μ˜€λŠ” κ²ƒλ³΄λ‹€λŠ” Redis와 같은 λ©”λͺ¨λ¦¬μ— μ €μž₯ν•˜λ©΄ 더 λΉ λ₯΄κ²Œ 검색결과λ₯Ό μ œκ³΅ν•  수 μžˆλ‹€!

쒀더 ꡬ체적으둜 μ„€λͺ…을 ν•˜μžλ©΄, 검색을 ν–ˆμ„ λ•Œ Redis에 기쑴에 κ²€μƒ‰ν–ˆλ˜ 것이 μžˆλŠ”μ§€ 찾아보고 μ—†μœΌλ©΄ μ—˜λΌμŠ€ν‹± μ„œμΉ˜μ™€ 같은 κΈ°λŠ₯을 톡해 λ§Œλ“€μ–΄λ†“μ€ 데이터λ₯Ό κ°€μ Έμ™€μ„œ μ“°κ³ , Redis에 μž„μ‹œμ €μž₯(캐싱)ν•œλ‹€. 즉, μΊμ‹±λ˜μ–΄ μžˆλŠ” 것은 Redis, μΊμ‹±λ˜μ–΄ μžˆμ§€ μ•Šμ€ 것은 Elastic Search 방식이 μ‚¬μš©λ˜λŠ” 것이닀.

μ—¬κΈ°μ„œ μž„μ‹œμ €μž₯ν•˜λŠ” μ‹œκ°„μ„ TTL이라고 ν•œλ‹€. μž„μ‹œμ €μž₯ μ‹œκ°„μ€ 상황에 따라 λ‹€λ₯΄λ‹€. 짧은 κΈ°κ°„ μ•ˆμ— 많이 κ²€μƒ‰λ˜λŠ” 것은 TTL이 짧은 νŽΈμ΄λ‹€.


검색 κΈ°λŠ₯ κ΅¬ν˜„ν•˜κΈ°

μ˜ˆμ „μ— λ§Œλ“€μ—ˆλ˜ κ²Œμ‹œνŒ λͺ©λ‘ νŽ˜μ΄μ§€μ— 검색 κΈ°λŠ₯을 μΆ”κ°€ν–ˆλ‹€.


  1. fetchBoards api의 μž…λ ₯κ°’μœΌλ‘œ searchλ₯Ό λ„£μ–΄μ€€λ‹€.
const FETCH_BOARDS = gql`
  query fetchBoards($page: Int, $search: String) {
    fetchBoards(page: $page, search: $search) {
      _id
      writer
      title
      contents
    }
  }
`;

  1. 검색창(input)κ³Ό 검색 λ²„νŠΌμ„ λ§Œλ“€κ³ , κ²€μƒ‰μ°½μ—λŠ” onChange 속성을, κ²€μƒ‰λ²„νŠΌμ—λŠ” onClick 속성을 λΆ€μ—¬ν•œλ‹€.
 return (
    <div>
      κ²€μƒ‰μ–΄μž…λ ₯: <input type="text" onChange={onChangeSearch} />
      <button onClick={onClickSearch}>κ²€μƒ‰ν•˜κΈ°</button>
      {data?.fetchBoards.map((el) => (
        <div key={el._id}>
          <span style={{ margin: "10px" }}>{el.title}</span>
          <span style={{ margin: "10px" }}>{el.writer}</span>
        </div>
      ))}
      {new Array(10).fill("철수").map((_, index) => (
        <span key={index + 1} id={String(index + 1)} onClick={onClickPage}>
          {index + 1}
        </span>
      ))}
    </div>
  );
}

  1. searchλΌλŠ” stateλ₯Ό λ§Œλ“€μ–΄μ£Όκ³ , onChangeSearch ν•¨μˆ˜μ—λŠ” κ²€μƒ‰ν–ˆμ„ λ•Œ, μž…λ ₯ν•œ 값을 setState둜 μ„€μ •ν•œλ‹€. onClickSearch ν•¨μˆ˜μ—λŠ” 검색 ν›„ ν‚€μ›Œλ“œκ°€ μΌμΉ˜ν•˜λŠ” κ²Œμ‹œκΈ€λ“€μ΄ λΈŒλΌμš°μ €μ— λ‚˜μ˜€λ„λ‘ refetchν•œλ‹€.
export default function StaticRoutingPage(): JSX.Element {
  const [search, setSearch] = useState("");

  const { data, refetch } = useQuery(FETCH_BOARDS);

  const onClickPage = (event: MouseEvent<HTMLSpanElement>): void => {
    void refetch({
      page: Number(event.currentTarget.id),
    });
  };

  const onChangeSearch = (event: ChangeEvent<HTMLInputElement>): void => {
    setSearch(event.currentTarget.value);
  };

  const onClickSearch = (): void => {
    void refetch({ search: search, page: 1 });
    // 검색 ν‚€μ›Œλ“œκ°€ λ“€μ–΄κ°„ κ²Œμ‹œκΈ€μ„ 10개(1νŽ˜μ΄μ§€) κ°€μ Έμ˜€λ„λ‘ 함.
  };

검색 λ²„νŠΌ 없이 κ²€μƒ‰ν•˜κΈ°


검색 κΈ°λŠ₯을 μΆ”κ°€ν•΄λ³΄λ‹ˆ, 검색어λ₯Ό μž…λ ₯ν• λ•Œλ§ˆλ‹€ onchange, refetchκ°€ μ΄λ£¨μ–΄μ§„λ‹€λŠ” 문제점이 μžˆλ‹€. 예λ₯Ό λ“€μ–΄ "μ•ˆλ…•"μ΄λΌλŠ” 검색어λ₯Ό μž…λ ₯ν•œλ‹€λ©΄, 'γ…‡', 'ㅏ'λ₯Ό μž…λ ₯ν• λ•Œλ§ˆλ‹€ api μš”μ²­μ„ λ³΄λ‚΄μ„œ λ§Œμ•½ λ‹€μˆ˜μ˜ μ‚¬λžŒμ΄ 검색을 ν•΄μ„œ μ ‘μ†λŸ‰μ΄ λŠ˜μ–΄λ‚œλ‹€λ©΄ λ¬Έμ œκ°€ λ°œμƒν•  수 μžˆλ‹€.

이λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄μ„œλŠ” λ””λ°”μš΄μ‹±μ„ ν™œμš©ν•˜λ©΄ λœλ‹€!


λ””λ°”μš΄μ‹±, μ“°λ‘œν‹€λ§


λ””λ°”μš΄μ‹±μ΄λž€, 연이어 λ°œμƒν•œ 이벀트λ₯Ό ν•˜λ‚˜μ˜ 그룹으둜 λ¬Άμ–΄μ„œ μ²˜λ¦¬ν•˜λŠ” 방식이닀. λ§ˆμ§€λ§‰ 호좜이 λ°œμƒν•œ 후에 일정 μ‹œκ°„μ΄ 지날 λ•ŒκΉŒμ§€ μΆ”κ°€ μž…λ ₯이 없을 λ•Œ μ‹€ν–‰λœλ‹€.

μ“°λ‘œν‹€λ§μ€ 연이어 λ°œμƒν•œ μ΄λ²€νŠΈμ— λŒ€ν•΄ μΌμ •ν•œ delayλ₯Ό ν¬ν•¨μ‹œμΌœμ„œ μ—°μ†μ μœΌλ‘œ λ°œμƒν•˜λŠ” μ΄λ²€νŠΈλŠ” λ¬΄μ‹œν•˜λŠ” λ°©μ‹μœΌλ‘œ μ‚¬μš©λœλ‹€. μ“°λ‘œν‹€λ§μ€ λ¬΄ν•œμŠ€ν¬λ‘€μ— 주둜 쓰인닀. νŠΉμ •μ‹œκ°„ 이내에 μΆ”κ°€ 슀크둀이 이루어져도 좔가적인 refetchλŠ” μ‹€ν–‰λ˜μ§€ μ•ŠλŠ”λ‹€.

λ””λ°”μš΄μ‹±: νŠΉμ • μ‹œκ°„ 이내, μΆ”κ°€ μž…λ ₯ 없을 μ‹œ, λ§ˆμ§€λ§‰ 1회만 μ‹€ν–‰
μ“°λ‘œν‹€λ§: νŠΉμ • μ‹œκ°„ 이내, μΆ”κ°€ μž…λ ₯ μžˆμ–΄λ„, 처음 1회만 μ‹€ν–‰

λ””λ°”μš΄μ‹±μ˜ 예λ₯Ό λ“€λ©΄, "철수"라고 κ²€μƒ‰ν–ˆμ„ λ•Œ, "철수"λ₯Ό μž…λ ₯ν•˜κ³  νŠΉμ • μ‹œκ°„λ™μ•ˆ μΆ”κ°€ μž…λ ₯이 μ—†λ‹€λ©΄, κ·Έλ•Œ refetchλ₯Ό μ‹€ν–‰ν•œλ‹€.

λ””λ°”μš΄μ‹±κ³Ό μ“°λ‘œν‹€λ§μ€ lodash에 λΌμ΄λΈŒλŸ¬λ¦¬κ°€ μžˆμ–΄μ„œ, 이λ₯Ό ν™œμš©ν•΄μ„œ 검색 λ²„νŠΌ 없이 μž…λ ₯창에 ν‚€μ›Œλ“œ μž…λ ₯ μ‹œ λ°”λ‘œ 검색이 λ˜λ„λ‘ ν–ˆλ‹€. 방법은 λ‹€μŒκ³Ό κ°™λ‹€.


  1. lodash μ„€μΉ˜ν•΄μ„œ, import ν•΄μ£ΌκΈ°
yarn add lodash
yarn add --Dev @types/lodash // νƒ€μž…μŠ€ν¬λ¦½νŠΈ μ‚¬μš© μ‹œ μΆ”κ°€ μ„€μΉ˜ ν•„μš”

import _ from "lodash";

  1. getDebounce λ§Œλ“€μ–΄μ£ΌκΈ°
const getDebounce = _.debounce((value) => {
    // onChangeSearch μ‹€ν–‰λ˜μ—ˆμ„ λ•Œ, event.currentTarget.value 값이 value 자리둜 λ“€μ–΄μ˜€κ²Œ 됨.
    void refetch({ search: value, page: 1 });
  // μž…λ ₯κ°’(value)λ₯Ό λ°”λ‘œ search ν•΄μ£Όλ©΄ λœλ‹€. search state ν•„μš” μ—†μŒ!
  }, 500); // debounce μΆ”κ°€ μž…λ ₯이 μžˆλŠ”μ§€ μΈ‘μ •ν•˜λŠ” μ‹œκ°„

  1. onChangeSearch ν•¨μˆ˜κ°€ μ‹€ν–‰λ˜μ—ˆμ„ λ•Œ(검색어 μž…λ ₯ μ‹œ), getDebounce에 event.currentTarget.value κ°’ λ„£μ–΄μ£ΌκΈ°(μž…λ ₯된 검색어 λ””λ°”μš΄μ‹±λ˜λ„λ‘ 함)
const onChangeSearch = (event: ChangeEvent<HTMLInputElement>): void => {
    getDebounce(event.currentTarget.value);
  };

검색 ν‚€μ›Œλ“œ 색상 λ³€κ²½ μ‹€μŠ΅

κ²€μƒ‰ν•œ ν‚€μ›Œλ“œλ§Œ 색상이 λ°”λ€Œμ–΄μ„œ κ²°κ³Όκ°€ λ‚˜μ˜€λ„λ‘ ν•˜κ³  싢을 λ•Œμ—λŠ” μ–΄λ–»κ²Œ ν•΄μ•Όν• κΉŒ?


  1. μ‹œν¬λ¦Ώ μ½”λ“œλ₯Ό μ‚¬μš©ν•΄μ„œ 검색 ν‚€μ›Œλ“œμ™€, ν‚€μ›Œλ“œκ°€ μ•„λ‹Œ κ΅¬κ°„μœΌλ‘œ λ‚˜λˆˆλ‹€.
"μ² μˆ˜κ°€ 점심을 λ¨Ήμ—ˆλ‹€".replace("철수", "@#$철수@#$")
// '@#$철수@#$κ°€ 점심을 λ¨Ήμ—ˆλ‹€'

"μ² μˆ˜κ°€ 점심을 λ¨Ήμ—ˆλ‹€".replace("철수", "@#$철수@#$").split("@#$")
// ['', '철수', 'κ°€ 점심을 λ¨Ήμ—ˆλ‹€']

  1. keyword state λ§Œλ“€μ–΄μ€€λ‹€.
const [keyword, setKeyword] = useState("");

  1. getDebounce ν•¨μˆ˜ μ•ˆμ—μ„œ μž…λ ₯값을 keyword의 λ³€κ²½κ°’(setKeyword)으둜 λ„£μ–΄μ€€λ‹€.
setKeyword(value);

  1. title을 검색 ν‚€μ›Œλ“œμ™€, ν‚€μ›Œλ“œκ°€ μ•„λ‹Œ κ΅¬κ°„μœΌλ‘œ λ‚˜λˆ μ„œ, 색상을 λ‹€λ₯΄κ²Œ μ„€μ •ν•΄μ€€λ‹€.
<span style={{ margin: "10px" }}>
            {el.title
              .replaceAll(keyword, `@#$${keyword}@#$`)
              // ν•œ λ¬Έμž₯에 keywordκ°€ μ—¬λŸ¬ 번 λ‚˜μ˜¬ μˆ˜λ„ μžˆμœΌλ‹ˆκΉŒ replace λŒ€μ‹  replaceAll μ‚¬μš© //
              .split("@#$")
              .map((el) => (
                <span
                  key={uuidv4()} // el._idλ₯Ό μ‚¬μš©ν•  수 μ—†κΈ° λ•Œλ¬Έμ— uuid μ‚¬μš©. μ„±λŠ₯μ΅œμ ν™” λ©΄μ—μ„œλŠ” 쒋지 μ•Šμ€ 방법..
                  style={{ color: el === keyword ? "red" : "black" }}
                >
                  {el}
                </span>
              ))}
          </span>

μ˜€λŠ˜μ€ 검색기λŠ₯에 λŒ€ν•΄ κ³΅λΆ€ν–ˆλ‹€. κ°€μž₯ λ² μ΄μ§ν•œ 검색 κΈ°λŠ₯λΆ€ν„° λ°°μ›Œλ΄€λŠ”λ°, 그러면 κΌ­ 검색 ν‚€μ›Œλ“œκ°€ ν¬ν•¨λ˜μ§€ μ•Šμ•„λ„ κ΄€λ ¨λœ μœ μ‚¬ν•œ 데이터듀이 κ²€μƒ‰λ˜λŠ” κ²½μš°λ„ λ§Žμ€λ° μ΄λŸ¬ν•œ κ²½μš°μ—λŠ” ν”„λ‘ νŠΈμ—”λ“œμ—μ„œλŠ” μ–΄λ–»κ²Œ λΈŒλΌμš°μ €μ— κ΅¬ν˜„ν•˜λŠ” 건지 κΆκΈˆν–ˆλ‹€. μ•„λ§ˆ 훨씬 λ³΅μž‘ν•œ λ‘œμ§μ΄κ² μ§€..? κ°€λ©΄ 갈수둝 λ°±μ—”λ“œκ°€ μ „λ‹¬ν•˜λŠ” 데이터λ₯Ό ν™œμš©ν•˜λŠ” λ²”μœ„κ°€ λŠ˜μ–΄λ‚˜λŠ” 것 κ°™μ•„μ„œ λΏŒλ“―ν•˜λ‹€!



profile
Web FE 개발자 취쀀생

0개의 λŒ“κΈ€