-
[React] Firebase로 CRUD 구현하기SPA/React.js 2022. 11. 4. 11:26
이번엔 Firestore를 이용해 CRUD 기능을 구현해보자.
1. Firestore
(1) Database 생성
Firebase 콘솔 페이지에서 빌드 > Firestore Database 탭으로 이동한 뒤, 데이터베이스 만들기 버튼을 눌러 데이터 베이스를 생성하자.
이 때 원활하게 테스트를 진행할 수 있도록 테스트 모드로 설정하고
asia-northeast3으로 위치를 설정하여 생성해주자.
이 위치는 본인이 거주하고 있는 곳의 위치와 가까운 곳으로 설정하면 된다.
어느정도의 시간이 지난 뒤 데이터베이스가 생성된다.
(2) Firestore(NoSQL) 구조
본격적으로 데이터베이스를 이용하기 전에 Firestore(NoSQL)의 구조에 대해 알아보자.
Firestore는 콜렉션(collection)과 도큐먼트(document)로 구성된 트리구조로 이루어져 있다.
콜렉션은 도큐먼트를 저장하는 공간(폴더),
도큐먼트는 딕셔너리 형태로 자료를 저장하는 공간이다.
즉, 도큐먼트는 테이블의 한 행(데이터)이고 이를 데이터별로 그룹화해서 콜렉션에 저장하는 구조인 것이다.
Database > Collection > Document
2. 구조 변경
(1) Header
우선 본격적으로 진행하기 전에 저번에 설계했던 구조를 살짝 변경해보자.
로그인이 되고 나서 어디에서든 Home으로 돌아가거나 로그아웃이 가능하도록 Header라는 고정 component를 생성해주자.
// components/Header.js import React from "react"; import { Link } from "react-router-dom"; import { auth } from "../fbase"; const Header = () => { const onLogout = () => { auth.signOut(); } return ( <ul> <Link to='/'>Home</Link> <li><button onClick={onLogout}>Logout</button></li> </ul> ) } export default Header;
생성한 Header를 Router에서 import할텐데 조건은 로그인이 된 상태를 기준으로 Header component가 보이도록 한다.
// components/Router.js <Router> {isLoggedIn && <Header/>} <Routes>
그럼 아래와 같이 로그인 후에 Home으로 돌아갈 수 있는 버튼과 로그아웃할 수 있는 버튼이 생긴다.
(2) Auth
Auth component도 변경할텐데 회원가입 시에는 유저의 이름을 입력하도록 하고
로그인의 경우에는 이메일과 비밀번호만 입력하여 로그인하게끔 해보자.
이를 위해 사용자가 입력한 이름을 담을 name state를 생성하고
const [name, setName] = useState('');
onChange 함수에 target의 name이 name일 경우를 추가해주자.
const onChange = (e) => { const { target: { name, value } } = e; if (name === 'name') { setName(value);
이어서 onSubmit에서 updateProfile로 입력된 유저의 이름을 displayName으로 대입해주는데
이는 회원가입의 과정에서 자동으로 등록되는 요소가 이메일과 비밀번호뿐이기에 updateProfile 함수로 회원가입과 동시에 프로필을 업데이트해줌으로써 유저의 이름을 등록하는 것이다.
이는 onSocialClick 함수의 최하단에도 추가해주자.
const onSubmit = async (e) => { ... await updateProfile(auth.currentUser, { displayName: name }); } catch (error) {
우선 로그인 시에는 이름 입력창이 보이지 않도록 조건문으로 설정하고 그 외에는 추후에 css를 적용하기 편하도록 묶어주고 약간의 수정을 진행했다.
return ( <div className="auth-container"> <form onSubmit={onSubmit}> {newAccount ? <input onChange={onChange} name="name" type="text" placeholder="Name" required value={name} /> : ''} <input onChange={onChange} name="email" type="email" placeholder="Email" required value={email} /> <input onChange={onChange} name="password" type="password" placeholder="Password" required value={password} /> <input type="submit" value={newAccount ? "Create Account" : "Login"} /> </form> <span onClick={toggleAccount}> {newAccount ? "Login" : "Create Account"} </span> <p className="error">{error}</p> <div> <p>Continue with</p> <button onClick={onSocialClick} name="google">Google</button> <button onClick={onSocialClick} name="github">Github</button> </div> </div> )
3. Post
Firestore의 기능들을 사용하기 위해 fbase.js에서 export하자.
// fbase.js import { initializeApp } from "firebase/app"; import { getAuth } from "firebase/auth"; import { getFirestore } from "firebase/firestore"; const firebaseConfig = { apiKey: "", authDomain: "", projectId: "", storageBucket: "", messagingSenderId: "", appId: "" }; const app = initializeApp(firebaseConfig); export const auth = getAuth(); export const database = getFirestore();
(1) userObj
이어서 우리는 App.js에서 새로운 유저에 대한 정보를 담는 객체 state를 생성해줄 것이다.
// components/App.js const [userObj, setUserObj] = useState(null);
이어서 useEffect에 유저가 존재할 경우(로그인했을 경우), setUserObj로 유저의 이름과 유저의 uid(고유 식별 id)를 객체에 담는다.
useEffect(() => { auth.onAuthStateChanged((user) => { if (user) { setUserObj({ displayName: user.displayName, uid: user.uid, }) setIsLoggedIn(true); } else { setUserObj(null); setIsLoggedIn(false); } setInit(true); }) }, [])
그 후 AppRouter에 props로 전달함으로써 로그인한 유저의 정보를 공유한다.
return ( <> {init ? <AppRouter isLoggedIn={isLoggedIn} userObj={userObj} /> : "initializing..." } </> );
이어서 Router.js에서도 props를 받은 뒤 Header와 Home에게 props로 userObj를 넘겨준다.
// components/Router.js <Router> {isLoggedIn && <Header userObj = {userObj}/>} <Routes> {isLoggedIn ? <> <Route exact path = "/" element={<Home userObj={userObj}/>}></Route>
이렇게 넘겨받은 userObj를 이용해서 Header에서 '{유저 네임}님의 Home'으로 각 유저에 맞는 이름이 매칭될 수 있다.
// components/Header.js <Link to='/'>{userObj.displayName}님의 Home</Link>
(2) collection
Router.js에서 userObj를 받고 게시물 데이터들을 담을 하나의 공간을 Home.js에 state로 생성해주자.
이 역시 게시물 데이터들의 모음이므로 객체 type이 될 것이다.
// routes/Home.js const Home = ({userObj}) => { const [posts, setPosts] = useState([]);
useEffect로 Home이 mount되었을 때 게시물 데이터들을 불러올텐데 여기서 query문을 사용한다.
[query문이란?]
Firestore에서 제공하는 문법으로, 원하는 조건으로 데이터를 가져올 수 있게 한다.
다양한 방식이 있는데 자세한 설명은 아래 블로그에서 잘 설명해주셨다.[Firebase] Firebase 쿼리 알아보기(FireStore Query)
안녕하세요 Foma 👟 입니다! 오늘은 파이어스토어에서 원하는 조건으로 데이터를 가져올 수 있는 여러 쿼리문에 대해서 알아보겠습니다! WhereField isEqualTo 필드에 포마라는 값과 같은 문서가 전부
fomaios.tistory.com
우리의 데이터베이스 중 posts collection을 게시물 데이터가 생성된 시간별 내림차순으로 가져온다.
그리고 onSnapshot으로 실시간 database를 가져오고 이를 posts에 대입해준다.
useEffect(() => { const postData = query( collection(database, "posts"), orderBy("createdAt", "desc") ); onSnapshot(postData, (snapshot) => { const postArray = snapshot.docs.map((document) => ({ id: document.id, ...document.data(), })); setPosts(postArray); }); }, []);
그리고 구조를 잡아주는데 게시물을 생성해주는 PostFactory에게 userObj를 전달해주고
각 게시물인 Posts component가 각자의 key와 데이터 정보를 갖도록 해준다.
여기서 Link를 사용했는데 각 게시물의 제목을 클릭할 시 /posts/{게시물 id}의 형태로 url이 생성되도록 설정해줬다.
(원래는 '/posts/{게시물 제목}'의 형태로 했었으나 이럴 경우, 추후 추가될 게시물 수정 시 게시물 제목이 변경되며 충돌이 일어나 id의 형식으로 변경했다.)
return ( <div> <PostFactory userObj={userObj}/> <div> {posts.map((post) => { return ( <Link key={post.id} to={`/posts/${post.id}`}> <h4>{post.title}</h4> </Link> ) })} </div> </div> )
(3) Posts
각 게시물의 모양을 잡아줄 Posts component를 routes 디렉토리에 생성하고
useParams를 통해 현재 url의 파라미터 값(게시물 id)을 받아온다.
이를 통해 우리는 클릭한 게시물을 구분하여 내용을 담아줄 수 있다.
구분한 게시물 객체를 postList에 넣어주고 이를 바탕으로 내용이 화면에 보여지도록 한다.
// routes/Posts.js import React, { useEffect } from "react"; import { useParams } from "react-router-dom"; const Posts = ({posts, userObj}) => { const {post_id} = useParams(); let postList; for(var i = 0; i < posts.length; i++) { if(posts[i].id === post_id) { postList = posts[i]; } } return ( <> <h4> {postList.title} </h4> <h4> {postList.contents} </h4> </> ) } export default Posts;
(4) PostFactory
본격적으로 게시물을 생성해주는 PostFactory.js를 componets 디렉토리에 생성해주고 제목과 내용을 담을 state를 선언해주자.
// components/ChatFactory.js const [title, setTitle] = useState(''); const [contents, setContents] = useState('');
게시물의 제목과 내용을 입력하는 input창이 변화할(입력될) 때마다 입력된 값을 각 state에 대입해주는 onChange function을 정의해주고
const onChange = (e) => { const {target: {value, name}} = e; if(name === 'title'){ setTitle(value); } else if(name === 'contents') { setContents(value); } }
이어서 게시물 등록 버튼을 눌렀을 때 실행되는 onSubmit function을 정의해주는데 async await문을 이용한다.
입력창이 빈칸인 상태로 전송 버튼을 눌렀을 경우에는 return한다.
그 외의 경우에는 입력된 제목과 내용, 생성된 시간, 게시물을 작성한 유저의 id를 데이터로 Posting 객체를 만들어 우리의 posts database에 도큐먼트(데이터)를 추가해주고 각 입력창의 내용을 초기화한다.
const onSubmit = async (e) => { e.preventDefault(); if(title === '' || contents === '') { return; } const Posting = { title: title, contents: contents, createdAt: Date.now(), creatorId: userObj.uid, } await addDoc(collection(database, 'posts'), Posting); setTitle(''); setContents(''); }
onSubmit은 form이 submit되었을 때, onChange는 입력창이 변화할 때마다 실행되도록 하고 입력창의 value는 state로 대입해준다.
return ( <form onSubmit={onSubmit}> <div> <input name='title' value={title} onChange={onChange} type="text" placeholder="제목을 입력해주세요." required/> <input name='contents' value={contents} onChange={onChange} type="text" placeholder="내용을 입력해주세요." required/> <input type="submit" value="→"/> </div> </form> )
이렇게 작성한 결과는 아래와 같다.
그러나 회원가입 시 유저의 이름이 바로 반영되지 않고 새로고침해야 반영되는 것은 해결해야 할 문제일 것 같다.
4. 수정 및 삭제
(1) 구조
Posts component에 수정과 삭제 기능을 추가해보자.
우선 게시물 작성자와 현재 로그인한 유저의 일치 여부를 확인하는 isOwner와 수정 여부를 담을 edit, 그리고 수정된 후의 게시물 제목과 내용을 담을 state를 생성해주는데
newTitle과 newContents의 경우에는 이전에 입력했던 값으로 시작해야 하므로 받아온 postList의 값을 초기값으로 지정해준다.
// components/Chat.js const Posts = ({posts, userObj}) => { const [isOwner, setIsOwner] = useState(postList.creatorId === userObj.uid); const [edit, setEdit] = useState(false); const [newTitle, setNewTitle] = useState(postList.title); const [newContents, setNewContents] = useState(postList.contents);
이어서 구조를 잡아주는데 edit가 true일 경우, 게시물을 수정할 수 있는 form이 보여지도록 한다.
false라면 게시물 텍스트가 기본적으로 보여지게 하고 해당 게시물을 현재 로그인한 유저가 입력한거라면 수정 및 삭제 버튼이 보여지도록 한다.
return ( <div> {edit ? ( <> <form onSubmit={onSubmit}> <input name="title" onChange={onChange} value={newTitle} required placeholder="제목을 수정해주세요" autoFocus /> <input name="contents" onChange={onChange} value={newContents} required placeholder="내용을 수정해주세요" autoFocus /> <input type='submit' value='update chat' /> </form> <button onClick={toggleEditing}>Cancel</button> </> ) : ( <> <h4>{postList.title}</h4> <h4>{postList.contents}</h4> {isOwner && ( <div> <span onClick={onDelete}> Delete </span> <span onClick={toggleEditing}> Edit </span> </div> )} </> ) } </div> )
이제 function들을 정의해주는데 수정 입력창의 값이 변화할 때마다 해당값이 대입되도록 한다.
const onChange = (e) => { const { target: { value, name } } = e; if(name === 'title') { setNewTitle(value); } else if(name === 'contents') { setNewContents(value); } }
(2) 게시물 삭제
삭제 버튼을 눌렀을 때 실행되는 onDelete는 삭제 여부를 확인하는 confirm을 띄워준 뒤 유저가 확인 버튼을 눌렀을 때 deleteDoc을 이용해서 해당 게시물 데이터를 지우도록 한다.
게시물 데이터가 지워지는 것이므로 게시물 id도 사라져 현재 url에서는 아무런 화면이 안 보여지기에 지우는 즉시 페이지 경로도 Home으로 변경되도록 useNavigate를 사용한다.
const navigate = useNavigate(); const onDelete = () => { const yes = window.confirm('삭제하시겠습니까?'); if (yes) { deleteDoc(doc(database, 'posts', postList.id)); navigate('/'); } }
(3) toggleEditing
toggleEditing은 실행될 때마다 edit의 상태를 반대로 변경시켜준다.
const toggleEditing = () => setEdit((prev) => !prev);
(4) 수정
게시물 내용을 수정하고 submit했을 때 실행되는 onSubmit이다.
updateDoc을 이용해 해당 게시물의 내용을 수정 input창에 기입된 내용으로 업데이트해주고 edit의 상태를 false로 변경해준다.
const onSubmit = (e) => { e.preventDefault(); updateDoc(doc(database, 'posts', postList.id), { title: newTitle, contents: newContents }); setEdit(false); }
그럼 이렇게 게시물 내용도 보여지고 삭제, 수정 버튼도 보여진다.
삭제 버튼 클릭 시 confirm도 뜨며 확인할 시 실제로 게시물 데이터가 지워지며 Home으로 돌아오는 걸 확인할 수 있다.
5. Style
이제 어느정도 주요 기능은 구현되었으니 Auth 부분에만 style을 입혀보자.
우선 구조적으로 조금 더 수정을 해보자.
// routes/Auth.js return ( <div className="auth-container"> <h1>Firebase 블로그</h1> <form onSubmit={onSubmit}> {newAccount ? <input onChange={onChange} name="name" type="text" placeholder="Name" required value={name} /> : ''} <input onChange={onChange} name="email" type="email" placeholder="Email" required value={email} /> <input onChange={onChange} name="password" type="password" placeholder="Password" required value={password} /> <input type="submit" value={newAccount ? "Create Account" : "Login"} /> </form> <span onClick={toggleAccount}> {newAccount ? "계정이 있으신가요? 로그인하기" : "처음이신가요? 회원가입하기"} </span> <p className="error">{error}</p> <div> <div className="social"> <p>Continue with</p> <button className="authBtn" onClick={onSocialClick} name="google">Google <FontAwesomeIcon icon = {faGoogle}/></button> <button className="authBtn" onClick={onSocialClick} name="github">Github <FontAwesomeIcon icon = {faGithub}/></button> </div> </div> </div> )
여기서 구글과 깃허브로 로그인 시에는 아이콘을 사용할텐데 이를 위해 fortawesome을 설치해주자.
npm i @fortawesome/react-fontawesome npm i @fortawesome/free-brands-svg-icons
그리고 Auth.css를 생성하고 아래와 같이 입력해주면
/* Auth.css */ h1 {font-size: calc(100vw*60/1920); font-weight: bold; text-align: center; margin-bottom: calc(100vw*80/1920);} .auth-container {width: 400px; margin: 0 auto;} .auth-container input {margin-bottom: 30px; width: 100%; height: 44px; padding: 12px; padding-top: 11px; padding-bottom: 13px; margin: 0 0 20px; font-size: 18px; line-height: 1.33333333; border-radius: 4px; border: 1px solid #868686; box-sizing: border-box; transition: border 80ms ease-out,box-shadow 80ms ease-out;} .auth-container input[type='submit'] {background: #000; color: #fff; font-weight: bold;} .auth-container span {color: #1264a3; text-align: center; display: block; cursor: pointer;} .auth-container .error {color: #e01e5a; margin: 8px 0 16px; font-weight: bold;} .auth-container .social {text-align: center;} .auth-container .social p {margin: 0; margin-top: 20px; margin-bottom: 5px;} .auth-container .authBtn {cursor: pointer; border-radius: 20px; border: none; padding: 10px 0px; font-size: 16px; text-align: center; width: 150px; background: white; cursor: pointer;}
아래와 같은 화면 UI가 완성된다.
이렇게 콘텐츠의 기본인 CRUD 기능을 구현해봤고 다음에는 사진, 영상 업로드 기능을 추가해볼까 한다.
'SPA > React.js' 카테고리의 다른 글
[React] Firebase로 카테고리 생성 및 적용하기 (0) 2022.11.11 [React] Firebase로 사진, 영상 업로드 (0) 2022.11.10 [React] Firebase로 회원가입/로그인, 로그아웃 (0) 2022.10.26 React.js(5)_React Redux (0) 2022.09.28 React.js(4)_React Ajax (0) 2022.09.28