상품관리 - Part1 상품등록

Project Diary/React + Firebase (Snack ShoppingMall)

 쇼핑몰의 상품 등록 기능을 구현 기록


 Firebase를 이용해 상품 데이터를 관리하고, React와 Redux를 사용하여 데이터를 처리하는 방법을 다룹니다.

 

 

1. Firebase 설정

import firebase from "firebase/compat/app";
import "firebase/compat/database";
import "firebase/compat/storage";

// Firebase 설정 초기화
const firebaseConfig = firebase.initializeApp({
  apiKey: "YOUR_API_KEY",
  authDomain: "YOUR_PROJECT_ID.firebaseapp.com",
  databaseURL: "https://YOUR_PROJECT_ID.firebaseio.com",
  projectId: "YOUR_PROJECT_ID",
  storageBucket: "YOUR_PROJECT_ID.appspot.com",
  messagingSenderId: "YOUR_SENDER_ID",
  appId: "YOUR_APP_ID",
  measurementId: "YOUR_MEASUREMENT_ID",
});

// Firebase 데이터베이스와 스토리지 참조 생성
const kuwazawaDB = firebaseConfig.database();
export const kuwazawa_productDB = kuwazawaDB.ref("kuwazawa_products");
export const oStorage = firebaseConfig.storage();

 

 

2. 상품 등록 폼 컴포넌트 구현

상품을 등록하는 폼을 생성합니다. 사용자가 입력한 데이터를 Firebase 데이터베이스에 저장하고, 파일을 스토리지에 업로드합니다

const OnlineShopInsertSection = () => {
  const navigate = useNavigate();
  const [product, setProduct] = useState({
    category: "클래식상품",
    name: "",
    price: "",
    description: "",
    inventory: "",
    photo: "",
    detailPhotos: [],
  });

  const [photoValue, setPhotoValue] = useState("");
  const [detailPhotos, setDetailPhotos] = useState([]);

  // 입력 필드 변경 핸들러
  const handleChange = (e) => {
    const { value, name } = e.target;
    setProduct((product) => ({ ...product, [name]: value }));
  };

  // 대표 사진 파일 변경 핸들러
  const handleFileChange = (e) => {
    const file = e.target.files[0];
    setProduct((prevProduct) => ({ ...prevProduct, photo: file }));
    setPhotoValue(e.target.value);
  };

  // 상세 사진 파일 변경 핸들러
  const handleDetailFileChange = (e) => {
    const files = e.target.files;
    setDetailPhotos(Array.from(files));
  };

  // 폼 제출 핸들러
  const onSubmit = async (e) => {
    e.preventDefault();
    const addProduct = { ...product, id: Date.now() };
    try {
      const storageRef = oStorage.ref();

      // 대표 사진 업로드
      if (product.photo) {
        const fileRef = storageRef.child(product.photo.name);
        await fileRef.put(product.photo);
        addProduct.photo = await fileRef.getDownloadURL();
      }

      // 상세 사진 업로드
      if (detailPhotos.length > 0) {
        const detailPhotoURLs = [];
        await Promise.all(
          detailPhotos.map(async (file, index) => {
            const fileName = `detailPhoto${index + 1}_${Date.now()}_${file.name}`;
            const detailFileRef = storageRef.child(fileName);
            await detailFileRef.put(file);
            detailPhotoURLs.push(await detailFileRef.getDownloadURL());
          })
        );
        addProduct.detailPhotos = detailPhotoURLs;
      }

      // Firebase 데이터베이스에 상품 데이터 추가
      await kuwazawa_productDB.push(addProduct);

      // 폼 초기화
      setProduct({
        category: "클래식상품",
        name: "",
        price: "",
        description: "",
        inventory: "",
        photo: "",
        detailPhotos: [],
      });
      setPhotoValue("");
      setDetailPhotos([]);
      navigate("/product");
    } catch (error) {
      console.log("오류 : ", error);
    }
  };

  return (
    <OnlineShopInsertSectionBlock>
      // form 생략
    </OnlineShopInsertSectionBlock>
  );
};

export default OnlineShopInsertSection;

 

 

3. Redux 설정 및 상품 데이터 패치

상품 데이터를 관리하기 위해 Redux를 사용합니다. 상품 등록 후 상품 리스트를 업데이트하기 위해 Redux를 설정하고, Firebase에서 데이터를 가져옵니다.

 

Redux Slice 생성

import { createSlice } from "@reduxjs/toolkit";
import { kuwazawa_productDB } from "@/assets/firebase";

const productSlice = createSlice({
  name: "products",
  initialState: {
    products: [],
  },
  reducers: {
    initProducts(state, action) {
      state.products = action.payload;
    },
  },
});

export const { initProducts } = productSlice.actions;

export const fetchProducts = () => async (dispatch) => {
  try {
    // Firebase 데이터베이스에서 상품 데이터를 실시간으로 가져옴
    kuwazawa_productDB.on("value", (snapshot) => {
      // snapshot.val()을 통해 데이터를 객체 형태로 가져옴
      const productsObj = snapshot.val();
      // 객체를 배열 형태로 변환, key를 포함한 새로운 객체 배열 생성
      const productsArr = Object.entries(productsObj).map(([key, value]) => {
        return { key, ...value };
      });
      // 변환된 배열을 Redux 스토어에 저장
      dispatch(initProducts(productsArr));
    });
  } catch (error) {
    console.error("Error fetching products:", error);
  }
};


