[TIL 0410] ๊ฒ€์ƒ‰

zittoยท2023๋…„ 4์›” 10์ผ
0

TIL

๋ชฉ๋ก ๋ณด๊ธฐ
36/77
post-thumbnail

๐Ÿท๏ธ ๋ชฉ์ฐจ

  • ๊ฒ€์ƒ‰ ํ”„๋กœ์„ธ์Šค ์ดํ•ด (ES, Redis ๋“ฑ)
  • ๊ฒ€์ƒ‰๋ฒ„ํŠผ ์—†์ด ๊ฒ€์ƒ‰ ์‹œ, ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ
  • ๋””๋ฐ”์šด์‹ฑ & ์“ฐ๋กœํ‹€๋ง
  • Lodash ๋””๋ฐ”์šด์‹ฑ ๊ตฌํ˜„
  • ์‹œํฌ๋ฆฟ ์ฝ”๋“œ ์‚ฌ์šฉํ•˜๊ธฐ

๐Ÿ’ก ๊ฒ€์ƒ‰ ํ”„๋กœ์„ธ์Šค ์ดํ•ด (ES, Redis ๋“ฑ)

Browser์—์„œ ๊ฒ€์ƒ‰์„ ์š”์ฒญํ•˜๋ฉด Back-end์—์„œ DB ๋‚ด๋ถ€์˜ ์ˆ˜ ๋งŽ์€ Data๋“ค ์†์—์„œ ์š”์ฒญ๋ฐ›์€ keyword๋ฅผ ๊ฐ€์ง€๊ณ  full-scan์„ ํ•˜๊ฒŒ๋œ๋‹ค.
๊ธฐ๋ณธ์ ์ธ ์˜ต์…˜์„ ์ฃผ์ง€ ์•Š์œผ๋ฉด ์œ„์—์„œ ๋ถ€ํ„ฐ ํ•˜๋‚˜์”ฉ ์ฐพ์•„๋‚˜๊ฐ€๊ธฐ ๋•Œ๋ฌธ์—
Data๊ฐ€ ๋งŽ์„ ์ˆ˜๋ก ์†๋„๊ฐ€ ๋Š๋ฆผ!

๋น ๋ฅด๊ฒŒ ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ์‹์€?

ํ‚ค์›Œ๋“œ๋ฅผ ๋„์›Œ์“ฐ๊ธฐ ๊ธฐ์ค€์œผ๋กœ ์ž๋ฅธ ๋’ค ๊ฒ€์ƒ‰์ „์šฉ ํ…Œ์ด๋ธ”์„ ๋งŒ๋“ ๋‹ค.
(๊ตฌ๊ธ€์—”์ง„์ด ์ฒ˜์Œํ–ˆ๋˜ ๋ฐฉ์‹์ด๋ผ๊ณ  ํ•œ๋‹ค)
์ด๋ ‡๊ฒŒ ์ž๋ฅด๋Š” ๊ฒƒ์„ ํ† ํฌ๋‚˜์ด์ง•ํ–ˆ๋‹ค, ํ† ํฐํ™”ํ–ˆ๋‹ค๋ผ๊ณ  ํ•จ!
Data๋ฅผ ํŠน์ • ํ‚ค์›Œ๋“œ๋“ค๋กœ ๊ตฌ๋ถ„์ง€์–ด, ํ•ด๋‹นํ•˜๋Š” ๊ธ€๋“ค์„ ๋ชจ์•„ ์˜ค๋ฅธ์ชฝ์— ๋งตํ•‘ํ•จ์œผ๋กœ์จ ์—ญ๋ฐฉํ–ฅ์œผ๋กœ ์ €์žฅํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ,
์ด์™€ ๊ฐ™์€ ๋ฐฉ์‹์„ ์—ญ์ธ๋ฑ์Šค ๋ฐฉ์‹(Inverted Index) ๋˜๋Š” ์—ญ์ƒ‰์ธ ์ด๋ผ๊ณ  ํ•œ๋‹ค.

