한시간만에 Firebase Hosting에 Vite React 프로젝트 배포하기: 마니또 매칭 프로젝트

미키오·2024년 11월 17일
1

Manitto-Maker

목록 보기
2/4

들어가며..

어쩌다보니 11월 말에 하게 될 동창회 준비 총대를 매게 되어서
어떠한 컨텐츠를 준비할까 고민하던 중 연말 파티 분위기에 맞게 마니또
(aka 비밀 산타 🎅) 아이디어가 나왔다.

랜덤 매칭 프로그램을 어떤 것을 돌릴까 고민하다가
기존의 사다리타기는 자기 자신이 걸릴 확률도 있고,
무엇보다 누군가(주최자)는 전체 정보를 보고 전달을 해야하는데
그럼 나의 재미는 누가 보장해주나..!!


이런 모임을 위해 이미 시중에 너무나 괜찮은 앱이 존재했지만
별도의 매칭 결과를 이메일이나 휴대폰 번호로 알리기 때문에
모두의 이메일이나 번호를 받고 하나하나 기입해야 했다.

시간이 충분했다면 이 앱을 사용했을것 같지만
단체 모임 카톡 특성상 늦게 카톡을 확인하는 사람도 많고
어제 하루 내에 공지를 다 했어야하기 때문에

차라리 내가 랜덤으로 돌리고 별도의 비밀번호를 지정해서
결과만 본인이 확인할 수 있는 웹사이트를 만드는게 낫다고 판단했다.

🎯 프로젝트 요구사항

내가 생각한 플로우는 다음과 같았다 :

주최자가 참가자 이름을 등록 ➡️ 이름 목록에서 랜덤으로 마니또 매칭 ➡️ 랜덤 비밀번호도 함께 발급 ➡️ 참가자는 본인의 이름과 비밀번호를 입력 ➡️ 자신의 마니또 확인

1. 주요 기능

이를 위한 핵심 기능은 다음과 같다 :

💡 참가자 등록: 주최자가 이름을 입력해 참가자들을 등록.

💡 랜덤 매칭: 입력된 이름 목록에서 랜덤으로 마니또를 매칭.

💡 결과 확인: 본인의 이름과 비밀번호를 입력하면 자신의 마니또를 확인.

💡 비밀번호 관리: 각 참가자는 랜덤으로 생성된 비밀번호(멸종 위기 동물 이름)로 보호된 결과를 확인 가능.

2. 시각적 요구사항

❄️ 연말 분위기: 눈 내리는 애니메이션, 크리스마스 테마 색상

❄️ 직관적인 UI: 사용하기 쉬운 입력 폼, 버튼, 결과 화면.

❄️ 모바일 및 데스크톱 지원: 다양한 화면 크기에 적합한 반응형 디자인.

3. 기술스택

현 시점 나에게 가장 빠르게 구현부터 배포까지 할 수 있는 스택들로만 구성해보았다.

📚 React (Vite): 빠른 개발 환경과 빌드 시스템.

📚 MUI (Material-UI): 현대적인 디자인 컴포넌트 라이브러리 사용.

📚 Firebase Hosting: 애플리케이션 배포 및 관리.

📚 Firebase Firestore: 참가자 데이터와 매칭 결과를 저장.

환경설정

Vite React 프로젝트 생성

터미널에서 아래 명령어를 실행하여 새 Vite 프로젝트를 생성한다:

npm create vite@latest manitto --template react

프로젝트 디렉토리로 이동하고 종속성을 설치:

cd manitto
npm install

Firebase 프로젝트 생성

Firebase Hosting을 사용하려면 Firebase 프로젝트가 필요하다.

  1. Firebase Console로 이동
  2. '프로젝트 만들기' 버튼을 클릭
  3. 프로젝트 이름을 입력하고 설정을 완료
  4. Firebase Hosting을 활성화

Firebase CLI 설치 및 초기화

Firebase CLI 설치

Firebase CLI를 설치하여 Firebase Hosting을 관리할 수 있다.
터미널에서 아래 명령어를 실행해보자:

npm install -g firebase-tools

CLI가 설치되었는지 확인하려면:

firebase --version

이제 Firebase CLI를 통해 프로젝트를 초기화한다.

firebase init

그렇다면 이제 다음과 같은 문구가 나올 것이다.

초기화 과정에서 선택 항목

Which Firebase features do you want to set up?

  • Firestore, Hosting 선택.

Use an existing project:

  • Firebase Console에서 생성한 프로젝트 선택.

What do you want to use as your public directory?

  • dist 입력 (Vite의 빌드 폴더).

Configure as a single-page app (rewrite all URLs to /index.html)?

  • y 입력.

Firestore와 React 연동

React 프로젝트에 Firebase SDK를 추가한다:

npm install firebase

Firebase를 초기화하고 Firestore를 연결한다.
이제 리액트의 /src 내에 firebase.jsx를 만들어준다.