export default productSlice.reducer;

 

 

fetchProduct 코드에서 각 부분을 자세히 설명하겠습니다.

 

kuwazawa_productDB.on("value", callback):
  • Firebase 데이터베이스에서 value 이벤트를 통해 데이터를 실시간으로 가져옵니다.
  • 데이터가 변경될 때마다 callback 함수가 호출됩니다.
snapshot.val():
  • snapshot 객체를 통해 데이터베이스에서 가져온 데이터를 얻습니다.
  • val() 메서드는 데이터를 객체 형태로 반환합니다.
Object.entries(productsObj).map(([key, value]) => { ... }):
  • 객체를 배열로 변환합니다.
  • Object.entries() 메서드는 객체의 키-값 쌍을 배열로 반환합니다.
  • map() 메서드를 사용하여 각 키-값 쌍을 새로운 객체로 변환합니다.
  • 이때 key와 value를 분해하여 사용합니다.
dispatch(initProducts(productsArr)):
  • 변환된 배열을 Redux 스토어에 저장합니다.
  •  initProducts 액션을 디스패치하여 스토어의 상태를 업데이트합니다.

 

4. 상품 리스트 컴포넌트 구현

Redux를 통해 패치한 상품 데이터를 리스트로 표시하는 컴포넌트를 구현합니다.

 

const OnlineShopsection = ({ title }) => {
  const dispatch = useDispatch();
  const navigate = useNavigate();

  const user = useSelector((state) => state.members.user);
  const allData = useSelector((state) => state.products.products);
  const [products, setProducts] = useState(allData);
  const [loading, setLoading] = useState(false);

  // 상품 정렬
  const sortProduct = (keyname) => {
  setProducts((products) => {
    // 기존 상품 배열을 복사하여 새로운 배열 생성
    let sortedProducts = [...products];
    // 배열을 정렬
    sortedProducts.sort((a, b) => (a[keyname] < b[keyname] ? -1 : 1));
    // 정렬된 배열 반환
    return sortedProducts;
  });
};


  useEffect(() => {
    dispatch(fetchProducts());
  }, [dispatch]);

  useEffect(() => {
    if (allData.length > 0) {
      setLoading(true);
      if (title === "all") {
        setProducts(allData);
      } else {
        setProducts(allData.filter((item) => item.category === title));
      }
    }
  }, [allData, title]);

  if (!loading) {
    return (
      <OnlineShopsectionBlock>
        <ProductInsert>
          <Link to="/productInsert">상품등록</Link>
        </ProductInsert>
      </OnlineShopsectionBlock>
    );
  }

  return (
    <OnlineShopsectionBlock className="row">
      <ButtonBlock>
        <button onClick={() => sortProduct("name")}>상품명순</button>
        <button onClick={() => sortProduct("price")}>가격순</button>
      </ButtonBlock>
      <UlBlock>
        {products.map((item, index) => (
          <ListBlock key={index}>
            <div className="photo">
              <Link to={`/product/${item.id}`} state={{ item: item }}>
                <img src={item.photo} alt={item.name} />
              </Link>
            </div>
            <div className="info">
              <p>
                <a href="#">{item.name}</a>
              </p>
              <p>{parseInt(item.price).toLocaleString()}&yen;</p>
            </div>
          </ListBlock>
        ))}
      </UlBlock>
      {user && user.userId === "admin@example.com" && (
        <ProductInsert>
          <Link to="/productInsert">상품등록</Link>
        </ProductInsert>
      )}
    </OnlineShopsectionBlock>
  );
};

export default OnlineShopsection;

 

 

sortProduct 코드에서 각 부분을 자세히 설명하겠습니다.

 

setProducts((products) => { ... }):
  • setProducts는 상태 업데이트 함수입니다.
  • products는 현재 상태의 상품 배열을 나타냅니다.
    콜백 함수 내부에서 상태를 업데이트합니다.
.let sortedProducts = [...products]
  • 기존 상품 배열을 복사하여 sortedProducts라는 새로운 배열을 생성합니다.
  • 이렇게 하면 기존 상태를 직접 변경하지 않고 새로운 상태를 생성할 수 있습니다.
sortedProducts.sort((a, b) => (a[keyname] < b[keyname] ? -1 : 1)):
  • sort() 메서드를 사용하여 배열을 정렬합니다. 정렬 기준은 keyname에 따라 다릅니다.
  • 비교 함수 (a, b) => (a[keyname] < b[keyname] ? -1 : 1)은 두 객체의 keyname 속성을 비교하여 정렬 순서를 결정합니다.
  • a[keyname] < b[keyname]이 참이면 a가 b보다 앞에 위치합니다 (-1 반환).
  • [keyname] >= b[keyname]이면 a가 b보다 뒤에 위치합니다 (1 반환).
return sortedProducts:
  • 정렬된 배열을 반환하여 상태를 업데이트합니다.

 

이번 글에서는 Firebase와 React, Redux를 사용하여 쇼핑몰 상품을 등록하고, Redux를 통해 상품 데이터를 관리하는 방법에 대해 알아봤습니다.