들어가며

하단 네비게이션바를 작업하면서 페이지를 전환과 현재 페이지 상태를 불러오는 기능이 필요했습니다.

이에 대한 방안으로

  1. window.location.href를 파싱하여 문자열로 상태를 받고, 해당 문자열에 따라 state를 설정하고 이에 맞는 style을 부여한다.
  2. onClick 이벤트를 사용하여 라우팅과 색 입히는 것을 동시

이 두가지 방법이 떠올랐고, 실행에 옮기고자 하였으나 여러가지 의문이 들었습니다.

window.location.href 파싱

url문자열을 받아오고 파싱을 하는 방법은 가장 떠올리기 쉽고, 직관적인 해결방법입니다. 눈에 보이는 요소를 통해 처리를 하기 때문에 그렇게 생각했던 것 같습니다.

그러나 몇가지 의문이 드는 점 또한 존재했습니다.

  1. 이전에 query string을 사용 할 때, 문자열 파싱을 이용했는데, 찾아보니 urlSearchParams ~ 속성을 활용하여 파싱하는 것을 학습했고, 이처럼 편한 방법으로 현재 주소 상태를 가져오는 방법이 있을 것 같다.
  2. 파싱을 하게되면 ‘/’기준으로 파싱을 할 것이다. 지금이야 프로덕트의 depth가 얕아서 ‘/’가 하나지만 추후 여러가지 depth가 생길 수 있음을 고려해야된다.

와 같은 고민을 하게 되었습니다.

이에 두번째 방식인 onClick 이벤트를 사용에 대해서 고민했습니다.

Onclick 이벤트 부여

선택에 대한 근거는 다음과 같았습니다.

어차피 Navbar에서 클릭을 통해 페이지가 전환되야하고, 전환되는 이벤트를 줄 때 같이 style을 부여하는 이벤트만 추가하게되면 해결되는 문제 아닌가? 라는 생각과 함께 진행을 했습니다.

여기서도 추가적인 고민을 했던 것이

  1. Onclick으로 이벤트를 부여하고 색을 입히면 페이지가 전환될 때 이전에 부여됐던 컴포넌트의 색 클래스를 제거해주는 로직이 필요하지 않나? ex) A화면 : A아이콘(파란색), B아이콘(회색) → B화면: A아이콘(클래스 제거 → 회색) B아이콘(이벤트 추가 → 파란색) 이 과정이 불필요하다고 느껴졌고, 코드가 필요 이상으로 길어질 수 있겠다고 생각했습니다.
  2. 만일 현재 띄워져있는 페이지를 받는 상태를 이용해서 onclick을 사용한다면, 그래도 state를 써야하므로 이 상태 없이 가능할 수 있지 않을까?
  • 사실 코드 안에 다량의 if문과 다량의 state를 쓰기 싫었습니다.

NavLink

결국 NavigationBar의 본질은 라우팅이라고 생각했고, 라우팅에 관련된 글을 읽어보다가 NavLink에 대해서 발견했습니다.

NavLink v6.4.3

NavLink가 뭔데요?

<NavLink> is a special kind of <Link> that knows whether or not it is "active".
This is useful when building a navigation menu such as a breadcrumb or 
a set of tabs where you'd like to show which of them is currently selected. 
It also provides useful context for assistive technology like screen readers.'
  • ‘활성화’ 여부를 제공하는 Link의 종류이고, 현재 선택을 보여줘야하는 navigation menu를 만드는데 유용하다. 또한 여러가지 유용한 컨텍스트를 제공한다.

NavLink를 왜 사용했는가?

만일 onclick, link를 사용하여 작업을 하는 경우, 현재의 URL과 이동할 URL을 비교하여 클래스를 추가 혹은 삭제를 해주는 불필요한 로직이 생기게됨.

const prevPath = useLocation();

return (
	<Link
		to={path}
		className={classNames("foo", {
		isActive: path === prevPath
		})}
	>
);

