230628~230703 - React(์‡ผํ•‘๋ชฐ)

๋ฐฑ์Šน์—ฐยท2023๋…„ 7์›” 3์ผ
0

๐Ÿšฉ React

๋ฆฌ์•กํŠธ์™€ ํŒŒ์ด์–ด๋ฒ ์ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‡ผํ•‘๋ชฐ ์‚ฌ์ดํŠธ ๊ตฌํ˜„ํ•˜๊ธฐ 3~6์ผ์ฐจ

๐Ÿ“ ์„ค๋ช…

  • react์™€ firebase๋ฅผ ์ด์šฉํ•˜์—ฌ ์‡ผํ•‘๋ชฐ ์‚ฌ์ดํŠธ ๊ตฌํ˜„ํ•˜๊ธฐ
  • ์ƒ๋‹จ ํ—ค๋”์™€ ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฐ”, firebase ๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ ๊ตฌํ˜„
  • ๋กœ๊ทธ์ธํ•˜๋ฉด ๋กœ๊ทธ์•„์›ƒ์œผ๋กœ ๊ธ€์”จ ๋ณ€๊ฒฝ๋˜๊ฒŒ ๊ตฌํ˜„(๋ฐ˜๋Œ€๋„ ๊ตฌํ˜„)
  • firebase์— ์ƒˆ๋กœ์šด ์ œํ’ˆ ๋“ฑ๋กํ•˜๋Š” ํŽ˜์ด์ง€ ๊ตฌํ˜„(admin user๋งŒ)
  • firebase์— ๋“ฑ๋กํ•œ ์ƒˆ ์ œํ’ˆ์„ ํ™”๋ฉด์— ๋‹ค์‹œ ๊ฐ€์ ธ์˜ด
  • ์ƒํ’ˆ ์ƒ์„ธํŽ˜์ด์ง€ ๊ตฌํ˜„
  • ๋ฆฌ์•กํŠธ ์Šฌ๋ผ์ด๋”(slick)๋ฅผ ์ด์šฉํ•˜์—ฌ ๋ฉ”์ธํŽ˜์ด์ง€ ์Šฌ๋ผ์ด๋“œ ๊ตฌํ˜„
  • ์žฅ๋ฐ”๊ตฌ๋‹ˆ ํŽ˜์ด์ง€ ๊ตฌํ˜„


โœ’๏ธ ์ฝ”๋“œ ์ž‘์„ฑ

์ž…๋ ฅ

App.js

import "./App.css";
import { Outlet } from "react-router-dom";
import Navbar from "./components/Navbar";
import { AuthContextProvider } from "./components/context/AuthContext";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <AuthContextProvider>
        <Navbar />
        <Outlet />
      </AuthContextProvider>
    </QueryClientProvider>
  );
}

export default App;



index.css

@tailwind base;
@tailwind components;
@tailwind utilities;

@import url("https://fonts.googleapis.com/css2?family=Noto+Sans+KR&family=Playfair+Display&display=swap");

body {
  margin: 0;
  font-family: Noto Sans KR;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;

  @apply text-slate-900;
}

#root {
  @apply w-full text-inherit;
}

input {
  @apply  p-4 border border-slate-200 my-1 rounded-sm
}
input:focus{
  @apply outline outline-slate-700 bg-slate-100
}



index.js

import React from "react";
import ReactDOM from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import "./index.css";
import App from "./App";
import NotFound from "./pages/NotFound";
import Home from "./pages/Home";
import AllProducts from "./pages/AllProducts";
import NewProduct from "./pages/NewProduct";
import ProductDetail from "./pages/ProductDetail";
import MyCart from "./pages/MyCart";
import ProtectedRoute from "./components/ProtectedRoute";

