useSearchParams
를 사용 (클라이언트 컴포넌트에서만 사용가능)클라이언트 컴포넌트에서 sizeId와 colorId로 쿼리스트링을 만들어서 router.push(만들어진 쿼리스트링)으로 router.push를 하게 되면
colorId
=35287ade-ba1f-4077-baf7-6e7f52e8ef49&sizeId
=82c7e603-9098-4e4b-9372-384e7f8e32ac최상단인 page에서 ServerComponent에서 params : categoryId와 searchParams: colorId, sizeId를 props로 받아서 getProducts를 실행시킨다.
<aside>
💡 내가 보니 필터가 포함된 클라이언트 컴포넌트에서 router.push(url)
을 하게 되면, 다시 해당 page를 부르게 되고 다시 page 서버컴포넌트가 그려지면서 호출하는 느낌이다.
</aside>
'use client';
import Button from '@/components/ui/Button';
import { cn } from '@/lib/utils';
import { Color, Size } from '@/types';
import { useRouter, useSearchParams } from 'next/navigation';
import qs from 'query-string';
interface FilterProps {
data: (Size | Color)[];
name: string;
valueKey: string;
}
export default function Filter({ data, name, valueKey }: FilterProps) {
const searchParams = useSearchParams();
const router = useRouter();
const selectedValue = searchParams.get(valueKey);
// 필터 누를때마다 작동
const onClick = (id: string) => {
const current = qs.parse(searchParams.toString());
const query = {
...current,
[valueKey]: id,
};
if (current[valueKey] === id) {
query[valueKey] = null;
}
const url = qs.stringifyUrl(
{
url: window.location.href,
query,
},
{ skipNull: true }
);
// 이 부분이 중요함!
// 1. 쿼리를 포함한 url로 push
// 2. 서버 컴포넌트에서 params와 searchParams를 받아서 fetch
router.push(url);
};
return (
<div className='mb-8'>
<h3 className='text-lg font-semibold'>{name}</h3>
<hr className='my-4' />
<div className='flex flex-wrap gap-2'>
{data.map((filter) => (
<div key={filter.id} className='flex items-center'>
<Button
className={cn(
'rounded-md text-sm text-gray-800 p-2 bg-white border border-gray-300',
selectedValue === filter.id && 'bg-black text-white'
)}
onClick={() => onClick(filter.id)}
>
{filter.name}
</Button>
</div>
))}
</div>
</div>
);
}
import getCategory from '@/actions/get-category';
import getColors from '@/actions/get-colors';
import getProducts from '@/actions/get-products';
import getSizes from '@/actions/get-sizes';
import Billboard from '@/components/billboard';
import Container from '@/components/ui/container';
import Filter from './components/Filter';
import NoResult from '@/components/ui/no-result';
import ProductCard from '@/components/ui/product-card';
import MobileFilters from './components/MobileFilter';
export const revalidate = 0;
interface CategoryPageProps {
params: {
categoryId: string;
};
searchParams: {
colorId: string;
sizeId: string;
};
}
export default async function CategoryPage({
params,
searchParams,
}: CategoryPageProps) {
const products = await getProducts({
categoryId: params.categoryId,
colorId: searchParams.colorId,
sizeId: searchParams.sizeId,
});
console.log('🥰 서버컴포넌트 새로 호출 🥰');
const sizes = await getSizes();
const colors = await getColors();
const category = await getCategory(params.categoryId);
return (
<div className='bg-white'>
<Container>
<Billboard data={category.billboard} />
<div className='px-4 sm:px-6 lg:px-8 pb-24'>
<div className='lg:grid lg:grid-cols-5 lg:gap-x-8'>
{/* Add Mobile Filters */}
<MobileFilters sizes={sizes} colors={colors} />
<div className='hidden lg:block'>
<Filter valueKey='sizeId' name='Sizes' data={sizes} />
<Filter valueKey='colorId' name='Colors' data={colors} />
</div>
<div className='mt-6 lg:col-span-4 lg:mt-0'>
{products.length === 0 && <NoResult />}
<div className='grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4'>
{products.map((item) => (
<ProductCard key={item.id} data={item} />
))}
</div>
</div>
</div>
</div>
</Container>
</div>
);
}