But

NavLink를 사용하면 별도의 처리 로직 없이 isActive 속성을 활용하여 현재 활성화 되어있는지에 대한 여부를 받고, 이에 따른 처리만 해주면 됨.

과 같은 근거로 사용을 했습니다.

실제로 작성한 코드에서 해당 부분에 대한 처리는 다음과 같았습니다.

<NavLink to=path className={({ isActive }) => (isActive ? "foo" : "")}>

언뜻 별 차이가 없어보이고, 결국 isActive 상태 받아서 삼항연산 처리 했을 뿐, 위의 코드와 다를게 없지 않나..?라고 생각이 들기도하지만, 불필요한 prevPath 상태제거와, 직접 비교하는 연산을 제거해서 코드를 간결하게 했다는 점이 긍정적인 요소라고 생각합니다.

  • 가장 큰 장점은 한번 설정함으로써 개발자가 불필요하게 현재 경로와 Link에서 사용하는 경로에 대해서 계속해서 신경쓰지 않아도 된다는 점 같습니다.

'Project > Fitory' 카테고리의 다른 글

Fitory - Fitory 검색페이지 개발 기록 & 성능 개선기  (0) 2022.12.29

들어가며

안녕하세요 🙂

저는 이번 프로젝트 기간 동안 진행했던 다양한 기능 중 검색페이지에 대한 경험을 작성해보고자 합니다.

이 글은 검색페이지를 구현하면서 진행했던 고민의 흐름과 개선의 흔적을 담았습니다.

검색페이지 기능

  • 키워드를 입력하면 유저 리스트에 있는 정보를 기반으로 자동완성이 된다.
  • 검색 결과로 나타난 프로필을 누르면 해당 유저의 프로필로 이동한다.

searchGif

검색 키워트 이벤트 개선기

초기 기획

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라는 시간이 걸림. → 유저 볼륨이 커지면 더 오래걸릴 것

ㅇ

 

123123


834ms 중 유저 리스트를 받아오는 것이 240ms가 소요됨.

  • 하단 list의 경우 추천친구 데이터 → 디폴트로 필요한 값임.

Problem2. 불필요한 상황에서 API 호출이 일어남.

111

개선 사항

  • 유저 검색을 처음에 한번에 불러오지 말고 필요에 따라 불러오자.

    → 유저는 검색이 아닌 추천 친구를 보러만 들어올 수도 있는데, 초기 속도가 느리면 불편하다.

검색 자동완성 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);
    개선 결과

123123124

  • 빈 값일 때 API 요청이 날아가지 않음
  • 기존의 약 800ms가 소요되던 것을 480ms로 축소 시킬 수 있었음.
  • 이를 통한 더 빠른 초기 렌더링을 통해 사용성을 개선할 수 있었음.

후기

항상 구현을 하는 단계에서 프로젝트가 끝나면 성능에 대해서 신경을 쓰지 못하는 경우가 많았습니다.

그리고 평소에 ms의 단위로 인한 지연은 크게 고려하지 않았고, 항상 확장 가능성에 대해서 고려를 안했던 것 같습니다.

그러나 이번 기회를 통해 유저가 나 혼자일 때의 환경과, 5000명, 그리고 그 이상을 불러올 때 얼마나 큰 변화를 가져오는지 알게 되었습니다.

이번 과정을 통해 성능 개선에 대해서 큰 관심이 생겼고, 추후 개발을 진행할 때 성능 개선을 하는 것에 흥미를 가지고 임할 수 있을 것 같습니다.

이번 프로젝트 뿐만 아니라 개선하는 과정에서 정말 많은 질문들을 했고, 어쩌면 당연하다고 생각되는 질문들도 확실 하기 위해 한번 더 할만큼

귀찮게 한거 같은데 항상 즐겁게 대답해주셨던 팀원들 모두 감사합니다 :)

+ Recent posts