firebase console 설정으로 다시 들어가서 SDK 설정 및 구성 파일을 복사해온다.

import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";

const firebaseConfig = {
  apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
  authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
  projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
  storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
  appId: import.meta.env.VITE_FIREBASE_APP_ID,
};

const app = initializeApp(firebaseConfig);
const db = getFirestore(app);

export { db };

이때 사용되는 각종 key와 id들은 환경변수화해서 .env로 관리하길 권장한다.
또한 .env.production 파일을 사용해 미리 Vite 앱의 환경 변수를 설정하는 것도 좋다.

구현

이제 본격적으로 구현해보자.

참가자 등록

이후 참가자 이름을 Firestore에 저장하는 addUser 함수와 입력 UI를 구현했다.

Firestore에 이름 저장 (src/firebase/addUser.js)

import { collection, addDoc } from "firebase/firestore";
import { db } from "./firebase";

const addUser = async (name) => {
  try {
    const docRef = await addDoc(collection(db, "users"), {
      name,
      createdAt: new Date(),
    });
    console.log("Document written with ID: ", docRef.id);
  } catch (error) {
    console.error("Error adding user: ", error);
  }
};

export default addUser;

이름 입력 UI (src/components/NameInput.jsx)

import { useState } from "react";
import { TextField, Button } from "@mui/material";
import addUser from "../firebase/addUser";

const NameInput = () => {
  const [name, setName] = useState("");

  const handleAddName = async () => {
    if (name.trim() === "") return alert("이름을 입력하세요!");
    try {
      await addUser(name);
      alert("참가자가 등록되었습니다!");
      setName("");
    } catch (error) {
      console.error("Error:", error);
    }
  };

  return (
    <div>
      <TextField value={name} onChange={(e) => setName(e.target.value)} />
      <Button onClick={handleAddName}>참가자 추가</Button>
    </div>
  );
};

export default NameInput;

랜덤 매칭

Firestore에서 참가자 목록을 가져와 랜덤 매칭을 수행하고 결과를 Firestore에 저장한다.

랜덤 매칭 및 저장 (src/firebase/saveMatches.js)

import { collection, addDoc } from "firebase/firestore";
import { db } from "./firebase";

const saveMatchesToFirestore = async (matches) => {
  try {
    await addDoc(collection(db, "matches"), {
      matches,
      createdAt: new Date(),
    });
    console.log("Matches saved!");
  } catch (error) {
    console.error("Error saving matches:", error);
  }
};

export default saveMatchesToFirestore;

랜덤 매칭 함수

names.sort(() => Math.random() - 0.5)를 이용하여 참가자 목록을 무작위로 섞었다.

const handleStartMatching = async () => {
  const shuffled = names.sort(() => Math.random() - 0.5);
  const result = shuffled.map((name, idx) => ({
    giver: name,
    receiver: shuffled[(idx + 1) % shuffled.length],
    password: getRandomAnimal(),
  }));
  await saveMatchesToFirestore(result);
};

또한 (idx + 1) % shuffled.length를 사용해 1:1 매칭을 보장하는 순환 구조를 사용하여 모든 참가자가 반드시 한 명의 "receiver"를 갖도록 하였다. 즉, 모든 사람이 "giver""receiver"의 역할을 수행하므로, 특정 참가자가 제외되는 상황이 없다.

또한 getRandomAnimal()을 사용해 각 매칭마다 고유 비밀번호를 생성하므로 보안과 독립성을 더했다.

참고 : getRandomAnmial()

// 한국 멸종 위기종 동물 이름 목록
const endangeredAnimals = [
  "수달",
  "산양",
  "반달가슴곰",
  "삵",
  "저어새",
  "따오기",
  "황새",
  "붉은박쥐",
  "독수리",
  "검은머리갈매기",
  "표범장지뱀",
  "가시고기",
  "긴꼬리딱새",
  "금개구리",
  "두꺼비",
  "붉은배새매",
  "참매",
  "검독수리",
  "양비둘기",
  "검은머리촉새",
  "호사비오리",
  "노랑부리저어새",
  "알락꼬리마도요",
  "흰목물떼새",
  "새홀리기",
  "검은머리딱새",
  "붉은점모시나비",
  "왕은점표범나비",
  "참호박벌",
  "날개띄기민물장어",
  "꼬치동자개",
  "팔색조",
  "따개비고둥",
  "큰주홍부전나비",
  "주름날개멸치",
  "자주방게",
  "장수하늘소",
  "황금박쥐",
  "백조",
  "붉은여우",
  "늑대",
  "마라도딱새",
  "울릉도독도박쥐",
];

export const getRandomAnimal = () => {
  const index = Math.floor(Math.random() * endangeredAnimals.length);
  return endangeredAnimals[index];
};

결과 확인

Firestore에서 특정 사용자의 결과를 가져오는 함수이다.

