[Firebase 웹] 파이어스토어에서 데이터 불러오고 쓰기 feat. React

2022. 2. 25. 22:51Firebase

728x90

​​이번 포스트에서는 파이어 베이스의 파이어 스토어(firestore) 사용법에 대해 알아보도록 하자.

파이어 베이스에 대한 포스트를 아직 읽지 않았다면 한번 보고 오길 바란다.

 

2022.02.21 - [Firebase] - [Firebase] 파이어 베이스란? 주요 기능들과 유사 제품들

 

[Firebase] 파이어베이스란? 주요 기능들과 유사 제품들

파이어 베이스 기능들은 하나하나 알아가 보기 전에 Firebase(파이어 베이스)에 대해서 간단히 알아보는 시간을 가져보겠다. 필자도 Firebase를 사용하기 시작한지 얼마 되지는 않았지만, 회사 입사

mingeesuh.tistory.com

 

보통 앱을 만들때 데이터베이스에 수없이 많은 요청을 하듯이, 파이어 베이스를 통해 앱을 개발할 때도 가장 많이 쓰게 되는 건 파이어 베이스의 noSQL 클라우드 데이터베이스인 파이어 스토어다.

 

서버가 있을 경우 프런트엔드 개발자가 데이터베이스를 직접 건드릴 일은 없지만 파이어 베이스를 사용하면 클라이언트에서 바로 데이터베이스에 접근해야 하므로 기본적인 메서드는 숙지해두는게 좋아보인다. 물론 찾아보면 모두 공식문서에 있는 내용이지만 필자가 처음 파이어베이스를 사용했을때 이해되지 않는 부분이 몇개 있었기에 좀 도움이 되고자 포스트를 작성했다.

 

일단 세팅은 아래 포스트를 참고 바란다.

2022.02.23 - [Firebase] - [Firebase] 파이어베이스 시작하기 - 웹 앱 초기 설정

 

[Firebase] 파이어베이스 시작하기 - 웹 앱 초기설정

이번 포스트에서는 파이어 베이스에서 새 프로젝트를 생성하는 방법에 대해 알아보겠다. 마침 관리자 페이지를 만들어야하기 때문에 앞으로 Firebase 관련 예제는 React로 작성을 해보겠다. 일단

mingeesuh.tistory.com

 

버전 8 VS 버전 9

일단 2022년 기준, 웹은 두 가지 버전을 사용할 수 있다. 스택 오버플로우나 블로그들을 보면 아직 웹 버전 8 예제가 많이 보일 텐데 되도록이면 트리 쉐이킹이 되어 좀 더 가볍게 사용이 가능한 웹 버전 9를 사용하는 걸 추천한다.

 

두가지 버전

아래 코드 샘플을 보고 버전 8과 9의 차이를 한번 확인하길 바란다.

버전 9는 필요한 메서드만 불러서 사용하기 때문에  번들 사이즈에서 이점을 얻을 수 있다.

( 코드는 그냥 파이어 스토어에 접근해서 문서들을  전부 가져와서 나열하는 작업으로만 알고 있자.)

 

버전 8 (namespaced)

const fetchData = async (collectionName, order, sort) => {
      // ... try, catch 생략
      const dataSnapShot = await db
        .collection(collectionName)
        .orderBy(order, sort)
        .get();

      const data = dataSnapShot.docs.map(doc => ({
        ...doc.data(),
        id: doc.id
      }));
      return data
  };

버전 9 (modular)

import { db } from "api/firebase";
import {
  collection,
  doc,
  query,
  orderBy,
  getDocs,
} from "firebase/firestore";

const fetchData = async (collectionName, order, sort) => {
      // ... try, catch 생략
      const q = query(
      collection(db, collectionName),
      orderBy("timestamp", "desc"),
      );
      
      const dataSnapShot = await getDocs(q)
      const data = dataSnapShot.docs.map(doc => ({
        ...doc.data(),
        id: doc.id
      }));
     return data; 
  };

앞으로 작성될 포스트에서의 예제는 되도록이면 모듈 방식을 사용하도록 하겠다

 


데이터 모델

그럼 본격적으로 메서드를 설명하기에 앞서 파이어 베이스의 데이터 모델 구조에 대해 간단히 알아보자. 

 

파이어베이스 공식문서

