개발냥발/TS (Trouble Shooting)⚡

비제어 컴포넌트에서 검색어 상태 관리하기

승이버섯 2024. 6. 18. 10:47

 

[ 문제 발생 배경 ]


프로젝트에서 글로벌 내비게이션 바(GnB)의 검색 기능을 구현하던 중, 페이지를 이동해도 검색바에 입력된 검색어가 유지되는 문제가 발생했다. 이는 사용자 경험 측면에서 바람직하지 않았기 때문에, 페이지 이동 시 검색어를 초기화하여 사용자에게 일관된 검색 환경을 제공하고자 했다.

GnB에서의 검색바 컴포넌트에서는 react-hook-form을 사용하여 폼 상태를 관리하고 있었다. 때문에 GnB의 검색바인 GnbSearch 컴포넌트는 비제어 컴포넌트로 구현되어 있었고, 그래서 페이지 이동 후에도 입력된 검색어가 유지되는 문제가 발생했다.

 


 

[ 해결 과정 ]

 

문제를 해결하기 위해 멘토님께 조언을 구한 결과, 상태 관리 로직을 개선하기로 결정했다. 우선 검색어 상태를 zustand로 관리하여 검색어의 상태를 다른 컴포넌트에서도 접근 가능하게 하는 방법을 선택했다. 이를 통해 검색바 컴포넌트를 react-hook-form과 함께 제어 컴포넌트로 동작하게 할 수 있었고, 검색어를 초기화하는 로직을 구현할 수 있었다.

해결 과정을 3단계로 나누어보면 다음과 같다.

 

1. 문제 확인 및 원인 분석

   - react-hook-form을 사용한 GnbSearch (검색바) 컴포넌트가 비제어 컴포넌트로 구현되어 있었고, 때문에 페이지 이동 시 검색어가 유지되는 문제가 발생했다.

   - 페이지 이동 시 검색어를 초기화할 필요가 있었다.

 

2. 상태 관리 라이브러리 도입

   - zustand를 사용하여 검색어 상태를 중앙에서 관리하기로 했다.

   - zustand를 통해 검색어 상태를 설정(set)하고 초기화(clear)할 수 있도록 했다.

// store/useSearchStore.ts
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';

interface SearchState {
  searchTerm: string;
  setSearchTerm: (term: string) => void;
  clearSearchTerm: () => void;
}

export const useSearchStore = create<SearchState>()(
  devtools(
    (set) => ({
      searchTerm: '',
      setSearchTerm: (term) => set(() => ({ searchTerm: term })),
      clearSearchTerm: () => set(() => ({ searchTerm: '' })),
    }),
    { name: 'SearchStore' },
  ),
);

 

3. 상태 초기화 로직 추가하여 해결

   - zustand 스토어를 설정하고, GnbSearch 컴포넌트에서 react-hook-form의 setValue를 통해 검색어 상태를 제어하도록 구현했다.

   - useEffect를 사용하여 페이지를 이동하여 URL이 변경될 때 검색어 상태를 초기화하는 로직을 추가했다.

      - URL이 변경되는 것은 Next.js의 usePathname 훅을 사용하여 경로 변경을 감지했다.

'use client';

import { useRef, useState, useEffect } from 'react';
import { useForm, SubmitHandler } from 'react-hook-form';
import { useRouter, useParams, usePathname } from 'next/navigation';
import { useSearchStore } from '@/stores/useSearchStore';

const GnbSearchBar = ({ isOpenMobileSearchBar, handleToggledSearchBar }) => {
  const { register, handleSubmit, setValue } = useForm();
  const router = useRouter();
  const params = useParams<{ category: string }>();
  const category = params?.category;
  const pathname = usePathname();
  const searchTerm = useSearchStore((state) => state.searchTerm);
  const setSearchTerm = useSearchStore((state) => state.setSearchTerm);
  const clearSearchTerm = useSearchStore((state) => state.clearSearchTerm);

  const onSubmit: SubmitHandler<{ search: string }> = (data) => {
    const { search } = data;
    setSearchTerm(search);
    if (!category) {
      router.replace(`/no_category?keyword=${search}`, { scroll: false });
      return;
    }
    router.replace(`/${category}?keyword=${search}`);
  };

  const searchBarRef = useRef<HTMLDivElement>(null);

  // 페이지 이동 시 검색어 초기화
  useEffect(() => {
    clearSearchTerm();
    setValue('search', '');
  }, [pathname, clearSearchTerm, setValue]);

  // 검색어 입력 시 검색어 상태 업데이트
  useEffect(() => {
    setValue('search', searchTerm);
  }, [searchTerm, setValue]);

  return (
    <div
      ref={searchBarRef}
      className={`${
        isOpenMobileSearchBar
          ? 'block mobile:absolute mobile:z-10 mobile:right-[20px] md:ml-auto lg:ml-auto'
          : 'mobile:hidden md:block md:ml-auto lg:ml-auto lg:block'
      }`}
    >
      <form
        onSubmit={handleSubmit(onSubmit)}
        className="mobile:w-[291px] md:w-[300px] lg:w-[320px] mobile:h-[48px] md:h-[50px] lg:h-[50px] flex items-center justify-start rounded-[28px] bg-black-25"
      >
        <label
          htmlFor="search"
          className="mobile:pl-[15px] mobile:py-4 md:pl-5 md:py-4 lg:pl-5 lg:py-4"
        >
          <Icon name="SearchIcon" className="w-[24px] h-[24px] fill-gray-9F" />
        </label>
        <input
          className="mobile:px-[15px] mobile:py-4 md:pl-[10px] md:pr-[20px] md:py-4 lg:pl-[10px] lg:pr-[20px] lg:py-4 placeholder:text-gray-6E placeholder:text-[14px] placeholder:not-italic placeholder:font-normal placeholder:leading-normal text-gray-F1 text-[14px] not-italic font-normal leading-normal w-full h-full bg-black-25 rounded-r-[28px] outline-none"
          maxLength={30}
          autoComplete="off"
          id="search"
          placeholder="상품 이름을 검색해 보세요"
          type="text"
          {...register('search', {
            required: true,
            maxLength: 30,
          })}
        />
      </form>
    </div>
  );
};

 

[ 결론 ]

 

이번 경험을 통해 react-hook-form을 사용하는, 비제어 컴포넌트인 GnbSearch 컴포넌트를 제어 컴포넌트로 변경하고, 상태 관리를 zustand를 통해 중앙에서 관리함으로써 페이지 이동 시 검색어가 유지되는 문제를 해결했다.

이를 통해 일관된 검색 경험의 중요성과, 상태 관리와 라우팅 이벤트를 적절하게 처리하는 방법을 배울 수 있었다.