결과 검색 (src/firebase/fetchMatches.js)

import { collection, getDocs, query, where } from "firebase/firestore";
import { db } from "./firebase";

const fetchMatches = async (name, password) => {
  const q = query(collection(db, "matches"), where("giver", "==", name));
  const querySnapshot = await getDocs(q);
  return querySnapshot.docs.map((doc) => doc.data()).find((item) => item.password === password);
};

export default fetchMatches;

눈 내리는 효과 구현 react-snowfall

하나의 라이브러리로 연말 분위기를 낼 수 있다니..
먼저, react-snowfall 라이브러리 설치

npm install react-snowfall

사용자가 개별적으로 자신의 이름과 비밀번호를 입력하는
ShowPage.jsx 와 통합했다.

import { useState } from "react";
import {
  Box,
  Typography,
  TextField,
  Button,
  Card,
  CardContent,
} from "@mui/material";
import Snowfall from "react-snowfall";
import fetchMatchesFromFirestore from "../firebase/fetchMatches";

const ShowPage = () => {
  const [name, setName] = useState("");
  const [password, setPassword] = useState("");
  const [result, setResult] = useState(null);

  const handleSubmit = async (e) => {
    e.preventDefault();

    try {
      const allMatches = await fetchMatchesFromFirestore();

      const match = allMatches[0].matches.find(
        (doc) => doc.giver === name.trim() && doc.password === password.trim()
      );

      if (match) {
        console.log("Match Found:", match);
        setResult(match);
      } else {
        alert("이름 또는 비밀번호가 잘못되었습니다.");
        setResult(null);
      }
    } catch (error) {
      console.error("Error fetching matches:", error);
      alert("매칭 결과를 불러오는 중 오류가 발생했습니다.");
    }
  };
  return (
    <Box
      sx={{
        height: "100vh",
        width: "100vw",
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        backgroundImage: "url(/background.webp)",
        backgroundSize: "cover",
        backgroundPosition: "center",
        backgroundColor: "black",
        position: "relative",
        overflow: "hidden",
      }}
    >
      <Snowfall color="white" snowflakeCount={80} />
      <Card
        sx={{
          background: "rgba(0, 0, 0, 0.7)",
          color: "white",
          padding: 3,
          borderRadius: 2,
          textAlign: "center",
          width: "90%",
          maxWidth: "400px",
        }}
      >
        <CardContent>
          <Typography variant="h5" sx={{ marginBottom: 2 }}>
            🔍 마니또 확인 🎄
          </Typography>
          <form onSubmit={handleSubmit}>
            <TextField
              variant="outlined"
              fullWidth
              value={name}
              onChange={(e) => setName(e.target.value)}
              placeholder="이름을 입력하세요"
              label="이름"
              sx={{
                marginBottom: 2,
                backgroundColor: "white",
                borderRadius: 1,
              }}
            />
            <TextField
              variant="outlined"
              fullWidth
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              placeholder="비밀번호(동물 이름)"
              label="비밀번호"
              sx={{
                marginBottom: 2,
                backgroundColor: "white",
                borderRadius: 1,
              }}
            />
            <Button
              variant="contained"
              type="submit"
              sx={{
                backgroundColor: "#d32f2f",
                "&:hover": { backgroundColor: "#b71c1c" },
              }}
            >
              마니또 확인
            </Button>
          </form>

          {result && (
            <Box sx={{ marginTop: 3 }}>
              <Typography variant="h6">🎉 결과 🎉</Typography>
              <Typography>
                <strong>{result.giver}</strong>님의 마니또는{" "}
                <strong>{result.receiver}</strong>입니다!
              </Typography>
            </Box>
          )}
        </CardContent>
      </Card>
    </Box>
  );
};

export default ShowPage;

빌드 및 배포

Firebase Hosting에 배포하려면 React 프로젝트를 빌드해야 한다..

npm run build

이 명령어를 실행하면 Vite환경에서는 dist 폴더에 빌드된 정적 파일이 생성된다.
이제 Firebase Hosting에 Vite 앱을 배포한다.

firebase deploy

배포가 완료되면 Firebase CLI가 배포된 URL을 출력한다.

해당 URL을 브라우저에서 열어 앱이 제대로 표시되는지 확인해보자

다행히 기능도 다 잘 되어서 열심히 유저명을 넣구


개별적으로 전달도 하구

내 마니또도 잘 확인했다 👍
모두 즐거운 연말 보내시길~

구현부터 배포까지 1시간
블로그 2시간

profile
교육 전공 개발자 💻

4개의 댓글

comment-user-thumbnail
2024년 11월 17일

와웅 대박 귀엽고 개발자님은 짱이에요 🥹🔥🤍

1개의 답글
comment-user-thumbnail
2024년 11월 17일

와웅 귀엽다 !!! 벌써 크리스마스라니,, 🥹

1개의 답글

관련 채용 정보