위 그림에서 보이는 것처럼 Cloud Firestore의 주 저장소 단위는 문서 (document)이고 문서들의 집합체를 컬렉션(collection)이라고 한다. 예를 들어 사용자 관련 정보들을 데이터베이스에 저장해야 한다면 Users라는 컬렉션 안에 사용자 정보가 담겨있는 유저 문서를 만들면 된다. 

파이어 스토어에는 스키마가 없기 때문에 유저 문서 필드에 뭐가 들어갈지는 알아서 정하면 된다.

 

컬렉션 - 문서 예시

보통 컬렉션에 문서를 저장할 때는 위와 같이 고유한 아이디를 준다 (유저 id). 이 아이디는 직접 만들어도 되고 문서를 만들게 되면 위 사진처럼 기본적으로 firebase에서 중복이 되지 않는 아이디를 제공해준다.

 


데이터 불러오기

컬렉션에서 전체 문서 가져오기

import {
  collection,
  getDocs,
} from "firebase/firestore";

const fetchUsers = async () => {
    // ... try, catch 생략
    const usersCollectionRef = collection(db, 'users'); // 참조
    const userSnap = await getDocs(usersCollectionRef); // 데이터 스냅 받아오기 - 비동기처리
    const data = userSnap.docs.map(doc => ({
        ...doc.data(),
        id: doc.id
    }));
return data;
}

전체 문서를 가져올 때는 getDocs 메서드에 유저 컬렉션 참조를 넘겨주면 된다.

그럼 데이터베이스의 한 공간을 사진을 찍어서 기억한다는 의미로 snapshot이란 걸 반환하는데 이 문서들의 데이터 값(data())과 id를 받아주면 된다. id 같은 경우는 추후에 데이터를 수정하거나 삭제할 때 필요함으로 id값을 데이터 필드에 따로 기재하지 않는 이상 함께 반환해주면 좋다. 수정을 하지 않더라도 어떤 데이터에 대한 유니크한 값이 하나 있는 건 언제나 유용한 것 같다.

 

컬렉션에서 특정 아이디를 가진 문서 가져오기

import {
  doc,
  getDoc,
} from "firebase/firestore";

const fetchUser = async (uid) => {
    // ... try, catch 생략
    const userRef = doc(db, "users", uid);
    const userSnap = await getDoc(userRef); // 데이터 스냅 받아오기 - 비동기처리
    if (userSnap.exists()){
      return userSnap.data();
    }
    return null;

return data;
}

문서 이름 (id)를 통해 불러오기 위해선 collection 대신 doc, getDocs 대신 getDoc을 사용해주면 된다.

doc의 세 번째 인자로 문서 이름을 넣어주면 해당 문서의 참조가 반환이 되고 getDoc 메서드에 이 참조값을 전달해주면 문서의 스냅숏을 반환받을 수 있다. 이 스냅숏은 문서 하나를 가리키기 때문에 userSnap.exists()로 해당 문서를 찾았는지 못 찾았는지 여부만 확인 후 해당 유저 데이터를 반환해주면 된다.

 

 실시간으로 문서 업데이트 가져오기 (onSnapshot)

 

문서 하나 

import { useState, useEffect } from "react";
import { doc, onSnapshot } from "firebase/firestore";

  //...생략 (훅으로 사용)
  const [user, setUser] = useState(null);
  useEffect(() => {
    const fetchUser = async (uid) => {
      // ... try, catch 생략
      const userRef = doc(db, "users", uid);
      const unsub = onSnapshot(userRef, (document) => {
        setUser(document.data());
      });
      return unsub;
    };
    return fetchUser(uid);
  }, [doc, onSnapshot,uid]);

여러 문서 

