개발자 박가나
내일배움캠프 62일차 ('냠냠로그' 코드리뷰) 본문
'냠냠로그' 프로젝트를 진행하는 과정에서, 튜터님께서 코드 리뷰를 해주신 내용을 정리하고자 한다.
페이지네이션과 관련된 타입을 제네릭 타입으로 선언해서 사용하기
useInfiniteQuery를 이용해서 무한 스크롤을 구현하였다.
ResultType을 선언해서 사용해 주었는데, 이렇게 하면 data가 FoodType[ ]으로 고정되기 때문에 다른 타입의 데이터에 대해서 페이지네이션을 적용해주려고 할 때마다 매번 ResultType을 선언해주어야 하기 때문에 비효율적이고 중복 코드가 늘어날 수 있다고 말씀해 주셨다.
interface ResultType {
data: FoodType[];
nextPage: number;
hasMore: boolean;
}
const { data, fetchNextPage, hasNextPage, isPending, isError } = useInfiniteQuery({
queryKey: ['search', keyword],
queryFn: async ({ pageParam = 1 }: PageProps) => {
const res = await fetch(`/api/search?page=${pageParam}&keyword=${encodeURIComponent(keyword)}`);
const data: ResultType = await res.json();
return data;
},
getNextPageParam: (lastPage) => (lastPage.hasMore ? lastPage.nextPage : undefined),
enabled: !!keyword
});
ResultType을 제네릭으로 선언해줌으로써 코드 재사용성을 높일 수 있다.
export interface PaginationType<T> {
data: T[];
nextPage: number;
hasMore: boolean;
}
const { data, fetchNextPage, hasNextPage, isPending, isError } = useInfiniteQuery({
queryKey: ['search', keyword],
queryFn: async ({ pageParam = 1 }: PageProps) => {
const res = await fetch(`/api/search?page=${pageParam}&keyword=${encodeURIComponent(keyword)}`);
const data: PaginationType<FoodType> = await res.json();
return data;
},
getNextPageParam: (lastPage) => (lastPage.hasMore ? lastPage.nextPage : undefined),
enabled: !!keyword
});
queryFn의 내용이 여러 줄일 경우, 별도의 함수로 분리하기
TanStack Query를 이용해서 검색 결과 데이터를 관리하였다.
queryFn에 바로 작성해 주었는데, 보통은 해당 로직을 별도의 함수로 선언해주는 경우가 많다고 말씀해 주셨다.
const { data, fetchNextPage, hasNextPage, isPending, isError } = useInfiniteQuery({
queryKey: ['search', keyword],
queryFn: async ({ pageParam = 1 }: PageProps) => {
const res = await fetch(`/api/search?page=${pageParam}&keyword=${encodeURIComponent(keyword)}`);
const data: PaginationType<FoodType> = await res.json();
return data;
},
getNextPageParam: (lastPage) => (lastPage.hasMore ? lastPage.nextPage : undefined),
enabled: !!keyword
});
별도의 함수로 선언해줌으로써 가독성을 높일 수 있다.
const fetchData = async ({ pageParam = 1 }: PageProps) => {
const res = await fetch(`/api/search?page=${pageParam}&keyword=${encodeURIComponent(keyword)}`);
const data: PaginationType<FoodType> = await res.json();
return data;
};
const { data, fetchNextPage, hasNextPage, isPending, isError } = useInfiniteQuery({
queryKey: ['search', keyword],
queryFn: fetchData,
getNextPageParam: (lastPage) => (lastPage.hasMore ? lastPage.nextPage : undefined),
enabled: !!keyword
});
여러 개의 조건부 렌더링을 사용하는 대신 early return문 사용하기
조건부 렌더링을 구현하였다.
하나의 컴포넌트에 조건부 렌더링이 많아지면 가독성이 떨어지기 때문에 각각의 경우를 별도의 컴포넌트로 분리하거나 early return문을 사용해주는 것이 좋다고 말씀해 주셨다.
const FoodList = ({ keyword }: FoodListProps) => {
const { data, fetchNextPage, hasNextPage, isPending, isError } = useSearch({ keyword });
return (
<div>
{/* 검색어를 입력했고 데이터도 존재하는 경우 */}
{data &&
data.pages.map(
(page) => page.data && page.data.map((food: FoodType) => <FoodItem key={food.FOOD_CD} data={food} />)
)}
{/* 검색어를 입력했지만 데이터가 존재하지 않는 경우 */}
{data &&
data.pages.map(
(page, index) =>
!page.data && (
<div key={index} className="flex flex-col items-center justify-center h-40">
<p>찾으시는 음식 데이터가 존재하지 않습니다.</p>
</div>
)
)}
{/* 검색어를 입력하지 않은 경우 */}
{!data && (
<div className="flex flex-col items-center justify-center h-40">
<p>다양한 음식들의 영양 성분을 확인해보세요!</p>
</div>
)}
</div>
);
};
export default FoodList;
early return문을 사용해줌으로써 가독성을 높일 수 있다.
const FoodList = ({ keyword }: FoodListProps) => {
const { data, fetchNextPage, hasNextPage, isPending, isError } = useSearch({ keyword });
/* 검색어를 입력하지 않은 경우 */
if (!data) {
return (
<div className="flex flex-col items-center justify-center h-40">
<p>다양한 음식들의 영양 성분을 확인해보세요!</p>
</div>
);
}
/* 검색어를 입력했지만 데이터가 존재하지 않는 경우 */
if (!data.pages[0].data) {
return (
<div className="flex flex-col items-center justify-center h-40">
<p>찾으시는 음식 데이터가 존재하지 않습니다.</p>
</div>
);
}
return (
<div>
{data.pages.map((page) => page.data.map((food: FoodType) => <FoodItem key={food.FOOD_CD} data={food} />))}
{hasNextPage && (
<div ref={observerRef} className="flex flex-col items-center justify-center h-40">
<div className="w-12 h-12 border-4 border-white border-t-primary rounded-full animate-spin"></div>
</div>
)}
</div>
);
};
export default FoodList;
데이터 포맷 시, 조건문을 반복하는 대신 객체 데이터 사용하기
API에서 받아온 데이터를 원하는 형태로 포맷하였다.
switch문을 사용해 주었는데, 조건문이기 때문에 항목이 많아지게 되면 리소스를 많이 잡아먹을 것이라고 말씀해 주셨다.
export const formatNutrients = (nutrient: string) => {
switch (nutrient) {
case 'AMT_NUM3':
return { name: '단백질', unit: 'g' };
case 'AMT_NUM4':
return { name: '지방', unit: 'g' };
case 'AMT_NUM7':
return { name: '탄수화물', unit: 'g' };
case 'AMT_NUM8':
return { name: '당류', unit: 'g' };
case 'AMT_NUM14':
return { name: '나트륨', unit: 'mg' };
default:
return { name: '', unit: '' };
}
};
조건문 대신 객체 데이터를 사용해 줌으로써, 효율성을 높일 수 있다.
export const FORMATTED_NUTRIENTS = {
AMT_NUM3: { name: '단백질', unit: 'g' },
AMT_NUM4: { name: '지방', unit: 'g' },
AMT_NUM7: { name: '탄수화물', unit: 'g' },
AMT_NUM8: { name: '당류', unit: 'g' },
AMT_NUM14: { name: '나트륨', unit: 'mg' }
};