본문 바로가기

Server/Firebase

<파이어베이스> 게시글 CRUD

 

1. 초기 세팅

* 파이어베이스

- 이번 장에서는 파이어베이스를 이용해서 realtime database를 만들어 볼 것이다.

 

- 실시간으로 글이 만들어지고 지워지는 환경을 구축하는데 있어 파이어베이스는 아주 좋은 도구이다.

 

- 데이터베이스를 만들어 보자.

 

- 데이터베이스 만들기를 클릭하고, 테스트모드를 클릭하여 다음으로 넘어간다. 파이어스토어 위치는 가까울수록 좋기때문에 asia-northeast 중에서 선택한다.

 

 

- 데이터베이스가 만들어지면 위와같이 표시된다. 직접 컬렉션을 만들 수도 있지만 우리는 코드를 통해 만들고 접근할 것이다.

 

 

- 참고로 위 DB의 구조는 컬렉션 > 다큐먼트 > 데이터 순으로 이루어진다. 컬렉션은 다큐먼트의 집합이며 하나의 폴더라고 생각하면 좋다. 다큐먼트는 데이터 하나를 갖고 있으며 하나의 id를 갖는다. 내부에는 객체형식 배열형식등으로 데이터를 담고있다.

 

* 규칙

 - 콘솔에 있는 규칙을 클릭하여 규칙을 지정할 수 있다.

 - 규칙을 확인하면 위와 같이 기본적으로 읽기와 쓰기를 날짜로 제한해 둔 것을 알 수 있다.

 

 - 이는 테스트 용으로 만들어져 있는 것이다. 우리는 로그인 한 유저만 읽고 쓸 수 있도록 제한할 것이다.

* 파일

- 저번 장과 동일한 파일구조를 갖고 있으며, components 디렉토리에는 Post.js를 추가했다.

 

- App.js를 일부 수정했다. 게시글을 다룸에 있어서 유저의 정보가 필요하기 때문에 유저의 정보를 props로 넘겨주도록 했다.

 

// components/App.js

import React, { useState, useEffect } from "react";
import AppRouter from "./Router";
import { authService } from "fbase";

function App() {
  const [init, setInit] = useState(false);
  const [isLoggedIn, setIsLoggedIn] = useState(false);
  const [userObj, setUserObj] = useState(null);
  useEffect(() => {
    authService.onAuthStateChanged((user) => {
      if (user) {
        setIsLoggedIn(true);
        setUserObj(user);
      } else {
        setIsLoggedIn(false);
      }
      setInit(true);
    });
  }, []);
  return (
    <>
      {init ? (
        <AppRouter isLoggedIn={isLoggedIn} userObj={userObj} />
      ) : (
        "Initializing."
      )}
    </>
  );
}
export default App;

 

- userObj라는 상태를 추가했으며, 지난 장에서만든 onAuthStateChanged메서드를 이용해서 user의 정보를 담는다.

 

- 이 porps는 라우터에 넘긴 후에 Home 에서 사용될 수 있도록 다시 넘긴다.

 

// components/Router.js
// ...
const AppRouter = ({ isLoggedIn, userObj }) => {
  return (
    <Router>
      {isLoggedIn && <Navigation />}
      <Switch>
        {isLoggedIn ? (
          <>
            <Route exact path="/">
              <Home userObj={userObj} />
            </Route>
            <Route exact path="/profile">
              <Profile />
            </Route>
            <Redirect from="*" to="/" />
          </>
        ) : (
          <>
            <Route exact path="/">
              <Auth />
            </Route>
            <Redirect from="*" to="/" />
          </>
        )}
      </Switch>
    </Router>
  );
};
// ...

 

- 실시간 db인 firestore을 사용하기 위해 import한 후, 다른 곳에서 쓰일 수 있도록 export하자.

 

// fbase.js 
import firebase from 'firebase/app'; 
import "firebase/auth"; 
import "firebase/firestore"; 
// ... 
export const firebaseInstance = firebase; 
export const authService = firebase.auth() 
export const dbService = firebase.firestore();

 

2. Create & Read

* Create

// routes/Home.js
import React, { useState, useEffect } from "react";
import { dbService } from "fbase";
const Home = ({ userObj }) => {
  const [post, setPost] = useState("");
  const onSubmit = async (event) => {
    event.preventDefault();
    await dbService
      .collection("posts")
      .add({ text: post, createdAt: Date.now(), creatorId: userObj.uid });
    setPost("");
  };
  const onChange = (event) => {
    const {
      target: { value },
    } = event;
    setPost(value);
  };
  return (
    <div>
      <form onSubmit={onSubmit}>
        <input
          value={post}
          onChange={onChange}
          type="text"
          placeholder="게시글을 입력하세요"
          maxLength={120}
        />
        <input type="submit" value="Post!!" />
      </form>
    </div>
  );
};
export default Home;

 

- 글을 작성하기 위한 폼을 Home.js에 추가하였다.

 

- firebase.firestore() 을 사용하기 위해서 dbService를 import하였다.

 

- 폼 제출이 일어나면 post상태에 담긴 게시글을 db에 추가한다. 이 때 collection().add()메서드를 사용한다.

 

- collection의 인자에는 컬렉션 이름이 들어가는데 만일 없는 이름을 적으면 자동으로 초가된다.

 

- add 를 사용해서 데이터를 추가한다. 문서id는 자동으로 추가되고, 객체로 받은 데이터들을 넣게된다. userObj 내부에 유저id값을 의미하는 uid 속성이 있으므로 이 또한 넣어준다.

 

- 위와 같이 만들고 submit을 하면 db콘솔에서 확인 가능하다.

 

* Read