const router = createBrowserRouter([
  {
    path: "/",
    element: <App />,
    errorElement: <NotFound />,
    children: [
      { index: true, path: "/", element: <Home /> },
      { path: "/products", element: <AllProducts /> },
      {
        // * 7. admin์ผ ๋•Œ๋ฅผ ๊ตฌ๋ถ„ํ•˜๋Š” ์กฐ๊ฑด ์ถ”๊ฐ€true๋ฉด ์ƒ๋žต ๊ฐ€๋Šฅ)
        path: "/products/new",
        element: (
          <ProtectedRoute requireAdmin={true}>
            <NewProduct />
          </ProtectedRoute>
        ),
      },
      { path: "/products/:id", element: <ProductDetail /> },
      {
        path: "/cart",
        element: (
          <ProtectedRoute>
            <MyCart />
          </ProtectedRoute>
        ),
      },
    ],
  },
]);

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);



Navbar.jsx

import { Link } from "react-router-dom";
import { HiPencilAlt } from "react-icons/hi";
import User from "./User";
import Button from "./ui/Button";
import { useAuthContext } from "./context/AuthContext";
import CartStatus from "./CartStatus";

// // * 1-1. useState ์„ ์–ธ
// ๋กœ๊ทธ์ธ ์—ฌ๋ถ€
export default function Navbar() {

  // * 7-6. useAuthContext ์‚ฌ์šฉ
  const { user, login, logout } = useAuthContext();

  // const [user, setUser] = useState();

  // // * 2. ํ™”๋ฉด์ด ๋งˆ์šดํŠธ๋  ๋•Œ(reload๋  ๋•Œ) ๋กœ๊ทธ์ธ์ด ๋˜์–ด์žˆ๋Š”์ง€ ์•„๋‹Œ์ง€ ์ƒํƒœ๋ฅผ ์•Œ์•„๋ณด๋Š” ํ•จ์ˆ˜ ํ˜ธ์ถœ
  // useEffect(() => {
  //   onUserStateChange((user) => {
  //     setUser(user);
  //     console.log("user? : ", user); // admin์„ ๋งŒ๋“ค๊ณ  ์‹ถ์€ ์œ ์ €์˜ uid๋ฅผ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ์ž‘์„ฑ
  //   });
  // }, []);

  // // * 1-2. onClick์— login ํ•จ์ˆ˜๋ฅผ ๋„ฃ์ง€ ์•Š๊ณ  ์ด๋ ‡๊ฒŒ ์ž‘์„ฑํ•˜๋Š” ์ด์œ ๋Š” firebase.js์— ์žˆ๋Š” user๋ฅผ ๋ฐ›์•„์™€์„œ useState์— ์ง‘์–ด๋„ฃ๊ธฐ ์œ„ํ•จ
  /**
   * ๋กœ๊ทธ์ธํ•  ๋•Œ ์‚ฌ์šฉ๋˜๋Š” ํ•จ์ˆ˜
   */
  // ๋ฆฌํŒฉํ† ๋ง
  // const handleLogin = () => {
  //   login().then(setUser);
  // };
  // const handleLogout = () => {
  //   logout().then(setUser); // useState์˜ user๋ฅผ ๋น„์šด๋‹ค. (null ์ƒํƒœ๋กœ ๋งŒ๋“ฆ)
  // };

  return (
    <div className="fixed w-full z-10 border-b border-slate-50/20 text-slate-500 hover:text-black hover:bg-white transition duration-500 bg-white bg-opacity-10">
      <div className="w-full max-w-screen-2xl m-auto">
        <header className="flex justify-between items-center p-2 md:p-5">
          <Link to="/">
            <h1 className="text-lg md:text-3xl font-logoFont tracking-normal md:tracking-widest">
              RALPH<span className="pl-3 md:pl-6">LAUREN</span>
            </h1>
          </Link>
          <nav className="flex items-center gap-2 md:gap-4 text-sm md:text-base">
            <Link to="/products">Product</Link>

            {/* cart๋ฅผ CartStatus๋ผ๋Š” ์ปดํฌ๋„ŒํŠธ๋กœ ๋”ฐ๋กœ ๋นผ์„œ ์ž‘์„ฑ */}
            {user && <Link to="/cart"><CartStatus /></Link>}
            
            {/* // * 5. isAdmin์ด true์ผ ๋•Œ๋งŒ ๋ณด์ด๋„๋ก */}
            {user && user.isAdmin && (
              <Link to="/products/new">
                <HiPencilAlt />
              </Link>
            )}

            {/* // *3. User.jsx - user๊ฐ€ ์—†์„ ๊ฒฝ์šฐ ์‹คํ–‰ */}
            {user && <User user={user} />}

            {/*// * 1-2. */}
            {/* // * 2-1. ๋”ฐ๋กœ ์„ ์–ธํ•˜์ง€ ์•Š๊ณ  firebase ์•ˆ์— ์žˆ๋Š” ํ•จ์ˆ˜๋ฅผ ๋ฐ”๋กœ ํ˜ธ์ถœ */}
            {/* // * 6. Button ์ปดํฌ๋„ŒํŠธ๋กœ ์ „ํ™˜ */}
            {!user && <Button onClick={login} text={"login"} />}
            {user && <Button onClick={logout} text={"logout"} />}
          </nav>
        </header>
      </div>
    </div>
  );
}