ํ•˜์ง€๋งŒ ๋งค๋ฒˆ ์ด๋ ‡๊ฒŒ ํ•˜๋Š” ๊ฒƒ๋„ ๊ท€์ฐฎ์€ ์ผ์ž„
์ž๋™์œผ๋กœ ํ•ด์ฃผ๋Š” ๊ฒƒ์ด ์—†๋‚˜?
๊ฒ€์ƒ‰์ „์šฉ๋””๋น„๋ฅผ ๋งŒ๋“ค์–ด์ฃผ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ๋”ฐ๋กœ ์žˆ์Œ!

โœ… Elastic Search(ES)

Disk์— ์ €์žฅ๋˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ปดํ“จํ„ฐ๊ฐ€ ๊บผ์ ธ๋„ ์ €์žฅ์ด ์œ ์ง€๋˜๊ณ  ์•ˆ์ „ํ•˜๋‹ค๋Š” ํŠน์ง•์ด ์žˆ์ง€๋งŒ, ๋น„๊ต์  ์†๋„๋Š” ์กฐ๊ธˆ ๋–จ์–ด์ง„๋‹ค.

โœ… Redis

Memory์— ์ €์žฅ๋˜๋Š” ์ž„์‹œ์ €์žฅ๋ฐฉ์‹์œผ๋กœ, Disk์ €์žฅ๋ณด๋‹ค๋Š” ์•ˆ์ •์„ฑ์ด ๋–จ์–ด์ง€์ง€๋งŒ ์†๋„๊ฐ€ ๋น ๋ฅด๋‹ค.


์„œ๋น„์Šค ์ œ๊ณต ํ›„ ์œ ์ €๋“ค์˜ ์ผ์ • ๊ฒ€์ƒ‰ ํŒจํ„ด์ด ์ƒ๊ธฐ๊ฒŒ ๋˜๊ณ , ๊ฒ€์ƒ‰๋„๊ฐ€ ๋นˆ๋ฒˆํ•œ ํ‚ค์›Œ๋“œ๋Š” `Disk`์—์„œ ๊บผ๋‚ด์˜ค๋Š” ๊ฒƒ๋ณด๋‹ค `Memory ๊ธฐ๋ฐ˜ DB`์— ๋„ฃ์–ด๋‘๋ฉด ๊ทธ๋•Œ ๊ทธ๋•Œ ๋” ๋น ๋ฅธ ์ œ๊ณต์ด ๊ฐ€๋Šฅํ•ด์ง„๋‹ค.

์ด๋Ÿฐ ๋ฐฉ์‹์„ ๊ฒ€์ƒ‰๋กœ๊ทธ ์บ์‹ฑ ์ด๋ผ๊ณ  ํ•œ๋‹ค.
์ฆ‰, ๊ฒ€์ƒ‰์ง„ํ–‰ ์‹œ, ์บ์‹ฑ(์ €์žฅ)์ด ๋˜์–ด์žˆ๋‹ค๋ฉด Redis ,
์บ์‹ฑ๋˜์–ด ์žˆ์ง€ ์•Š์€ ๊ธฐ๋ก์€ Elastic Search ๋ฐฉ์‹ ์‚ฌ์šฉ
-> ์บ์‹œ-์–ด์‚ฌ์ด๋“œ-ํŒจํ„ด


[์‹ค์Šต section 20-1]

