full table scan
백엔드의 검색 시스템 구조 중 기본적으로 테이블을 풀 스캔하는 방법
➡️ 데이터가 많아질수록 속도가 느려지는 문제 발생
➡️ 개선하기 위해 데이터베이스에 저장할 때, 문장을 키워드 단위로 토크나이징하고, 역인덱스(Inverted Index)
를 만들어서 저장한다
➡️ 이런 방식을 쉽게 만들어 주는 도구(DB)가 Elastic Search(ES)
서비스가 커짐에 따라, 사람들의 검색 패턴이 생기는데 검색결과를 매번 disk에서 꺼내오지 않고 메모리에 저장(검색로그 캐싱) 후, 빠르게 찾아쓸 수 있다
➡️ 이런 방식을 쉽게 만들어 주는 도구(DB)가 redis
Elastic Search(ES) 하드에 저장시켜 데이터가 보존되는 disk기반
redis 휘발성 데이터 저장방식 memory기반
1) 검색어를 입력하고 검색하기 버튼을 눌러야 검색 활성
export default function SearchHomework() {
const { data, refetch } = useQuery(FETCH_BOARDS);
const [search, setSearch] = useState("");
const onClickPage = (event) => { --->> ❗️문제점 발생❗️
refetch({ search: search, page: Number(event.target.id) });
};
const onChangeSearch = (event) => {
setSearch(event.target.value);
};
const onClickSearch = () => {
refetch({ search: search, page: 1 });
};
return (
<div>
<h1>게시물 제목 검색 🔎 </h1>
검색어 입력 <input type="text" onChange={onChangeSearch} />
<button onClick={onClickSearch}>검색하기</button>
{data?.fetchBoards.map((el) => (
<div key={el._id}>
<span> {el.writer} </span>
<span> {el.title} </span>
</div>
))}
{new Array(10).fill(1).map((_, index) => (
<span key={index + 1} onClick={onClickPage} id={String(index + 1)}>
{` ${index + 1} `}
</span>
))}
</div>
);
}
❗️문제점 : a를 검색한 상태에서 b를 검색창에 입력하고 (검색하기 버튼은 누르지 않고) 페이지 번호를 클릭하면 b의 검색결과가 나온다
❓ 해결방법 : 검색하기 버튼을 클릭할 때 새로운 state(keyword)에 저장한다
const [search, setSearch] = useState("");
const [keyword, setKeyword] = useState("");
const onClickPage = (event) => { 3️⃣ 페이지번호를 클릭하면 keyword에 저장된 a 검색
refetch({ search: keyword, page: Number(event.target.id) });
};
const onChangeSearch = (event) => { 1️⃣ 검색창에 a입력 -> search에 저장
setSearch(event.target.value);
};
const onClickSearch = () => {
refetch({ search: search, page: 1 }); 2️⃣ 검색하기 버튼 클릭하면 a에 해당하는 페이지 보여주고, keyword에 a 저장
setKeyword(search);
};
2) 검색하기 버튼 누르지 않고 실시간으로 검색
const [keyword, setKeyword] = useState("");
const onClickPage = (event) => {
refetch({ search: keyword, page: Number(event.target.id) });
};
const onChangeSearch = (event) => { ❗️ 문제점 발생
refetch({ search: event.target.value, page: 1 });
setKeyword(event.target.value);
};
❗️ 실시간 검색은 가능하지만 문제점 발생
: 검색어를 입력할때마다 gql요청이 연속적으로 보내지고 백엔드 컴퓨터의 메모리,cpu 낭비가 심해져 효율이 떨어지게된다
❓ 해결방법 : 디바운싱
Debouncing 디바운싱
연이어 발생한 이벤트를 하나의 그룹으로 묶어 처리하는 방식으로 주로 그룹에서 마지막, 혹은 처음에 처리된 함수를 처리하는 방식으로 사용됩니다. 마지막 호출이 발생한 후 일정 시간이 지날때까지 추가적 입력이 없을때 실행이 됩니다.📌 검색기능
🔎 lodash 라이브러리 를 이용
yarn add lodash
yarn add @types/lodash --dev
Throttling 쓰로틀링
연이어 발생한 이벤트에 대해 일정한delay
를 포함 시켜, 연속적으로 발생하는 이벤트는 무시하는 방식으로 사용됩니다. 즉, 지정한delay
동안 호출된 함수는 무시합니다.📌 스크롤 기능
시간지정을 해줘야한다 -> 1000ms (1초가 지난 후 실행)
const [keyword, setKeyword] = useState("");
const onChangeSearch = (event) => { 1️⃣ 검색창에서 aaa 입력
getDebounce(event.target.value); 2️⃣ aaa를 넘겨서 디바운스 실행
};
const getDebounce = _.debounce((data) => {
refetch({ search: data, page: 1 }); 3️⃣ 1초(1000ms)뒤에 data로 받은 aaa 보여주기, 4️⃣ keyword에 aaa(data) 저장
setKeyword(data);
}, 1000);
const onClickPage = (event) => { 5️⃣ 페이지 클릭하면 keyword(aaa)에 해당하는 페이지 보여주기
refetch({ search: keyword, page: Number(event.target.id) });
};
🙏🏻 검색결과를 티내줘~!
키워드를
시크릿코드 #$%
기준으로 쪼개버리기!
최종 코드
import { v4 as uuidv4 } from "uuid";
interface IProps {
isMatched: boolean;
}
const Word = styled.span`
color: ${(props: IProps) => (props.isMatched ? "red" : "black")};
`; ---> ❗️검색 결과에 해당하는 키워드를 빨간색으로 표시
export default function SearchPage() {
const [search, setSearch] = useState("");
const [keyword, setKeyword] = useState("");
const { data, refetch } = useQuery<
Pick<IQuery, "fetchBoards">,
IQueryFetchBoardsArgs
>(FETCH_BOARDS);
const getDebounce = _.debounce((data) => {
refetch({ search: data, page: 1 });
setKeyword(data);
}, 200);
const onChangeSearch = (event: ChangeEvent<HTMLInputElement>) => {
getDebounce(event.target.value);
};
const onClickPage = (event: MouseEvent<HTMLSpanElement>) => {
if (event.target instanceof Element)
refetch({ search: keyword, page: Number(event.target.id) });
};
return (
<div>
<h1>제목 검색페이지 🔎 </h1>
검색어 입력: <input type="text" onChange={onChangeSearch} />
{data?.fetchBoards.map((el) => (
<div key={el._id}>
<span> {el.writer} </span>
<span>
{el.title
.replaceAll(keyword, `#$%${keyword}#$%`) 1️⃣ keyword를 #$%keyword#$% 로 바꾼다
.split("#$%") 2️⃣ #$%를 기준으로 쪼갠다
.map((el) => (
<Word key={uuidv4()} isMatched={el === keyword}> 3️⃣ 해당하는 keyword를 찾는다
{el}
</Word>
))}
</span>
</div>
))}
{new Array(10).fill(1).map((_, index) => (
<span key={uuidv4()} onClick={onClickPage} id={String(index + 1)}>
{` ${index + 1} `}
</span>
))}
</div>
);
}