※ 이 글은 저의 삽질 과정이 그대로 담겨있는 글로, 사담이 많습니다. 결과만 알고 싶으신 분은 최종 코드를 읽어주세요
검색창을 구현할 일이 생겼는데 글자가 하나 입력될때마다 결과를 다시 불러와서 화면이 계속 깜빡거리는 현상이 발생했다. 이걸 어떻게 해결할까 찾아보다가 디바운싱이라는걸 알게 되었고, 내 코드에 적용시키면서 삽질한 이야기를 해볼까한다.
문제 상황
우선 처음에 만든 코드는 다음과 같다. 나는 아직 초보라 처음부터 성능 개선하는건 잘 못해서 생각나는대로 코드를 짠 후 리팩토링하는 방식을 선호한다. 초기에 떠올린 아이디어는 다음과 같다.
1. 입력값, 검색대상, 입력값과 일치하는 검색대상을 필터링한 값이 필요하다
2. 변화가 있을 때마다 바로 필터링하고 결과를 렌더링해야하므로 useState로 입력값과 필터링 값을 관리한다
3. 필터링할 때 입력값, 검색 대상의 공백을 제거하고 include로 비교한다
1차 시도
그래서 만들어진 코드는 아래와 같다. 제대로 동작하지 않았다. filter 함수의 동작이 끝나기도 전에 화면에 렌더링해버려서 일부 문장은 공백 제거가 제대로 반영되지 않았다.
const dummyItems={
data:[
{
boardIndex: 1,
userIndex: 1,
boardRegTime: "2014-01-02",
boardInputTime: "2014-01-02",
boardContent: "조회한 글입니다",
boardLocation: 1,
boardAccess: "OPEN",
boardLike: 3,
tagContent: ["밥", "저녁", "싸피"]
},
{
boardIndex: 2,
userIndex: 2,
boardRegTime: "2014-01-04",
boardInputTime: "2014-01-04",
boardContent: "조회한 글입니다2",
boardLocation: 2,
boardAccess: "OPEN",
boardLike: 4,
tagContent: ["오운완", "운동", "일기"]
},
{
boardIndex: 2,
userIndex: 2,
boardRegTime: "2014-01-04",
boardInputTime: "2014-01-04",
boardContent: "조회한 글입니다3",
boardLocation: 2,
boardAccess: "OPEN",
boardLike: 4,
tagContent: ["오운완", "운동", "일기"]
},
...
]
}
function SearchBar(){
const [searchValue, setSearchValue]=useState("");
const [filterData, setFilterData]=useState([]);
const handleSearchValue=(e)=>{
setSearchValue(e.target.value);
}
const filterContents=dummyItems.data.filter((it)=>{
return it.boardContent.toLocaleLowerCase().replace(" ","").includes(searchValue.toLocaleLowerCase().replace(" ",""));
});
return(
<div className="searchBar">
<input value={searchValue} onChange={handleSearchValue}/>
<div className="searchList">
{filterContents.map((it)=>
<li key={it.boardIndex}>{it.boardRegTime} {it.boardInputTime} {it.boardContent}</li>
)}
</div>
</div>
)
}
export default SearchBar;
2차 수정
useEffect를 사용해서 입력값이 달라질 때마다 필터링되게 만들었다. 필터링이 완전히 끝난 후에 결과값을 보여준다. 원하는 대로 기능은 작동하지만 위에 첨부한 움짤처럼 계속해서 깜빡거리는 현상이 발생했다. 그리고 검색창을 여러 컴포넌트에서 재사용하기 위해 검색창과 결과창을 분리해야했다. mock API도 연결해야했다.
import React, { useState, useEffect } from 'react';
const dummyItems = {
...
};
function SearchBar() {
const [searchValue, setSearchValue] = useState('');
const [filterData, setFilterData] = useState([]);
const handleSearchValue = (e) => {
setSearchValue(e.target.value);
};
useEffect(() => {
const filterContents = dummyItems.data.filter((it) =>
it.boardContent
.toLocaleLowerCase()
.replace(/\s/g, '')
.includes(searchValue.toLocaleLowerCase().replace(/\s/g, ''))
);
// 필터링이 완료된 후에 상태를 업데이트합니다.
setFilterData(filterContents);
}, [searchValue]);
return (
<div className="searchBar">
<input value={searchValue} onChange={handleSearchValue} />
<div className="searchList">
{filterData.map((it) => (
<li key={it.boardIndex}>
{it.boardRegTime} {it.boardInputTime} {it.boardContent}
</li>
))}
</div>
</div>
);
}
export default SearchBar;
3차 수정
먼저 더미 데이터를 삭제하고 API를 연결한 후 컴포넌트를 분리하는 작업을 했다.
list에서는 API를 받아오고 필터링 값을 렌더링한다. searchBar에서는 input창으로 입력을 받고 값을 필터링한다.
1. list 컴포넌트에서 API로 전체 데이터를 받아와 listState atom에 저장한다
2. searchBar에서 입력값과 listState를 비교하여 일치하는 데이터를 filterState atom으로 저장한다
3. list 컴포넌트에서 filterState 값을 가져와 렌더링한다
[list.js]
import React, {useEffect} from "react";
import SearchBar, {filterState} from "./SearchBar";
import {atom, useSetRecoilState, useRecoilValue} from "recoil";
import axios from "axios";
export const listState=atom({
key:"listState",
default: [],
})
function List(){
const setListData=useSetRecoilState(listState);
const filterData=useRecoilValue(filterState);
// 추후 url {url}/{userIndex}로 변경, 의존성 배열에 userIndex 넣기
// 리스트 전체 값 불러오기
useEffect(()=>{
const fetchData=async()=>{
await axios.get({url})
.then((response)=>{
setListData(response.data);
}).catch((e)=>console.log(e));
}
fetchData();
},[])
return(
<div className="reusableList">
<SearchBar />
<div className="searchList">
{filterData.map((it) => (
<li key={it.boardIndex}>
{it.boardRegTime} {it.boardInputTime} {it.boardContent}
</li>
))}
</div>
</div>
)
}
export default List;
[searchBar.js]
import React, { useState, useEffect } from 'react';
import {atom, useRecoilValue, useSetRecoilState} from "recoil";
import {listState} from "./List";
export const filterState=atom({
key: "filterState",
default: [],
})
function SearchBar() {
const [searchValue, setSearchValue] = useState("");
const setFilterData = useSetRecoilState(filterState);
const listItems=useRecoilValue(listState);
const handleSearchValue = (e) => {
setSearchValue(e.target.value);
};
// 검색 결과와 일치하는 결과만 가져옴
useEffect(() => {
const filterContents = listItems.filter((it) =>
it.boardContent
.toLocaleLowerCase()
.replace(/\s/g, '')
// 필터링이 완료된 후에 상태를 업데이트
setFilterData(filterContents);
}, [searchValue, listItems]);
return (
<div className="searchBar">
<input value={searchValue} onChange={handleSearchValue} />
</div>
);
}
4차 수정
이제 디바운싱을 이용해서 검색 성능을 향상시키려고 했다. 입력값에 디바운싱을 적용시키고 변경된 코드는 이것뿐이다. 입력값이 변경될 때마다 타이머를 설정하고, 지연 시간이 경과한 후에 입력값을 업데이트한다. 이로써 이벤트가 연속으로 발생하는 걸 방지할 수 있다. 수정된 부분만 가져왔다.
하지만 입력값과 결과가 일치하지 않았고.. 원인을 찾지 못해서 몇시간째 삽질을 하던 중, 한가지 문제를 발견했다.
필터링 동작을 하는 useEffect의 의존성 배열에 디바운싱된 값이 아니라 입력값을 넣어둔게 문제였다. 처음에는 어떤 타이밍에서 useEffect가 호출되든 디바운싱된 값으로 filter하니까 의존성 배열 안에 있는 값이 디바운싱되지 않은 값이어도 상관없지 않나? 라고 생각했지만 아니었다. 디바운싱은 사용자의 입력이 일정시간동안 발생하지 않으면 업데이트된다. 그런데 의존성 배열에 날 것의 입력값을 넣어버리면 입력값이 발생할 때마다 useEffect가 즉각적으로 실행되고, 아직 입력값에 대한 디바운싱 처리가 완료되지 않았을 수 있다. 의존성 배열에 searchValue가 아닌 debouncedSearchValue를 넣어야 입력중에 filter이 실행되지 않고, 입력이 멈춘 후 최종 검색어에 대해 한번만 실행된다. 최종 검색어를 기준으로 검색 결과를 업데이트할 수 있는 것이다.
어떻게 생각하면 당연한 이야기인데 왜 그때는 몰랐을까ㅎ 이렇게 또 엉뚱한 삽질을 했다. 디바운싱에 처리되는 시간을 생각 안한것이다.
[searchBar.js]
const useDebounce=((value,delay)=>{
const [debounceValue, setDebounceValue]=useState(value);
useEffect(()=>{
const timer=setTimeout(()=>{
setDebounceValue(value);
}, delay);
return()=>{
clearTimeout(timer);
}
}, [value]);
return debounceValue;
})
function SearchBar() {
const [searchValue, setSearchValue] = useState("");
const setFilterData = useSetRecoilState(filterState);
const listItems=useRecoilValue(listState);
const debouncedSearchValue=useDebounce(150);
const handleSearchValue = (e) => {
setSearchValue(e.target.value);
};
// 검색 결과와 일치하는 결과만 가져옴
useEffect(() => {
it.boardContent
.toLocaleLowerCase()
.replace(/\s/g, '')
.includes(searchValue.toLocaleLowerCase().replace(/\s/g, ''))
);
// 필터링이 완료된 후에 상태를 업데이트
setFilterData(filterContents);
}, [searchValue, listItems]);
그런데 결과값이 제대로 뜨지 않는 문제가 발생했다. 이를테면 이런거..
1. 검색어를 입력했다가 지우면 모든 데이터가 떠야하는데 아무 데이터도 뜨지 않는다
2. 친구가 밥을 먹었다 / 친구는 예쁘다 라는 문장이 주어졌을 때 '친구가'를 입력했다가 '가'를 지우고 '친구'만 입력된 상태에서 여전히 '친구가 밥을 먹었다' 라는 문장만 뜬다. 한번 더 입력값에 변화를 줘야 제대로된 값이 나온다
최종 코드 (수정된 부분만 가져옴)
useDebounce 훅의 useEffect의 의존성 배열에 delay값을 추가하지 않은게 원인이었다. delay를 추가하여 delay 값이 변경될 때마다 새 타이머를 설정하고 기존의 타이머를 클리어하여 디바운싱을 적절하게 동작시킬 수 있었다.
const useDebounce = (value, delay) => {
const [debounceValue, setDebounceValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebounceValue(value);
}, delay);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debounceValue;
};
function SearchBar() {
const [searchValue, setSearchValue] = useState('');
const setFilterData = useSetRecoilState(filterState);
const listItems = useRecoilValue(listState);
const debouncedSearchValue = useDebounce(searchValue, 150);
const handleSearchValue = (e) => {
setSearchValue(e.target.value);
};
// 검색 결과와 일치하는 결과만 가져옴
useEffect(() => {
const filterContents = listItems.filter((it) =>
it.boardContent
.toLocaleLowerCase()
.replace(/\s/g, '')
.includes(debouncedSearchValue.toLocaleLowerCase().replace(/\s/g, ''))
);
// 필터링이 완료된 후에 상태를 업데이트
setFilterData(filterContents);
}, [debouncedSearchValue, listItems]);
디바운싱과 쓰로틀링이 궁금하다면?
https://ggubbanglovesherlife.tistory.com/190
디바운싱과 쓰로틀링
목차 https://ggubbanglovesherlife.tistory.com/132 react로 검색기능 만들면서 삽질한 이야기 (feat. 디바운스) ※ 이 글은 저의 삽질 과정이 그대로 담겨있는 글로, 사담이 많습니다. 결과만 알고 싶으신 분은
ggubbanglovesherlife.tistory.com
'나도 공부한다 > 삽질' 카테고리의 다른 글
React axios 데이터가 제대로 담기지 않는 문제 해결 (0) | 2024.01.24 |
---|---|
(React) API로 받아온 값을 상태값에 넣어주기 전에 화면이 렌더링되는 문제 (0) | 2024.01.18 |
[C++] vector find 함수 사용시 error: no matching function for call to ‘find’ 에러 발생 (0) | 2023.04.29 |
[git] git bash에서 anaconda 끄기 (0) | 2022.12.07 |
[git] 실수로 다른 브랜치에 커밋했을 때 (0) | 2022.12.06 |