import { useQuery, gql } from "@apollo/client";
import { ChangeEvent, MouseEvent, useState } from "react";
import type {
  IQuery,
  IQueryFetchBoardsArgs,
} from "../../../src/commons/types/generated/types";
const FETCH_BOARDS = gql`
  query fetchBoards($page: Int, $search: String) {
    fetchBoards(page: $page, search: $search) {
      _id
      writer
      title
      contents
    }
  }
`;
export default function StaticRoutingPage(): JSX.Element {
  const [search, setSearch] = useState(""); //์ดˆ๊ธฐ๊ฒ€์ƒ‰์–ด
  const { data, refetch } = useQuery<
    Pick<IQuery, "fetchBoards">,
    IQueryFetchBoardsArgs
(FETCH_BOARDS);
  console.log(data);
  const onClickPage = (event: MouseEvent<HTMLSpanElement>): void => {
    void refetch({ page: Number(event.currentTarget.id) });
  };
  const onChangeSearch = (event: ChangeEvent<HTMLInputElement>): void => {
    setSearch(event.currentTarget.value); //๊ฒ€์ƒ‰ํ•œ ํ‚ค์›Œ๋“œ๊ฐ€ ์—ฌ๊ธฐ์— ์ €์žฅ
  };
  //๊ฒ€์ƒ‰๋ฒ„ํŠผ ๋ˆ„๋ฅด๋ฉด useQuery๋ฅผ refetchํ•œ๋‹ค.
  const onClickSearch = (): void => {
    void refetch({ search: search, page: 1 }); //์ฃผ์˜ํ•  ์ ! ํ•ด๋‹น๊ฒ€์ƒ‰์–ด์— ๋Œ€ํ•œ 1ํŽ˜์ด์ง€๋ฅผ ๊ฐ€์ ธ์˜ค๋ผ๋Š” ๊ฒƒ์ž„!
  }; //๊ฒ€์ƒ‰ํ•œ ํ‚ค์›Œ๋“œ๋กœ ๋ฆฌํŒจ์น˜ ํ•˜๊ธฐ (๋‹ค์‹œ ๊ฐ€์ ธ์˜ค๊ธฐ)
  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>
  );
}

onClickSearchํ•จ์ˆ˜๋Š”(๊ฒ€์ƒ‰๋ฒ„ํŠผ)ํ•ด๋‹น๊ฒ€์ƒ‰์–ด์— ๋Œ€ํ•œ ์ผํŽ˜์ด์ง€๋ฅผ ๊ฐ€์ ธ์˜ค๋ผ๋Š” ๋œป!
refetch๋ฅผ ํ•œ๋ฒˆ ํ•œ ๊ฒƒ๋“ค์€ ์ €์žฅ์ด ๋˜์–ด์žˆ๋‹ค.
๋”ฐ๋ผ์„œ onClickPage๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ํŽ˜์ด์ง€๋ฐ–์— ์ž‘์„ฑํ•˜์ง€ ์•Š์•˜๋Š”๋ฐ๋„
์ž…๋ ฅํ•˜์ง€์•Š์€ search๊ฐ€ ๋“ค์–ด๊ฐ€์žˆ์Œ(์ด๋ฏธ ๊ธฐ์กด์— ์žˆ๋˜ search๊ฐ€ ๊ฐ™์ด ๋“ค์–ด๊ฐ)


๐Ÿ’ก ๊ฒ€์ƒ‰๋ฒ„ํŠผ ์—†์ด ๊ฒ€์ƒ‰ ์‹œ, ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ

