일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- TMDB
- useEffect
- bootstrap
- Github Pages
- Cloud
- github
- firestoredatabase
- web
- REACT
- supabase
- IntersectionObserver
- db
- data
- nosql
- 배포
- W
- HTML
- SQL
- API
- JavaScript
- Boostrap
- Fetch
- CSS
- Protocol
- Database
- this
- http
- jQuery
- til
- url
- Today
- Total
072DATA
친환경 스토어 지도 개발 로그 3편 ( 리스트 렌더링,클릭 이벤트, 디바운싱 기법을 사용한 검색, 로딩처리 ) 본문
친환경 스토어 지도 개발 로그 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
2편
https://0723-0725.tistory.com/108
'FrontEnd > Next.js' 카테고리의 다른 글
친환경 스토어 지도 개발 로그 2편 ( 카카오맵 불러오기, 마커와 인포 윈도우 생성 ) (0) | 2024.11.12 |
---|---|
친환경 스토어 지도 개발 로그 1편 ( 데이터 분석 및 삽입 ) (1) | 2024.11.09 |
react-hook-form으로 폼 유효성 검사 구현 (0) | 2024.11.04 |
다중으로 이미지 업로드 구현하기 ( supabase Storage ) (0) | 2024.10.29 |
OOTW 프로젝트 - 글 작성 구현하기 ( 이미지 업로드 ) (2) | 2024.10.18 |