20241120(수) ▶️ 굿즈샵 select 클라이언트에서 sort 해결하기+메모이제이션
sort 정렬을 다 해놓고 보니, `가격 낮은 순` 정렬과 `가격 높은 순` 정렬이 할인 안한 가격으로 나오고 있었다.
가격과 할인 값을 받아서 계산한 것은 클라이언트에서 처리해주고 있기 때문에
할인을 받은 최종 결과도 클라이언트에서 처리해줘야 했던 것이었다.
열심히 구현해봤는데 다시 처음부터 시작하게 되었다 🥲
할인가격 계산 포스팅 : https://seokachu.tistory.com/281
정렬 포스팅 : https://seokachu.tistory.com/284
기존 코드
//데이터 불러와서 정렬하기
export const getGoodsShop = async (sortBy: SortOptionList) => {
try {
const sortOption = SORT_OPTIONS[sortBy];
if (!sortOption) {
throw new Error(`sortBy error : ${sortBy}`);
}
const { data, error } = await supabase
.from("goods")
.select("*")
.order(sortOption.column, { ascending: sortOption.ascending })
.limit(20);
if (error) throw error;
return data;
} catch (error) {
if (error instanceof Error) {
throw new Error(`상품 정보를 불러오는 데 실패했습니다. ${error.message}`);
}
throw error;
}
};
기존 supabase에서 데이터 불러오던 것에 대해 sort 처리를 진행하였는데, 다시 리셋하였다.🥲
다시 리셋
//상품 목록
export const getGoodsShop = async () => {
try {
const { data, error } = await supabase.from("goods").select("*").limit(20);
if (error) throw error;
return data;
} catch (error) {
if (error instanceof Error) {
throw new Error(`상품 정보를 불러오는 데 실패했습니다. ${error.message}`);
}
throw error;
}
};
sort 매개변수를 지우고, 일단 전체 데이터를 불러온다.
데이터 가져와서 sorting
//sorting.ts
import { Tables } from "@/types/supabase";
import { calculateDiscount } from "./calculateDiscount";
import { SortOptionList } from "@/types";
export const sortItems = (items: Tables<"goods">[], sortBy: SortOptionList) => {
const itemsCopy = [...items];
const getSafeDate = (date: string | null): number => {
if (!date) return 0;
const parsedDate = new Date(date);
return isNaN(parsedDate.getTime()) ? 0 : parsedDate.getTime();
};
switch (sortBy) {
case "price_low_to_high":
case "price_high_to_low": {
return itemsCopy.sort((a, b) => {
const priceA = calculateDiscount(a.price, a.discount_rate);
const priceB = calculateDiscount(b.price, b.discount_rate);
return sortBy === "price_low_to_high"
? priceA - priceB
: priceB - priceA;
});
}
case "latest":
return itemsCopy.sort((a, b) => {
return getSafeDate(b.created_at) - getSafeDate(a.created_at);
});
case "best":
return itemsCopy.sort((a, b) => {
return (b.review_count ?? 0) - (a.review_count ?? 0);
});
default:
return itemsCopy;
}
};
위 코드 상세분석
먼저 상품과 정렬 옵션을 받아와서 계산한다.
const itemsCopy = [...items];
원본 배열을 변경하지 않기 위해서 스프레드 연산자로 복사본을 만든다.
const getSafeDate = (date: string | null): number => {
if (!date) return 0;
const parsedDate = new Date(date);
return isNaN(parsedDate.getTime()) ? 0 : parsedDate.getTime();
};
입력값 date는 string이나 null이 될 수 있고 number타입으로 반환한다.
만약에 date가 없으면 0을 반환한다.
문자열로 된 날짜를 new Date 객체로 반환해서, parsedDate 변수에 담는다.
ex) 2024.11.22 → Date객체로 반환한다.
return isNaN(parsedDate.getTime()) ? 0 : parsedDate.getTime();
`parsedDate.getTime()`은 1970년 1월 1일부터 해당 날짜까지의 밀리초(milliseconds)를 반환한다.
ex) "2024-03-21"의 경우 → 1711065600000
`sNaN(parsedDate.getTime())`은 날짜가 유효한지 검사한다.
유효하지 않은 날짜(예: "invalid-date")가 들어오면 true
삼항 연산자 사용해서 유효하지 않은 날짜면 0을 반환, 유효한 날짜면 밀리초 값을 반환
switch (sortBy) {
case "price_low_to_high":
case "price_high_to_low": {
return itemsCopy.sort((a, b) => {
const priceA = calculateDiscount(a.price, a.discount_rate);
const priceB = calculateDiscount(b.price, b.discount_rate);
return sortBy === "price_low_to_high"
? priceA - priceB
: priceB - priceA;
});
}
case "latest":
return itemsCopy.sort((a, b) => {
return getSafeDate(b.created_at) - getSafeDate(a.created_at);
});
case "best":
return itemsCopy.sort((a, b) => {
return (b.review_count ?? 0) - (a.review_count ?? 0);
});
default:
return itemsCopy;
}
};
swich문을 사용해서 sortBy의 케이스에 따라 정렬한다.
기본값은(default) 원본의 순서를 유지한다.
case "price_low_to_high":
case "price_high_to_low": {
return itemsCopy.sort((a, b) => {
const priceA = calculateDiscount(a.price, a.discount_rate);
const priceB = calculateDiscount(b.price, b.discount_rate);
return sortBy === "price_low_to_high"
? priceA - priceB
: priceB - priceA;
});
}
할인이 적용된 실제가격으로 정렬하고, 오름차순 내림차순으로 정리한다.
case "latest":
return itemsCopy.sort((a, b) => {
return getSafeDate(b.created_at) - getSafeDate(a.created_at);
});
생성한 날짜 기준으로 최신으로 정렬한다.
case "best":
return itemsCopy.sort((a, b) => {
return (b.review_count ?? 0) - (a.review_count ?? 0);
});
리뷰 개수가 많은 순으로 정렬한다.
`??`연산자로 리뷰 개수가 없으면 0으로 처리한다.
적용하기
//ShopList.tsx
const ShopList = ({ sort }: SortProps) => {
const [shopItems, setShopItems] = useState<Tables<"goods">[]>([]);
const { data, error, isLoading } = useShops(sort);
useEffect(() => {
if (!data) return;
const sortedItems = sortItems(data, sort);
setShopItems(sortedItems);
}, [data,sort]);
return (
<ul className="flex flex-wrap gap-6 sm:justify-center md:justify-start">
{shopItems.map((el) => (
<ShopListItems key={el.title} item={el} />
))}
</ul>
);
};
export default ShopList;
기존 코드에서 useEffect에 데이터를 넣어준다.
const sortedItems = sortItems(data, sort);
sortItems함수에 매개변수로 data와 sort를 넘겨주고, sortedItems 변수에 담는다.
성능 최적화 과정
컴포넌트가 리렌더링 될때마다(부모나 다른 상태변경 시…) 정렬이 다시 실행되는 문제가 있었다.
이렇게 구현하면 나중에 리스트들이 100개 200개가 되었을 때를 가정해보면 성능이 저하 될 것 같아 이 부분을 해결하기 위해서 메모이제이션을 사용했다.
메모이제이션으로 백엔드에서 다시 요청해서 가져오는게 아니고 클라이언트에서 정렬만 진행해서, data나 sort가 변경될 때만 정렬할 수 있도록 리펙터링을 진행해 보았다.
//ShopList.tsx
const ShopList = ({ sort }: SortProps) => {
const { data, error, isLoading } = useShops(sort);
//추가
const sortedItems = useMemo(() => {
return sortItems(data || [], sort);
}, [data, sort]);
return (
<ul className="flex flex-wrap gap-6 sm:justify-center md:justify-start">
{sortedItems.map((el) => (
<ShopListItems key={el.title} item={el} />
))}
</ul>
);
};
export default ShopList;
state와 useEffect를 제거하고, sortedItems를 렌더링에 사용했더니 코드가 더 단순해지면서 깔끔해진 기분이었다. 😊