072DATA

친환경 스토어 지도 개발 로그 3편 ( 리스트 렌더링,클릭 이벤트, 디바운싱 기법을 사용한 검색, 로딩처리 ) 본문

FrontEnd/Next.js

친환경 스토어 지도 개발 로그 3편 ( 리스트 렌더링,클릭 이벤트, 디바운싱 기법을 사용한 검색, 로딩처리 )

0720 2024. 11. 12. 23:11

 

 

이전에는 데이터 분석 및 카카오맵을 불러오고

데이터의 위치를 기반으로 마커와 인포윈도우를 생성했습니다..

 

이번에는 데이터를 리스트 형식으로 렌더링하고 데이터를 검색할 때 디바운싱 기법을 적용하며

카드를 클릭하거나 마커를 클릭했을 때 지도를 이동시키는 이벤트와 지도를 불러올 때 로딩처리까지 구현해볼 계획입니다

 

StoreList 컴포넌트

 

1. StoreList 컴포넌트 Props


const StoreList = ({ stores, onClick, selectedStoreId }: StoreListProps) => {

 

 

StoreList에 전달되는 props 들은 상위 페이지에서 제공되는 상태와 클릭 함수입니다

 

상위 페이지 구조

const Page = () => {
  const [selectedStoreId, setSelectedStoreId] = useState<string | null>(null);
  
  const { data: storeList, isLoading, error } = useStoreList();

  const handleStoreClick = (store: Store) => {
    setSelectedStoreId(store.store_id);
  };
  
  return (
    <div className="flex flex-row w-[1200px] mx-auto h-screen border border-gray-500">
      <div className="w-1/3 border-r">
        <StoreList
          stores={storeList || []}
          onClick={handleStoreClick}
          selectedStoreId={selectedStoreId}
        />
      </div>
      <KakaoMap
        storeList={storeList || []}
        selectedStoreId={selectedStoreId}
        onClick={handleStoreClick}
      />
    </div>
  );
};

 

 

이렇게 상위 페이지에서 선택된 스토어의 아이디를 관리하고

setState하는 함수를 props로 전달하여 각 컴포넌트에서 

현재 선택된 스토어의 상태를 공유할 수 있습니다

 

 

2. StoreCard 컴포넌트

 

스토어 정보를 표현하는 카드 컴포넌트를 구현했습니다.

각 카드는 가게 이름, 주소, 운영시간을 표시하며, 선택된 상태에 따라 다른 스타일을 적용합니다.

const StoreCard = ({ store, selectedStoreId, onClick }: Props) => {
  return (
    <div
      onClick={() => onClick(store)}
      className={`p-4 border rounded-lg cursor-pointer transition-colors ${
        selectedStoreId === store.store_id
          ? "bg-green-50 border-green-500"
          : "hover:bg-gray-50"
      }`}
    >
      <h3 className="font-bold text-gray-800 mb-[10px]">{store.store_name}</h3>
      <p className="text-sm text-gray-600">{store.road_address}</p>
      <p className="text-sm text-gray-500 truncate">{store.operating_hours}</p>
    </div>
  );
};

 

선택된 스토어에 따라 스타일을 다르게 적용하고 스토어의 시간 정보의

길이가 넘치는 현상 때문에 truncate 속성을 넣어서 넘치는 글자는 "..." 표시를 해두었습니다

 

 

3. 검색 기능 - 디바운싱 기법

 

물론 검색할 때마다 API 요청을 하는게 아니기 때문에

성능에 대한 최적화는 크게 이루어지지 않을 수 있지만,

입력시마다 변화되는 부분을 줄여 UX를 향상시키고

보다 효율적으로 데이터를 관리할 수 있었습니다

 


1.먼저 검색어를 읽어야 하는데  입력값이 변경될 때마다 상태를 변경시켜주는 로직을 구현합니다

 const [searchTerm, setSearchTerm] = useState("");

 const handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => {
	setSearchTerm(e.target.value);
 };
  
 <input
    type="text"
    placeholder="가게 이름 또는 주소 검색..."
    value={searchTerm}
    onChange={handleSearchChange}
    className="w-full p-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500"
 />

 

2. 사용자가 입력을 시작하면 searchTerm의 값이 변경되고 useEffect를 실행합니다

-> 타이머 함수를 사용하여 debouncedSearchTerm을 변경시키는데

 

만약 300ms가 지나기전에 입력값이 새로 들어오게 된다면 클린업 함수가 실행되면서

이전 타이머를 취소 시킨 뒤 새로운 타이머가 설정됩니다

 

이렇게 사용자의 검색이 완전히 종료되었을 때 

debouncedSearchTerm의 상태가 변경됩니다

  const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedSearchTerm(searchTerm);
    }, 300);

    return () => {
      clearTimeout(timer);
    };
  }, [searchTerm]);

 

 

3. 검색된 데이터 필터처리 및 리스트 렌더링

 