User.jsx

import React from "react";

// * 3. ์œ ์ €๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” User.jsx ๋งŒ๋“ฆ
export default function User({ user: { displayName, photoURL } }) {
  // console.log("user: ", user);
  return (
    // shrink : ๋ถ€๋ชจ ์˜์—ญ์ด ์ค„๋ฉด ์ž์‹ item๋“ค๋„ ๊ฐ™์ด ์ค„์–ด๋“ฆ - shrink-0์€ ๊ทธ๊ฒƒ์„ ๋ฐฉ์ง€ํ•ด์คŒ
    <div className="flex items-center shrink-0">
      <img
        className="w-10 h-10 rounded-full mr-2"
        src={photoURL}
        alt={displayName}
      />
      <span className="hidden md:block">{displayName}</span>
    </div>
  );
}



Button.jsx

import React from "react";

export default function Button({ onClick, text }) {
  return (
    <button
      className="bg-brand text-white py-2 px-4 rounded-sm hover:brightness-200 text-sm"
      onClick={onClick}
    >
      {text}
    </button>
  );
}



firebase.js

import { initializeApp } from "firebase/app";
import {
  getAuth,
  signInWithPopup,
  GoogleAuthProvider,
  signOut,
  onAuthStateChanged,
} from "firebase/auth";
import { getDatabase, get, set, ref } from "firebase/database";
import uuid from "react-uuid";

const firebaseConfig = {
  apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
  authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
  databaseURL: process.env.REACT_APP_FIREBASE_DB_URL,
};

const app = initializeApp(firebaseConfig);
const provider = new GoogleAuthProvider();
const auth = getAuth();

// * 1-1. ํ•จ์ˆ˜ ์„ ์–ธ์€ firebase ์•ˆ์—์„œ ํ•จ
export function login() {
  return signInWithPopup(auth, provider) // ์•„๋ž˜ ๋ฆฌํ„ด๋œ user ๊ฐ’์„ ๋ฐ›์•„์™€์„œ ๊ฒฐ๊ณผ๊ฐ’์œผ๋กœ ๋‚ด๋ณด๋‚ด๊ธฐ ์œ„ํ•ด return ์ž‘์„ฑ
    .then((result) => {
      // ๋กœ๊ทธ์ธ ๋˜์—ˆ๋Š”์ง€ ๊ฒฐ๊ณผ๋ฅผ ์–ป์–ด์˜ด
      const user = result.user;
      // console.log("user? : ", user);
      return user;
    })
    .catch(console.error);
}

export async function logout() {
  return signOut(auth) // null๊ฐ’์ด ๋ฆฌํ„ด๋จ
    .then(() => null); // null
  // .catch((error) => {});
}

// * 2. ํ™”๋ฉด์ด ๋งˆ์šดํŠธ๋  ๋•Œ(reload๋  ๋•Œ) ๋กœ๊ทธ์ธ์ด ๋˜์–ด์žˆ๋Š”์ง€ ์•„๋‹Œ์ง€ ์ƒํƒœ๋ฅผ ์•Œ์•„๋ณด๋Š” ํ•จ์ˆ˜ ์„ ์–ธ (callback)
export function onUserStateChange(callback) {
  // ๋งŒ์•ฝ user๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ
  onAuthStateChanged(auth, async (user) => {
    // ? 1. ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธ ํ•œ ๊ฒฝ์šฐ
    // user && adminUser(user);
    const updatedUser = user ? await adminUser(user) : null;
    callback(updatedUser);
  });
}

