
지난 프로젝트 때 가장 아쉬웠던 점은 코드 정리가 미흡했던 것이다. 따라서 이번 프로젝트에서는 이를 개선하기 위해 코드가 복잡해질 때마다 정리하며 개발을 진행하고 있다.
아래에 있는 코드는 내가 맡은 검색 페이지의 코드이다. 컴포넌트가 일부(SearchButtons, SearchList) 분리되어 있으나 여전히 UI를 그리는 부분과 비즈니스 로직이 혼재되어 있다. 이에 따라 코드가 길어져 가독성도 떨어지고 재사용성 또한 저하된 것을 알 수 있다.
// Search.jsx
import { useQuery } from '@tanstack/react-query';
import SearchMap from '../../features/SearchMap';
import supabase from '../../../supabase/supabase';
import { useState } from 'react';
import SearchList from './SearchList';
import SearchButtons from './SearchButtons';
const Search = () => {
const [colorFilter, setColorFilter] = useState('all');
const [searchText, setSearchText] = useState('');
const fetchRestaurantData = async () => {
const { data } = await supabase.from('restaurants').select('*');
return data;
};
const {
data: restaurants,
isPending,
error
} = useQuery({
queryKey: ['restaurants'],
queryFn: fetchRestaurantData
});
if (isPending) return <div>로딩 중...</div>;
if (error) {
console.error(error);
return <div>에러가 발생했습니다: {error.message}</div>;
}
// 버튼 클릭에 따라 컬러 필터 상태(카테고리별 매장 필터링) + 선택된 버튼 상태 변경(선택 버튼 이펙트)
const handleColorType = (e) => {
const selectedValue = e.target.value;
setColorFilter(selectedValue);
};
// 상태에 따라 다르게 필터를 돌려 레스토랑 데이터 뽑기
const filterdRestaurants = () => {
let filtered = restaurants;
// 검색 필터 적용
if (searchText.trim() !== '') {
filtered = filtered.filter((restaurant) => restaurant.name.includes(searchText));
}
// 색상 필터 적용
switch (colorFilter) {
case 'black':
return filtered.filter((restaurant) => restaurant.color === 'black');
case 'white':
return filtered.filter((restaurant) => restaurant.color === 'white');
case 'all':
default:
return filtered;
}
};
const handleSearch = (e) => {
e.preventDefault();
setSearchText(e.target.value);
};
return (
<div className="bg-[#0E0E0E] h-screen">
<div className="max-w-[1440px] w-full h-full p-[40px] m-auto flex flex-row gap-[40px]">
<div className="max-w-[520px] bg-[#F9F9F9] border rounded-[24px] h-full p-[20px] flex flex-col">
<SearchButtons handleColorType={handleColorType} colorFilter={colorFilter} />
<div className="w-[480px] h-[40px] p-3 border border-[3px] border-[#3396FF] rounded-[10px] bg-white mt-3 drop-shadow-md flex items-center justify-around">
<input type="text" value={searchText} onChange={handleSearch} className="w-10/12 outline-none" />
<button className="bg-search-icon w-[24px] h-[24px]"></button>
</div>
<ul className="mt-[20px] flex flex-col gap-[20px] overflow-auto">
{filterdRestaurants().map((restaurant) => {
return <SearchList key={restaurant.id} restaurant={restaurant} />;
})}
</ul>
</div>
<div className="max-w-[794px] w-[794px] bg-[#F9F9F9] h-full rounded-[24px]">
<SearchMap />
</div>
</div>
</div>
);
};
export default Search;
UI와 비즈니스 로직을 분리하여 코드의 응집도를 높이고 관심사를 명확히 나누는 것이 목표였다. 이를 위해 현재 비즈니스 로직을 다음과 같은 두 가지 관심사로 구분했다.
Supabase와 Tanstack Query 사용 부분)따라서 총 두 개의 커스텀 훅으로 분리하면 좋을 것이라 생각했다.
// useRestaurants.js
import { useQuery } from '@tanstack/react-query';
import supabase from '../supabase/supabase';
const useRestaurants = () => {
const fetchRestaurantData = async () => {
const { data } = await supabase.from('restaurants').select('*');
return data;
};
const {
data: restaurants,
isPending,
error
} = useQuery({
queryKey: ['restaurants'],
queryFn: fetchRestaurantData
});
return { restaurants, isPending, error };
};
export default useRestaurants;
Supabase에서 데이터를 가져오는fetchRestaurantData 함수와 Tanstack Query로 요청을 관리하는 부분을 하나의 커스텀 훅으로 분리했다.
// useRestaurantFilters.js
import { useState } from 'react';
const useRestaurantFilters = (restaurants) => {
const [colorFilter, setColorFilter] = useState('all');
const [searchText, setSearchText] = useState('');
// 버튼 클릭에 따라 컬러 필터 상태(카테고리별 매장 필터링) + 선택된 버튼 상태 변경(선택 버튼 이펙트)
const handleColorType = (e) => setColorFilter(e.target.value);
const handleSearch = (e) => setSearchText(e.target.value);
// 상태에 따라 다르게 필터를 돌려 레스토랑 데이터 뽑기
const filterdRestaurants = () => {
let filtered = restaurants;
// 검색 필터 적용
if (searchText.trim() !== '') {
filtered = filtered.filter((restaurant) => restaurant.name.includes(searchText));
}
// 색상 필터 적용
switch (colorFilter) {
case 'black':
return filtered.filter((restaurant) => restaurant.color === 'black');
case 'white':
return filtered.filter((restaurant) => restaurant.color === 'white');
case 'all':
default:
return filtered;
}
};
return { handleColorType, colorFilter, searchText, handleSearch, filterdRestaurants };
};
export default useRestaurantFilters;
검색어 필터와 색상 필터 상태(colorFilter, searchText)를 관리하고 이를 기반으로 데이터를 필터링하는 로직들을 분리했다. 필터링 로직이 길어 가독성 측면에서 걱정되는 부분이 좀 있었는데, 분리해두니 로직이 눈에 더 잘 들어오는 것 같아서 기분이 좋았다(?)
// Search.jsx
import SearchMap from '../../features/SearchMap';
import SearchList from './SearchList';
import SearchButtons from './SearchButtons';
import useRestaurants from '../../../hooks/useRestaurants';
import useRestaurantFilters from '../../../hooks/useRestaurantFilters';
const Search = () => {
const { restaurants, isPending, error } = useRestaurants();
const { handleColorType, colorFilter, searchText, handleSearch, filterdRestaurants } =
useRestaurantFilters(restaurants);
if (isPending) return <div>로딩 중...</div>;
if (error) {
console.error(error);
return <div>에러가 발생했습니다: {error.message}</div>;
}
return (
<div className="bg-[#0E0E0E] h-screen">
<div className="max-w-[1440px] w-full h-full p-[40px] m-auto flex flex-row gap-[40px]">
<div className="max-w-[520px] bg-[#F9F9F9] border rounded-[24px] h-full p-[20px] flex flex-col">
<SearchButtons handleColorType={handleColorType} colorFilter={colorFilter} />
<div className="w-[480px] h-[40px] p-3 border border-[3px] border-[#3396FF] rounded-[10px] bg-white mt-3 drop-shadow-md flex items-center justify-around">
<input type="text" value={searchText} onChange={handleSearch} className="w-10/12 outline-none" />
<button className="bg-search-icon w-[24px] h-[24px]"></button>
</div>
<ul className="mt-[20px] flex flex-col gap-[20px] overflow-auto">
{filterdRestaurants().map((restaurant) => {
return <SearchList key={restaurant.id} restaurant={restaurant} />;
})}
</ul>
</div>
<div className="max-w-[794px] w-[794px] bg-[#F9F9F9] h-full rounded-[24px]">
<SearchMap />
</div>
</div>
</div>
);
};
export default Search;
비즈니스 로직들을 분리시킨 Search.jsx 코드이다!!! 우선 컴포넌트가 매우 간결해졌고, 분리된 로직을 다른 컴포넌트에서도 사용할 수 있게 되었다!
분리해보니 금방 하는데 그동안 왜 미뤘는지 잘 모르겠다. 앞으로는 항상 구조와 가독성을 생각하며 더 나은 코드를 위해 노력하는 습관을 기르겠다고 다짐한 하루!