그 뒤 디바운싱 기법이 적용된 검색 값을 토대로 데이터를 필터처리하고

필터처리된 데이터를 map 메소드를 사용하여 이전에 만들어둔

StoreCard 컴포넌트 형태로  리스트 렌더링을 적용합니다

  const filteredStores = stores.filter(
    (store) =>
      store.store_name
        .toLowerCase()
        .includes(debouncedSearchTerm.toLowerCase()) ||
      store.road_address
        .toLowerCase()
        .includes(debouncedSearchTerm.toLowerCase())
  );
  
 <div className="flex-1 overflow-y-auto p-2 space-y-2">
    {filteredStores.map((store) => (
      <StoreCard
        key={store.store_id}
        store={store}
        selectedStoreId={selectedStoreId}
        onClick={onClick}
      />
    ))}
 </div>

 

StoreList 결과물

 

 

 

 

지도 이동 기능

 

지도와 리스트의 상호작용을 위한 이벤트 처리 로직도 구현했습니다

useRef를 사용하여 지도 인스턴스를 참조하고,

useEffect를 통해 선택된 스토어가 변경될 때마다 지도 중심을 이동시킵니다.

 

(KaKaoMap 컴포넌트)

const mapRef = useRef<kakao.maps.Map>(null);

useEffect(() => {
  if (!mapRef.current || !selectedStoreId || !storeList) return;

  const selectedStore = storeList.find(
    (store) => store.store_id === selectedStoreId
  );

  if (!selectedStore) return;

  const moveLatLng = new kakao.maps.LatLng(
    selectedStore.lat,
    selectedStore.lon
  );
  mapRef.current.setCenter(moveLatLng);
  setLevel(3);
}, [selectedStoreId, storeList]);

 

StoreList 페이지와 마찬가지로

selectedStoreId와 클릭 함수를 props로 전달 받습니다

( 클릭 함수는 지도에서도 마커를 클릭시 선택된 스토어의 아이디를 변경시켜야 하기 때문에 받음)

 

useEffect에 구현된 로직 덕분에 선택된  스토어의 위치를 moveLatLng에 담아서

setCenter 메소드를 사용하여 실제 지도의 위치를 이동시키는 로직입니다.

 

이제 해당 로직을 사용하여 KakaoMap 컴포넌트에 있는 마커나 StoreList 컴포넌트에 있는 카드를 클릭시

선택된 스토어의 아이디 상태가 변경되면서 선택한 스토어의 위치를 기반으로 지도가 이동됩닌다

 

지도 이동 로직 결과물

 

 

로딩처리

 

마지막으로 Tanstack Query로 데이터를 요청할 때 useQuery에서 제공하는 isLoading 옵션을 사용하여

스켈레톤 UI를 적용시켜 UX를 고려한 페이지 로딩을 적용했습니다

  const { data: storeList, isLoading, error } = useStoreList();

  if (error) {
    return <div>데이터를 패치 오류</div>;
  }

  if (isLoading) {
    return (
      <div className="flex flex-row w-[1200px] mx-auto h-screen border border-gray-500">
        <div className="w-1/3 border-r">
          <StoreSkeleton />
        </div>
        <MapSkeleton />
      </div>
    );
  }

로딩 처리 결과물

 

마치며

지도에는 상당히 많은 기능들이 들어가고 지도를 컨트롤 하는게 상당히 까다로운 것 같습니다..

하지만 그만큼 하나의 기능이 완성될 때 마다 뿌듯하고

 

검색기능에 api 요청이 포함되었던 건 아니지만 디바운싱 기법이 적용되어

최적화 하는 과정을 학습하는데 큰 도움이 되었던 것 같습니다

 

다음편에서는 어떤 기능을 구현할지 아직 감이 안잡혀서 모르겠지만

사용자의 현 위치 기반으로 이동하거나 확대 축소 버튼을 추가하고

 

각 카드별로 북마크 기능 추가 그리고 리스트에서 탭 기능을 추가할듯 싶네요!!

그럼 다음 TIL에서 뵙겠습니다...

 

 

1편

https://0723-0725.tistory.com/107

 

친환경 스토어 지도 개발 로그 1편 ( 데이터 분석 및 삽입 )

시작하기 전.. 처음에는 프로젝트에서 서울맵의 지도 api를 활용하여 친환경 스토어를 보여주려고 했지만서울시 지도의 UI/UX가 너무 구리(너구리)길래 카카오맵에서 보여주면 좋겠다는 의견이

0723-0725.tistory.com


2편

https://0723-0725.tistory.com/108

 

친환경 스토어 지도 개발 로그 2편 ( 카카오맵 불러오기, 마커와 인포 윈도우 생성 )

지난 시간에는 서울 맵의 친환경 스토어 데이터를 분석하고supabase에 저장하는 과정을 기록했는데욥 이번 글에서는 실제로 카카오맵을 불러오는 과정을 기록하겠습니다. 먼저 카카오맵 API 문

0723-0725.tistory.com