[유데미x스나이퍼팩토리] 10주 완성 프로젝트 캠프 - 카페 좌석 확인 구독 시스템

강경서·2023년 7월 6일
0
post-thumbnail

☕️ 카페 좌석 확인 구독 시스템

리액트 및 파이어베이스를 활용한 서비스 개발

  • 프론트엔드 : 스타일링과 프론트엔드 데이터 흐름에 초점
  • 백엔드 : 파이어베이스 docs를 바탕으로 서버 구축

백엔드

Firebase

  • firebase.js
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries

// Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: process.env.REACT_APP_API_KEY,
  authDomain: process.env.REACT_APP_AUTH_DOMAIN,
  projectId: process.env.REACT_APP_PROJECT_ID,
  storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
  messagingSenderId: process.env.REACT_APP_MESSAGING_ID,
  appId: process.env.REACT_APP_APP_ID,
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const db = getFirestore(app);

Firebase를 사용하기 위해 Firebase 초기화 과정을 진행하였습니다. 해당 서비스에서는 Firebase의 Authentication와 Cloud Firestore를 사용했습니다. Storage에 이미지를 저장하여 사용하기는 했지만 저장 및 불러오는 과정이 없어 불러오지는 않았습니다.


사용자 인증 (Google 인증)

  • src/pages/Auth.js
import React from "react";
import { GoogleAuthProvider, signInWithPopup } from "firebase/auth";
import { auth } from "../firebase";

const Auth = () => {
  const provider = new GoogleAuthProvider();
  // 팝업을 통한 구글 로그인입니다.
  const handleGoogleLogin = async () => {
    await signInWithPopup(auth, provider);
  };
  return (
    <div>
      <span style={{ cursor: "pointer" }} onClick={handleGoogleLogin}>
        Google 로그인으로 시작하기
      </span>
    </div>
  );
};

export default Auth;

"firebase/auth"로부터 GoogleAuthProvider, signInWithPopup를 import했습니다. GoogleAuthProvider를 사용하여 provider을 만들고 이를 signInWithPopup에 넣어 Google을 사용하여 인증과정을 통해 로그인을 진행하였습니다.


  • src/App.js
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { auth } from "./firebase";
import { onAuthStateChanged } from "firebase/auth";
import { useState } from "react";
import Auth from "./pages/Auth";
import Home from "./pages/Home";
import Detail from "./pages/Detail";
import Seat from "./pages/Seat";

function App() {
  // 로그인 유무와 로그인시 유저의 정보를 상태 관리합니다.
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  const [user, setUser] = useState(null);
  // firebase의 onAuthStateChanged를 사용하여 유저를 확인합니다.
  useState(() => {
    onAuthStateChanged(auth, (user) => {
      if (user) {
        setIsLoggedIn(true);
        setUser(user);
      } else {
        setIsLoggedIn(false);
      }
    });
  }, []);
  return (
    <Router>
      <Routes>
        {/* 로드인 유무에 따라 다른 컴포넌트를 라우팅 합니다. */}
        {isLoggedIn ? (
          <Route path="/" element={<Home user={user} />} />
        ) : (
          <Route path="/" element={<Auth />} />
        )}
        <Route path="/cafe/:id" element={<Detail />} />
        <Route path="/cafe/:id/seat" element={<Seat />} />
      </Routes>
    </Router>
  );
}

export default App;

인증과정을 통과하여 로그인을 성공하면 onAuthStateChanged를 이용하여 유저를 확인할 수 있습니다. 이를 이용해 로그인 유무 상태와 유저의 정보를 상태에 저장하여 관리할 수 있습니다.

데이터베이스로부터 데이터 읽기

  • src/pages/Home.js
import React, { useEffect, useState } from "react";
import { collection, getDocs } from "firebase/firestore";
import { db } from "../firebase";
import { Link } from "react-router-dom";

const Home = ({ user }) => {
  const [cafes, setCafes] = useState([]);
  // 부모 컴포넌트로 받은 유저의 아이디와 firestore에 저장되어있는 카페의 유저 아이디 배열과 비교하여 카페를 구분합니다.
  const getCafes = async () => {
    const querySnapshot = await getDocs(collection(db, "cafes"));
    querySnapshot.forEach((doc) => {
      if (doc.data().users.includes(user.uid)) {
        // 카페 정보와 카페 id를 담아 카페를 저장합니다.
        const cafe = { ...doc.data(), id: doc.id };
        setCafes((pre) => [...pre, cafe]);
      }
    });
  };
  useEffect(() => {
    getCafes();
  }, []);
  return (
    <div>
      {cafes.map((cafe) => (
        <Link key={cafe.id} to={`/cafe/${cafe.id}`} state={cafe}>
			{cafe.name}
        </Link>
      ))}
    </div>
  );
};

