Code Monkey home page Code Monkey logo

react-online-store's Introduction

๐Ÿ›’ ์ฐฝ์ž‘ ์˜จ๋ผ์ธ ์Šคํ† ์–ด, 'CVXV' ํ”„๋กœ์ ํŠธ

cvxv-thumb


๐Ÿ”— CVXV [Live Demo]



1. Project

1-1. Project Description

CVXV๋Š” ๊ฐ€์ƒ์˜ ์˜จ๋ผ์ธ ์Šคํ† ์–ด์ž…๋‹ˆ๋‹ค. '๋ณต์‚ฌ ๋ถ™์—ฌ๋„ฃ๊ธฐํ•˜๋“ฏ ์–ธ์ œ ์–ด๋””์„œ๋‚˜, ์ž์œ ๋กญ๊ฒŒ'๋ผ๋Š” ์•„์ด๋””์–ด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ 'Copy Cut Paste'๋ผ๋Š” ๋ธŒ๋žœ๋“œ๋ฅผ ์ฐฝ์กฐํ–ˆ์Šต๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” ๋กœ๊ทธ์ธ ํ›„ ์ƒํ’ˆ์„ ์‚ดํŽด๋ณด๊ณ  ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ์ถ”๊ฐ€ํ•˜๋Š” ๋“ฑ ์‡ผํ•‘์„ ์‰ฝ๊ฒŒ ์ฆ๊ธธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Firebase Google Auth๋ฅผ ํ†ตํ•œ ๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ๊ณผ Realtime Database๋ฅผ ์ด์šฉํ•œ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ React Query๋ฅผ ์‚ฌ์šฉํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ  ์บ์‹ฑํ•˜์—ฌ ๊ด€๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. ๋”๋ถˆ์–ด ๋ฐ˜์‘ํ˜• ์›น ๋””์ž์ธ๊ณผ Adobe Photoshop์„ ์‚ฌ์šฉํ•œ ์ƒํ’ˆ ๋””์ž์ธ ์ž‘์—…, ๊ทธ๋ฆฌ๊ณ  ํผ๋ธ”๋ฆฌ์‹ฑ ์ž‘์—…์„ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.


1-2. Project Duration & Participants

  • 2023-06-15 ~ 2023-07-02
  • ๊ฐœ์ธ ํ”„๋กœ์ ํŠธ (1์ธ)


2. Skills

JAVASCRIPT REACT POSTCSS Git Firebase



3. Pages

  1. Home - ๋ฉ”์ธ ํŽ˜์ด์ง€(/)
  2. Shop - ์ „์ฒด ์ƒํ’ˆ ๋ชฉ๋ก ํŽ˜์ด์ง€(/shop)
  3. Shop - ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ƒํ’ˆ ๋ชฉ๋ก ํŽ˜์ด์ง€(/shop/:category)
  4. ProductDetail - ์ƒํ’ˆ ์ƒ์„ธ ํŽ˜์ด์ง€(/products/:id)
  5. NewProduct - ์ƒํ’ˆ ๋“ฑ๋ก ํŽ˜์ด์ง€(/products/new)
  6. MyBags - ์žฅ๋ฐ”๊ตฌ๋‹ˆ ํŽ˜์ด์ง€(/bags)
  7. NotFound - 404 ํŽ˜์ด์ง€
const router = createBrowserRouter([
  {
    path: '/',
    element: <App />,
    errorElement: <NotFound />,
    children: [
      {
        index: true,
        path: '/',
        element: <Home />,
      },
      {
        path: '/products/:id',
        element: <ProductDetail />,
      },
      {
        path: '/shop',
        element: <Shop />,
      },
      {
        path: '/shop/:category',
        element: <Shop />,
      },
      {
        path: '/products/new',
        element: (
          <ProtectedRoute requireAdmin>
            <NewProduct />
          </ProtectedRoute>
        ),
      },
      {
        path: '/bags',
        element: (
          <ProtectedRoute>
            <MyBags />
          </ProtectedRoute>
        ),
      },
    ],
  },
]);


4. Main Features

  1. ๋กœ๊ทธ์ธ ๋ฐ ๋กœ๊ทธ์•„์›ƒ
  2. ์ƒํ’ˆ ๋ฐ์ดํ„ฐ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
  3. ์–ด๋“œ๋ฏผ ๊ณ„์ •์ผ ๊ฒฝ์šฐ ์ƒํ’ˆ ๋“ฑ๋ก
  4. ์ƒํ’ˆ ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ํ•„ํ„ฐ๋ง ๋ฐ ์ƒํ’ˆ ์ •๋ ฌ
  5. ์ƒํ’ˆ ์ƒ์„ธ๋ณด๊ธฐ
  6. ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๊ตฌํ˜„
  7. ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ์ƒํ’ˆ ์ถ”๊ฐ€
  8. ์ƒํ’ˆ ์ˆ˜๋Ÿ‰ ๋ณ€๊ฒฝ
  9. ์žฅ๋ฐ”๊ตฌ๋‹ˆ์—์„œ ์ƒํ’ˆ ์‚ญ์ œ
  10. ์œ„์‹œ๋ฆฌ์ŠคํŠธ ๊ตฌํ˜„

