본문 바로가기

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모듈 규칙


  1. 항상 reducer()란 이름의 함수를 export default 해야합니다.
    • Combine시에 해당 파트 이름으로 불러와 사용하면 되기 때문에
  2. 항상 모듈의 action 생성자들을 함수형태로 export 해야합니다.
  3. 항상npm-module-or-app/reducer/ACTION_TYPE 형태의 action 타입을 가져야합니다.
  4. 어쩌면 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로 변경됨