export default function StaticRoutingPage(): JSX.Element {
  const [search, setSearch] = useState(""); //์ดˆ๊ธฐ๊ฒ€์ƒ‰์–ด
  const { data, refetch } = useQuery<
    Pick<IQuery, "fetchBoards">,
    IQueryFetchBoardsArgs
(FETCH_BOARDS);
  console.log(data);
  const onClickPage = (event: MouseEvent<HTMLSpanElement>): void => {
    void refetch({ page: Number(event.currentTarget.id) });
  };
  const onChangeSearch = (event: ChangeEvent<HTMLInputElement>): void => {
     void refetch({ search: event.currentTarget.value, page: 1 });
     setSearch(event.currentTarget.value); //๊ฒ€์ƒ‰ํ•œ ํ‚ค์›Œ๋“œ๊ฐ€ ์—ฌ๊ธฐ์— ์ €์žฅ
  };

๋งŒ์•ฝ ๊ฒ€์ƒ‰์–ด๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ๋ณ€๊ฒฝ๋œ ๊ฒ€์ƒ‰์–ด๋กœ ๋‚ ๋ฆฌ๋ฉด ๋จ. ๊ทธ ๊ฐ’ ๊ทธ๋Œ€๋กœ refetch!
ํ•˜์ง€๋งŒ ์ด๋Ÿฐ๋ฐฉ์‹์€ ๋ฌธ์ œ๊ฐ€ ์žˆ์Œ
์–ด๋–ค ๋ฌธ์ œ๋ƒ?

API์š”์ฒญ์ด ์—„์ฒญ ๋‚˜๊ฐ€๊ณ  ์žˆ์Œ
onChange ์•ˆ์—์„œ refetch๊ฐ€ ๋‚˜๊ฐ€๋ฏ€๋กœ page๋ฅผ ๋ณ€๊ฒฝํ•˜๋ฉฐ refetch๋  ๋•Œ,
state๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒ€์ƒ‰ inputํ‚ค์›Œ๋“œ ๊ฐ’์ด ๊ฒ€์ƒ‰์„ ๋ˆ„๋ฅด์ง€ ์•Š์•„๋„ ๊ฒ€์ƒ‰๋˜์–ด
ํ•˜๋‚˜ํ•˜๋‚˜ ์ž…๋ ฅํ•  ๋•Œ๋งˆ๋‹ค refetch์š”์ฒญ์ด ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ!
๋งŒ์•ฝ ์ด๋Ÿฐํ–‰๋™์„ 1์–ต๋ช…์ด ํ•œ๋‹ค๊ณ  ํ•œ๋‹ค๋ฉด ??!!
์š”์ฒญ ๋˜ํ•œ ๋ฌด์ˆ˜ํžˆ ๋งŽ์•„์ง€๊ณ  ๊ทธ๋งŒํผ ๋„คํŠธ์›Œํฌ ๋น„์šฉ ๋“ฑ์˜ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.
์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๋””๋ฐ”์šด์‹ฑ๊ณผ ์“ฐ๋กœํ‹€๋ง์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.


๐Ÿ’ก Debouncing & Throttling

  • ๋””๋ฐ”์šด์‹ฑ : ์ฃผ๋กœ ๊ทธ๋ฃน์—์„œ ๋งˆ์ง€๋ง‰, ํ˜น์€ ์ฒ˜์Œ์— ์ฒ˜๋ฆฌ๋œ ํ•จ์ˆ˜๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹
    ๋Œ€ํ‘œ์ ์˜ˆ์ œ) ๊ฒ€์ƒ‰๊ธฐ๋Šฅ

  • ์“ฐ๋กœํ‹€๋ง : ์—ฐ์ด์–ด ๋ฐœ์ƒํ•œ ์ด๋ฒคํŠธ์— ๋Œ€ํ•ด ์ผ์ •ํ•œ delay๋ฅผ ํฌํ•จ ์‹œ์ผœ, ์—ฐ์†์ ์œผ๋กœ ๋ฐœ์ƒํ•˜๋Š” ์ด๋ฒคํŠธ๋Š” ๋ฌด์‹œํ•˜๋Š” ๋ฐฉ์‹
    ์ฆ‰, ์ง€์ •ํ•œ delay๋™์•ˆ ํ˜ธ์ถœ๋œ ํ•จ์ˆ˜๋Š” ๋ฌด์‹œํ•œ๋‹ค.
    ๋Œ€ํ‘œ์  ์˜ˆ์ œ) ์Šคํฌ๋กค ๊ธฐ๋Šฅ

๐Ÿ’ก Lodash ๋””๋ฐ”์šด์‹ฑ ๊ตฌํ˜„

Lodash : ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์˜ ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ (์œ ์šฉํ•œ ๋‚ด์žฅํ•จ์ˆ˜ ๋‹ค๋Ÿ‰๋ณด์œ )

  • ์„ค์น˜ ๋ช…๋ น์–ด
    yarn add lodash
    yarn add -D @types/lodash



โœ… Debounce