4-1. Login & Logout

cvxv-login

์‚ฌ์šฉ์ž ๊ด€๋ฆฌ๋Š” Firebase์˜ Authentication์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ์„ ํ†ตํ•ด ์ด๋ฃจ์–ด์ง‘๋‹ˆ๋‹ค.

import {
  getAuth,
  signInWithPopup,
  GoogleAuthProvider,
  signOut,
  onAuthStateChanged,
} from 'firebase/auth';

const auth = getAuth();
const provider = new GoogleAuthProvider();
const database = getDatabase(app);

๋กœ๊ทธ์ธ ๋ฐ ๋กœ๊ทธ์•„์›ƒ์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด signInWithPopup() ๋ฐ signOut() ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.

// ๋กœ๊ทธ์ธ
export function login() {
  signInWithPopup(auth, provider).catch(console.error);
}

// ๋กœ๊ทธ์•„์›ƒ
export function logout() {
  signOut(auth).catch(console.error);
}

์ƒˆ๋กœ๊ณ ์นจ์‹œ ์‚ฌ์šฉ์ž์˜ ๋กœ๊ทธ์ธ ์ƒํƒœ๊ฐ€ ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š๋„๋ก onAuthStateChanged() ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๋กœ๊ทธ์ธ ์ƒํƒœ๋ฅผ ๊ด€์ฐฐํ•ฉ๋‹ˆ๋‹ค.

// ๋กœ๊ทธ์ธ ์ƒํƒœ ํ™•์ธ
export function onUserStateChange(callback) {
  onAuthStateChanged(auth, async (user) => {
    const updatedUser = user ? await adminUser(user) : null;
    callback(updatedUser);
  });
}

์ดํ›„ ์‚ฌ์šฉ์ž๊ฐ€ ์–ด๋“œ๋ฏผ ๊ณ„์ •์ธ์ง€ ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด Firebase์˜ Realtime Database์— ์ €์žฅ๋œ ์‚ฌ์šฉ์ž์˜ admins ๋ฐฐ์—ด์„ get ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์‚ฌ์šฉ์ž์˜ uid๋ฅผ ํฌํ•จํ•˜๊ณ  ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

// ์–ด๋“œ๋ฏผ ๊ถŒํ•œ ํ™•์ธ
async function adminUser(user) {
  return get(ref(database, 'admins')) //
    .then((snapshot) => {
      if (snapshot.exists()) {
        const admins = snapshot.val();
        const isAdmin = admins.includes(user.uid);
        return { ...user, isAdmin };
      }
      return user;
    });
}

๋กœ๊ทธ์ธ ์ƒํƒœ์™€ ๊ฐ™์ด ์•ฑ ์ „๋ฐ˜์ ์œผ๋กœ ๋™์ผํ•œ ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผํ•˜๋ ค๋ฉด ์ปจํ…์ŠคํŠธ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. onUserStateChange() ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๊ณ  ์ฝœ๋ฐฑ ํ•จ์ˆ˜(setUser)๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

import { login, logout, onUserStateChange } from '../api/firebase';

const AuthContext = createContext();

export function AuthContextProvider({ children }) {
  const [user, setUser] = useState();

  // ๋กœ๊ทธ์ธ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
  useEffect(() => {
    onUserStateChange((user) => {
      setUser(user);
    });
  }, []);

  return (
    <AuthContext.Provider
      value={{ user, uid: user && user.uid, login, logout }}
    >
      {children}
    </AuthContext.Provider>
  );
}

export function useAuthContext() {
  return useContext(AuthContext);
}


4-2. Show Products

cvxv-show

Shop ํŽ˜์ด์ง€์—์„œ๋Š” ์นดํ…Œ๊ณ ๋ฆฌ์— ํ•ด๋‹นํ•˜๋Š” ์ƒํ’ˆ ๋ชฉ๋ก์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. getProducts ํ•จ์ˆ˜๋กœ Firebase Realtime Database์—์„œ 'products' ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋‹ค๋ฉด Object.values ๋ฉ”์„œ๋“œ๋กœ ๊ฐ์ฒด์˜ ๊ฐ’๋งŒ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

import { ref, get } from 'firebase/database';

// ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
export async function getProducts() {
  return get(ref(database, 'products')) //
    .then((snapshot) => {
      if (snapshot.exists()) {
        return Object.values(snapshot.val());
      }
      return [];
    });
}

React Query๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ƒํ’ˆ ๋ฐ์ดํ„ฐ๋ฅผ ์บ์‹œํ•˜๊ณ  ์—…๋ฐ์ดํŠธํ•˜๋Š” useProducts ์ปค์Šคํ…€ ํ›…์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ƒํ’ˆ๊ณผ ๊ด€๋ จ๋œ ์š”์ฒญ์„ ํ•œ ๊ณณ์—์„œ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

import { useQuery, useMutation, useQueryClient } from 'react-query';
import { getProducts as fetchProducts, addNewProduct } from '../api/firebase';

