Redux
20210621 Redux05 : Ducks Pattern, react-router + redux (history Thunk 방식, connected-react-router방식)
Redux 05
Ducks Pattern
- 어떤 라이브러리가 아닌, 많은 사람들이 redux를 가지고 코드를 짜는 좋은 패턴을 말함
- ducks-modular-redux by JisuPark
- 기존 redux에서 ActionCreator, reducer, type이 계속 한 벌씩 생성되므로, 이를 한 묶음으로 모듈화 하여 관리하는 패턴을 제안함
- ActionCreator, reducer, type을 한벌로 만들어 모듈화 시키고 해당 모듈을 combine하여 하나의 reducer로 만들어 해당 reducer로 store을 만들게 됨
Ducks : 1모듈 규칙
- 항상
reducer()
란 이름의 함수를 export default 해야합니다.- Combine시에 해당 파트 이름으로 불러와 사용하면 되기 때문에
- 항상 모듈의
action 생성자
들을 함수형태로 export 해야합니다. - 항상
npm-module-or-app/reducer/ACTION_TYPE
형태의 action 타입을 가져야합니다. - 어쩌면 action 타입들을 UPPER_SNAKE_CASE로 export 할 수 있습니다. 만약, 외부 reducer가 해당 action들이 발생하는지 계속 기다리거나, 재사용할 수 있는 라이브러리로 퍼블리싱할 경우에 말이죠.
Ducks Pattern 으로 변경하기
- redux에 modules 폴더 생성
- 각 파트 별로, ActionCreator, Type, Reducer로 모듈 작성
- type 값은
projectName/moduleName/typeName
형식으로 - reducer 함수의 함수명은 reducer로 통일
- combineReducer에서서 각 reducer 경로 변경
- 각 Container에서 actionCreator 가져와 쓰는 경우 경로 변경
// redux/modules/filter.js
// Action Type
const SHOW_ALL = "redux-start/filter/SHOW_ALL";
const SHOW_COMPLETE = "redux-start/filter/SHOW_COMPLETE";
// Action Creator
export function showAll() {
return { type: SHOW_ALL };
}
export function showComplete() {
return { type: SHOW_COMPLETE };
}
// Reducer
// Initial Data structure
const initialState = "ALL";
export default function reducer(previousState = initialState, action) {
// Change Filter
if (action.type === SHOW_ALL) {
return "ALL";
}
if (action.type === SHOW_COMPLETE) {
return "COMPLETE";
}
return previousState;
}
// redux/modules/todos.js
// Action Type
const ADD_TODO = "redux-start/todos/ADD_TODO";
const COMPLETE_TODO = "redux-start/todos/COMPLETE_TODO";
// Action Creator
export function addTodo(text) {
return {
type: ADD_TODO,
text,
};
}
export function completeTodo(index) {
return {
type: COMPLETE_TODO,
index,
};
}
// Reducer
// Initial Data structure
const initialState = [];
export default function reducer(previousState = initialState, action) {
// Add toDo
if (action.type === ADD_TODO) {
return [...previousState, { text: action.text, done: false }];
}
// Change Done
if (action.type === COMPLETE_TODO) {
return previousState.map((todo, index) => {
if (index === action.index) {
return { ...todo, done: true };
}
return todo;
});
}
return previousState;
}
// redux/modules/users.js
import axios from "axios";
// Action Types
// redux-thunk
const GET_USERS_START = "redux-start/users/GET_USERS_START";
const GET_USERS_SUCCESS = "redux-start/users/GET_USERS_SUCCESS";
const GET_USERS_FAIL = "redux-start/users/GET_USERS_FAIL";
// redux-promise-middleware
const GET_USERS = "redux-start/users/GET_USERS";
const GET_USERS_PENDING = "redux-start/users/GET_USERS_PENDING";
const GET_USERS_FULFILLED = "redux-start/users/GET_USERS_FULFILLED";
const GET_USERS_REJECTED = "redux-start/users/GET_USERS_REJECTED";
// ActionsCreator
export function getUsersStart() {
return {
type: GET_USERS_START,
};
}
export function getUsersSuccess(data) {
return {
type: GET_USERS_SUCCESS,
data,
};
}
export function getUsersFail(error) {
return {
type: GET_USERS_FAIL,
error,
};
}
// Thunk ActionCreator : 비동기 작업
export function getUsersThunk() {
return async (dispatch) => {
try {
dispatch(getUsersStart());
const res = await axios.get("https://api.github.com/users");
dispatch(getUsersSuccess(res.data));
} catch (error) {
dispatch(getUsersFail(error));
}
};
}
// redux-promise-middleware ActionCreator
export function getUsersPromise() {
return {
type: GET_USERS,
payload: async () => {
const res = await axios.get("https://api.github.com/users");
return res.data;
},
};
}
// Reducer
// Initial Data structure
const initialState = {
loading: false,
data: [],
error: null,
};
export default function reducer(state = initialState, action) {
// redux-thunk
if (action.type === GET_USERS_START) {
return {
...state,
loading: true,
error: null,
};
}
if (action.type === GET_USERS_SUCCESS) {
return {
...state,
loading: false,
data: action.data,
};
}
if (action.type === GET_USERS_FAIL) {
return {
...state,
loading: false,
error: action.error,
};
}
// redux-promise-middleware Reducer
if (action.type === GET_USERS_PENDING) {
return {
...state,
loading: true,
error: null,
};
}
if (action.type === GET_USERS_FULFILLED) {
return {
...state,
loading: false,
data: action.payload,
};
}
if (action.type === GET_USERS_REJECTED) {
return {
...state,
loading: false,
error: action.payload,
};
}
return state;
}
react-router 와 redux 함께 사용하기
- redux 로직안에 react-router 로직 섞어 쓰기
npm i react-router-dom
Route Page 형식 만들기
Router 만들기
import "./App.css";
import Home from "./pages/Home";
import Todos from "./pages/Todos";
import Users from "./pages/Users";
import { BrowserRouter, Route } from "react-router-dom";
// App
function App() {
return (
<BrowserRouter>
<Route path="/" exact component={Home} />
<Route path="/todos" exact component={Todos} />
<Route path="/users" exact component={Users} />
</BrowserRouter>
);
}
export default App;
Page 만들기
- src에 pages 폴더 만들기
- page 만들기
// Home.jsx
import { Link } from "react-router-dom";
export default function Home() {
return (
<div>
<h1>Home</h1>
<ul>
<li>
<Link to="/todos">Todos</Link>
</li>
<li>
<Link to="/users">Users</Link>
</li>
</ul>
</div>
);
}
// Todos.jsx
import TodoFormContainer from "../containers/TodoFormContainer";
import TodoListContainer from "../containers/TodoListContainer";
export default function Todos() {
return (
<div>
<TodoListContainer />
<TodoFormContainer />
</div>
);
}
// Users.jsx
import UserListContainer from "../containers/UserListContainer";
export default function Users() {
return (
<div>
<UserListContainer />
</div>
);
}
history 기능 사용하기01 : redux-thunk의 withExtraArgument 방식
- 이제 App이 redux 구조로 변경되면서 기능 관련 함수, 변수등이 redux를 통해서 사용되어 지고 있다.
- 그래서, 어떤 기능을 구현하더라도 작업이 끝난후 다른 페이지로 이동하던 react-router-dom의 history기능을 사용하려면, redux로 history를 가져와 사용해야 함
History Object 만들기
- History object 만드는 부분을 따로 history 파일로 src에 만듦
createBrowserHistory()
함수를 통해서 history Object를 만들어서 전달
import { createBrowserHistory } from "history";
// history 패키지 : react-router-dom 설치시 같이 오는 내부 모듈
const history = createBrowserHistory();
export default history;
History Object -> Router 연결
- redux 도입 전에는 react에서 Router를 설정하는 경우 BrowserRouter Component를 사용했음
- redux 도입 후에는 custom history Object를 사용하기 위해서는 Router Component 를 사용
- history Object 연결
import "./App.css";
import Home from "./pages/Home";
import Todos from "./pages/Todos";
import Users from "./pages/Users";
import { Router, Route } from "react-router-dom";
import history from "./history";
// App
function App() {
return (
<Router history={history}>
<Route path="/" exact component={Home} />
<Route path="/todos" exact component={Todos} />
<Route path="/users" exact component={Users} />
</Router>
);
}
// history 객체를 만들어서 Thunk에 withExtraArgument로 전달하는 방식
export default App;
History Object -> Thunk 미들웨어에 전달하기
- Thunk 방식으로 작업 함수를 구현하는 경우, history를 미들웨어 영역에서 전달해서 처리해야 함
- 일반적인, redux store State를 변경하는 것이 아닌 외부 작업인 경우에는 Thunk 를 사용하여 구현하게 되는데 이처럼 history 기능을 사용하기 위해서는 Thunk 미들웨어를 사용하게 됨
- Thunk에서는
withExtraArgument()
함수를 지원하는데 해당 부분에 해당 기능을 전달하여 function에서 사용할수 있게 함
// store.js
import { applyMiddleware, createStore } from "redux";
import reducer from "./modules/reducer";
import { composeWithDevTools } from "redux-devtools-extension";
import thunk from "redux-thunk";
import promise from "redux-promise-middleware";
import history from "../history";
const store = createStore(
reducer,
composeWithDevTools(
applyMiddleware(thunk.withExtraArgument({ history }), promise)
)
);
export default store;
history 기능 사용하기02 : reducer로 router를 통째로 연결하는 방법
Connected-react-router 패기지
npm i connected-react-router
- redux와 react-router-dom을 강력하게 연결하는 패키지
- router 마저 action을 만들어 dispatch로 요청하는 모델로 만들어 redux에서 관리
History Object -> ConnectedRouter에 연결
ConnectedRouter
컴포넌트 사용- history 연결
import "./App.css";
import Home from "./pages/Home";
import Todos from "./pages/Todos";
import Users from "./pages/Users";
import { Route } from "react-router-dom";
import { ConnectedRouter } from "connected-react-router";
import history from "./history";
// App
function App() {
return (
<ConnectedRouter history={history}>
<Route path="/" exact component={Home} />
<Route path="/todos" exact component={Todos} />
<Route path="/users" exact component={Users} />
</ConnectedRouter>
);
}
export default App;
reducer 에 router 연결 관리
- redux reducer에 router 항목을 넣어 관리
connectRouter()
함수를 할당하고 history를 함수에 인자로 넣어 연결
import { combineReducers } from "redux";
import todos from "./todos";
import filter from "./filter";
import users from "./users";
import { connectRouter } from "connected-react-router";
import history from "../../history";
const reducer = combineReducers({
todos,
filter,
users,
router: connectRouter(history),
});
export default reducer;
dispatch를 처리할 미들웨어 연결
routerMiddleware()
미들웨어를 연결하고, history를 연결
import { applyMiddleware, createStore } from "redux";
import reducer from "./modules/reducer";
import { composeWithDevTools } from "redux-devtools-extension";
import thunk from "redux-thunk";
import promise from "redux-promise-middleware";
import history from "../history";
import { routerMiddleware } from "connected-react-router";
const store = createStore(
reducer,
composeWithDevTools(
applyMiddleware(
thunk.withExtraArgument({ history }),
promise,
routerMiddleware(history)
)
)
);
export default store;
page에서 history 사용하기
- dispatch를 가져와서, connected-react-router 에서 제공하는
push()
함수를 사용해서 action을 만듦- push의 경우 원하는 이동 경로를 받아 Action을 만듦
import { push } from "connected-react-router";
import { useDispatch } from "react-redux";
import { Link } from "react-router-dom";
export default function Home() {
const dispatch = useDispatch();
return (
<div>
<h1>Home</h1>
<ul>
<li>
<Link to="/todos">Todos</Link>
</li>
<li>
<Link to="/users">Users</Link>
</li>
</ul>
<button onClick={click}>todos로 이동</button>
</div>
);
function click() {
dispatch(push("/todos"));
}
}
확인하기
- push를 통해 dispatch 하게 되면
@@router/LOCATION_CHANGE
값의 type을 가진 Action이 만들어 져서 처리됨- location에 pathname에 해당 경로가 들어가 있음
- action에 push로 변경됨