export default Home;

Firestore에서 데이터를 읽기 위해서 "firebase/firestore"로부터 getDocs, collection를 import했습니다. 이를 이용해 데이터의 collection 이름을 넣어 querySnapshot울 받을 수 있습니다. 부모 컴포넌트로 부터 받은 user 정보의 id 이용하여 카페 데이터에 담긴 구독 유저의 id와 비교하여 화면에 출력할 데이터를 구할 수 있습니다.


프론트엔드

Router

  • src/App.js
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { auth } from "./firebase";
import { onAuthStateChanged } from "firebase/auth";
import { useState } from "react";
import Auth from "./pages/Auth";
import Home from "./pages/Home";
import Detail from "./pages/Detail";
import Seat from "./pages/Seat";
import Styles from "./styles/App.module.css";

function App() {
  // 로그인 유무와 로그인시 유저의 정보를 상태 관리합니다.
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  const [user, setUser] = useState(null);
  // firebase의 onAuthStateChanged를 사용하여 유저를 확인합니다.
  useState(() => {
    onAuthStateChanged(auth, (user) => {
      if (user) {
        setIsLoggedIn(true);
        setUser(user);
      } else {
        setIsLoggedIn(false);
      }
    });
  }, []);
  return (
    <Router>
      <div className={Styles.app}>
        <Routes>
          {/* 로드인 유무에 따라 다른 컴포넌트를 라우팅 합니다. */}
          {isLoggedIn ? (
            <Route path="/" element={<Home user={user} />} />
          ) : (
            <Route path="/" element={<Auth />} />
          )}
          <Route path="/cafe/:id" element={<Detail />} />
          <Route path="/cafe/:id/seat" element={<Seat />} />
        </Routes>
      </div>
    </Router>
  );
}

export default App;

react-router-dom을 사용하여 Router를 구성했습니다. 유저의 로그인 유무 상태와 삼항 연상자를 이용하여 같은 path에 다른 element가 출력됩니다. 로그인이 되었다면 유저의 정보를 prop형태로 페이지에 전달해줍니다.


Home

  • src/pages/Home.js
import React, { useEffect, useState } from "react";
import { collection, getDocs } from "firebase/firestore";
import { db } from "../firebase";
import { Link } from "react-router-dom";
import Styles from "../styles/Home.module.css";
import Cafe from "../components/Cafe";

const Home = ({ user }) => {
  const [cafes, setCafes] = useState([]);
  // 부모 컴포넌트로 받은 유저의 아이디와 firestore에 저장되어있는 카페의 유저 아이디 배열과 비교하여 카페를 구분합니다.
  const getCafes = async () => {
    const querySnapshot = await getDocs(collection(db, "cafes"));
    querySnapshot.forEach((doc) => {
      if (doc.data().users.includes(user.uid)) {
        // 카페 정보와 카페 id를 담아 카페를 저장합니다.
        const cafe = { ...doc.data(), id: doc.id };
        setCafes((pre) => [...pre, cafe]);
      }
    });
  };
  useEffect(() => {
    getCafes();
  }, []);
  return (
    <div className={Styles.home}>
      <h1 className={Styles.title}>구독 리스트</h1>
      <div className={Styles.cafeList}>
        {cafes.map((cafe) => (
          <Link key={cafe.id} to={`/cafe/${cafe.id}`} state={cafe}>
            <Cafe
              name={cafe.name}
              url={cafe.imageUrl}
              address={cafe.address}
              types={cafe.types}
            />
          </Link>
        ))}
      </div>
    </div>
  );
};

export default Home;

카페 데이터들을 map을 이용하여 출력합니다. 출력되는 Cafe 컴포넌트에게 필요한 데이터를 prop형태로 전달해 줍니다. Cafe 컴포넌트를 클릭하면 디테일 페이지로 넘어가기 위해 react-router-dom의 Link를 사용합니다. 디테일 페이지는 param을 이용해 페이지를 구분하기에 템플릿 리터럴을 통해 id값을 추가하여 주소를 구성합니다. 또한 카페 데이터를 넘겨주기위해 Link의 state속성에 데이터를 넣어줍니다.


Detail

  • src/pages/Detail.js
import React from "react";
import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
import Styles from "../styles/Detail.module.css";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
  faClock,
  faHeart,
  faLocationPin,
  faPhone,
} from "@fortawesome/free-solid-svg-icons";
import { faSquareInstagram } from "@fortawesome/free-brands-svg-icons";