export default function useProducts() {
  const queryClient = useQueryClient();

  // ์ฟผ๋ฆฌ ์ƒ์„ฑ
  const productsQuery = useQuery(['products'], fetchProducts, {
    staleTime: 1000 * 60,
  });

  // addProduct...

  return { productsQuery, addProduct };
}

useProducts ์ปค์Šคํ…€ ํ›…์„ ์‚ฌ์šฉํ•ด products ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ต๋‹ˆ๋‹ค. useFilterAndSort ์ปค์Šคํ…€ ํ›…์— products์™€ option, category๋ฅผ ์ „๋‹ฌํ•˜์—ฌ ํ•„ํ„ฐ๋ง ๋œ ๋ฐ์ดํ„ฐ(sortedData)๋ฅผ ๋ฐ›์•„์˜ต๋‹ˆ๋‹ค. map ๋ฉ”์„œ๋“œ๋กœ ๋ฐฐ์—ด์„ ์ˆœํšŒํ•˜๋ฉฐ <ul> ์š”์†Œ ๋‚ด์— ๋™์ ์œผ๋กœ ๋ Œ๋”๋ง ํ•ฉ๋‹ˆ๋‹ค.

export default function Shop() {
  const { category } = useParams();
  const [option, setOption] = useState();
  const {
    productsQuery: { isLoading, error, data: products },
  } = useProducts();
  const { sortedData } = useFilterAndSort(products, option, category);

  return (
    <div className={styles.layout}>
      <Sort products={sortedData} onChange={setOption} />
      <ul className={styles.items}>
        {sortedData &&
          sortedData.map((product) => (
            <ProductCard key={product.id} product={product} />
          ))}
      </ul>
    </div>
  );
}


4-3. Register a New Product

cvxv-register

์–ด๋“œ๋ฏผ ๊ณ„์ •์€ '/products/new' ํŽ˜์ด์ง€์—์„œ ์ƒˆ๋กœ์šด ์ƒํ’ˆ์„ ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. addNewProduct() ํ•จ์ˆ˜๋Š” product์™€ image URL์„ ์ „๋‹ฌ๋ฐ›์•„ ref ๋ฉ”์„œ๋“œ๋กœ ๊ฒฝ๋กœ๋ฅผ ์„ค์ •ํ•˜๊ณ  set ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ Firebase Realtime Database์— ๋ฐ์ดํ„ฐ๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

import { ref, set } from 'firebase/database';

// ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ƒํ’ˆ ๋“ฑ๋ก
export async function addNewProduct(product, image) {
  const id = uuid();
  return set(ref(database, `products/${id}`), {
    ...product,
    id,
    price: parseInt(product.price),
    image,
    tags: product.tags.split(','),
  });
}

useProducts ์ปค์Šคํ…€ ํ›…์˜ addProduct ํ•จ์ˆ˜๋Š” useMutation ํ›…์„ ์‚ฌ์šฉํ•ด product์™€ url ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ์ƒˆ ์ƒํ’ˆ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๋น„๋™๊ธฐ ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค. ์ƒํ’ˆ ๋“ฑ๋ก์ด ์„ฑ๊ณตํ•˜๋ฉด invalidateQueries๋ฅผ ์‚ฌ์šฉํ•ด ์ฟผ๋ฆฌ๋ฅผ ๋ฌดํšจํ™”ํ•˜๊ณ  ์ตœ์‹  ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

import { getProducts as fetchProducts, addNewProduct } from '../api/firebase';

export default function useProducts() {
  const queryClient = useQueryClient();

  // productsQuery...

  const addProduct = useMutation(
    ({ product, url }) => addNewProduct(product, url),
    {
      onSuccess: () => queryClient.invalidateQueries(['products']),
    }
  );

  return { productsQuery, addProduct };
}

NewProduct ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉ์ž๊ฐ€ ์–‘์‹์„ ์ œ์ถœํ•ฉ๋‹ˆ๋‹ค. uploadImage() ํ•จ์ˆ˜์— ์‚ฌ์šฉ์ž๊ฐ€ ์—…๋กœ๋“œํ•œ file์„ ์ „๋‹ฌํ•˜์—ฌ ํด๋ผ์šฐ๋””๋„ˆ๋ฆฌ URL์„ ํš๋“ํ•˜๊ณ , addProduct.mutate๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์ƒํ’ˆ ๋“ฑ๋ก ์š”์ฒญ์„ ๋ณด๋ƒ…๋‹ˆ๋‹ค.

import { uploadImage } from '../api/uploader';

export default function NewProduct() {
  // ...
  const { addProduct } = useProducts();

  const hanleSubmit = (e) => {
    e.preventDefault();
    setIsUploading(true);

    uploadImage(file) //
      .then((url) => {
        addProduct.mutate(
          { product, url },
          {
            onSuccess: () => {
              setSuccess('The ITEM HAS BEEN ADDED.');
              setTimeout(() => {
                setSuccess(null);
              }, 1500);
            },
          }
        );
      }) //
      .finally(() => {
        fileRef.current.value = '';
        setProduct({});
        setIsUploading(false);
      });
  };

  // ...
}


4-4. Filter and Sort Products

cvxv-filter cvxv-sort