- 위에서 db에 게시글을 넣게 하였으므로, 이 게시글들을 웹페이지에 보이도록 해야한다.

 

- 게시글 하나에는 많은 정보와 메서드들이 필요하므로 별도의 컴포넌트로 분리하였다.

 

// components/Post.js
import React from "react";
const Post = ({ postObj }) => {
  return (
    <div>
      <h4>{postObj.text}</h4>
    </div>
  );
};
export default Post;

 

- 위와 같이 간단하게 컴포넌트를 만들고 다시 Home을 수정한다.

 

// routes/Home.js
import React, { useState, useEffect } from "react";
import { dbService } from "fbase";
import Post from "components/Post";
const Home = ({ userObj }) => {
  const [post, setPost] = useState("");
  const [posts, setPosts] = useState([]);
  useEffect(() => {
    dbService.collection("posts").onSnapshot((snapshot) => {
      const postArray = snapshot.docs.map((doc) => ({
        id: doc.id,
        ...doc.data(),
      }));
      setPosts(postArray);
    });
  }, []);
  // ...
  return (
    <div>
      <form onSubmit={onSubmit}>
        <input
          value={post}
          onChange={onChange}
          type="text"
          placeholder="게시글을 작성하세요"
          maxLength={120}
        />
        <input type="submit" value="Post!!" />
      </form>
      <div>
        {posts.map((post) => (
          <Post
            key={post.id}
            postObj={post}
            isOwner={post.creatorId === userObj.uid}
          />
        ))}
      </div>
    </div>
  );
};
export default Home;

 

- useEffect를 이용해서 컴포넌트 실행시 포스트들을 읽을 수 있도록 하였다.

 

- onSnapshot 메서드를 이용해서 db의 데이터에 변화가 있을 때마다 감지되도록 하였다. 변화가 있으면 자동으로 snapshot을 가져온다. 이는 db의 데이터들을 의미한다.

 

- postArray 변수를 선언하여 데이터를 담는데, 이 때 게시글마다 id값을 할당하기 위해 파이어베이스에서 자동으로 할당된 다큐먼트의 id를 넣는다. data()메서드로 다큐먼트의 데이터들을 가져올 수 있다. 스프레드를 사용해서 id와 같은 수준에 있도록 하였다.

 

- Posts state에 위의 배열을 넣었으며, 이를 map을 이용해서 Post 컴포넌트에 랜더링 시켰다.

 

- isOwner은 뒤에 있을 삭제나 수정을 위해서 불린값을 넣을 수 있도록했다.

 

 

3. Delete & Update

* Delete

// components/Post.js
import { dbService } from "fbase";
import React, { useState } from "react";
const Post = ({ postObj, isOwner }) => {
  const onDeleteClick = async () => {
    const ok = window.confirm("삭제하시겠습니까?");
    console.log(ok);
    if (ok) {
      await dbService.doc(`posts/${postObj.id}`).delete();
    }
  };
  return (
    <div>
      <h4>{postObj.text}</h4>
      {isOwner && (
        <> 
          <button onClick={onDeleteClick}>삭제</button>
        </>
      )}
    </div>
  );
};
export default Post;

 

- 먼저 삭제 버튼이 필요하다. props로 받은 isOwner가 true일 때에만 삭제버튼이 활성화 되도록 하였다.

 

- 버튼을 클릭하면 window.confirm으로 얼럿창을 띄우고, 수락할 경우에 삭제를 진행하도록 하였다.

 

- 삭제하는 방법은 매우 간단하다. doc메서드에 인자로 디렉토리형식으로 담으면 된다. '컬렉션/다큐먼트id' 를 전달하고, delete 메서드를 호출하여 다큐먼트를 삭제하였다.

 

- 이 때, 데이터의 변화가 있으므로 Home에서 미리 설정해두었던 onSnapshot이 이를 감지하고 다시 렌더링이 진행된다. 즉 실시간으로 포스트가 삭제되는 것을 볼 수 있다.

 

* Update

// components/Post.js
// ...
const Post = ({ postObj, isOwner }) => {
  const [editing, setEditing] = useState(false);
  const [newPost, setNewPost] = useState(postObj.text);
  //...
  const toggleEditing = () => setEditing((prev) => !prev);
  const onSubmit = async (event) => {
    event.preventDefault();
    await dbService.doc(`posts/${postObj.id}`).update({ text: newPost });
    setEditing(false);
  };
  const onChange = (event) => {
    const {
      target: { value },
    } = event;
    setNewPost(value);
  };
  return (
    <div>
      {editing ? (
        <> 
          <form onSubmit={onSubmit}>  
            <input
              onChange={onChange}
              type="text"
              value={newPost}
              required
            />
            <input type="submit" value="Update" />
          </form>
          <button onClick={toggleEditing}>Cancel</button>
        </>
      ) : (
        <>
          <h4>{postObj.text}</h4>
          {isOwner && (
            <>    
              <button onClick={onDeleteClick}>삭제</button>
              <button onClick={toggleEditing}>수정</button>
            </>
          )}
        </>
      )}
    </div>
  );
};
export default Post;

 

- 수정도 비슷하다. 수정버튼을 추가하였고, 별도의 수정폼을 활성화 시키기 위해서 toggle상태를 만들어주었다.

 

- 삭제와 같이 doc메서드에 정확한 경로를 인자로 넣어서 update() 메서드를 호출한다. 이 때, 정확하게 키와 값을 명시해주어야 그 키에 해당하는 값이 변경된다.

 


 

참고

 

 

 

Watch Now - 노마드 코더 Nomad Coders

 

nomadcoders.co

 

 

firestore | JavaScript SDK  |  Firebase

Reference for firestore

firebase.google.com