9/2 TID : 파이어베이스 연동하기

그른손·2023년 9월 2일
0

뭐했나요?

포트폴리오 애플리케이션에 파이어베이스에서 제공하는 Firestore database와 Firebase Storage로 문서와 이미지파일을 저장하고 불러올 수 있있도록 연결했습니다.

왜 그랬나요?

지금까지는 애플리케이션을 그려내는 데 필요한 이미지 파일 등을 클라이언트에 다 쑤셔넣고 빌드해버렸는데, 실제로는 그렇게 하지 않을 것 같다는 생각을 했습니다. (클라이언트가 무거워지니까요!) 이전에 Peony 프로젝트에서 사용했던 Firestore database를 떠올리고, '여기에다 클라이언트에서 그려줘야 하는 데이터들을 넣어놓고 불러오는 식으로 하면 괜찮지 않을까?' 라고 생각했습니다.

어떻게 했나요?

Firebase 초기화

우선 Firebase 프로젝트에 내 앱을 추가해주고 (놀랍게도 아직 안하고 있었음), Firebase SDK를 앱에 설치합니다. (npm i firebase) firebase/index.ts에 firebase 기본 config 설정을 해준 후 초기화합니다.

import { initializeApp } from "firebase/app";

export 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,
  measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID,
};

export const app = initializeApp(firebaseConfig);

apiKey 등은 보안을 위해 .env에서 환경변수로 관리해야 합니다! 다만 typescript를 사용하고 있기 때문에, 이 환경변수들의 타입을 타입스크립트가 알 수 있도록 정의해줄 필요가 있습니다.

//vite-env.d.ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
  VITE_FIREBASE_API_KEY: string;
  VITE_FIREBASE_AUTH_DOMAIN: string;
  VITE_FIREBASE_PROJECT_ID: string;
  VITE_FIREBASE_STORAGE_BUCKET: string;
  VITE_FIREBASE_MESSAGING_SENDER_ID: string;
  VITE_FIREBASE_APP_ID: string;
  VITE_FIREBASE_MEASUREMENT_ID: string;
}

vite + react + typescript로 앱을 시작했을 때 생성된 vite-env.d.ts 파일을 여기서 사용합니다. 이 파일 내에 각각의 환경변수들의 타입을 정의할 수 있습니다.

Firestore database 초기화

다음은 파이어베이스 db를 초기화해줄 차례입니다. firebase/firestore/index.ts에 간단하게 초기화 코드를 작성합니다.

import { getFirestore } from "firebase/firestore";
import { app } from "../index";

export const db = getFirestore(app);

useFetchCollection 훅으로 데이터 불러오는 로직 모듈화

이제 이렇게 정의된 db에 문서를 추가하거나, 업데이트하거나, 불러올 수 있습니다! Peony 프로젝트를 구성할 때는 CRUD에 해당하는 addDoc, getDoc, updateDoc, deleteDoc 등의 기능을 만들어주었는데, 이번에는 일단 불러오는 기능만 구현할 예정이므로 서버에서 데이터를 불러오는 커스텀 훅을 구현해주었습니다.

import { useState, useEffect } from "react";
import { collection, getDocs } from "firebase/firestore";
import { db } from "../../firebase/firestore";

const useFetchCollection = <T>(collectionPath: string) => {
  const [data, setData] = useState<T[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      try {
        const querySnapshot = await getDocs(collection(db, collectionPath));
        const tempArray: T[] = [];
        querySnapshot.forEach((doc) => {
          tempArray.push(doc.data() as T);
        });
        setData(tempArray);
      } catch (e) {
        if (e instanceof Error) {
          setError(e);
        } else {
          setError(new Error("An unknown error occurred."));
        }
      }
      setLoading(false);
    };

    fetchData();
  }, [collectionPath]);

  return { data, loading, error };
};

export default useFetchCollection;

useFetchCollection은 불러오고자 하는 문서의 타입(제네릭 타입 T를 이용해 여러 유형의 데이터 컬렉션에 유연하게 적용 가능) 과 경로를 받아, 특정 경로의 문서를 배열 형태로 모두 가져오는 훅입니다.

data는 가져온 데이터를 배열 형태로 저장합니다. <T[]>란, T 변수에 정의된 타입에 해당하는 요소들이 들어있는 배열을 의미합니다. tempArray도 동일한 타입으로 설정해줍니다.

데이터를 가져오는 절차는 아래와 같습니다.

  • setLoading(true)로 로딩 상태를 true로 설정합니다.
  • Firestore에서 데이터를 비동기적으로 가져옵니다.
  • 성공적으로 데이터를 가져오면, data 상태를 업데이트합니다.
  • 오류가 발생하면, error 상태를 업데이트합니다.
    • error가 Error 객체의 인스턴스이면 이를 error 상태로 업데이트
    • Error 객체의 인스턴스가 아닐 경우, unknown error 메세지로 error 상태 업데이트 (예외처리~!)
  • 마지막으로 setLoading(false)로 로딩 상태를 false로 설정합니다.

리턴값인 data, loading, error를 useFetchCollection 훅을 사용한 컴포넌트에서 활용하여 불러온 데이터와 로딩, 에러 정보를 띄워줄 수 있습니다.

이 useFetchCollection 훅은 기존에 목적으로 한 Skill 정보 (타이틀, 이미지, 디스크립션으로 이루어짐)를 가져오는 데 뿐만 아니라, 추후에 db에서 특정 콜렉션을 모두 불러와야 하는 경우가 있을 때에도 재사용이 가능합니다. 여러 프로젝트 정보를 띄워줘야 하는 Projects 페이지 등에서도 쉽게 활용할 수 있습니다! 재사용성! 재사용성! 재사용성!!!