์‚ฌ์šฉ์ž๋Š” ์‚ฌ์ด๋“œ ๋ฐ”์—์„œ ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ํด๋ฆญํ•˜์—ฌ ํ•„ํ„ฐ๋ง๋œ ์ƒํ’ˆ ๋ชฉ๋ก์„ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ƒํ’ˆ ๋ชฉ๋ก์€ ์‹ ์ƒํ’ˆ ์ˆœ, ์ด๋ฆ„ ์ˆœ, ๋‚ฎ์€ ๊ฐ€๊ฒฉ ์ˆœ, ๋†’์€ ๊ฐ€๊ฒฉ ์ˆœ์œผ๋กœ ์ •๋ ฌ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.


useFilterAndSort ์ปค์Šคํ…€ ํ›…์„ ๋งŒ๋“ค์–ด ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„๋กœ ์ƒํ’ˆ ๋ฐ์ดํ„ฐ๋ฅผ ํ•„ํ„ฐ๋งํ•˜๊ณ , ํ•„ํ„ฐ๋ง๋œ ์ƒํ’ˆ๋“ค์„ ์˜ต์…˜์— ๋”ฐ๋ผ ์ •๋ ฌํ•ฉ๋‹ˆ๋‹ค.

filter ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด category์— ํ•ด๋‹นํ•˜๋Š” products๋งŒ ํ•„ํ„ฐ๋งํ•ฉ๋‹ˆ๋‹ค. useMemo ํ›…์„ ์‚ฌ์šฉํ•ด products์™€ category ๊ฐ’์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งŒ filteredData๋ฅผ ๋‹ค์‹œ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค.

export default function useFilterAndSort(products, option, category) {
  const filteredData = useMemo(() => {
    return products && category
      ? products.filter(
          (product) => product.category.toLowerCase() === category
        )
      : products || [];
  }, [products, category]);

  // ...
}

switch ๋ฌธ๊ณผ sort ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด option ๊ฐ’์— ๋”ฐ๋ผ ๊ฐ๊ฐ ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•˜๊ณ  ์ •๋ ฌ๋œ ๋ฐฐ์—ด์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

// ...
const sortedData = useMemo(() => {
  const sortedArray = [...filteredData];
  switch (option) {
    case 'new':
      sortedArray.sort((a, b) => (a.id > b.id ? 1 : -1));
      break;
    case 'alphabetical':
      sortedArray.sort((a, b) =>
        a.title.toLowerCase() > b.title.toLowerCase() ? 1 : -1
      );
      break;
    case 'low price':
      sortedArray.sort((a, b) => a.price - b.price);
      break;
    case 'high price':
      sortedArray.sort((a, b) => b.price - a.price);
      break;
    default:
      return filteredData;
  }

  return sortedArray;
}, [filteredData, option]);

return {
  sortedData,
};


4-5. Show Product Details

cvxv-detail

์ƒํ’ˆ ๋ชฉ๋ก ํŽ˜์ด์ง€์—์„œ ๊ฐœ๋ณ„ ์ƒํ’ˆ์„ ํด๋ฆญํ•˜๋ฉด React Router์˜ useNavigate ํ›…์„ ์‚ฌ์šฉํ•ด ์ƒํ’ˆ ์ƒ์„ธ ํŽ˜์ด์ง€(/products/${id})๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค. ์ด๋•Œ state ๊ฐ์ฒด์— product๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

export default function ProductCard({
  product,
  product: { id, image, title, price },
}) {
  // ...
  const navigate = useNavigate();

  // handleAdd...
  // handleRemove...

  return (
    <li className={styles.item}>
      <div className={styles.image}>
        <img
          src={image}
          alt={title}
          onClick={() => {
            navigate(`/products/${id}`, { state: { product } });
          }}
        />
      </div>
    </li>
  );
}

ProductDetail ์ปดํฌ๋„ŒํŠธ๋Š” Firebase Realtime Database์— ์ €์žฅ๋œ ์ƒํ’ˆ์˜ ์„ธ๋ถ€ ์ •๋ณด๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. useLocation ํ›…์„ ์‚ฌ์šฉํ•ด ์ „๋‹ฌ๋ฐ›์€ state ๊ฐ์ฒด์—์„œ product ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.

export default function ProductDetail() {
  // ...
  const {
    state: {
      product: { id, image, title, description, price, tags, category },
    },
  } = useLocation();

  // handleAdd...
  // addOrUpdateBagItem.mutate...

  // return...
}


4-6. My Bags

cvxv-tab

์žฅ๋ฐ”๊ตฌ๋‹ˆ๋ฅผ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด Firebase Realtime Database์—์„œ userId์— ํ•ด๋‹นํ•˜๋Š” bags ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. get ๋ฉ”์„œ๋“œ์™€ ref ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด ๊ฒฝ๋กœ๋ฅผ ๋ช…์‹œํ•˜๊ณ , Object.values๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ•ด๋‹น ๊ฐ’๋งŒ์„ ์ฝ์–ด์˜ต๋‹ˆ๋‹ค.

import { ref, get } from 'firebase/database';

// ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
export async function getBag(userId) {
  return get(ref(database, `bags/${userId}`)) //
    .then((snapshot) => {
      const items = snapshot.val() || {};
      return Object.values(items);
    });
}

์žฅ๋ฐ”๊ตฌ๋‹ˆ์™€ ๊ด€๋ จ๋œ ์ฟผ๋ฆฌ ๋กœ์ง์„ ํ•œ ๊ณณ์—์„œ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด useBag์ด๋ผ๋Š” ์ปค์Šคํ…€ ํ›…์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์œ ์ง€ํ•˜๊ณ , ์บ์‹ฑ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


์‚ฌ์šฉ์ž ๋ณ„๋กœ ์บ์‹œ๊ฐ€ ์ด๋ฃจ์–ด์ง€๋„๋ก ์ฟผ๋ฆฌ ํ‚ค๋กœ uid๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ (uid๊ฐ€ falsy์ผ ๋•Œ), ์ฟผ๋ฆฌ๊ฐ€ ์ˆ˜ํ–‰๋˜์ง€ ์•Š๋„๋ก enabled๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. mutation์ด ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜๋ฉด invalidateQueries๋ฅผ ํ†ตํ•ด ์ฟผ๋ฆฌ๋ฅผ ๋‹ค์‹œ ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค.

import { useAuthContext } from '../context/AuthContext';
import { addOrUpdateToBag, getBag, removeFromBag } from '../api/firebase';

export default function useBag() {
  const { uid } = useAuthContext();
  const queryClient = useQueryClient();

  // ์ฟผ๋ฆฌ ์ƒ์„ฑ
  const bagQuery = useQuery(['bags', uid || ''], () => getBag(uid), {
    enabled: !!uid,
  });

  // ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ์ƒํ’ˆ์„ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ์—…๋ฐ์ดํŠธ
  const addOrUpdateBagItem = useMutation(
    (product) => addOrUpdateToBag(uid, product),
    {
      onSuccess: () => queryClient.invalidateQueries(['bags', uid]),
    }
  );

  // ์žฅ๋ฐ”๊ตฌ๋‹ˆ์—์„œ ํŠน์ • ์ƒํ’ˆ ์‚ญ์ œ
  const removeBagItem = useMutation(
    (productId) => removeFromBag(uid, productId),
    {
      onSuccess: () => {
        queryClient.invalidateQueries(['bags', uid]);
      },
    }
  );

  return { bagQuery, addOrUpdateBagItem, removeBagItem };
}


4-7. Add item to Cart

cvxv-add

์‚ฌ์šฉ์ž๊ฐ€ ์ƒํ’ˆ์„ ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ์ถ”๊ฐ€ํ•˜๋ฉด Firebase Realtime Database์— ์ฆ‰๊ฐ์ ์œผ๋กœ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค. addOrUpdateToBag ํ•จ์ˆ˜์—์„œ ref ๋ฉ”์„œ๋“œ๋กœ ๊ฒฝ๋กœ๋ฅผ ๋ช…์‹œํ•˜๊ณ , set ๋ฉ”์„œ๋“œ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ฑฐ๋‚˜ ์—…๋ฐ์ดํŠธ ํ•ฉ๋‹ˆ๋‹ค.

import { ref, set } from 'firebase/database';

// ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ์ƒํ’ˆ ์ถ”๊ฐ€ ๋˜๋Š” ์—…๋ฐ์ดํŠธ
export async function addOrUpdateToBag(userId, product) {
  return set(ref(database, `bags/${userId}/${product.id}`), product);
}


4-8. Change quantity

cvxv-quantity

์‚ฌ์šฉ์ž๋Š” ์ถ”๊ฐ€ํ•œ ์ƒํ’ˆ์˜ + ๋˜๋Š” - ์•„์ด์ฝ˜์„ ํด๋ฆญํ•˜์—ฌ ์ˆ˜๋Ÿ‰์„ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. BagItem ์ปดํฌ๋„ŒํŠธ๋Š” MyBag ์ปดํฌ๋„ŒํŠธ๋กœ๋ถ€ํ„ฐ product๋ฅผ props๋กœ ๋ฐ›์•„์™€ ๊ฐœ๋ณ„ ์ƒํ’ˆ์„ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ์ˆ˜๋Ÿ‰์„ ๋ณ€๊ฒฝํ•˜๋ฉด ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋œ ๊ฒƒ์ด๋ฏ€๋กœ ์บ์‹œ๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.

export default function BagItem({
  product,
  product: { id, title, price, quantity, image },
}) {
  const { addOrUpdateBagItem, removeBagItem } = useBag();
  const navigate = useNavigate();

  // handleRemove...

  // ์ˆ˜๋Ÿ‰ ๊ฐ์†Œ
  const handleMinus = () => {
    if (quantity < 2) return;
    addOrUpdateBagItem.mutate({ ...product, quantity: quantity - 1 });
  };

  // ์ˆ˜๋Ÿ‰ ์ฆ๊ฐ€
  const handlePlus = () => {
    addOrUpdateBagItem.mutate({ ...product, quantity: quantity + 1 });
  };

  // ...
}


4-9. Remove item

