이번 회차 강의는, 이전 버전을 기준으로 제작되었기 때문에 최신 버전으로 강의를 따라가면 많은 오류들이 발생한다.. 그래서 나도 이전까지 진행한 vanila-redux앱이 아닌, react-redux앱을 새로 만들어 다시 환경 설정을 진행했다.
$ yarn create react-app react-redux
$ npm i react@17.0.2
$ npm i react-dom@17.0.2
$ yarn add react-redux react-router-dom@5.1.2
노마드 코더 강의를 보다보면 종종 이런 버전 차이로 인해 오류가 나는 경우가 많은데 해결을 위한 방법은 크게 두가지 인 것 같다.
이전 vanila redux 강의에서 진행한 것과 같이, 기본적인 store.js 세팅을 진행한다.
// store.js
import { createStore } from "redux";
import { connect } from "react-redux";
const ADD = "ADD";
const DELETE = "DELETE";
export const addToDo = (text) => {
return {
type: ADD,
text,
};
};
export const DeleteToDo = (id) => {
return {
type: DELETE,
id,
};
};
const reducer = (state = [], action) => {
switch (action.type) {
case ADD:
return [
{
text: action.text,
id: Date.now(),
},
...state,
];
case DELETE:
return state.filter((toDo) => toDo !== action.id);
default:
return state;
}
};
const store = createStore();
store.subscribe();
export default store;
여기까지 진행하였다면, 이제 react-redux에서 지원하는 function을 사용해보자.
connect() function은 components를 store에 연결해준다. 전체적인 형태는 아래와 같다.
function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)
그럼 계속해서 Home.js의 하단에 store에서 state를 component에게로 가져오는 MapStateToProps을 작성해보자. arguments로는 state를 쓰고 선택적으로 props를 쓰고 Object를 반환한다.
mapStateToProps?: (state, ownProps?) => Object
component내에서 state가 변경되면, store에 변경된 state를 전달해주는 dispatch action이 필요하다. 이때 react-redux에서는 MapDispatchToProps를 사용한다. MapDispatchToProps는 component내에서 function으로 선언하고, 2가지 parameter를 가지며 Objedct를 리턴한다.
mapDispatchToProps?: Object | (dispatch, ownProps?) => Object
이제 위에서 배운 두가지 function을 통해 component와 props를 관리해보자.
ToDo 컴포넌트를 Home에서 렌더링하는 구조이다.
// Home.js
import { createStore } from "redux";
import { connect } from "react-redux";
const ADD = "ADD";
const DELETE = "DELETE";
const addToDo = (text) => {
return {
type: ADD,
text,
};
};
const deleteToDo = (id) => {
return {
type: DELETE,
id: parseInt(id),
};
};
const reducer = (state = [], action) => {
switch (action.type) {
case ADD:
return [
{
text: action.text,
id: Date.now(),
},
...state,
];
case DELETE:
return state.filter((toDo) => toDo.id !== action.id);
default:
return state;
}
};
const store = createStore(reducer);
export const actionCreators = {
addToDo,
deleteToDo,
};
export default store;
To Do 리스트를 생성할 ToDo 컴포넌트의 코드는 아래처럼 작성한다.
코드를 보면 컴포넌트가 parameter로 {text, onClick} 오브젝트를 받고 있는데, onClick function을 mapDispatchToProps에서 선언하고 있으며 이를 connect()으로 감싸 export 하고 있다는 점에 주의해서 코드를 작성하자.
// components/ToDo.js
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { actionCreators } from "../store";
function ToDo({ text, onBtnClick, id }) {
return (
<li>
<Link to={`/${id}`}>
{text} <button onClick={onBtnClick}>DEL</button>
</Link>
</li>
);
}
function mapDispatchToProps(dispatch, ownProps) {
return {
onBtnClick: () => dispatch(actionCreators.deleteToDo(ownProps.id)),
};
}
export default connect(null, mapDispatchToProps)(ToDo);
connect()를 통해 Detail page content를 쉽게 관리할 수 있다. 지금 ToDo 컴포넌트의 코드를 살펴보면, ToDo 리스트를 클릭하면 그 리스트가 가진 id에 맞는 Detail 페이지로 이동하도록 하고 있다. ToDo Detail 페이지를 렌더링 하기 위해 Props로 toDo 오브젝트를 받고 그 오브젝트의 state json 데이터를 이용하여 mapStateToProps 함수를 작성해 컴포넌트를 렌더링할 수 있다.
import React from "react";
import { connect } from "react-redux";
function Detail({ toDo }) {
return (
<>
<h1>{toDo?.text}</h1>
<h5> Created at : {toDo?.id}</h5>
</>
);
}
function mapStateToProps(state, ownProps) {
const {
match: {
params: { id },
},
} = ownProps;
return { toDo: state.find((toDo) => toDo.id === parseInt(id)) };
}
export default connect(mapStateToProps)(Detail);
리덕스를 사용할 때, 새로고침을 하게되면 Store로부터 받아온 state 정보가 날아가게 된다. 새로고침 시 데이터가 날아가지 않게 만들려면 client의 local storage 혹은 session storage에 reducer state를 저장해 사용한다. Client-side storage의 원리 혹은 개념에 대한 내용은 링크를 더 살펴보자.
리덕스 없이 local storage에 데이터를 저장할 땐 localStorage.setItem("name", "Chris");
와 같이 작성해 로컬 스토리지에 접근할 수 있다.
위의 setItem method와 같이 역할을 수행해주는 라이브러리가 redux-persist 이다.
React app에서 redux-persist를 쓸 때엔 가장먼저 리액트의 render root 컴포넌트를 PersistGate
로 감싸야 한다. PersistGate는 client side로부터 State load전까지 렌더링을 방지하고 그 받아온 데이터를 Store에 저장한다. 보통 Redux를 사용하낟면 Provider가 이미 루트의 상위 컴포넌트 이므로, App.js는 아래와 같이 작성된다.
import { PersistGate } from 'redux-persist/integration/react'
// ... normal setup, create store and persistor, import components etc.
const App = () => {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<RootComponent />
</PersistGate>
</Provider>
);
};
// configureStore.js
import { createStore } from 'redux'
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage' // defaults to localStorage for web
import rootReducer from './reducers'
const persistConfig = {
key: 'root',
storage,
}
const persistedReducer = persistReducer(persistConfig, rootReducer)
export default () => {
let store = createStore(persistedReducer)
let persistor = persistStore(store)
return { store, persistor }
}
위에서 살펴본 기본 개념처럼, Redux-persist 라이브러리는 Client 렌더링에 관여한다. 그렇기때문에 redux는 Data fetch가 언제이루어지는지 알 수없는 비동기 상태나 서버 상태 관리에는 적절하지 않다고 한다. 서버 상태 관리시에는 이전에 학습했던 React-Query
를 사용하도록 하자.