상품 리스트 컴포넌트는, 배포 중 오류가 생겨 주석처리한 코드들과 함께 리스트 목록도 표시가 안 되고 있는 상황입니다.
상품 리스트는 상품을 보여주는 컴포넌트인 List
그리고 페이지네이션 컴포넌트인 Pagenation
으로 이루어져있습니다. List
내부에는 찜하기 및 장바구니 담기 버튼이 컴포넌트로 import
되어있습니다.
'use client';
import React, { useLayoutEffect } from 'react';
import { useAppSelector } from '@/hooks/useAppSelector';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { AppDispatch } from '@/types/reduxTypes';
import List from './components/List';
import Pagenation from './components/Pagenation';
interface ProductProps {
params: {
slug: string;
};
}
const Product: React.FC<ProductProps> = ({ params }) => {
const dispatch: AppDispatch = useAppDispatch();
const prevCategory: string = useAppSelector(
(state) => state.product.currentCategory
);
const curCategory: string = params.slug[0];
const curPage: number = Number(params.slug[1]);
useLayoutEffect(() => {
// if (prevCategory !== curCategory) dispatch(setCategory(curCategory));
// if (curPage === undefined || curPage === null) dispatch(setCurrentPage(1));
//dispatch(setCurrentPage(curPage));
}, [curCategory, curPage]);
return (
<>
<div className="mt-14 flex flex-col justify-center w-full items-center">
<h2>{curCategory.charAt(0).toUpperCase() + curCategory.slice(1)}</h2>
</div>
<div className="flex flex-col mb-44">
<List />
</div>
<div className="mt-14">
<Pagenation />
</div>
</>
);
};
export default Product;
'use client';
import React from 'react';
import Image from 'next/image';
import { CartItems, Product, WishlistItems } from '@/types/globalTypes';
import { useAppSelector } from '@/hooks/useAppSelector';
import { ITEMSPERPAGE } from '@/constants/product';
import CartButton from './CartButton';
import WishlistButton from './WishlistButton';
import Link from 'next/link';
import { useFilteredProductList } from '@/hooks/useFilteredProductList';
const List: React.FC = () => {
const cartItems: CartItems = useAppSelector((state) => state.cart.cartItems);
const wishlist: WishlistItems = useAppSelector(
(state) => state.wishlist.wishlistItems
);
const currentPage: number = useAppSelector(
(state) => state.product.currentProductListPage
);
const curProductList: Product[] = useFilteredProductList();
const startIndex: number = ITEMSPERPAGE * (currentPage - 1);
const endIndex: number = startIndex + ITEMSPERPAGE;
const curProducts: Product[] = curProductList.slice(startIndex, endIndex);
return (
<ul className="grid grid-cols-1 gap-8 h-full sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{curProducts.map((product) => (
<li key={product.id} className="h-full">
<div className="h-4/5">
<Link
href={`/product/detail/${product.id}`}
className="flex items-center w-full h-full relative"
>
<Image
src={product.image}
alt={product.title}
width={0}
height={0}
sizes="100vw"
style={{
width: '100%',
height: 'auto',
padding: '20%',
}}
priority
/>
</Link>
</div>
<div>
<Link href={`/product/detail/${product.id}`}>{product.title}</Link>
<p className="my-2">${product.price}</p>
</div>
<div className="flex">
<div className="mr-2">
<WishlistButton product={product} wishlist={wishlist} />
</div>
<div>
<CartButton product={product} cartItems={cartItems} />
</div>
</div>
</li>
))}
</ul>
);
};
export default List;
'use client';
import React from 'react';
import { useFilteredProductList } from '@/hooks/useFilteredProductList';
import { useRouter } from 'next/navigation';
import { FiChevronLeft, FiChevronRight } from 'react-icons/fi';
import { ITEMSPERPAGE } from '@/constants/product';
import { useAppSelector } from '@/hooks/useAppSelector';
import { Product } from '@/types/globalTypes';
const Pagination: React.FC = () => {
const curCategory = useAppSelector((state) => state.product.currentCategory);
const curProductList: Product[] = useFilteredProductList();
const currentPage: number = useAppSelector(
(state) => state.product.currentProductListPage
);
const router = useRouter();
const totalPages: number = Math.ceil(curProductList.length / ITEMSPERPAGE);
const movePage = (page: number): void => {
router.push(`/product/${curCategory}/${page}`);
};
return (
<ul className="flex justify-center ">
<li className="p-2.5">
<button
name="newer"
className="flex items-center disabled:opacity-20 "
onClick={() => movePage(currentPage - 1)}
disabled={currentPage === 1}
style={{ fontSize: '20px' }}
aria-label="다음 페이지"
>
<FiChevronLeft />
</button>
</li>
{Array.from({ length: totalPages }, (_, index) => index + 1).map(
(page) => (
<li key={page} className="p-2.5">
<button
className="disabled:text-zinc-300 text-black text-base dark:text-white dark:disabled:text-zinc-300"
onClick={() => movePage(page)}
disabled={page === currentPage}
aria-label={`${page} page`}
>
{page}
</button>
</li>
)
)}
<li className="p-2.5">
<button
name="older"
className="flex items-center disabled:opacity-20"
onClick={() => movePage(currentPage + 1)}
disabled={currentPage === totalPages}
style={{ fontSize: '20px' }}
aria-label="이전 페이지"
>
<FiChevronRight />
</button>
</li>
</ul>
);
};
export default Pagination;
'use client';
import React, { useContext } from 'react';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { Product, CartItems } from '@/types/globalTypes';
import { doc, updateDoc } from 'firebase/firestore';
import { db } from '@/app/firebaseConfig';
import { PiShoppingBagFill, PiShoppingBagLight } from 'react-icons/pi';
import {
addCartItemsLocalStorage,
deleteCartItemsLocalStorage,
} from '../../../../utils/localstorage';
interface CartButtonProps {
product: Product;
cartItems: CartItems;
}
const CartButton: React.FC<CartButtonProps> = ({ product, cartItems }) => {
// const currentUser = null;
// const dispatch = useAppDispatch();
// // const toggleCartItem = async () => {
// // const productID = product.id.toString();
// // let newCartItems: CartItems = { ...cartItems };
// // if (currentUser && currentUser.email) {
// // if (newCartItems[productID]) {
// // delete newCartItems[productID];
// // } else {
// // newCartItems[productID] = { product, count: 1 };
// // }
// // const userRef = doc(db, 'users', currentUser.email);
// // await updateDoc(userRef, { cartItems: newCartItems });
// // } else {
// // if (newCartItems[productID]) {
// // delete newCartItems[productID];
// // deleteCartItemsLocalStorage([productID]);
// // } else {
// // newCartItems[productID] = { product, count: 1 };
// // addCartItemsLocalStorage(product);
// // }
// // }
// // dispatch(setCartItems(newCartItems));
// };
// return (
// <button
// onClick={toggleCartItem}
// aria-label="장바구니"
// //className="tooltip"
// // data-tip="장바구니 넣기"
// >
// {cartItems[product.id] ? (
// <PiShoppingBagFill style={{ fontSize: '28px' }} />
// ) : (
// <PiShoppingBagLight style={{ fontSize: '28px' }} />
// )}
// </button>
// );
return <></>;
};
export default CartButton;
'use client';
import React, { useContext } from 'react';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { Product, WishlistItems } from '@/types/globalTypes';
import { doc, updateDoc } from 'firebase/firestore';
import { db } from '@/app/firebaseConfig';
import { PiHeartFill, PiHeartLight } from 'react-icons/pi';
interface WishlistButtonProps {
product: Product;
wishlist: WishlistItems;
}
const WishlistButton: React.FC<WishlistButtonProps> = ({
product,
wishlist,
}) => {
const currentUser = null;
const dispatch = useAppDispatch();
const toggleWishlist = async () => {
// if (!currentUser || !currentUser.email) {
// alert('로그인이 필요한 기능입니다.');
// return;
// }
const newWishlist = { ...wishlist };
const productID = product.id.toString();
if (wishlist[productID]) {
delete newWishlist[productID];
} else {
newWishlist[productID] = product;
}
// const userRef = doc(db, 'users', currentUser.email);
// await updateDoc(userRef, { wishlist: newWishlist });
// dispatch(setWishlist(newWishlist));
};
return (
<button
onClick={toggleWishlist}
aria-label="찜"
// data-tip="찜"
// className="tooltip"
>
{wishlist[product.id] ? (
<PiHeartFill style={{ fontSize: '28px' }} />
) : (
<PiHeartLight style={{ fontSize: '28px' }} />
)}
</button>
);
};
export default WishlistButton;
다음은 Product
컴포넌트의 리팩토링을 마친, 관련 파일 구조입니다.
src/
├── app/
│ ├── product/
│ │ └── [...slug]/
│ │ ├── components/
│ │ │ ├── Pagenation.tsx
│ │ │ ├── ProductList.tsx
│ │ │ └── productList/
│ │ │ ├── ProductListSkeleton.tsx
│ │ │ ├── Products.tsx
│ │ │ └── Products/
│ │ │ ├── CartButton.tsx
│ │ │ └── WishlistButton.tsx
│ │ └── page.tsx
├── hooks/
│ ├── useProduct.ts
│ └── useStore.ts
├── _utils/
│ ├── setCartItemsFireStore.ts
│ ├── setCartItemsLocalStorage.ts
│ └── setWishlistItemsFireStore.ts
├── slices/
│ └── productSlice.ts
└── utils/
├── checkItemExistsById.ts
├── filteredProductsByCategory.ts
└── getProductsInPage.ts
Redux Store에서 category
와 page
를 제거한 점이 가장 크게 달라진 점이라고 할 수 있습니다.
기존에는 스토어에 저장된 category
와 현재 카테고리를 비교해서 달라진 경우에만 스토어에 다시 저장하는 코드였는데요. 왜 그렇게 작성했었는지 모르겠습니다..ㅋㅋ
page
는 Pagenation
컴포넌트에서만 필요해서 params
로 받은 page
를 props
로 넘겨주는 방식으로 변경했습니다.
'use client';
import React from 'react';
import ProductList from './components/ProductList';
import Pagenation from './components/Pagenation';
import { CategoryKey } from '@/types/globalTypes';
interface ProductProps {
params: {
slug: string;
};
}
const Product: React.FC<ProductProps> = ({ params }) => {
const category: CategoryKey = params.slug[0] as CategoryKey;
const page: number = Number(params.slug[1]);
return (
<>
<div className="mt-14 flex flex-col justify-center w-full items-center">
<h2>{category.charAt(0).toUpperCase() + category.slice(1)}</h2>
</div>
<div className="flex flex-col mb-44">
<ProductList category={category} page={page} />
</div>
<div className="mt-14">
<Pagenation category={category} page={page} />
</div>
</>
);
};
export default Product;
기존에는 page
와 category
상태와 해당 상태를 설정해 주는 리듀서가 있었습니다.
위에서 설명한 것과 같이 스토어에 저장할 필요성이 사라졌기 때문에 제거했습니다.
결과적으로는 외부 API를 통해 Fetch한 products
데이터만 저장하게 되었습니다.
import { Product } from '@/types/globalTypes';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface ProductState {
products: Product[];
}
const initialProductState: ProductState = {
products: [],
};
const productSlice = createSlice({
name: 'product',
initialState: initialProductState,
reducers: {
setProducts: (state, action: PayloadAction<Product[]>) => {
state.products = action.payload;
},
},
});
export const { setProducts } = productSlice.actions;
export const productReducer = productSlice.reducer;
기존에는 ul
요소 내부에서 map
메서드를 사용해 제품 아이템에 대한 li
요소를 만들었습니다.
이 부분을 Products
컴포넌트로 따로 분리했습니다.
제품 아이템은 productsInPage
배열을 기준으로 렌더링이 되는데요.
productsInPage
배열의 길이가 0인 경우에는 아직 제품의 로딩이 완료되지 않을 것이라고 보고, 스켈레톤을 나타내는 ProductListSkeleton
컴포넌트를 렌더하기로 했습니다.
'use client';
import React, { useEffect, useState } from 'react';
import { Product, ProductProps } from '@/types/globalTypes';
import useProduct from '@/hooks/useProduct';
import getProductsInPage from '@/utils/getProductsInPage';
import Products from './productList/Products';
import ProductListSkeleton from './productList/ProductListSkeleton';
const ProductList: React.FC<ProductProps> = ({ category, page }) => {
const { products } = useProduct();
const [productsInPage, setProductsInPage] = useState<Product[]>([]);
useEffect(() => {
if (products.length === 0) return;
setProductsInPage(getProductsInPage(category, page, products));
}, [products]);
return (
<>
{productsInPage.length === 0 ? (
<ProductListSkeleton />
) : (
<ul className="grid grid-cols-1 gap-8 h-full sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<Products products={productsInPage} />
</ul>
)}
</>
);
};
export default ProductList;
ProductListSkeleton
에서는 react-loading-skeleton
라이브러리를 사용해 스켈레톤을 구현했습니다.
그런데 이상하게도 배경색 펄스 애니메이션이 적용되지 않는 문제가 있어 DaisyUI
에서 제공하는 skeleton
클래스를 사용해 펄스 애니메이션을 적용했습니다.
import Skeleton from 'react-loading-skeleton';
const ProductListSkeleton: React.FC = () => {
return (
<ul className="mt-28 grid grid-cols-1 gap-8 min-h-screen sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{Array.from({ length: 12 }).map((_, index) => (
<li key={index} className="relative h-full animate-pulse pb-36">
<div className="h-4/5 mb-4 skeleton ">
<Skeleton height="100%" />
</div>
<div className="skeleton ">
<Skeleton height={20} width={`60%`} />
</div>
<div className="mt-2 mb-24 skeleton ">
<Skeleton height={20} width={`30%`} />
</div>
</li>
))}
</ul>
);
};
export default ProductListSkeleton;
사실, /src/app/loading.tsx
에서 이미 pathname.startsWith('/product/') && !pathname.startsWith('/product/detail')
코드로 현재 url
이 /product/
으로 시작하는 경우에는 스켈레톤을 표시하게 되어있으나 상품 리스트 페이지에서 새로고침을 하게 되면 스켈레톤은 안 보이고 공백만 보여 주는 문제가 있어, ProductListSkeleton
컴포넌트를 만들게 되었습니다.
그래서 현재 스켈레톤 UI가 아래처럼 다르게 나오는 문제가 있습니다.
이 부분은 다음에 시간될 때 고치기로..
cartItems
, wishlistItems
을 WishlistButton
와 CartButton
컴포넌트에서 각각 불러올까 하다가 중복성을 줄이기 위해 Products
에서 Props
로 전달해 주기로 했습니다.
이부분은 기존과 동일합니다.
import { Product } from '@/types/globalTypes';
import Image from 'next/image';
import Link from 'next/link';
import WishlistButton from './Products/WishlistButton';
import CartButton from './Products/CartButton';
import useStore from '@/hooks/useStore';
interface ProductsProps {
products: Product[];
}
const Products: React.FC<ProductsProps> = ({ products }) => {
const { cartItems, wishlistItems } = useStore();
return (
<>
{products.map((product) => (
<li key={product.id} className="h-full">
<div className="h-4/5">
<Link
href={`/product/detail/${product.id}`}
className="flex items-center w-full h-full relative"
>
<Image
src={product.image}
alt={product.title}
width={0}
height={0}
sizes="100vw"
style={{
width: '100%',
height: 'auto',
padding: '20%',
}}
priority
/>
</Link>
</div>
<div>
<Link href={`/product/detail/${product.id}`}>{product.title}</Link>
<p className="my-2">${product.price}</p>
</div>
<div className="flex">
<div className="mr-2">
<WishlistButton product={product} wishlist={wishlistItems} />
</div>
<div>
<CartButton product={product} cartItems={cartItems} />
</div>
</div>
</li>
))}
</>
);
};
export default Products;
기존 CartButton
컴포넌트에 작성된 토글 로직은 외부로 분리시켰습니다.
'use client';
import React from 'react';
import { Product, CartItems } from '@/types/globalTypes';
import { PiShoppingBagFill, PiShoppingBagLight } from 'react-icons/pi';
import useStore from '@/hooks/useStore';
interface CartButtonProps {
product: Product;
cartItems: CartItems;
}
const CartButton: React.FC<CartButtonProps> = ({ product, cartItems }) => {
const { toggleCartItem } = useStore();
return (
<button
onClick={() => {
toggleCartItem(product);
}}
aria-label="장바구니"
>
{cartItems[product.id] ? (
<PiShoppingBagFill style={{ fontSize: '28px' }} />
) : (
<PiShoppingBagLight style={{ fontSize: '28px' }} />
)}
</button>
);
};
export default CartButton;
기존 WishlistButton
컴포넌트에 작성된 토글 로직은 외부로 분리시켰습니다.
'use client';
import React from 'react';
import { Product, WishlistItems } from '@/types/globalTypes';
import { PiHeartFill, PiHeartLight } from 'react-icons/pi';
import { useSession } from 'next-auth/react';
import useStore from '@/hooks/useStore';
interface WishlistButtonProps {
product: Product;
wishlist: WishlistItems;
}
const WishlistButton: React.FC<WishlistButtonProps> = ({
product,
wishlist,
}) => {
const { toggleWishlistItems } = useStore();
const { status } = useSession();
return (
<button
onClick={() => {
if (status === 'unauthenticated') {
alert('로그인이 필요한 기능입니다.');
return;
}
toggleWishlistItems(product);
}}
aria-label="찜"
>
{wishlist[product.id] ? (
<PiHeartFill style={{ fontSize: '28px' }} />
) : (
<PiHeartLight style={{ fontSize: '28px' }} />
)}
</button>
);
};
export default WishlistButton;
페이지네이션 버튼을 클릭 시 해당 페이지로 라우팅 되는 동작을 Link
컴포넌트로 변경할까 고민했으나 동적으로 라우팅되기 때문에 useRouter
를 유지하기로 결정했습니다.
'use client';
import React from 'react';
import { FiChevronLeft, FiChevronRight } from 'react-icons/fi';
import { CATEGORIES, ITEMS_PER_PAGE } from '@/constants/product';
import { Product, ProductProps } from '@/types/globalTypes';
import useRouterPush from '@/hooks/useRouterPush';
import filterProductsByCategory from '@/utils/filterProductsByCategory';
import useProduct from '@/hooks/useProduct';
const Pagenation: React.FC<ProductProps> = ({ category, page }) => {
const { navigateToSelectedProductListPage } = useRouterPush();
const { products } = useProduct();
const productsInCategory: Product[] = filterProductsByCategory(
products,
CATEGORIES[category]
);
const total: number = Math.ceil(productsInCategory.length / ITEMS_PER_PAGE);
return (
<ul className="flex justify-center ">
<>
<li className="p-2.5">
<button
name="newer"
className="flex items-center disabled:opacity-20 "
onClick={() =>
navigateToSelectedProductListPage(category, page - 1)
}
disabled={page === 1}
style={{ fontSize: '20px' }}
aria-label="이전 페이지"
>
<FiChevronLeft />
</button>
</li>
{Array.from({ length: total }, (_, index) => index + 1).map(
(pageNumber) => (
<li key={pageNumber} className="p-2.5">
<button
className="disabled:text-zinc-300 text-black text-base dark:text-white dark:disabled:text-zinc-300"
onClick={() =>
navigateToSelectedProductListPage(category, pageNumber)
}
disabled={pageNumber === page}
aria-label={`${page} page`}
>
{pageNumber}
</button>
</li>
)
)}
<li className="p-2.5">
<button
name="older"
className="flex items-center disabled:opacity-20"
onClick={() =>
navigateToSelectedProductListPage(category, page + 1)
}
disabled={page === total}
style={{ fontSize: '20px' }}
aria-label="다음 페이지"
>
<FiChevronRight />
</button>
</li>
</>
</ul>
);
};
export default Pagenation;
useStore
는 products
데이터를 관리하기 위한 커스텀 훅입니다.
현재 이 훅은 스토어로부터 products
데이터를 가져오고, 데이터 dispatch
를 수행하는 기능만을 포함하고 있습니다."
import getAllProductsFakeStore from '@/_utils/getAllProductsFakeStore';
import { useAppDispatch } from './useAppDispatch';
import { Product } from '@/types/globalTypes';
import { setProducts } from '@/slices/productSlice';
import { useAppSelector } from './useAppSelector';
import { AppDispatch } from '@/types/reduxTypes';
const useProduct = () => {
const dispatch: AppDispatch = useAppDispatch();
const products = useAppSelector((state) => state.product.products);
const setProductsStore = async () => {
const products: Product[] = await getAllProductsFakeStore();
dispatch(setProducts(products));
};
return {
products,
setProductsStore,
};
};
export default useProduct;
useStore
는 위시리스트와 장바구니 데이터를 관리하기 위한 커스텀 훅입니다.
Redux의 useStore
가 자동으로 import되는 문제가 있어 이름을 바꿔야할지 고민 중입니다.
바꾼다면 useShoppingStore
으로 변경 예정입니다.
현재 각 모듈을 하나의 파일로 작성하려고 하고 있습니다만 이렇게 보니 import해 오는 코드가 길어 보여, 다시 연관있는 모듈은 하나의 파일로 합칠지 고민을 좀 해야겠습니다.
import { resetCartItems, setCartItems } from '@/slices/cartSlice';
import { useAppDispatch } from '@/hooks/useAppDispatch';
import { CartItems, Product, WishlistItems } from '@/types/globalTypes';
import { resetWishlistItems, setWishlistItems } from '@/slices/wishListSlice';
import getUserCartItems from '@/_utils/getUserCartItems';
import getUserWishlistItems from '@/_utils/getUserWishlistItems';
import getCartItemsLocalStorage from '@/_utils/getCartItemsLocalStorage';
import { useAppSelector } from './useAppSelector';
import checkItemExistsById from '@/utils/checkItemExistsById';
import { useSession } from 'next-auth/react';
import setCartItemsFireStore from '@/_utils/setCartItemsFireStore';
import setWishlistItemsFireStore from '@/_utils/setWishlistItemsFireStore';
import setCartItemsLocalStorage from '@/_utils/setCartItemsLocalStorage';
const useStore = () => {
const { data: session, status } = useSession();
const dispatch = useAppDispatch();
const cartItems = useAppSelector((state) => state.cart.cartItems);
const wishlistItems = useAppSelector((state) => state.wishlist.wishlistItems);
const resetStore = () => {
dispatch(resetCartItems());
dispatch(resetWishlistItems());
};
const initializeUserStore = async (email: string) => {
const cartItems: CartItems = await getUserCartItems(email);
const wishlistItems: WishlistItems = await getUserWishlistItems(email);
dispatch(setCartItems(cartItems));
dispatch(setWishlistItems(wishlistItems));
};
const initializeStore = async () => {
const cartItems: CartItems = getCartItemsLocalStorage();
dispatch(setCartItems(cartItems));
};
const toggleCartItem = (product: Product) => {
if (checkItemExistsById(product.id, cartItems)) {
removeCartItem(product);
} else {
addCartItem(product);
}
};
const addCartItem = (product: Product) => {
const newCartItems: CartItems = {
...cartItems,
[product.id]: { product: product, count: 1 },
};
dispatch(setCartItems(newCartItems));
if (status === 'unauthenticated') {
setCartItemsLocalStorage(newCartItems);
} else {
if (session?.user.email)
setCartItemsFireStore(newCartItems, session?.user.email);
}
};
const removeCartItem = (product: Product) => {
const { [product.id]: removeItem, ...newCartItems } = cartItems;
dispatch(setCartItems(newCartItems));
if (status === 'unauthenticated') {
setCartItemsLocalStorage(newCartItems);
} else {
if (session?.user.email)
setCartItemsFireStore(newCartItems, session?.user.email);
}
};
const toggleWishlistItems = (product: Product) => {
if (checkItemExistsById(product.id, wishlistItems)) {
removeWishlistItem(product);
} else {
addWishlistItem(product);
}
};
const addWishlistItem = (product: Product) => {
const newWishlistItems: WishlistItems = {
...wishlistItems,
[product.id]: product,
};
dispatch(setWishlistItems(newWishlistItems));
if (session?.user.email)
setWishlistItemsFireStore(newWishlistItems, session?.user.email);
};
const removeWishlistItem = (product: Product) => {
const { [product.id]: removeItem, ...newWishlistItems } = wishlistItems;
dispatch(setWishlistItems(newWishlistItems));
if (session?.user.email)
setWishlistItemsFireStore(newWishlistItems, session?.user.email);
};
return {
resetStore,
initializeUserStore,
initializeStore,
cartItems,
wishlistItems,
toggleCartItem,
toggleWishlistItems,
};
};
export default useStore;
장바구니에 변경이 있는 경우 Firestore에 해당 내용을 반영하는 함수입니다.
import { db } from '@/app/firebaseConfig';
import { CartItems } from '@/types/globalTypes';
import { doc, DocumentReference, updateDoc } from 'firebase/firestore';
const setCartItemsFireStore = (newCartItems: CartItems, email: string) => {
try {
const userDocumentReference: DocumentReference = doc(db, 'users', email);
updateDoc(userDocumentReference, { cartItems: newCartItems });
} catch (error) {
console.error('setting error in setCartItemsFireStore:', error);
}
};
export default setCartItemsFireStore;
장바구니에 변경이 있는 경우 LocalStorage에 해당 내용을 반영하는 함수입니다.
import { CARTITEMS_KEY } from '@/constants/localStorageKeys';
import { CartItems } from '@/types/globalTypes';
const setCartItemsLocalStorage = (newCartItems: CartItems) => {
try {
localStorage.setItem(CARTITEMS_KEY, JSON.stringify(newCartItems));
} catch (error) {
console.error('setting error in setCartItemsLocalStorage:', error);
}
};
export default setCartItemsLocalStorage;
위시리스트에 변경이 있는 경우 Firestore에 해당 내용을 반영하는 함수입니다.
import { db } from '@/app/firebaseConfig';
import { WishlistItems } from '@/types/globalTypes';
import { doc, DocumentReference, updateDoc } from 'firebase/firestore';
const setWishlistItemsFireStore = (
newWishlistItems: WishlistItems,
email: string
) => {
try {
const userDocumentReference: DocumentReference = doc(db, 'users', email);
updateDoc(userDocumentReference, { wishlistItems: newWishlistItems });
} catch (error) {
console.error('setting error in setWishlistItemsFireStore:', error);
}
};
export default setWishlistItemsFireStore;
장바구니와 위시리스트 데이터에 이미 존재하는 아이템인지 확인하기 위해 만든 함수입니다.
원래는 장바구니 데이터를 체크하는 함수와 위시리스트 데이터를 체크하는 함수를 따로 만들려고 했으나 동일한 코드 구조로 작성되기 때문에 checkItemExistsById
으로 통합했습니다.
const checkItemExistsById = (id: number, items: {} | any[]) => {
const stringifyId = id.toString();
if (Array.isArray(items)) {
return items.includes(stringifyId);
} else {
return Object.keys(items).includes(stringifyId);
}
};
export default checkItemExistsById;
전체 상품 목록에서 특정 카테고리에 해당하는 아이템만 필터링하여 반환하는 함수입니다.
import { Product } from '@/types/globalTypes';
const filterProductsByCategory = (products: Product[], category: string) => {
if (category === 'all') return products;
return products.filter((product) => product.category === category);
};
export default filterProductsByCategory;
filterProductsByCategory
을 사용해 필터링된 상품 목록에서 현재 페이지에 해당하는 상품들을 반환하는 함수입니다.
import { CATEGORIES, ITEMS_PER_PAGE } from '@/constants/product';
import { CategoryKey, Product } from '@/types/globalTypes';
import filterProductsByCategory from '@/utils/filterProductsByCategory';
const getProductsInPage = (
category: CategoryKey,
page: number,
products: Product[]
) => {
let filteredProducts: Product[] = products;
filteredProducts = filterProductsByCategory(products, CATEGORIES[category]);
const total = filteredProducts.length;
const startIndex = ITEMS_PER_PAGE * (page - 1);
const endIndex = startIndex + ITEMS_PER_PAGE;
if (endIndex <= total) {
return filteredProducts.slice(startIndex, endIndex);
} else {
return filteredProducts.slice(startIndex);
}
};
export default getProductsInPage;
어제부터 작업을 했었는데, 오늘 끝났습니다 🥲
4시간 넘게 작업한 게 예상대로 동작하지 않아 (페이지네이션 발생 시 두 번 렌더링 되는 문제) 기존으로 롤백 시키고 처음부터 다시 진행하게 되어서 시간이 오래걸렸습니다.
지금까지 리팩토링한 코드로 로딩 시간을 확인해 봤는데 확실히 짧아졌습니다.
3초에서 0.5초로 줄어든 느낌?..
내일부터는 개인 프로젝트의 기획부터 개발까지 진행할 예정이라 당분간은 리팩토링 작업이 어려울 거 같지만, 그래도 짬짬이 진행해 보려고 합니다.
아무래도 너무 오랜 시간 후에 다시 진행하면 파일 구조를 까먹을 수 있기 때문에 리팩토링 시 힘들 거 같아서요..