// 위 코드샘플에서 해당 부분만 변경
const q = query(collection(db, "users"), where("uid", "==", uid));
onSnapshot(q, (querySnapshot) => {
  const users = [];
  querySnapshot.forEach((doc) => {
      users.push({doc.data()});
  });
  setUsers(users);
//...생략

 

파이어 스토어가 매력적인 이유 중 하나는 실시간 업데이트를 수신할 수 있다는 점이다. 

위 예제와 같이 사용자 문서에 실시간 데이터를 받아오는 경우는 많지 않겠지만, 알림이라던지, 채팅 관련 기능을 만들 때

위와 같이 데이터를 불러오게 되면 사용자가 새로고침을 하지 않고도 데이터 변경이 실시간으로 반영된다.

메모리 누수 방지를 위해 꼭 unsubscribe를 해주는 걸 잊지 말자

 


데이터 쓰기 

새로운 문서 생성 (addDoc)

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

const userRef = await addDoc(collection(db, "users"), {
  name: "Min Gee",
  job: "developer",
  joined: serverTimestamp(), // 현재 날짜,시간
  age: 29
});

addDoc에 첫 번째 인자로 컬렉션 참조를 넘겨주고 필드 값을 객체 형식으로 전달해주면 된다.

addDoc를 사용해 문서를 컬렉션에 추가할 경우 파이어 베이스가 알아서 임의의 아이디 값을 문서의 ID로 지정해  추가한다.

반환되는 값은 해당 문서의 참조이다

 

새로운 문서 생성 - 문서 아이디 지정 (setDoc)

import { doc, setDoc, serverTimestamp } from "firebase/firestore";

const userRef = await setDoc(doc(db, "users", "내가 지정한 아이디"), {
  name: "Min Gee",
  job: "developer",
  joined: serverTimestamp(), // 현재 날짜,시간
  age: 29
});

만약 파이어 베이스가 지정해준 문서 id가 아닌 임의로 id를 지정하고 싶다면 setDoc을 사용하면 된다.

지정한 아이디 값을 문서의 3번째 인자로 전달해주고 나머지는 동일하게 작성하면 된다.

아이디를 넘겨주지 않을 경우 addDoc와 동일하게 작동한다. 

 

실제로. add(...)와. doc(). set(...)은 완전히 동일하므로 더 편리한 것을 사용하면 됩니다.

 

만약에 지정한 아이디를 가진 문서가 이미 존재한다면, 해당 문서를 덮어쓴다.

기존의 필드를 덮어 씌우지 않기 위해선 merge: true 옵션을 지정해주면 된다

 

const userRef = await setDoc(doc(db, "users", "내가 지정한 아이디"), {
  name: "Min Gee"
}, {merge: true});

 

기존 문서 수정  (updateDoc)

import { doc, updateDoc } from "firebase/firestore";

const uid = "사용자 ID";
const userRef = await updateDoc(doc(db, "users", uid), {
 job: "frontEndDeveloper"
});

기존 문서를 업데이트하기 위해선 문서의 참조를 updateDoc의 첫 번째 인자로 전달해주면 된다. 두 번째 인자는 업데이트할 필드만 객체 형식으로 전달해주면 된다.

마찬가지로 반환되는 값은 해당 문서의 참조이다.

 

필드 업데이트를 좀 더 편리하게 할 수 있도록 제공해주는 메서드도 있으니 참고 하자.

 

increment, decrement (숫자 필드 증가, 감소)

import { doc, updateDoc, increment } from "firebase/firestore";

const uid = "사용자 ID";
const userRef = await updateDoc(doc(db, "users", uid), {
 job: "frontEndDeveloper",
 age: increment(1),
 stack: ['react', 'js']
});

arrayUnion, arrayRemove (배열 요소 추가, 삭제)

 

import { doc, updateDoc, arrayUnion } from "firebase/firestore";

const uid = "사용자 ID";
const userRef = await updateDoc(doc(db, "users", uid), {
 job: "frontEndDeveloper",
 age: increment(1),
 stack: arrayUnion("firebase"),
});

deleteField (필드 삭제)

import { doc, updateDoc, deleteField } from "firebase/firestore";

const uid = "사용자 ID";
const userRef = await updateDoc(doc(db, "users", uid), {
 job: deleteField(),
});

 

 


데이터 지우기

import { doc, deleteDoc } from "firebase/firestore";

await deleteDoc(doc(db, "users", "사용자 아이디"));

 

deleteDoc에 삭제할 문서의 아이디를 지정해주면 해당 문서가 삭제된다.

 


 

이상으로 파이어 스토어에서 자주 쓰이는 메서드 몇 개를 살펴봤다.

이 외에도 컬렉션 그룹, 쿼리 등 다뤄야 할 주제가 많지만 이 포스트에서 다 다루기엔 다소 무리가 있기에

이후 포스트에서 나눠서 설명해보도록 하겠다.

 

 

참고자료: https://firebase.google.com/docs

 

문서  |  Firebase

Firebase SDK 참조, 통합 가이드, 샘플 코드, 라이브러리

firebase.google.com