리덕스에서 파이어스토어의 데이터를 가져와보자. 파이어스토어는 비동기 통신을 통해 데이터를 가져오는데 리덕스에서 비동기 통신으로 얻은 데이터를 처리 하기 위해서는 중간에서 미들웨어로써 역할을 해줄 redux-thunk
가 필요하다. 다음과 같이 설치한다.
yarn add redux-thunk
이제 만들어둔 리덕스의 설정 파일인 configStore.js
에 적용하면 된다.
import { createStore, combineReducers, applyMiddleware } from "redux";
import bucket from "./modules/bucket";
import { createBrowserHistory } from "history";
import thunk from 'redux-thunk';
export const history = createBrowserHistory();
const middlewares = [thunk];
const enhancer = applyMiddleware(...middlewares);
const rootReducer = combineReducers({ bucket });
const store = createStore(rootReducer, enhancer);
export default store;
이 과정은 하나의 기능을 추가 할 때마다 일련의 작업을 반복할 것이다.
bucket.js
에 파이어스토어와 통신하는 함수를 추가한다.
import { firestore } from '../../firebase'
const bucket_db = firestore.collection("bucket");
export const loadBucketFB = () => {
return function (dispatch) {
bucket_db.get().then((docs) => {
let bucket_data = [];
docs.forEach((doc) => {
if (doc.exists) {
bucket_data = [...bucket_data, { id: doc.id, ...doc.data() }]
}
})
dispatch(loadBucket(bucket_data));
});
}
};
이번엔 리듀서를 수정해 액션이 실행되게 할 차례이다. bucket.js
의 리듀서 스위치 케이스를 수정한다.
// in reducer switch
case "bucket/LOAD": {
if (action.bucket.length > 0) {
return {list: action.bucket};
}
return state;
}
홈이 호출될 때 로드 될 수 있도록 App.js
를 수정하자.
import { loadBucket, createBucket, loadBucketFB } from "./redux/modules/bucket";
const mapDispachToProps = (dispatch) => {
return {
load: () => {
dispatch(loadBucketFB());
},
create: (bucket) => {
dispatch(createBucket(bucket));
}
}
};
componentDidMount() {
this.props.load();
};
이제 콘솔을 통해 데이터를 확인할 수 있다.
조회와 마찬가지로 입력 함수를 추가하고 리듀서를 수정한다.
export const addBucketFB = (bucket) => {
return function (dispatch) {
let bucket_data = { text: bucket, completed: false };
bucket_db.add(bucket_data).then(docRef => {
bucket_data = { ...bucket_data, id: docRef.id };
dispatch(createBucket(bucket_data));
})
}
}
...
//in switch
case "bucket/CREATE": {
const new_bucket_list = [
...state.list,
action.bucket
];
return { list: new_bucket_list };
}
App.js
에서 연결해주자.
import { loadBucket, createBucket, loadBucketFB, addBucketFB } from "./redux/modules/bucket";
...
const mapDispachToProps = (dispatch) => {
return {
load: () => {
dispatch(loadBucketFB());
},
create: (bucket) => {
dispatch(addBucketFB(bucket));
}
}
};
수정 함수를 만든다. 이번에는 리듀서에서 수행하는 작업이 기존과 동일하므로 리듀서를 수정할 필요는 없다.
export const updateBucketFB = (index) => {
return function (dispatch, getState) {
const _bucket_data = getState().bucket.list[index];
if (!_bucket_data.id) return;
let bucket_data = { ..._bucket_data, completed: true };
bucket_db.doc(bucket_data.id).update(bucket_data).then(docRef => {
dispatch(updateBucket(index));
}).catch(error => {
console.error(error);
});
}
}
Detail.js
파일에서 updateBucketFB
함수를 불러와 기존의 액션과 교체해준다.
import { deleteBucket, updateBucketFB } from './redux/modules/bucket';
...
<Button onClick={() => {
dispatch(updateBucketFB(bucket_index));
props.history.goBack();
}}>완료</Button>
...
업데이트와 마찬가지로 삭제 함수를 만든다. 이번에도 리듀서에서 수행하는 작업이 기존과 동일하므로 리듀서를 수정할 필요는 없다.
export const deleteBucketFB = (index) => {
return function (dispatch, getState) {
const _bucket_data = getState().bucket.list[index];
if (!_bucket_data.id) return;
bucket_db.doc(_bucket_data.id).delete().then(docRef => {
dispatch(deleteBucket(index))
}).catch(error => {
console.error(error);
});
}
}
Detail.js
파일에서 deleteBucketFB
함수를 불러와 기존의 액션과 교체해준다.
import { deleteBucketFB, updateBucketFB } from './redux/modules/bucket';
...
<Button onClick={() => {
dispatch(deleteBucketFB(bucket_index));
props.history.goBack();
}}>삭제
</Button>
...
BootStrap
처럼 기본적인 UI 템플릿을 제공하는 Material UI를 추가해보자. bash
에서 아래와 같이 입력해 설치한다.
yarn add @material-ui/core @material-ui/icons
버튼 UI를 불러와서 몇가지 속성을 사용해보자.
import { Button } from '@material-ui/core';
import { ButtonGroup } from '@material-ui/core';
...
<ButtonGroup>
<Button color="primary" />
<Button color="secondary" />
<Button />
</ButtonGroup>
잘 적용 된것을 확인할 수 있다.
요청과 응답사이의 공백에서 기본값이 보이는 문제를 해결하기 위해 로딩을 가려주는 스피너를 만들어보자. 먼저 UI에 해당되는 Spinner.js
컴포넌트를 만들자.
import React from 'react'
import styled from 'styled-components'
import { Eco } from "@material-ui/icons"
export const Spinner = () => {
return (
<Outer>
<Eco style={{ fontSize: "150px", color: "#673ab7" }} />
</Outer>
)
}
const Outer = styled.div`
position: fixed;
top:0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: #ede2ff;
`;
bucket.js
파일에서 기본 값인 initialState
에 로딩 여부를 확인 할 수 있는 변수 is_loaded
를 추가하고 로드를 담당하는 리듀서의 bucket/LOAD
케이스를 수정한다.
const initialState = {
list: [
{ text: "영화관 가기", completed: false },
{ text: "매일 책읽기", completed: false },
{ text: "수영 배우기", completed: false },
],
is_loaded: false,
};
...
// in reducer switch
case "bucket/LOAD": {
if (action.bucket.length > 0) {
return { list: action.bucket, is_loaded: true };
}
return state;
}
...
App.js
에서 스피너를 불러오고 render
에서 is_loaded
의 값을 확인해 삼형연사자로 나뉘게 한 뒤 기존 홈화면을 <React.Fragment>
안으로 넣어준다.
import { Spinner } from './Spinner'
<div>
{!this.props.is_loaded ? (<Spinner />) : (
<React.Fragment>
// 기존의 홈
</React.Fragment>
)
}
</div>
이제 추가, 수정, 삭제 이후 홈으로 돌아갈때도 스피너가 동작하도록 해보자. bucket.js
파일에서 액션, 액션 크리에이터, 리듀서 케이스를 추가하자.
const LOADED = "bucket/LOADED";
...
export const isLoaded = (loaded) => {
return { type: LOADED, loaded };
}
...
// in reducer switch
case "bucket/LOADED": {
return { ...state, is_loaded: action.loaded };
}
기존의 입력, 수정, 삭제를 위한 비동기 통신 함수 안에서 스피너가 호출되도록 수정해보자.
export const addBucketFB = (bucket) => {
return function (dispatch) {
let bucket_data = { text: bucket, completed: false };
dispatch(isLoaded(false));
bucket_db.add(bucket_data).then(docRef => {
bucket_data = { ...bucket_data, id: docRef.id };
dispatch(createBucket(bucket_data));
dispatch(isLoaded(true));
})
}
}
export const updateBucketFB = (index) => {
return function (dispatch, getState) {
const _bucket_data = getState().bucket.list[index];
let bucket_data = { ..._bucket_data, completed: true };
if (!_bucket_data.id) return;
dispatch(isLoaded(false));
bucket_db.doc(bucket_data.id).update(bucket_data).then(docRef => {
dispatch(updateBucket(index));
dispatch(isLoaded(true));
}).catch(error => {
console.error(error);
});
}
}
export const deleteBucketFB = (index) => {
return function (dispatch, getState) {
const _bucket_data = getState().bucket.list[index];
if (!_bucket_data.id) return;
dispatch(isLoaded(false));
bucket_db.doc(_bucket_data.id).delete().then(docRef => {
dispatch(deleteBucket(index))
dispatch(isLoaded(true));
}).catch(error => {
console.error(error);
});
}
}
아마존에서 제공하는 파일 스토리지 중 하나인 S3를 이용해 지금까지 만든 bucket-list
를 정적 웹사이트 형식으로 배포해보자.
AWS 콘솔에 접속해 S3 서비스를 선택하고 버킷을 만들자. 이 때 모든 퍼블릭 액세스 차단 설정
을 해제하고 밑의 현재 설정으로 인해 이 버킷과 그 안에 포함된 객체가 퍼블릭 상태가 될 수 있음을 알고 있습니다.
를 체크하여 모두에게 서비스를 노출한다.
완성된 버킷목록의 액세스 탭 하위의 객체를 퍼블릭으로 설정할 수 있음
이 나오면 완료 된 것이다.