다음으로 Skills 페이지에서 실제로 useFetchCollection 훅을 적용해보았습니다.

export default function Skills() {
  const darkMode = useDarkMode();
  const { data: skills, loading, error } = useFetchCollection<Skill>("skills");

  return (
    <div
      className={`${
        darkMode ? "text-white bg-slate-500" : "text-gray-700"
      } transition-all duration-300 ease-in-out`}
    >
      <section className="w-screen h-screen flex flex-col justify-center items-center">
        {loading && <p>Loading...</p>}
        {error && <p>Error: {error.message}</p>}
        {skills &&
          skills.map((skill, index) => <SkillBox key={index} item={skill} />)}
        <Link
          to="/"
          className={`${
            darkMode ? "text-blue-300" : "text-blue-500"
          } mt-10 inline-block`}
        >
          return to main
        </Link>
        <DarkModeButton />
      </section>
    </div>
  );
}

useFetchCollection에 Skill 타입을 넣고, "skills" 경로를 인자로 전달해 data, loading, error를 가져오고, data 배열에 map을 걸어 각 skill을 SkillBox 컴포넌트의 item 프롭으로 전달해 렌더링합니다.

Skill 타입은 src/types/index.ts에서 다른 타입들과 함께 관리하기로 했습니다.

//types/index.ts

export interface Skill {
  title: string;
  icon: string;
  description: string;
}

Firebase Storage 사용해서 이미지와 비디오 등을 저장, 관리하기

Firebase의 Firestore는 NoSQL 데이터베이스로 텍스트, 숫자, 리스트, 맵 등의 데이터 구조를 저장하도록 설계되었습니다. Firestore에는 이미지나 비디오와 같은 바이너리 데이터를 직접 저장할 수 없습니다. 이런 종류의 데이터를 저장하려면 Firebase의 다른 서비스인 Firebase Storage를 사용해야 합니다.

내 앱에서 파이어베이스 스토리지에 파일을 업로드할 수 있게 하려면 그에 따른 설정이 필요합니다. 다만, 본 프로젝트에서는 이미지 업로드 기능은 일단 구현 예정이 없기 때문에 (추후에 관리자 단에서 이미지, 데이터 업로드를 구현할 수는 있겠지만) 일단 콘솔에서 스토리지에 수동으로 직접 업로드해보았습니다.

이렇게 업로드해주고, '액세스 토큰'란의 토큰 정보를 클릭하면 접근용 토큰을 동반한 이미지 주소가 카피됩니다. 이제 이걸 firestore db의 각 skill 문서에 icon 필드값으로 할당해주면, 이를 img의 src로 넣어서 스토리지의 이미지를 불러올 수 있습니다!

쨘~
이제 db에서 skills 콜렉션을 불러와서 각각 SkillBox로 렌더링하면, 아이콘 이미지가 담긴 상자가 표시되고 클릭하면 expand되면서 title과 description이 표시됩니다! 멋지네요! (사실 안멋져서 캡쳐 안함)
이렇게 해 두면, 새로 익힌 스킬이 있을 때에도 db에 정보를 추가해주면 (굳이 정보를 클라이언트 코드에 추가한 뒤 빌드, 배포하지 않아도) 자동으로 앱 페이지에도 반영이 되겠죠! 와! 멋져!

대단한 건가요?

저한테는요!
기존에 뭣도 모르고 클라이언트에 모든 데이터를 우겨넣을 때에 비해서 클라이언트를 가볍게 만들 수 있고, 메인 프로젝트에서 배운 이미지 처리 로직 (S3 저장소에 이미지를 업로드하고, 이를 imageUrl로 반환받아 서버에 저장)을 다시 한 번 적용해볼 수 있었다는 점에서 의의가 있는 것 같습니다. 나중에 사진을 포함한 CRUD 기능을 갖춘 앱을 만들 때에도 비슷한 방식으로 구현할 수 있을 것 같구요! (그 때는 firebase storage에 이미지파일을 업로드하는 방법도 배울 수 있을 것 같아요)
또한 useFetchCollection 훅이 분리되어 있어서, react-query를 배워서 useQuery를 사용하는 방식으로 마이그레이션할 때에도 편리하게 적용할 수 있을 것 같습니다. firebase와 호환이 잘 되는지는 정확히 알진 못하지만...

근데 이제 뭐함?

  • SkillBox와 Skills 페이지 레이아웃을 수정해서 보기좋게 정렬하는 법 알아보기
  • 기술스택과 간단한 설명, 기술스택 아이콘 db에 업로드하기
  • 다크모드를 적용했을 때 SkillBox의 스타일 변화 고려하기

그 다음으로는...

  • Projects 페이지와 ProjectList, ProjectDetail(?) 컴포넌트 구현하기
  • Projects 페이지 레이아웃 멋지게 꾸미기
  • About 페이지
  • Contacts 페이지
  • 그리고 모든 페이지 만들 때 반응형 멋지게 적용하기!
  • 애니메이션...최선을 다해 보기

정도의 순서가 될 것 같습니다! About이나 Contacts의 경우는 보여줘야 하는 정보가 한정적이라 추가나 삭제 등의 업데이트도 비교적 수월할 것 같아서, 여러 개의 정보를 가져와서 보여줘야 하는 Skills와 Projects의 우선순위를 좀 더 높게 잡아보았습니다.

기분이 좋군요! 속도는 거북이지만!🐢

profile
프론트엔드 개발자

0개의 댓글