Dev/Projects

[Cloning] (Nomad Code) Twitter clone code with Firebase & React (2) - Firestore, Firebase storage, FileReader API

youngst 2022. 8. 14. 17:00

데이터 베이스 - Firestore

Firestore는 파이어베이스에서 제공하는 NoSQL 데이터베이스 서비스이다.

NoSQL

NoSQL은 말 그대로 SQL이 아닌 방식의 데이터베이스다. SQL형 데이터베이스는 '관계형 데이터베이스'라고도 부르며, 이름처럼 모든 데이터들이 관계로 연결되어있다. 가장 간단하게는 행-열 형태의 데이터를 갖는 엑셀도 SQL과 비슷한 데이터베이스라고 볼 수 있다(실제로 연동하여 사용하기도 한다). 반면 NoSQL 방식은 데이터 간의 관계를 통해 데이터를 구조화하는 것이 아니라, key를 통해 데이터를 접근한다. Javascript 객체의 key, value 모델이 대표적인 NoSQL 형식의 데이터 저장 방식이다. SQL 방식은 인덱스와 데이터 관계를 통해 데이터에 접근해야 하기 때문에 대용량 데이터를 저장하기 어렵고, 각 데이터를 분리하여 유동적으로 사용하기 어렵다. 하지만 데이터를 정렬해야 하거나 일괄적으로 처리할 일이 있을 때에는 SQL 방식의 데이터베이스가 훨씬 편리하다. 즉, SQL과 NoSQL은 어느 것이 더 좋은 것이 아니라 각각의 장단점을 비교하여 적절하게 사용하는 것이 중요하다.

 

Firestore 사용하기

v9부터는 다음과 같은 방식으로 firestore.firestore() 대신 getFirestore() 로 firebase app을 받아 사용한다.

import { getFirestore } from 'firebase/firestore';

export const db = getFirestore(fApp); //fApp은 이전 글 참조

 

Firestore는 컬렉션이라는 단위로 데이터를 저장한다. 컬렉션이 key 값이고, 이를 이용하여 각 Document(value)에 접근할 수 있다. 이 컬렉션을 지정하여 접근하기 위해서는 Query를 상속 받는 CollectionReference가 필요하다. 이것은 collection() 함수를 이용하여 생성할 수 있다. dbService.collection('yweets') 대신 collection(db, 'yweets')를 사용한다.

 

데이터 쓰기

컬렉션 레퍼런스를 통해 원하는 컬렉션을 지정했다면, add가 아닌 addDoc 함수를 통해 데이터(Document)를 기록할 수 있다. 사용법은 아래와 같다.

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

try {
    const docRef = await addDoc(collection(db, 'yweets'), yweetObj);
    console.log(docRef);
    setYweet('');
} catch (error) {
    console.log(error);
}

 

** Firestore addDoc을 실행하는데 아무 응답도 없는(심지어는 오류도 발생도 하지 않았다) 문제가 생겨서 2시간 동안 영어로 검색도 해보고 공식문서도 보고 별짓을 다해봤는데... 알고 보니 문제는 전역 변수를 잘 못 설정한 탓이었다...ㅎㅎ

혹시 addDoc을 실행하는데 아무런 오류도 뜨지 않은채 Promise가 완료되지 않는다면 전역변수(firebaseConfig)를 잘 살펴보자!

 

데이터 읽기

v9의 데이터 읽기는 get이 아닌 add처럼 getDocs인데 주의할 점은 getDoc이 아니다!! 복수형임을 주의하자.

* useState 사용할 때 setState에 함수를 전달하면 이전 값을 매개변수로 받을 수 있음! state 값을 그대로 쓰면 setState 병합처리 때문에 오류가 발생할 수 있으니 내부에서 prev로 처리하는 것이 안전하다.

 

데이터 읽기는 getDocs 대신 onSnapshot이라는 Event Listener를 통해서도 가능하다. onSnapshot은 지정한 컬렉션을 observing하며 해당 컬렉션에 변화가 일어났을 때마다 콜백함수를 실행한다. 콜백함수에는 Firestore 데이터 단위인 Document Snapshot을 전달한다.

  useEffect(() => {
    onSnapshot(collection(db, 'yweets'), (snapshot) => {
      setLoading(true);

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

    });
  }, []);

snapshot.docs에서 제공하는 map 함수를 통해 각 문서(document)들을 매핑하여 저장할 수 있다.

 

데이터 삭제

최신버전의 데이터 삭제는 deleteDoc으로 가능하다.

  const handleDelete = async (e) => {
    const ok = confirm('Are you sure you want to delete this yweet?');

    if (ok) {
      //delete
      await deleteDoc(doc(db, `yweets/${yweet.id}`));
    }
  };

deleteDoc() > doc() > db

 

이미지 업로드 - Firebase storage

