리액트 및 파이어베이스를 활용한 서비스 개발
- 프론트엔드 : 스타일링과 프론트엔드 데이터 흐름에 초점
- 백엔드 : 파이어베이스 docs를 바탕으로 서버 구축
// 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에 이미지를 저장하여 사용하기는 했지만 저장 및 불러오는 과정이 없어 불러오지는 않았습니다.
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을 사용하여 인증과정을 통해 로그인을 진행하였습니다.
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를 이용하여 유저를 확인할 수 있습니다. 이를 이용해 로그인 유무 상태와 유저의 정보를 상태에 저장하여 관리할 수 있습니다.
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와 비교하여 화면에 출력할 데이터를 구할 수 있습니다.
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형태로 페이지에 전달해줍니다.
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속성에 데이터를 넣어줍니다.
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를 통해 페이지의 이동을 관리할 수 있습니다.
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을 통해 데이터를 전달할때는 그러한 로딩 과정없이 바로 출력되는것을 보고 유저의 서비스 경험을 위해 데이터를 불러오는 과정또한 매우 중요하다고 생각했습니다.
#프로젝트캠프 #프로젝트캠프후기 #유데미 #스나이퍼팩토리 #웅진씽크빅 #인사이드아웃 #IT개발캠프 #개발자부트캠프 #리액트 #react #부트캠프 #리액트캠프