cvxv-remove

์‚ฌ์šฉ์ž๋Š” X ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ํŠน์ • ์ƒํ’ˆ์„ ์žฅ๋ฐ”๊ตฌ๋‹ˆ์—์„œ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

export default function BagItem({
  product,
  product: { id, title, price, quantity, image },
}) {
  const { addOrUpdateBagItem, removeBagItem } = useBag();

  const handleRemove = () => {
    removeBagItem.mutate(id);
  };

  // handleMinus...
  // handlePlus...
  // ...
}

Firebase Realtime Database์—์„œ ref ๋ฉ”์„œ๋“œ๋กœ ๊ฒฝ๋กœ๋ฅผ ์ฐพ์•„ remove ๋ฉ”์„œ๋“œ๋กœ productId์— ํ•ด๋‹นํ•˜๋Š” ์ƒํ’ˆ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค.

import { ref, remove } from 'firebase/database';

// productId์™€ ์ผ์น˜ํ•˜๋Š” ์ƒํ’ˆ ์‚ญ์ œ
export async function removeFromBag(userId, productId) {
  return remove(ref(database, `bags/${userId}/${productId}`));
}


4-10. Wishlist

cvxv-wish-add

์‚ฌ์šฉ์ž๊ฐ€ ProductCard์˜ ๋ถ๋งˆํฌ ์•„์ด์ฝ˜์„ ํด๋ฆญํ•˜์—ฌ ์œ„์‹œ๋ฆฌ์ŠคํŠธ์— ์ƒํ’ˆ์„ ์ถ”๊ฐ€ ๋˜๋Š” ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ธฐ๋Šฅ์€ ์žฅ๋ฐ”๊ตฌ๋‹ˆ์™€ ์œ ์‚ฌํ•œ ๋กœ์ง์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์€ Firebase Realtime Database์— ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค.