: ๋ฐ˜๋ณต์ ์ธ ๋™์ž‘์„ ๊ฐ•์ œ์ ์œผ๋กœ ๋Œ€๊ธฐํ•˜๋Š” ๊ฒƒ(์ค‘๊ฐ„๊ณผ์ •์„ ์—†์• ๊ณ  ๊ฒฐ๊ณผ๋งŒ ํ•œ๋ฒˆ์— ์‹คํ–‰)

  • debounce( ์ฝœ๋ฐฑํ•จ์ˆ˜ (์‹คํ–‰์‹œํ‚ค๊ณ  ์‹ถ์€ ํ•จ์ˆ˜) , ์‹œ๊ฐ„)
    • setTimeout๊ณผ ์‚ฌ์šฉ๋ฐฉ๋ฒ• ๊ฐ™์Œ.
  • ํ•ด๋‹น ์‹œ๊ฐ„ ๋™์•ˆ ์•„๋ฌด ์ผ๋„ ํ•˜์ง€ ์•Š์•˜์„ ๋•Œ ์ฝœ๋ฐฑํ•จ์ˆ˜๋ฅผ ์‹คํ–‰์‹œํ‚จ๋‹ค.
  • ์ด ์‹œ๊ฐ„์„ ttl์ด๋ผ๊ณ  ํ•œ๋‹ค. ๊ฐ€๊ธ‰์  ์งง๊ฒŒ ํ•ด์ฃผ๋Š” ๊ฒƒ์ด ์ข‹์Œ.

์ฆ‰, ์ž…๋ ฅ ing๋ฉด ํ•จ์ˆ˜ ์‹คํ–‰X
์ž…๋ ฅ is done ์ด๋ฉด ๊ทธ๋•Œ ํ•จ์ˆ˜ ๊ฒฐ๊ณผ ๋ณด์—ฌ์คŒ.



[์‹ค์Šต section 20-2]