// * 4. ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์‚ฌ์šฉ ์„ ์–ธ
const database = getDatabase(app);

// ? 2. ์‚ฌ์šฉ์ž๊ฐ€ ์–ด๋“œ๋ฏผ ๊ถŒํ•œ์ด ์žˆ๋Š”์ง€ ํ™•์ธ -> isAdmin์„ user ์•ˆ์— ๋„ฃ์Œ
// ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์•ˆ์— ์žˆ๋Š” admins๋ฅผ ์ฐธ์กฐํ•˜๋Š” ํ•จ์ˆ˜
async function adminUser(user) {
  // database ์•ˆ์— admins key๊ฐ€ ์žˆ์Œ
  return (
    get(ref(database, "admins")) // ref(database์ด๋ฆ„, key๊ฐ’)
      // ๋งŒ์•ฝ snapshot(๊ฒฐ๊ณผ๊ฐ’)์ด ์กด์žฌํ•˜๋ฉด
      .then((snapshot) => {
        if (snapshot.exists()) {
          const admins = snapshot.val(); // snapshot์˜ value
          // admins๊ฐ€ user.uid๋ฅผ ํฌํ•จํ•จ
          const isAdmin = admins.includes(user.uid);
          return { ...user, isAdmin }; // ๋งŽ์€ user๋“ค์˜ ํ•ญ๋ชฉ ์ค‘์—์„œ isAdmin๋งŒ ๋ผ์›Œ๋„ฃ์Œ
        }
        return user;
      })
  );
}

//์ œํ’ˆ๋“ฑ๋ก
export async function addNewProduct(product, image) {
  const id = uuid();
  console.log(id);
  return set(ref(database, `products/${id}`), {
    ...product,
    id,
    price: parseInt(product.price),
    options: product.options.split(","),
    image,
  });
}

//์ œํ’ˆ๊ฐ€์ ธ์˜ค๊ธฐ
export async function getProduct() {
  return get(ref(database, "products")).then((snapshot) => {
    // exists : ์กด์žฌํ•  ๋•Œ๋งŒ value๊ฐ’ ๋ถˆ๋Ÿฌ์˜ด
    if (snapshot.exists()) {
      return Object.values(snapshot.val());
    }
  });
}

// ์‚ฌ์šฉ์ž์˜ ์นดํŠธ์— ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ์—…๋ฐ์ดํŠธ (์ œํ’ˆ๋“ฑ๋ก๊ณผ ๋น„์Šทํ•œ ๋กœ์ง)
export async function addOrUpdateToCart(userId, product) {
  return set(ref(database, `carts/${userId}/${product.id}`), product);
}

// ํŠน์ • ์‚ฌ์šฉ์ž์˜ ์žฅ๋ฐ”๊ตฌ๋‹ˆ(cart)๋ฅผ ๊ฐ€์ ธ์˜ด
export async function getCart(userId) {
  // products๋งŒ ๋ถˆ๋Ÿฌ์˜ค๋Š”๊ฒŒ ์•„๋‹Œ carts์•ˆ์˜ ํŠน์ • id๋ฅผ ๊ฐ€์ ธ์™€์•ผ ํ•จ
  return get(ref(database, `carts/${userId}`))
  .then((snapshot) => {
    const items = snapshot.val() || {};
    return Object.values(items);
  });
}

/*
  1. ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธ ํ•œ ๊ฒฝ์šฐ
  2. ์‚ฌ์šฉ์ž๊ฐ€ ์–ด๋“œ๋ฏผ ๊ถŒํ•œ์ด ์žˆ๋Š”์ง€ ํ™•์ธ
  3. ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ ค์คŒ
 */



uploader.js