const Detail = () => {
  const navigate = useNavigate();
  // useParams을 통해 카페 id를 가져옵니다.
  const params = useParams();
  //  Link state를 통해 받아온 카페 데이터 입니다.
  const location = useLocation();
  return (
    <div className={Styles.detail}>
      <div className={Styles.detailHeader}>
        <div className={Styles.detailBackBtn} onClick={() => navigate(-1)}></div>
        <h1 className={Styles.detailTitle}>{location.state.name}</h1>
        <div className={Styles.detailLike}>
          <FontAwesomeIcon icon={faSquareInstagram} color="black" />
          <FontAwesomeIcon icon={faHeart} />
        </div>
      </div>
      <div className={Styles.detailBox}>
        <img src={location.state.imageUrl} className={Styles.detailImg} />
        <div className={Styles.detailContent}>
          <div className={Styles.detailInfo}>
            <div>
              <FontAwesomeIcon icon={faLocationPin} />
              {location.state.address.slice(0, 12)}...
            </div>
            <div>
              <FontAwesomeIcon icon={faClock} />
              {location.state.time.open} - {location.state.time.close}
            </div>
            <div>
              <FontAwesomeIcon icon={faPhone} />
              {location.state.phone}
            </div>
          </div>
          <Link to={`/cafe/${params.id}/seat`} state={location.state}>
            좌석 현황
          </Link>
        </div>
        <div className={Styles.detailMenu}>
          <span className={Styles.detailMenuTitle}>메뉴</span>
          <ul>
            {location.state.menu.map((item) => (
              <li>
                {item.name} ----- {item.price}</li>
            ))}
          </ul>
        </div>
      </div>
    </div>
  );
};

export default Detail;

useParams을 통해 id를 가져올 수 있습니다. useLocation를 통해 Link state를 통해 전달한 데이터를 받아올 수 있습니다. useNavigate를 통해 페이지의 이동을 관리할 수 있습니다.


Seat

  • src/pages/Seat.js
import React, { useEffect, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import Styles from "../styles/Seat.module.css";

const Seat = () => {
  const [seats, setSeats] = useState([]);
  const navigate = useNavigate();
  const location = useLocation();
  // 좌석은 객체로 구성하여 자리가 비어있으면 true, 차 있으면 false, 존재하지 않는 자리면 null값을 가지고 있습니다.
  //좌석 데이터를 map을 사용하기위해 배열 상태를 만들어줍니다.
  const seatArray = () => {
    for (let i = 0; i < Object.keys(location.state.seat).length; i++) {
      setSeats((pre) => [...pre, location.state.seat[`seat-${i + 1}`]]);
    }
  };
  useEffect(() => {
    seatArray();
  }, []);
  return (
    <div className={Styles.seat}>
      <div className={Styles.seatHeader}>
        <div className={Styles.seatBackBtn} onClick={() => navigate(-1)}></div>
        <h1 className={Styles.seatTitle}>{location.state.name}</h1>
        <div></div>
      </div>
      <div className={Styles.seatContainer}>
        {seats.map((seat, index) => (
          <div
            key={index}
            className={Styles.seatContainerList}
            // seat의 값에 따라 Style을 변경해줍니다.
            style={{
              backgroundColor: seat && "#30a2ff",
              display: seat === null && "none",
            }}
          ></div>
        ))}
      </div>
    </div>
  );
};

export default Seat;

좌석에 관한 데이터가 객체형태라 map을 통해 출력하기 위해 배열형태로 바꿔주었습니다.

결과


📝 후기

토이 프로젝트의 프론트엔드와 백엔드를 모두 만들어보니 데이터의 형태에 따라 코드의 형태가 및 효율성이 크게 변화가 될 것이라 생각했습니다. 데이터를 서버로 부터 받아와 사용할때 초기에 어는정도 시간이 걸려 용량이 큰 사진과 같은 데이터는 로딩시간이 필요했습니다. 이후 prop을 통해 데이터를 전달할때는 그러한 로딩 과정없이 바로 출력되는것을 보고 유저의 서비스 경험을 위해 데이터를 불러오는 과정또한 매우 중요하다고 생각했습니다.



본 후기는 유데미-스나이퍼팩토리 10주 완성 프로젝트캠프 학습 일지 후기로 작성 되었습니다.

#프로젝트캠프 #프로젝트캠프후기 #유데미 #스나이퍼팩토리 #웅진씽크빅 #인사이드아웃 #IT개발캠프 #개발자부트캠프 #리액트 #react #부트캠프 #리액트캠프

profile
기록하고 배우고 시도하고

0개의 댓글