들어가며
안녕하세요 🙂
저는 이번 프로젝트 기간 동안 진행했던 다양한 기능 중 검색페이지에 대한 경험을 작성해보고자 합니다.
이 글은 검색페이지를 구현하면서 진행했던 고민의 흐름과 개선의 흔적을 담았습니다.
검색페이지 기능
- 키워드를 입력하면 유저 리스트에 있는 정보를 기반으로 자동완성이 된다.
- 검색 결과로 나타난 프로필을 누르면 해당 유저의 프로필로 이동한다.
검색 키워트 이벤트 개선기
초기 기획
Keyword 자동완성, 유저리스트
구현 과정에서 관점
모든 유저 리스트를 한번에 받아와서 받아온 후 그 유저 리스트를 기반으로 검색해서 띄워주자
- 실 사용 유저가 많지 않을 것이라고 예상되니까 한번에 받아오는데 오랜 시간이 걸리지 않는다.
- 자동완성을 통해 검색어가 바뀔 때마다 API 호출이 일어나게 되면 너무 빈번하게 일어난다.
- API 호출도 결국 비용인데, 너무 빈번하게 일어나는거 아닌가.
- 검색 자동완성은 빈번하게 일어나는 이벤트인데, 이벤트 발생시마다 api 호출을 하면
응답 대기 시간이 발생하고, 그 대기 시간이 너무 빈번하게 발생한다.
그래서 결국 초기에는 모든 유저 리스트를 한번에 받아와서 그 리스트를 필터링하기로 하였습니다.
검색 자동완성 ver 1
console.log('너무 오래전에 작성한 코드라 코드가 남아있지 않습니다 😅')
Keyword.
Throttling : 이벤트를 일정한 주기마다 발생시키기.
쓰로틀링으로 구현을 한 이유는 단순했습니다.
“일정 기간마다 이벤트를 발생시키면 일정 기간마다 자동 완성이 되므로
유저가 더 편하게 사용할 수 있겠지?”
이에 대한 근거는 다음과 같았습니다.
siun을 칠 때 한 글자에 500ms가 걸린다고 가정, 쓰로틀링 간격 또한 500ms이라고 가정하겠습니다.
→ s에서 한번, i에서 한번, u에서 한번, n에서 한번,
즉, 타이핑마다 글자마다 자동완성을 해주면 유저가 원하는 키워드를 더 편하게 찾을수 있을 것이라고 생각했습니다.
BUT.
팀원들과 의견을 공유한 결과 나온 의견은 다음과 같습니다.
“불필요한 요청이 너무 많이 발생한다.”
“검색에 모르는 사람을 찾으려고 글자를 하나하나 치는 사람보다는 키워드를 정해놓고 치는 사람이 많을 것이다.”
이에 대한 의견을 바탕으로 개선을 시작했습니다.
검색 자동완성 ver2
// 모든 유저의 리스트를 가져옴.
const { allUserList } = useAllUserList();
// 검색 결과로 나온 유저가 담긴 배열.
const [searchValue, setSearchValue] = useState<string>("");
const [searchedUser, setSearchedUser] = useState<SearchedUserInfo[]>([]);
// searchValue가 바뀔 때 마다 검색을 시작.
useEffect(() => {
return SearchUtils.searchUser(searchValue, allUserList, setSearchedUser);
}, [searchValue]);
//유저를 검색.
const SearchUtils = {
// 검색 키워드를 기반으로 유저를 필터링
searchEvent: (
word: string,
userList: SearchedUserInfo[],
setSearchedUser: (searchResult: SearchedUserInfo[]) => void,
) => {
if (word.length === 0) {
return [];
}
const searchResult: SearchedUserInfo[] = userList.filter((user: SearchedUserInfo) => {
return user.name.includes(word);
});
setSearchedUser(searchResult);
return searchResult;
},
searchUser: (
searchValue: string,
userList: SearchedUserInfo[],
setSearchedUser: React.Dispatch<React.SetStateAction<SearchedUserInfo[]>>,
) => {
const timer = setTimeout(() => {
setSearchedUser(SearchUtils.searchEvent(searchValue, userList, setSearchedUser));
}, 500);
return () => clearTimeout(timer);
},
}
Keyword.
Debouncing : 마지막에 호출된 함수만 실행하기.
“검색에 모르는 사람을 찾으려고 글자를 하나하나 치는 사람보다는 키워드를 정해놓고 치는 사람이 많을 것이다.”
해당 가정에 시작해서 유저가 모든 키워드를 입력하고 난 후, 검색을 실행하기로 결정했습니다.
개선 과정
- 기존의 키보드 입력을 통한 값의 변화마다 이벤트가 호출 되던 것을 입력을 마친 후 500ms 동안 추가 입력이 발생하지 않을 경우 이벤트를 호출.
- 입력이 계속 되면 clearTimeout이 실행되어 검색을 하는 함수가 들어있는 timer를 지우게 되어 검색이 일어나지 않음.
- 검색을 멈추면 clearTimeout이 작동하지 않고, timer가 500ms를 기다린 후에 내부에 있는 setSearchedUser를 실행함.
개선 결과
- 기존 ‘핏토리’라는 검색어 입력시, 7번의 이벤트 호출이 발생하던 것을 1회 호출로 최적화.
- 불필요한 이벤트 호출 방지.
API 호출 방식 개선기
새로운 문제의 발생
테스트를 진행하면서 저희팀은 Mock data로 5000명의 유저를 넣었습니다.
그러다보니 이전에는 발생하지 않았던 모든 유저 리스트를 받아오는 것에서 문제가 발생했습니다.
생각보다 검색 페이지를 로딩하는데 불필요한 지연시간이 발생했습니다.
Problem1. list를 불러오는데 240ms라는 시간이 걸림. → 유저 볼륨이 커지면 더 오래걸릴 것
834ms 중 유저 리스트를 받아오는 것이 240ms가 소요됨.
- 하단 list의 경우 추천친구 데이터 → 디폴트로 필요한 값임.
Problem2. 불필요한 상황에서 API 호출이 일어남.
개선 사항
- 유저 검색을 처음에 한번에 불러오지 말고 필요에 따라 불러오자.
→ 유저는 검색이 아닌 추천 친구를 보러만 들어올 수도 있는데, 초기 속도가 느리면 불편하다.
검색 자동완성 ver3
// 검색 결과로 나온 유저가 담긴 배열.
const [searchedUser, setSearchedUser] = useState<SearchedUserInfo[]>([]);
// searchValue가 바뀔 때 마다 검색을 시작.
useEffect(() => {
return SearchUtils.searchUserByKeyword(searchValue, setSearchedUser);
}, [searchValue]);
const SearchUtils = {
searchUserByKeyword: (
searchValue: string,
setSearchedUser: React.Dispatch<React.SetStateAction<SearchedUserInfo[]>>,
) => {
const timer = setTimeout(async () => {
//값이 없을 경우 api 요청을 하지 않음.
if (!searchValue) return setSearchedUser([]);
const userList = await UserAPI.searchUserByKeyword(searchValue);
// 반환할 결과가 없을 때, 빈 배열로 반환
if (!userList) return setSearchedUser([]);
return setSearchedUser(userList);
}, 500);
return () => clearTimeout(timer);
},
}
Keyword
UX - 사용성 개선, 불필요한 요청 방지
개선 과정
- 기존에 한번에 불러오던 유저 리스트를 검색어 입력시 해당 키워드에 맞는 유저를 가져오는 방식으로 변경.
- 처음에 유저 리스트를 가져오지 않으므로 초기 렌더링 속도 상승
- 검색하는 키워드가 없으면 API 요청을 날리지 않음
개선 결과const timer = setTimeout(async () => { //값이 없을 경우 api 요청을 하지 않음. if (!searchValue) return setSearchedUser([]); const userList = await UserAPI.searchUserByKeyword(searchValue); // 반환할 결과가 없을 때, 빈 배열로 반환 if (!userList) return setSearchedUser([]); return setSearchedUser(userList); }, 500);
- 빈 값일 때 API 요청이 날아가지 않음
- 기존의 약 800ms가 소요되던 것을 480ms로 축소 시킬 수 있었음.
- 이를 통한 더 빠른 초기 렌더링을 통해 사용성을 개선할 수 있었음.
후기
항상 구현을 하는 단계에서 프로젝트가 끝나면 성능에 대해서 신경을 쓰지 못하는 경우가 많았습니다.
그리고 평소에 ms의 단위로 인한 지연은 크게 고려하지 않았고, 항상 확장 가능성에 대해서 고려를 안했던 것 같습니다.
그러나 이번 기회를 통해 유저가 나 혼자일 때의 환경과, 5000명, 그리고 그 이상을 불러올 때 얼마나 큰 변화를 가져오는지 알게 되었습니다.
이번 과정을 통해 성능 개선에 대해서 큰 관심이 생겼고, 추후 개발을 진행할 때 성능 개선을 하는 것에 흥미를 가지고 임할 수 있을 것 같습니다.
이번 프로젝트 뿐만 아니라 개선하는 과정에서 정말 많은 질문들을 했고, 어쩌면 당연하다고 생각되는 질문들도 확실 하기 위해 한번 더 할만큼
귀찮게 한거 같은데 항상 즐겁게 대답해주셨던 팀원들 모두 감사합니다 :)
'Project > Fitory' 카테고리의 다른 글
NavLink, isActive를 활용한 하단 네비게이션 바 개발기 (0) | 2023.01.15 |
---|