// https://console.cloudinary.com/documentation/upload_images
// * 11. uploadImage ํ•จ์ˆ˜ ์„ ์–ธ (cloudinary์— ์˜ฌ๋ผ๊ฐ)
export async function uploadImage(file) {
  const data = new FormData();
  const url = process.env.REACT_APP_CLOUDINARY_URL;

  // ํŒŒ์ผ์ด ํ•˜๋‚˜์ด๊ธฐ ๋•Œ๋ฌธ์— for๋ฌธ ํ•„์š” ์—†์Œ
  data.append("file", file);
  data.append("upload_preset", process.env.REACT_APP_CLOUDINARY_PRESET);

  return fetch(url, {
    method: "POST",
    body: data
  })
  .then((res) => res.json())
  .then((data) =>  data.url)
}



์ถœ๋ ฅ

  • ๋ฉ”์ธ ํ™”๋ฉด

  • ๋กœ๊ทธ์ธ ํ•œ ์œ ์ €์˜ ์ด๋ฆ„๊ณผ ํ”„๋กœํ•„ ์‚ฌ์ง„์ด ๋ณด์—ฌ์ง(๊ฐ€๋ ค๋†“์€ ๋ถ€๋ถ„)

  • ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ๋‹ด์€ item๋“ค์„ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ํƒญ์—์„œ ๋ณด์—ฌ์คŒ


๐Ÿ”— ์ฐธ๊ณ  ๋งํฌ & ๋„์›€์ด ๋˜๋Š” ๋งํฌ






profile
๊ณต๋ถ€ํ•˜๋Š” ๋ฒจ๋กœ๊ทธ

1๊ฐœ์˜ ๋Œ“๊ธ€

comment-user-thumbnail
2023๋…„ 8์›” 31์ผ

์•ˆ๋…•ํ•˜์„ธ์š”. ๋“œ๋ฆผ์ฝ”๋”ฉ ์šด์˜์ž ์ž…๋‹ˆ๋‹ค.
์ˆ˜๊ฐ•์ƒ๋ถ„์ด ๋ฐœ๊ฒฌํ•˜์—ฌ ์‹ ๊ณ ๊ฐ€ ๋“ค์–ด์™€ ๋ธ”๋กœ๊ทธ์— ๋Œ€ํ•ด ์•Œ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
ํ•ด๋‹น ๊ฒŒ์‹œ๊ธ€๊ณผ ๋ธ”๋กœ๊ทธ์— ์˜ฌ๋ฆฌ์‹  ๋‹ค์ˆ˜์˜ ๊ธ€๋“ค์ด ๋“œ๋ฆผ์ฝ”๋”ฉ ์•„์นด๋ฐ๋ฏธ ์œ ๋ฃŒ ๊ฐ•์˜์˜ ๋‚ด์šฉ์„ ์ •๋ฆฌ ํ•˜์‹ ๊ฑธ๋กœ ํ™•์ธ๋ฉ๋‹ˆ๋‹ค.
์ด๋Š” ์—„์—ฐํžˆ ์ €์ž‘๊ถŒ๋ฒ• ์œ„๋ฐ˜์ž…๋‹ˆ๋‹ค.
ํ•ด๋‹น ํฌ์ŠคํŠธ๋“ค์„ ๋น„๊ณต๊ฐœ ๋˜๋Š” ์‚ญ์ œ ์ฒ˜๋ฆฌํ›„ info@dream-coding.com ๋กœ ๋ฉ”์ผ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค.
์‹œ์ผ๋‚ด์— ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š์œผ์‹œ๋ฉด ๊ฐ•์˜ ์ทจ์†Œ ๋ฐ ๋ฒ•์  ๋Œ€์‘ ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.
๊ฐ•์˜ ์‹œ์ž‘์ „ ์ €์ž‘๊ถŒ๋ฒ•์— ๊ด€๋ จํ•ด์„œ ๋ธ”๋กœ๊ทธ์— ์ •๋ฆฌํ•˜์ง€ ๋ง์•„ ๋‹ฌ๋ผ๊ณ  ์•ˆ๋‚ดํ•ด ๋“œ๋ ธ์Šต๋‹ˆ๋‹ค.
https://academy.dream-coding.com/courses/player/react/lessons/1462

๋‹ต๊ธ€ ๋‹ฌ๊ธฐ