리액트 기초/Firestore + Redux

Firestore & Redux - redux-thunk

덕구공 2021. 9. 1. 11:30

미리 해야할 일

1. Redux와 React 프로젝트 연결

  • 혹은 아래 코드를 붙여넣기하고 필요한 라이브러리를 설치하자
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를 연결하자
  • 여기를 참고해서 Firestore 설정을 해서 아래처럼 Firestore 콜렉션을 만들어보자

  • 여기를 참고해서 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;