Firestore & Redux - redux-thunk리액트 기초/Firestore + Redux2021. 9. 1. 11:30
Table of Contents
미리 해야할 일
1. Redux와 React 프로젝트 연결
- 우선 아래 깃허브에서 Redux 모듈을 연결한 React 프로젝트를 설치하자.
- https://github.com/ejzl521/React_Redux-Router
- 혹은 아래 코드를 붙여넣기하고 필요한 라이브러리를 설치하자
redux/module/bucket.js
- 리덕스 모듈
// Action
const LOAD = 'bucket/LOAD';
const CREATE = 'bucket/CREATE';
// initialState
// 초기 상태값
const initialState = {list: ["치킨 먹기", "컴퓨터 게임하기", "여행 가기"]};
// Action Creators
export const loadBucket = (bucket) => {
// 불러오는 기능은 어떤 데이터를 줄 필요가 없지만 아래와 모양새를 맞추기 위해 추가.
return {type: LOAD, bucket};
}
export const createBucket = (bucket) => {
// 타입뿐만 아니라 데이터도 필요하다.
//CreateBucket 같은 경우에는 추가할 값이 필요하다.
return {type: CREATE, bucket};
}
// Reducer
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
// do reducer stuff
case "bucket/LOAD":
return state;
case "bucket/CREATE":
const new_bucket_list = [...state.list, action.bucket];
return {list: new_bucket_list};
default:
return state;
}
}
redux/configStore.js
- 리덕스 스토어
//configStore.js
import { createStore, combineReducers } from "redux";
//우리가 만든 리덕스 모듈의 리듀서
import bucket from './modules/bucket';
import { createBrowserHistory } from "history";
// 브라우저 히스토리를 만들어줍니다.
export const history = createBrowserHistory();
// root 리듀서를 만들어줍니다.
// 나중에 리듀서를 여러개 만들게 되면 여기에 하나씩 추가해주는 거예요!
const rootReducer = combineReducers({ bucket });
// 스토어를 만듭니다.
const store = createStore(rootReducer);
export default store;
index.js
- 리덕스 스토어와 리액트 프로젝트 연결
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
// 라우터 설정
import {BrowserRouter} from 'react-router-dom';
import reportWebVitals from './reportWebVitals';
// 리액트 프로젝트에 리덕스를 주입해줄 프로바이더를 불러옴!
import { Provider } from "react-redux";
// 연결할 스토어.
import store from "./redux/configStore";
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.getElementById("root")
);
reportWebVitals();
2. FireStore와 React 프로젝트 연결하기
- 여기를 참고해서 Firestore와 React 프로젝트를 연결한 후 firebase.js 파일을 만들자
firebase.js
//firebase.js
import firebase from "firebase/compat/app"
import 'firebase/compat/firestore';
const firebaseConfig = {
// firebase 설정과 관련된 개인 정보
};
// firebaseConfig 정보로 firebase 시작
firebase.initializeApp(firebaseConfig);
// firebase의 firestore 인스턴스를 변수에 저장
const firestore = firebase.firestore();
// 필요한 곳에서 사용할 수 있도록 내보내기
// 다른 곳에서 불러올때 firestore로 불러와야 함!!
export { firestore };
- 파일 구조는 아래와 같다
Redux & FireStore 연동 - Redux-thunk
- firestore에서 데이터를 가져올 때 비동기 통신을 한다.
- firestore에서 리덕스로 데이터를 가져오기 위해 비동기 통신을 할 때 필요한 미들웨어를 설치해야 한다!
- Firestore 뿐만 아니라 HTTP 통신으로 API를 호출할 때도 마찬가지이다!
- 즉, firestore에 요청을 보내고 답을 받아오는 동안 기다려줘야 하므로 미들웨어를 사용해야 한다!
- 미들웨어는 리덕스 데이터를 수정할 때 [액션이 디스패치 되고 → 리듀서에서 처리] 하는 과정 사이에 미리 사전작업을 할 수 있도록 하는 중간다리이다.
- 즉! [액션이 일어나고 → 미들웨어가 할 일 하기 → 리듀서에서 처리] 순서이다. 미들웨어가 할 일 하기 순서에서는 Firebase에서 데이터를 가져온다고 볼 수 있다!
1. redux-thunk
- 우선 아래 명령어를 입력해서 미들웨어를 설치하자.
yarn add redux-thunk
- redux-thunk는 뭐하는 미들웨어일까? 액션 생성 함수는 객체를 반환하지만 redux-thunk는 객체 대신 함수를 생성하는 액션 생성함수를 작성할 수 있게 해준다!🙂
- 그게 왜 필요하냐구요? 리덕스는 기본적으로는 액션 객체를 디스패치한다! 그러면, 객체 말고 함수를 생성해주면 어떤 액션이 발생하기 전에 조건을 줄 수 있고 어떤 행동을 사전에 처리할 수 있다.
- 한마디로 firestore에서 응답이 오기 올 때까지 기다려 줄 수 있다!
- 즉, 비동기 통신으로 서버의 자원을 가져올 때 까지 기다려 줄 수 있다
→ API로 서버의 자원을 가져올 때도 사용!!
2. configStore.js 수정
- redux-thunk를 설치했다면 configStore.js 파일을 아래처럼 수정하자!
// applyMiddleware는 스토어에 미들웨어를 적용하기 위해 불러옴
import { createStore, combineReducers, applyMiddleware } from "redux";
//우리가 만든 리덕스 모듈의 리듀서
import bucket from './modules/bucket';
import { createBrowserHistory } from "history";
// thunk 가져오기
import thunk from "redux-thunk"
// 미들웨어 만들기
const middlewares = [thunk];
// 스토어에 미들웨어를 적용하기 위한 변수 만들기
const enhancer = applyMiddleware(...middlewares);
// 브라우저 히스토리를 만들어줍니다.
export const history = createBrowserHistory();
// root 리듀서를 만들어줍니다.
// 나중에 리듀서를 여러개 만들게 되면 여기에 하나씩 추가해주는 거예요!
const rootReducer = combineReducers({ bucket });
// 스토어를 만든다. 리듀서와 미들웨어를 넣어줌!
const store = createStore(rootReducer, enhancer);
export default store;
Redux에서 FiresStore 데이터 조작하기
- 우선 리덕스 모듈에서 firestore 모듈을 가지고 와야한다.
- 그리고 Firestore와 통신하는 함수를 정의하고 그 함수 안에서 redux 모듈에서 정의한 action을 firestore에서 가져온 데이터를 넣고 dispatch 해준다. dispatch된 action은 reducer에서 처리되기 때문이다!
- Firestore를 사용하지않고 redux만 사용한다면 컴포넌트에서 직접 데이터를 넣어서 action을 dispatch 해줬지만 Firestore를 사용하면 컴포넌트에서 redux 모듈에 정의한 Firestore와 통신하는 함수를 dispatch()로 부르고 그 함수 안에서 Firestore에서 가져온 데이터를 가지고 action을 dispatch 해주고 reducer에서 처리를 해준다!
1. Firestore에서 데이터 불러오기 (LOAD)
- 리덕스 모듈에 Firestore와 통신하는 함수를 만들어서 Firestore에서 데이터를 가져올 수 있다!
- 아래와 같이 bucket.js 파일을 수정하자
bucket.js
// firestore 모듈 가져오기
import { firestore } from "../../firebase";
// Action
const LOAD = 'bucket/LOAD';
// initialState
// 초기 상태값
const initialState = { list: [
{text: "치킨 먹기"},
{text: "컴퓨터 게임하기"},
{text: "여행 가기"}
]};
// Action Creators
export const loadBucket = (bucket) => {
return {type: LOAD, bucket};
}
// Firestore에서 collection을 가져옴
const bucket_db = firestore.collection("bucket");
// Firebase와 통신하는 함수. 함수를 반환한다.
// Firebase에서 데이터를 가져오는 부분 (LOAD)
export const loadBucketFB = () => {
// 함수를 반환하는 미들웨어 부분
return function(dispatch){
bucket_db.get().then((docs) => {
// Firestore에서 가져온 데이터를 저장할 변수
let bucket_data = [];
// "bucket" 콜렉션의 모든 문서에서 데이터와 id를 가져옴!
docs.forEach((doc) => {
if(doc.exists){
bucket_data = [...bucket_data, {id: doc.id, ... doc.data()}]
}
})
console.log(bucket_data);
// firestore에서 가져온 데이터를 action에 넣어서 dispatch 해준다!
// 리덕스 모듈에서 action을 dispatch 해주므로 컴포넌트에서는 firestore와
// 통신하는 함수를 불러주면 된다!
dispatch(loadBucket(bucket_data))
});
}
}
// Reducer
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
// do reducer stuff
case "bucket/LOAD":
// Firestore에 데이터가 있을때 리턴
if(action.bucket.length > 0){
return { list: action.bucket };
}
// 없으면 initialState를 리턴해줌
return state;
default:
return state;
}
}
주의사항!
- firestore와 통신해서 데이터를 불러오는 함수를 리덕스 모듈에서 정의하고 컴포넌트에서 불러올 때, 아래처럼 useEffect 훅을 사용해서 렌더링 이후 딱 한번만 불러오게 만들거나 어떤 이벤트가 발생했을 때 한번만 불러오게 불러오게 만들어야 한다!
- 그렇게 하지않고 컴포넌트에서 firestore와 통신하는 함수를 호출하면 firestore에서 데이터를 가져오면서 reducer가 계속 값을 리턴하(상태 값이 계속 변함!)므로 무한으로 함수가 호출되면서 렌더링이 무한으로 일어난다!
App.js
import './App.css';
import { useEffect } from 'react';
import { useDispatch } from "react-redux";
// Firestore와 통신하는 함수
import { loadBucketFB } from './redux/modules/bucket';
function App() {
const dispatch = useDispatch();
useEffect(()=>{
dispatch(loadBucketFB());
}, [])
return (
<div className="App">
</div>
);
}
export default App;
- 만약 아래처럼 firestore와 통신하는 함수를 useEffect 훅 안에 쓰지않으면 무한으로 렌더링이 일어난다!
App.js
import './App.css';
import { useEffect } from 'react';
import { useDispatch } from "react-redux";
// Firestore와 통신하는 함수
import { loadBucketFB } from './redux/modules/bucket';
function App() {
const dispatch = useDispatch();
dispatch(loadBucketFB());
return (
<div className="App">
</div>
);
}
export default App;
2. Firestore에 데이터 추가하기 (CREATE)
- 리덕스 모듈에 Firestore와 통신하는 함수를 만들어서 Firestore에 데이터를 추가할 수 있다!
- 아래와 같이 bucket.js 파일을 수정하자
bucket.js
import { firestore } from "../../firebase";
// Action
const LOAD = 'bucket/LOAD';
const CREATE = 'bucket/CREATE';
// initialState
// 초기 상태값
const initialState = {
list: [
{ text: "치킨 먹기" },
{ text: "컴퓨터 게임하기" },
{ text: "여행 가기" }
]
};
// Action Creators
export const loadBucket = (bucket) => {
return { type: LOAD, bucket };
}
export const createBucket = (bucket) => {
return { type: CREATE, bucket };
}
// Firestore에서 collection을 가져옴
const bucket_db = firestore.collection("bucket");
// Firebase와 통신하는 함수. 함수를 반환한다.
// Firebase에서 데이터를 가져오는 부분 (LOAD)
export const loadBucketFB = () => {
// 함수를 반환하는 미들웨어 부분
return function (dispatch) {
bucket_db.get().then((docs) => {
// Firestore에서 가져온 데이터를 저장할 변수
let bucket_data = [];
// "bucket" 콜렉션의 모든 문서에서 데이터와 id를 가져옴!
docs.forEach((doc) => {
if (doc.exists) {
bucket_data = [...bucket_data, { id: doc.id, ...doc.data() }]
}
})
// console.log(bucket_data);
// firestore에서 가져온 데이터를 action에 넣어서 dispatch 해준다!
// 리덕스 모듈에서 action을 dispatch 해주므로 컴포넌트에서는 firestore와
// 통신하는 함수를 불러주면 된다!
dispatch(loadBucket(bucket_data))
});
}
}
// Firebase에 데이터를 추가하는 부분 (CREATE)
export const createBucketFB = (bucket) => {
return function (dispatch) {
let bucket_data = { text: bucket };
bucket_db
.add(bucket_data)
.then((docRef) => {
// id와 data를 추가한다!
bucket_data = { id: docRef.id, ...bucket_data };
// firestore에 데이터 추가를 성공했을 때는? 액션 디스패치!
dispatch(createBucket(bucket_data));
})
.catch((err) => {
// 여긴 에러가 났을 때 들어오는 구간입니다!
console.log(err);
window.alert('오류가 났네요! 나중에 다시 시도해주세요!');
});
};
}
// Reducer
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
// do reducer stuff
case "bucket/LOAD":
// Firestore에 데이터가 있을때 리턴
if (action.bucket.length > 0) {
return { list: action.bucket };
}
// 없으면 initialState를 리턴해줌
return state;
case "bucket/CREATE":
const new_bucket_list = [...state.list, action.bucket];
return { list: new_bucket_list };
default:
return state;
}
}
App.js
- 실행한 후 버튼을 클릭하면 "농구하기" 데이터가 firestore에 추가된다!
import './App.css';
import { useEffect } from 'react';
import { useDispatch } from "react-redux";
// Firestore와 통신하는 함수
import { loadBucketFB, createBucketFB } from './redux/modules/bucket';
function App() {
const dispatch = useDispatch();
useEffect(() => { dispatch(loadBucketFB()) }, [])
return (
<div className="App">
<button onClick={
()=>{dispatch(createBucketFB("농구하기"))}}>데이터 추가
</button>
</div>
);
}
export default App;
3. Firestore 데이터 수정하기 (UPDATE)
- LOAD와 CREATE와 다른 점은 업데이트 하려면 수정되기 전의 데이터를 가지고 와야하므로 Firestore와 통신하는 함수에서 리턴하는 함수의 두번째 인자로 getState를 넣어서 데이터를 가지고 와서 getState()로 접근해야 한다!
- 아래처럼 getState()를 출력해보자!
// Firebase 데이터 수정하는 부분 (UPADTE)
export const updateBucketFB = (bucket) => {
return function (dispatch, getState) {
console.log(getState());
};
};
- configStore.js(리덕스 스토어)에서 불러온 리덕스 모듈의 이름이 key 값으로 들어있고 value 값은 현재 상태 값(reducer 에서 마지막으로 리턴한 상태)가 들어있다!.
- 만약 리덕스 스토어에서 import bucket1 from './modules/bucket'; 으로 리덕스 모듈을 불러왔다면 getState()가 리턴한 값의 key는 아래와 다르게 bucket1이 될 것이다!!
- 아래처럼 리덕스 모듈에서 firestore 데이터를 업데이트하는 함수를 작성해보자!
- 아래와 같이 bucket.js 파일을 수정하자
bucket.js
import { firestore } from "../../firebase";
// Action
const LOAD = 'bucket/LOAD';
const UPDATE = "bucket/UPDATE";
// initialState
// 초기 상태값
const initialState = {
list: [
{ text: "치킨 먹기" },
{ text: "컴퓨터 게임하기" },
{ text: "여행 가기" }
]
};
// Action Creators
export const loadBucket = (bucket) => {
return { type: LOAD, bucket };
}
export const updateBucket = (bucket) => {
return { type: UPDATE, bucket };
}
// Firestore에서 collection을 가져옴
const bucket_db = firestore.collection("bucket");
// Firebase와 통신하는 함수. 함수를 반환한다.
// Firebase에서 데이터를 가져오는 부분 (LOAD)
export const loadBucketFB = () => {
// 함수를 반환하는 미들웨어 부분
return function (dispatch) {
bucket_db.get().then((docs) => {
// Firestore에서 가져온 데이터를 저장할 변수
let bucket_data = [];
// "bucket" 콜렉션의 모든 문서에서 데이터와 id를 가져옴!
docs.forEach((doc) => {
if (doc.exists) {
bucket_data = [...bucket_data, { id: doc.id, ...doc.data() }]
}
})
// console.log(bucket_data);
// firestore에서 가져온 데이터를 action에 넣어서 dispatch 해준다!
// 리덕스 모듈에서 action을 dispatch 해주므로 컴포넌트에서는 firestore와
// 통신하는 함수를 불러주면 된다!
dispatch(loadBucket(bucket_data))
});
}
}
// Firebase 데이터 수정하는 부분 (UPADTE)
// 파라미터 bucket을 인덱스로 사용
export const updateBucketFB = (index) => {
return function (dispatch, getState) {
console.log(getState());
// state에 있는 값을 가져옵니다!
// 아래에서 getState()뒤 bucket은 스토어에서 불러온 리덕스 모듈 이름
let bucket_data = getState().bucket.list[index];
// id가 없으면? 바로 끝내버립시다.
if (!bucket_data.id) {
return;
}
bucket_db
.doc(bucket_data.id)
.update({ text: "보드 타기" }) // index에 해당하는 firestore 문서의 text 변경
.then((res) => {
dispatch(updateBucket({ index: index, text: "보드 타기" }));
})
.catch((err) => {
console.log("err");
});
};
};
// Reducer
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
// do reducer stuff
case "bucket/LOAD":
// Firestore에 데이터가 있을때 리턴
if (action.bucket.length > 0) {
return { list: action.bucket };
}
// 없으면 initialState를 리턴해줌
return state;
case "bucket/UPDATE":
const update_bucket_list = state.list.map((item, index) => {
if (index === action.bucket.index) {
return { ...item, text: action.bucket.text };
}
return item;
})
console.log(update_bucket_list);
return { list: update_bucket_list };
default:
return state;
}
}
App.js
- 실행 하면 Firestore에서 첫번째 문서의 text 값이 변경된다!
import './App.css';
import { useEffect } from 'react';
import { useDispatch } from "react-redux";
// Firestore와 통신하는 함수
import { loadBucketFB, updateBucketFB } from './redux/modules/bucket';
function App() {
const dispatch = useDispatch();
useEffect(() => { dispatch(loadBucketFB()) }, [])
return (
<div className="App">
<button onClick={
// Firestore에서 첫번째 문서의 값 변경!
() => { dispatch(updateBucketFB(0)) }}>데이터 수정
</button>
</div>
);
}
export default App;
4. Firestore 데이터 삭제하기 (DELETE)
- 컴포넌트에 index를 넘겨서 firestore에 해당하는 index의 문서를 지워보자!
- 아래와 같이 bucket.js 파일을 수정하자
bucket.js
import { firestore } from "../../firebase";
// Action
const LOAD = 'bucket/LOAD';
const DELETE = "bucket/DELETE";
// initialState
// 초기 상태값
const initialState = {
list: [
{ text: "치킨 먹기" },
{ text: "컴퓨터 게임하기" },
{ text: "여행 가기" }
]
};
// Action Creators
export const loadBucket = (bucket) => {
return { type: LOAD, bucket };
}
export const deleteBucket = (index) => {
return { type: DELETE, index }
}
// Firestore에서 collection을 가져옴
const bucket_db = firestore.collection("bucket");
// Firebase와 통신하는 함수. 함수를 반환한다.
// Firebase에서 데이터를 가져오는 부분 (LOAD)
export const loadBucketFB = () => {
// 함수를 반환하는 미들웨어 부분
return function (dispatch) {
bucket_db.get().then((docs) => {
// Firestore에서 가져온 데이터를 저장할 변수
let bucket_data = [];
// "bucket" 콜렉션의 모든 문서에서 데이터와 id를 가져옴!
docs.forEach((doc) => {
if (doc.exists) {
bucket_data = [...bucket_data, { id: doc.id, ...doc.data() }]
}
})
// console.log(bucket_data);
// firestore에서 가져온 데이터를 action에 넣어서 dispatch 해준다!
// 리덕스 모듈에서 action을 dispatch 해주므로 컴포넌트에서는 firestore와
// 통신하는 함수를 불러주면 된다!
dispatch(loadBucket(bucket_data))
});
}
}
// Firestore에서 데이터를 삭제하는 부분 (DELETE)
export const deleteBucketFB = (index) => {
return function (dispatch, getState) {
const bucket_data = getState().bucket.list[index];
// id가 없으면? 바로 끝내버립시다.
if (!bucket_data.id) {
return;
}
// 삭제하기
bucket_db
.doc(bucket_data.id)
.delete()
.then((res) => {
dispatch(deleteBucket(index));
})
.catch((err) => {
console.log("err");
});
};
};
// Reducer
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
// do reducer stuff
case "bucket/LOAD":
// Firestore에 데이터가 있을때 리턴
if (action.bucket.length > 0) {
return { list: action.bucket };
}
// 없으면 initialState를 리턴해줌
return state;
case "bucket/DELETE":
const deleted_bucket_list = state.list.filter((item, index) => index !== action.index);
console.log(deleted_bucket_list);
return { list: deleted_bucket_list };
default:
return state;
}
}
App.js
- 버튼을 클릭하면 firestore의 첫번째 문서가 삭제된다!
import './App.css';
import { useEffect } from 'react';
import { useDispatch } from "react-redux";
// Firestore와 통신하는 함수
import { loadBucketFB, deleteBucketFB } from './redux/modules/bucket';
function App() {
const dispatch = useDispatch();
useEffect(() => { dispatch(loadBucketFB()) }, [])
return (
<div className="App">
<button onClick={
// firestore에서 첫번째 문서 삭제
() => { dispatch(deleteBucketFB(0)) }}>데이터 삭제
</button>
</div>
);
}
export default App;
@덕구공 :: Duck9s'
주니어 개발자에욤
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!