export default function ProductCard({
  product,
  product: { id, image, title, price },
}) {
  const { user, login } = useAuthContext();
  const {
    wishQuery: { isLoading: isLoadingWish, data: wishProducts },
    addOrUpdateWishItem,
    removeWishItem,
  } = useWish();

  const [wishId, setWishId] = useState([]);
  const [success, setSuccess] = useState();
  const navigate = useNavigate();
  const hasId = wishId && wishId.includes(id);

  // ์œ„์‹œ๋ฆฌ์ŠคํŠธ์— ์ƒํ’ˆ ์ถ”๊ฐ€
  const handleAdd = () => {
    if (!user) {
      login();
      return;
    }

    addOrUpdateWishItem.mutate(product, {
      onSuccess: () => {
        setSuccess('SAVING CHANGES');
        setTimeout(() => setSuccess(null), 1500);
      },
    });
  };

  // ์œ„์‹œ๋ฆฌ์ŠคํŠธ์—์„œ ์ƒํ’ˆ ์‚ญ์ œ
  const handleRemove = () => {
    removeWishItem.mutate(id, {
      onSuccess: () => {
        setSuccess('SAVING CHANGES');
        setTimeout(() => setSuccess(null), 2000);
      },
    });
  };

//...


5. UI/UX

5-1. Control Tab Focus

cvxv-tab

CVXV ์›น ์‚ฌ์ดํŠธ๋Š” ํ‚ค๋ณด๋“œ ์‚ฌ์šฉ ๋ณด์žฅ์„ ์œ„ํ•ด ํ‚ค๋ณด๋“œ์˜ ํƒญ(Tab) ํ‚ค๋กœ ์›น์„ ํƒ์ƒ‰ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” ํƒญ ํ‚ค๋ฅผ ๋ˆŒ๋Ÿฌ ์ „์ฒด ์ƒํ’ˆ ๋˜๋Š” ๋ณธ๋ฌธ์œผ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.



5-2. Sidebar

cvxv-sidebar

Navbar์˜ ํ–„๋ฒ„๊ฑฐ ์•„์ด์ฝ˜์„ ํด๋ฆญํ•˜๋ฉด Sidebar๊ฐ€ ๋‚˜ํƒ€๋‚˜๊ณ , x ๋˜๋Š” ๋ฐ”ํƒ•ํ™”๋ฉด์„ ํด๋ฆญํ•˜๋ฉด ์‚ฌ์ด๋“œ๋ฐ”๊ฐ€ ๋‹ซํž™๋‹ˆ๋‹ค. Sidebar ์ปดํฌ๋„ŒํŠธ์— ์‚ฌ์ด๋“œ ๋ฐ”์˜ ์—ด๋ฆผ ์ƒํƒœ(isOpen)์™€ ๋‹ซํž˜ ํ•จ์ˆ˜(onClose)๋ฅผ props๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.

export default function Navbar() {
  const [isSidebarOpen, setIsSidebarOpen] = useState(false);
  const location = useLocation();

  // ์‚ฌ์ด๋“œ๋ฐ” ์—ด๊ธฐ
  const handleSidebarOpen = () => {
    setIsSidebarOpen(true);
  };

  // ์‚ฌ์ด๋“œ๋ฐ” ๋‹ซ๊ธฐ
  const handleSidebarClose = () => {
    setIsSidebarOpen(false);
  };

  // ํŽ˜์ด์ง€ ์ด๋™ ์‹œ ์‚ฌ์ด๋“œ๋ฐ” ๋‹ซ๊ธฐ
  useEffect(() => {
    setIsSidebarOpen(false);
  }, [location]);

  return (
    <>
      <Sidebar isOpen={isSidebarOpen} onClose={handleSidebarClose} />
      <nav className={styles.nav}></nav>
    </>
  );
}

Sidebar ์ปดํฌ๋„ŒํŠธ๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ํŽ˜์ด์ง€ ์ด๋™ํ•  ์ˆ˜ ์žˆ๋Š” ๋งํฌ ๋ชฉ๋ก์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. ์‚ฌ์ด๋“œ๋ฐ”์˜ ์—ด๋ฆผ/๋‹ซํž˜ ์ƒํƒœ์ธ isOpen๊ณผ ์‚ฌ์ด๋“œ๋ฐ”๋ฅผ ๋‹ซ๋Š” ํ•จ์ˆ˜์ธ onClose ํ•จ์ˆ˜๋ฅผ props๋กœ ๋ฐ›์Šต๋‹ˆ๋‹ค.

export default function Sidebar({ isOpen, onClose }) {
  const { user } = useAuthContext();

  // isOpen์˜ ์ƒํƒœ์— ๋”ฐ๋ผ ํด๋ž˜์Šค๋ฅผ ๋‹ฌ๋ฆฌ ์ ์šฉ
  const sidebarClasses = `${styles.container} ${
    isOpen ? styles.open : styles.close
  }`;

  return (
    <>
      {isOpen && <Overlay onClose={onClose} />}
      <aside className={sidebarClasses}>
        <Icon onClick={onClose} option='close'>
          <VscChromeClose />
        </Icon>
        <ul className={styles.links}>
          {user && user.isAdmin && (
            <li className={styles.link}>
              <Link to='/products/new'>Register Product</Link>
            </li>
          )}
          {link.map((value, index) => (
            <li key={index} className={styles.link}>
              <Link to={value.link}>{value.title}</Link>
            </li>
          ))}
        </ul>
      </aside>
    </>
  );
}

aside ์š”์†Œ์— fixed postion์„ ์ ์šฉํ•˜์—ฌ ํ™”๋ฉด ์ƒ์œ„์— ๋‚˜ํƒ€๋‚˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. open/close ํด๋ž˜์Šค๋ฅผ ํ†ตํ•ด container๋ฅผ X๋ฐฉํ–ฅ์œผ๋กœ ์ด๋™์‹œํ‚ต๋‹ˆ๋‹ค.

.container {
  width: 30%;
  height: 100%;
  position: fixed;
  top: 0;
  left: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: var(--black-500);
  z-index: 999;
}

.open {
  transform: translateX(0);
  transition: transform 300ms ease-in;
}

.close {
  transform: translateX(-100%);
  transition: transform 300ms ease-in;
}

.overlay {
  width: 100%;
  height: 100%;
  position: fixed;
  top: 0;
  left: 0;
  background-color: rgba(0, 0, 0, 0.5);
  z-index: 998;
}


5-3. Responsive Web

CVXV ์›น ์‚ฌ์ดํŠธ๋Š” ๋ฐ˜์‘ํ˜•์œผ๋กœ ๋””์ž์ธ๋˜์–ด ๋‹ค์–‘ํ•œ ๋””๋ฐ”์ด์Šค์— ์ตœ์ ํ™”๋œ ๋ ˆ์ด์•„์›ƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋กœ์จ ์‚ฌ์šฉ์ž๊ฐ€ ์›น ์‚ฌ์ดํŠธ๋ฅผ ๋”์šฑ ํŽธ๋ฆฌํ•˜๊ฒŒ ์ด์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

cvxv-mobile01 cvxv-mobile02



6. Trouble shooting

6-1. Redirection to Home

url

1. ๋ชฉํ‘œ

category๋Š” useParams๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ URL ๊ฒฝ๋กœ(/shop/fashion)์—์„œ ๋™์  ํŒŒ๋ผ๋ฏธํ„ฐ(fashion)๋ฅผ ์ถ”์ถœํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๋Š” category๋ฅผ ํฌํ•จํ•œ ๊ฒฝ๋กœ๋กœ ์ง„์ž…ํ•˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•˜๊ณ , ์œ ํšจํ•œ ์นดํ…Œ๊ณ ๋ฆฌ์— ๋Œ€ํ•œ ์ƒํ’ˆ์„ ๋ณด์—ฌ์ฃผ๊ณ ์ž ํ–ˆ์Šต๋‹ˆ๋‹ค.


2. ๋ฌธ์ œ ์ƒํ™ฉ

์‚ฌ์šฉ์ž๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๋Š” category๋ฅผ ๊ฐ€๋ฆฌํ‚ค๋Š” ๊ฒฝ๋กœ, ์˜ˆ๋ฅผ ๋“ค์–ด /shop/apple๋กœ ์ด๋™ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด ๋นˆ ํ™”๋ฉด์ด๋‚˜ ์˜ค๋ฅ˜ ํŽ˜์ด์ง€๊ฐ€ ๋ Œ๋”๋ง ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.


3. ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

Array.some ๋ฉ”์„œ๋“œ๋Š” ๋ฐฐ์—ด ์•ˆ์˜ ์š”์†Œ๊ฐ€ ์ฃผ์–ด์ง„ ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜๋Š”์ง€ ๊ฒ€์‚ฌํ•˜์—ฌ boolean์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ์ด์šฉํ•ด product.category์™€ category ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์œผ๋ฉด useNavigate ํ›…์„ ์‚ฌ์šฉํ•˜์—ฌ Home('/')์œผ๋กœ ๋ฆฌ๋‹ค์ด๋ ‰์…˜ํ•ฉ๋‹ˆ๋‹ค.

export default function useFilterAndSort(products, option, category) {
  const navigate = useNavigate();

  if (
    products &&
    category &&
    !products.some((product) => product.category.toLowerCase() === category)
  ) {
    navigate('/');
  }
}

์ด๋ ‡๊ฒŒ ํ•จ์œผ๋กœ์จ, ์‚ฌ์šฉ์ž๊ฐ€ ์œ ํšจํ•œ ์นดํ…Œ๊ณ ๋ฆฌ๊ฐ€ ์•„๋‹Œ URL๋กœ ์ด๋™ํ•˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•˜๊ณ , ์ผ์น˜ํ•˜์ง€ ์•Š๋Š” ์นดํ…Œ๊ณ ๋ฆฌ์— ๋Œ€ํ•œ ์ƒํ’ˆ์„ ํ‘œ์‹œํ•˜์ง€ ์•Š๋„๋ก ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.



6-2. Display added Wish item

bookmark

1. ๋ชฉํ‘œ

์‚ฌ์šฉ์ž๊ฐ€ ์œ„์‹œ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€ํ•œ ์ƒํ’ˆ์˜ ๋ถ๋งˆํฌ ์ƒํƒœ๋ฅผ ๊ธฐ์–ตํ•˜๊ณ  ์œ ์ง€ํ•˜๋„๋ก ๋งŒ๋“ค๊ณ ์ž ํ–ˆ์Šต๋‹ˆ๋‹ค.


2. ๋ฌธ์ œ์ƒํ™ฉ

ํŽ˜์ด์ง€ ์ƒˆ๋กœ ๊ณ ์นจ ๋˜๋Š” ์ด๋™ ์‹œ ๋ถ๋งˆํฌ ์ƒํƒœ๊ฐ€ ์ดˆ๊ธฐํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด ์‚ฌ์šฉ์ž๋Š” ์œ„์‹œ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€ํ•œ ์ƒํ’ˆ์„ ์‹๋ณ„ํ•˜๋Š” ๋ฐ ์–ด๋ ค์›€์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.


3. ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

wishId ๋ฐฐ์—ด์„ ๋งŒ๋“ค์–ด ์œ„์‹œ๋ฆฌ์ŠคํŠธ์— ์ถ”๊ฐ€ํ•œ ์ƒํ’ˆ์˜ id๋ฅผ ์ €์žฅํ•˜๊ณ  ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. useEffect ํ›…์„ ์‚ฌ์šฉํ•ด wishProducts ๋ฐฐ์—ด์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ํ•ด๋‹น ๋ฐฐ์—ด์—์„œ ์ƒํ’ˆ id๋ฅผ ์ถ”์ถœํ•˜์—ฌ wishId ๋ฐฐ์—ด์„ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค. ์ƒํ’ˆ ์นด๋“œ๋ฅผ ๋ Œ๋”๋งํ•  ๋•Œ, Array.includes ๋ฉ”์„œ๋“œ ๊ฐ ์ƒํ’ˆ์˜ id๊ฐ€ wishId ๋ฐฐ์—ด์— ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. ํฌํ•จ๋˜์–ด ์žˆ๋‹ค๋ฉด ๋ถ๋งˆํฌ๋ฅผ ์ฑ„์šฐ๊ณ , ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด ๋นˆ ๋ถ๋งˆํฌ๋ฅผ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.

const [wishId, setWishId] = useState([]);
const hasId = wishId && wishId.includes(id);

useEffect(() => {
  const wishIds = wishProducts && wishProducts.map((item) => item.id);
  setWishId(wishIds);
}, [wishProducts]);

return (
  <Icon onClick={hasId ? handleRemove : handleAdd} option='mark'>
    {hasId ? <BsBookmarkFill /> : <BsBookmark />}
  </Icon>
);

์ด๋ ‡๊ฒŒ ํ•˜์—ฌ ์ƒํ’ˆ์˜ ๋ถ๋งˆํฌ ์ƒํƒœ๋ฅผ ๊ธฐ์–ตํ•˜๊ณ  ์œ ์ง€ํ•˜์—ฌ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.



๋งจ์œ„๋กœ ์ด๋™ํ•˜๊ธฐ

react-online-store's People

Contributors

cona-tus avatar

Watchers

 avatar

react-online-store's Issues

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.