Firebase storage는 말 그대로 클라우드 데이터 저장소 서비스이다. storage에서 제공되는 함수들과 Javascript의 fileReader API를 사용하여 이미지 등의 파일을 업로드할 수 있다. 최신버전에서는 다음과 같이 storage app을 만들어 사용한다.

import { getFirestore } from 'firebase/firestore';

export const storage = getStorage(fApp);

input 태그에서 type을 file로 하면 간편하게 파일 업로드 기능을 사용할 수 있다.  FileReader API는 new 키워드를 통해 인스턴스를 만들어 사용해야하며, onloadend 메서드를 통해 전송이 완료된 후의 파일의 정보를 받을 수 있다. 이때 readAsDataURL 메서드를 사용하면 loadend가 끝났을 때 파일은 Data URL의 형태로 해당 event의 target에 저장 되어있다. Data URL에는 이미지 파일이 string의 형태로 저장돼 있으며, 이를 이용하면 thumbnail을 보여줄 수 있다. 이 String을 웹 브라우저 주소창에 붙여넣으면 이미지를 그대로 볼 수 있다.

  const onFileChange = (e) => {
    const {
      target: { files },
    } = e;
    const theFile = files[0];
    const reader = new FileReader();
    reader.onloadend = (finishedEvent) => {
      const {
        currentTarget: { result },
      } = finishedEvent;
      setAttachment(result);
    };
    console.log(theFile);
    reader.readAsDataURL(theFile);
  };

attachment라는 state에 Data URL 형식으로 이미지를 받았고 이를 다음과 같은 방법으로 업로드할 수 있다. Storage에서 파일에 접근하기 위해서는 ref라는 함수를 이용해 Storage Reference를 만들어야한다. 최신 버전에서는 storageService.ref().child('~') 대신 ref(storage, `~`) 함수를 이용한다. ref().putString() 대신 uploadString 메소드를 사용하면 된다. 이는 UploadResult라는 인터페이스를 반환하는데 여기서 ref를 이용하면 StorageReference를 받을 수 있다. getDownloadURL을 통해 해당 이미지의 다운로드 URL을 받을 수 있다. getUploadURL도 마찬가지로 ref를 넣으면 된다. 

const attachmentRef = ref(storage, `${userObj.uid}/${uuid()}`);

if (attachment) {
	const response = await uploadString(
  		attachmentRef,
  		attachment,
  		'data_url'
);

attachmentURL = await getDownloadURL(response.ref);

** npm uuid -> 랜덤한 고유 id를 만들어 준다! 버전이 높아야 좋은게 아니고 버전별로 기능이 조금씩 다르다 필요한 사항에 따라 사용하면 된다! 여기서는 랜덤한 값을 반환해주는 v4를 사용하였다. 

 

wow 이미지 성공!

이미지 삭제

이미지를 삭제하려면 Storage Reference가 필요하다. 이를 위해선 URL에서 Reference를 받아야 한다. 이전 버전에서는 refFromUrl이라는 메서드를 사용했지만 최신 버전에서는 ref() 함수를 사용하면 URL을 통해 Reference를 반환 받을 수 있다.

await deleteDoc(doc(db, `yweets/${yweet.id}`));
await deleteObject(ref(storage, yweet.attachmentURL));

 

내 프로필 불러오기

Firestore 내 데이터 접근은 query 함수를 통해서 할 수 있다.

export declare function query<T>(query: Query<T>, ...queryConstraints: QueryConstraint[]): Query<T>;

export declare function where(fieldPath: string | FieldPath, opStr: WhereFilterOp, value: unknown): QueryConstraint;

Firestore 데이터를 관리할 때마다 사용했던 collection()함수는 CollectionReference를 반환하는 데 이게 바로 Query를 상속받은 객체이다. 즉, 첫번째 인자는 collection을 넣어주면 된다. where()는 Query Constraint(쿼리 제약? 규칙..?)를 만들어주기 때문에 'createrId'가 내 아이디(userObj.uid)와 같은('==') 데이터만을 골라서 받겠다는 규칙을 정해준다. 즉, query() 함수로 where(규칙)을 포함한 Query를 만들고 이걸 다시 getDocs() 함수에 전달하여 원하는 문서들(documents)을 전달 받을 수 있다.

const yweetsArr = await query(collection(db, 'yweets'), where('creatorId', '==', userObj.uid));
console.log(yweets.docs.map((doc) => doc.data()));

또한 orderBy() 함수를 이용하면 오름차순 또는 내림차순으로 정렬할 수 있다. 이때 where과 order를 같이 사용하기 위해서는 복합 index를 따로 등록해주어야한다! 이는 non SQL 데이터베이스의 단점이라고 한다. 개발자 도구의 콘솔창에서 친절하게 링크를 알려준다.