//Debounce๋ถˆ๋Ÿฌ์˜ค๊ธฐ
import { debounce } from 'lodash'; // or import _ from "lodash";
export default function StaticRoutingPage(): JSX.Element {
  // const [search, setSearch] = useState(""); //์ดˆ๊ธฐ๊ฒ€์ƒ‰์–ด
  const [keyword, setKeyword] = useState("");//ํ‚ค์›Œ๋“œ ๊ฐ’์„ ๋”ฐ๋กœ ์ €์žฅ์‹œ์ผœ์ฃผ๋Š” `state`๋ฅผ ๋ถ„๋ฆฌ
  const { data, refetch } = useQuery<
    Pick<IQuery, "fetchBoards">,
    IQueryFetchBoardsArgs
(FETCH_BOARDS);
  const onClickPage = (event: MouseEvent<HTMLSpanElement>): void => {
    void refetch({ page: Number(event.currentTarget.id) });
  };
 //์›ํ•˜๋Š” ๊ธฐ๋Šฅ debounce๋กœ ๊ฐ์‹ธ์ฃผ๊ธฐ
  const getDebounce = _.debounce((value) => {
    void refetch({ search: value, page: 1 });
    setKeyword(value); //์ฒ ์ˆ˜๋ผ๋Š” ๊ฐ’์ด ํ‚ค์›Œ๋“œ์— ์ €์žฅ๋จ.
  }, 1000); //1์ดˆ
  const onChangeSearch = (event: ChangeEvent<HTMLInputElement>): void => {
    // void refetch({ search: event.currentTarget.value, page: 1 });
    // setSearch(event.currentTarget.value); 
    getDebounce(event.currentTarget.value);
  };
  // const onClickSearch = (): void => {
  //   void refetch({ search: search, page: 1 }); 
  // }; 
  return (
    <div>
      ๊ฒ€์ƒ‰์–ด์ž…๋ ฅ:
      <input type="text" onChange={onChangeSearch} />

ํŽ˜์ด์ง€๋„ค์ด์…˜์€ ๋Š˜ ๊ฐ™์ด ์ ์šฉ ์‹œ์ผœ์ค˜์•ผ ํ•œ๋‹ค. search๊ฐ€ ๋˜์•ผ ํŽ˜์ด์ง€๋„ค์ด์…˜๋„ ์ž‘๋™ํ•˜๊ธฐ๋•Œ๋ฌธ์— setSearch ์˜ฎ๊ฒจ์ค€๋‹ค.

๐Ÿ’ก ์‹œํฌ๋ฆฟ ์ฝ”๋“œ ์‚ฌ์šฉํ•˜๊ธฐ

๋งŒ์•ฝ ๊ฒ€์ƒ‰ํ•œ ํ‚ค์›Œ๋“œ์—๋งŒ ์ƒ‰๊น” ๋“ฑ์˜ ํšจ๊ณผ๋ฅผ ์ ์šฉํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผํ• ๊นŒ?
.split๋ฉ”์†Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋‚˜๋ˆˆ๋‹ค๋ฉด?

-> ๋‚ด๊ฐ€ ์›ํ•˜๋Š” ๊ฑด "์ฒ ์ˆ˜"๋งŒ ์ธ๋ฐ ๊ฐ€๊นŒ์ง€ ๋ฐ–์— ๋ถ„๋ฆฌํ•  ์ˆ˜ ๊ฐ€ ์—†์Œ.
๋˜๋‹ค๋ฅธ ํ‚ค์›Œ๋“œ๊ฐ€ ํ•„์š”ํ•จ!
์ด๋Ÿด ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์‹œํฌ๋ฆฟ ์ฝ”๋“œ!
์œ ์ €๊ฐ€ ๋ชจ๋ฅด๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ๊ฒƒ!
(์œ ์ €๊ฐ€ ์‹ค์ˆ˜๋กœ๋ผ๋„ ๋”ฐ๋ผ ํ•  ์ˆ˜ ์—†๊ฒŒ๋”)

[์‹ค์Šต section 20-3]

import { useQuery, gql } from "@apollo/client";
import { ChangeEvent, MouseEvent, useState } from "react";
import type {
  IQuery,
  IQueryFetchBoardsArgs,
} from "../../../src/commons/types/generated/types";
import _ from "lodash";
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 StaticRoutingPage(): JSX.Element {
  const [keyword, setKeyword] = useState("");
  const { data, refetch } = useQuery<
    Pick<IQuery, "fetchBoards">,
    IQueryFetchBoardsArgs
(FETCH_BOARDS);
  const onClickPage = (event: MouseEvent<HTMLSpanElement>): void => {
    void refetch({ page: Number(event.currentTarget.id) });
  };
  const getDebounce = _.debounce((value) => {
    void refetch({ search: value, page: 1 });
    setKeyword(value); 
  }, 1000);
  const onChangeSearch = (event: ChangeEvent<HTMLInputElement>): void => {
    getDebounce(event.currentTarget.value);
  };
  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
              // .replaceAll("์ฒ ์ˆ˜","@#$์ฒ ์ˆ˜@#$")
              .replaceAll(keyword, `@#$${keyword}@#$`)
              .split("@#$")
              .map((el) => (
                <span
                  key={uuidv4()}
                  style={{ color: el === keyword ? "red" : "black" }}

                  {el}
                </span>
                // .map((el)=><span style={{color: el === "์ฒ ์ˆ˜" ? "red" : "black"}}>{el}</span>{
              ))}
          </span>
          {/* ํ•˜๋‚˜์˜ ํƒ€์ดํ‹€์˜ ๋‘๊ฐœ์˜ span์œผ๋กœ ๋‚˜๋ˆˆ๋‹ค. split */}
          <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>
  );
}

-> ์—ฌ๋Ÿฌ๊ฐœ ์žˆ์„ ์ˆ˜ ๋„ ์žˆ์œผ๋‹ˆ map์˜ key ๋“ฑ์— ๊ณ ์œ ํ•œ ๊ฐ’์„ ๋„ฃ์–ด์ค„ ๋•Œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” uuid์‚ฌ์šฉ!

  • ์„ค์น˜๋ช…๋ น์–ด
    yarn add uuid
    yarn add --dev @types/uuid

profile
JUST DO WHATEVER

0๊ฐœ์˜ ๋Œ“๊ธ€