11-2.Redux-advanced

Updated:


1.Async action with Redux

container 는 redux 의 로직만 다루고 (그래서 비동기 작업도 여기서 실행함)

이 방법은 redux 에 middleware 를 사용하지 않고 비동기 호출하는 방식임

actions.js 에서 users types 설정함

// users types

// github API 호출을 시작하는 것을 의미함
export const GET_USERS_START = "GET_USERS_START";
// github API 호출에대해서 응답이 성공적으로 돌아온 경우를 의미함
export const GET_USERS_SUCCESS = "GET_USERS_SUCCESS";
// github API 호출에대해서 응답이 실패한 경우를 의미함
export const GET_USERS_FAIL = "GET_USERS_FAIL";

export const getUsersStart = () => {
  return {
    type: GET_USERS_START,
  };
};

export const getUsersSuccess = (data) => {
  return {
    type: GET_USERS_SUCCESS,
    data,
  };
};

export const getUsersFail = (error) => {
  return {
    type: GET_USERS_FAIL,
    error,
  };
};

userListContainer.js 에서 비동기 작성 실행

import { useCallback } from "react";
import { useDispatch } from "react-redux";
import { useSelector } from "react-redux";
import UserList from "../components/UserList";
import { getUsersFail, getUsersStart, getUsersSuccess } from "../redux/actions";
import axios from "axios";

const UserListContainer = () => {
  // state 에서 users.data 를 불러옴
  const users = useSelector((state) => state.users.data);
  // useDispatch 사용
  const dispatch = useDispatch();

  // 비동기 함수 api 호출
  const getUsers = useCallback(async () => {
    try {
      // getUsersStart 액션 실행
      dispatch(getUsersStart());
      // API 호출
      const res = await axios.get("https://api.github.com/users");
      // 호출 성공시 response.data 가져옴
      dispatch(getUsersSuccess(res.data));
      // 에러 발생시 dispatch error 실행
    } catch (error) {
      dispatch(getUsersFail(error));
    }
    // dispatch 될때마다만 적용 dependency
  }, [dispatch]);

  // UserList 에 users, getUsers props 전송함
  return <UserList users={users} getUsers={getUsers} />;
};

export default UserListContainer;
// in UserListContainer.jsx

import { useCallback } from "react";
import { useDispatch } from "react-redux";
import { useSelector } from "react-redux";
import UserList from "../components/UserList";
import { getUsersFail, getUsersStart, getUsersSuccess } from "../redux/actions";
import axios from "axios";

const UserListContainer = () => {
  // state 에서 users.data 를 불러옴
  const users = useSelector((state) => state.users.data);
  // useDispatch 사용
  const dispatch = useDispatch();

  // 비동기 함수 api 호출
  const getUsers = useCallback(async () => {
    try {
      // getUsersStart 액션 실행
      dispatch(getUsersStart());
      // API 호출
      const res = await axios.get("https://api.github.com/users");
      // 호출 성공시 response.data 가져옴
      dispatch(getUsersSuccess(res.data));
      // 에러 발생시 dispatch error 실행
    } catch (error) {
      dispatch(getUsersFail(error));
    }
    // dispatch 될때마다만 적용 dependency
  }, [dispatch]);

  // UserList 에 users, getUsers props 전송함
  return <UserList users={users} getUsers={getUsers} />;
};

export default UserListContainer;

Present component 인 userList에 받은 props data 를 뿌려 줍니다

import { useEffect } from "react";

const UserList = ({ users, getUsers }) => {
  useEffect(() => {
    getUsers();
  }, [getUsers]);

  if (users.length === 0) {
    return <p>현재 유저 정보 없음</p>;
  }
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.login}</li>
      ))}
    </ul>
  );
};

export default UserList;

2.Redux middleware

Redux middleware 자세히 보기

  • middleware 가 dispatch 의 앞뒤에 코드를 추가할수 있게 해줍니다.

  • middleware 가 여러개면 middleware 가 순차적으로 실행됩니다.

  • 두가지 단계가 있는데.

    • store를 만들때, middleware를 성정하는 부분 {createStore, applyMiddleware} from redux

    • dispatch가 호출될때 실제로 middleware를 통과하는 부분

  • dispatch 메소드를 통해 store로 가고 있는 액션을 가로채는 코드임

  • middleware 는 createStore() 하는 부분에서 설정되어야 하기 때문에 store.js 파일에서 설정을 주로 해줍니다.

// in store.js

import { applyMiddleware, createStore } from "redux";
import todoApp from "./reducers/reducer";

const middleware1 = (store) => {
  //next 라고 다음 middleware를 지칭하는 형태로 사용함
  console.log("middleware1", 0);
  return (next) => {
    console.log("middleware1", 1, next);
    return (action) => {
      console.log("middleware1", 2);
      const returnValue = next(action);
      console.log("middleware1", 3);
      return returnValue;
    };
  };
};

const middleware2 = (store) => {
  //next 라고 다음 middleware를 지칭하는 형태로 사용함
  console.log("middleware2", 0);
  return (next) => {
    console.log("middleware2", 1, next);
    return (action) => {
      console.log("middleware2", 2);
      const returnValue = next(action);
      console.log("middleware2", 3);
      return returnValue;
    };
  };
};

// todoApp 뒤에있는 것이 enhancer 인 applyMiddleware 함수를 사용함
const store = createStore(todoApp, applyMiddleware(middleware1, middleware2));

export default store;

console.log 로 보게 되면 dispatch 될때 마다 순서가 확인하는게 중요합니다.

middleware1 2 => middleware2 2 => middleware2 3 => middleware3 3

image

redux middleware 는 위와 같이 구현하는 하기 위해 middleware 플러그인을 사용합니다.

3.redux-devtools

middleware를 설치해서 브라우저에 있는 devtools를 연결하는 작업을 하는것을 redux-devtools 입니다.

설치

yarn add redux-devtools-extension -D

applyMiddleware 의 실행 결과를 composeWithDevTools로 감싸서 실행해 줍니다. 그렇게 하면 redux-devtools-extension 으로 데이터를 보낼 준비가 된것입니다.

//  in store.js

const store = createStore(todoApp, composeWithDevTools(applyMiddleware()));

Redux DevTools Extension Github

  • 주로 chrome 을 사용하는데 크롬 웹 스토어에 Redux DevTools 를 설치합니다.

image

  • 주로 redux에 문제가 있을 경우에 devTools 를 사용해서 문제 해결을 하게 됩니다.

image

4.redux-thunk

  • redux middleware 중에서 가장많이 사용되고 있는 라이브러리 인 redux-thunk 입니다.

redux-thunk

  • redux를 만든 사람이 만든 라이브러리

  • redux에서 비동기 처리를 위한 라이브러리

  • 액선 생성자를 활용하여 비동기 처리

  • 액션 생성자가 액션을 리턴하지 않고, 함수를 리턴함

설치

yarn add redux-thunk
  • middleware는 함수이기 때문에 라이브러리부터 함수를 import 한 다음에 함수를 applyMiddleware 안으로 넣어야 함

  • thunk 는 함수 생성자가 함수를 return 할때만 반응하고, 원래대로 action 객체를 return 할때는 기존 동작 처럼 동작하기 합니다.

const store = createStore(todoApp, composeWithDevTools(applyMiddleware(thunk)));
  • action.js 에서 Thunk 함수에서 fetch API 를 사용합니다.
// action.js

export const getUsersThunk = () => {
  return async (dispatch) => {
    try {
      // getUsersStart 액션 실행
      dispatch(getUsersStart());
      // API 호출
      const res = await axios.get("https://api.github.com/users");
      // 호출 성공시 response.data 가져옴
      dispatch(getUsersSuccess(res.data));
      // 에러 발생시 dispatch error 실행
    } catch (error) {
      dispatch(getUsersFail(error));
    }
  };
};
  • 기존 fetch 한곳에서는 getUsersThunk() 를 호출해서 사용합니다
import { getUsersThunk } from "../redux/actions";

const getUsers = useCallback(() => {
  dispatch(getUsersThunk());
}, [dispatch]);
  • 기존에는 비동기 로직이 container 에서 이뤄졌다면 redux-thunk 를 사용하면 action 을 다루는 action 생성함수에서 처리하게 되고 container 는 그냥 action 생성자를 component 에 전달하는 역활을 합니다

  • 이렇게 될경우에는 실제로 dispatch 되는 로직들은 action 에서 관리되기 때문에 관심사가 적절히 분리가 됩니다.

  • 이와같이 redux-thunk 는 비동기 작업할때 보다 코드를 간결화 하면서 효율적으로 사용할 수 있는 라이브러리 입니다.

5.redux-promise-middleware

redux 에서 비동기 처리를 위한 또 다른 미들웨어인 promise-middleware 입니다

Redux Promise Middleware Github

설치

yarn add redux-promise-middleware
  • store 에서 promise-middleware
import promise from "redux-promise-middleware";

const store = createStore(
  todoApp,
  composeWithDevTools(applyMiddleware(thunk, promise))
);
  • promise action 생성

  • redux-promise-middleware 를 사용하게 되면 promise 의 객체가 생성된 직후에 pending 상태로 돌입하게 되고, 정상적으로 완료되면 fulfilled 라고 되고, error 가 되면 rejected 라고 나타나가 됩니다.

  • 그래서 promise-middleware 에 맞는 type을 사용해줘야 됩니다.

// in actions.js

// Promise type
const GET_USERS = "GET_USERS";

// export promise-middleware 에 맞도록 type 을 설정 한 후에 reducer 에서도 사용할 수 있도록 export 해줍니다.
export const GET_USERS_PENDING = "GET_USERS_PENDING";
export const GET_USERS_FULFILLED = "GET_USERS_FULFILLED";
export const GET_USERS_REJECTED = "GET_USERS_REJECTED";

export const getUsersPromise = () => {
  return {
    type: GET_USERS,
    payload: async () => {
      // dispatch 를 직접해줄 필요가 없음
      const res = await axios.get("https://api.github.com/users");
      return res.data;
    },
  };
};
  • reducer 에서도 마찬가지로 type에 맞게 변경 해주어야 합니다
// in reducers -> users.js

// promise-middleware 의 PENDING 상태를 포함해서 reducer 지정 해줌
const users = (state = initialState, action) => {
  if (action.type === GET_USERS_START || action.type === GET_USERS_PENDING) {
    return {
      ...state,
      loading: true,
      error: null,
    };
  }

  // SUCCESS 와 data 받는 type 이 다르기 때문에 action.payload 로 해줍니다
  if (action.type === GET_USERS_FULFILLED) {
    return {
      ...state,
      loading: false,
      data: action.payload,
    };
  }

    // promise-middleware 도 마찬가지로 REJECTED 시 reducer 를 설정해 줍니다.
  if (action.type === GET_USERS_REJECTED) {
    return {
      ...state,
      loading: false,
      error: action.payload,
    };
  }
  • 이로서 redux-promise-middleware 어떤 type 으로 dispatch 할 때, payload 에 promise 객체가 있으면 이 타입의 뒤에 부분에다가 PENDING, FULFILLED, REJECTED 라고 정해진 타입을 붙이고 성공할 경우에는 return 되는 data 를 action.payload 에 넣어주고 실패 할 경우에도 action.payload 로 넣어 주면 됩니다.

6.Ducks pattern

Ducks pattern github

  • redux 를 많이 쓰는 사람들이 이런식으로 많이 쓰니까 pattern 으로 만들어 놓은 예시라고 생각하면 됩니다.

규칙

1.항상 reducer()란 이름의 함수를 export default 해야합니다.

2.항상 모듈의 action 생성자들을 함수형태로 export 해야합니다.

3.항상 npm-module-or-app/reducer/ACTION_TYPE 형태의 action 타입을 가져야합니다.

4.어쩌면 action 타입들을 UPPER_SNAKE_CASE로 export 할 수 있습니다. 만약, 외부 reducer가 해당 action들이 발생하는지 계속 기다리거나, 재사용할 수 있는 라이브러리로 퍼블리싱할 경우에 말이죠.

재사용가능한 Redux 라이브러리 형태로 공유하는 {actionType, action, reducer} 묶음에도 위 규칙을 추천합니다.

구조

  • src/redux 라는 폴더를 두고 store.js 에는 createStore() 설정을 하고, modules 폴더를 만들어서 그안에 action type, function, reducer 를 합쳐서 모듈화 해서 관리합니다

예시)

import axios from "axios";

// action type 정의
// github API 호출을 시작하는 것을 의미함
export const GET_USERS_START = "redux-start/users/GET_USERS_START";
// github API 호출에대해서 응답이 성공적으로 돌아온 경우를 의미함
export const GET_USERS_SUCCESS = "redux-start/users/GET_USERS_SUCCESS";
// github API 호출에대해서 응답이 실패한 경우를 의미함
export const GET_USERS_FAIL = "redux-start/users/GET_USERS_FAIL";

// redux-promise-middleware types
// Promise type
const GET_USERS = "redux-start/users/GET_USERS";

export const GET_USERS_PENDING = "redux-start/users/GET_USERS_PENDING";
export const GET_USERS_FULFILLED = "redux-start/users/GET_USERS_FULFILLED";
export const GET_USERS_REJECTED = "redux-start/users/GET_USERS_REJECTED";

// action 생성 함수
export const getUsersStart = () => {
  return {
    type: GET_USERS_START,
  };
};

export const getUsersSuccess = (data) => {
  return {
    type: GET_USERS_SUCCESS,
    data,
  };
};

export const getUsersFail = (error) => {
  return {
    type: GET_USERS_FAIL,
    error,
  };
};

// 초기값
const initialState = {
  loading: false,
  data: [],
  error: null,
};

// reducer
// promise-middleware 의 PENDING 상태를 포함해서 reducer 지정 해줌
const users = (state = initialState, action) => {
  if (action.type === GET_USERS_START || action.type === GET_USERS_PENDING) {
    return {
      ...state,
      loading: true,
      error: null,
    };
  }
  if (action.type === GET_USERS_SUCCESS) {
    return {
      ...state,
      loading: false,
      data: action.data,
    };
  }
  // SUCCESS 와 data 받는 type 이 다르기 때문에 action.payload 로 해줍니다
  if (action.type === GET_USERS_FULFILLED) {
    return {
      ...state,
      loading: false,
      data: action.payload,
    };
  }
  if (action.type === GET_USERS_FAIL) {
    return {
      ...state,
      loading: false,
      error: action.error,
    };
  }
  // promise-middleware 도 마찬가지로 REJECTED 시 reducer 를 설정해 줍니다.
  if (action.type === GET_USERS_REJECTED) {
    return {
      ...state,
      loading: false,
      error: action.payload,
    };
  }

  return state;
};

// redux-thunk
export const getUsersThunk = () => {
  return async (dispatch) => {
    try {
      // getUsersStart 액션 실행
      dispatch(getUsersStart());
      // API 호출
      const res = await axios.get("https://api.github.com/users");
      // 호출 성공시 response.data 가져옴
      dispatch(getUsersSuccess(res.data));
      // 에러 발생시 dispatch error 실행
    } catch (error) {
      dispatch(getUsersFail(error));
    }
  };
};

// redux-promise-middleware
export const getUsersPromise = () => {
  return {
    type: GET_USERS,
    payload: async () => {
      // dispatch 를 직접해줄 필요가 없음
      const res = await axios.get("https://api.github.com/users");
      return res.data;
    },
  };
};

export default users;
  • 그리고 modules 안에 reducer.js 파일에 각 모듈은 합칠 수 있는 combineReducers()를 사용하여 합쳐 줍니다.

ducks pattern 장점

  • module 이라는 관심사로 하나로 묶게 되면 type, function, reducer 를 한번에 관리하기 수월해지게 됩니다.

7.react-router 와 redux 연결하기

  • redux 와 react-router 를 연결한다는 것은 redux 의 로직 안에서 (예를 들어 비동기 요청에서 결과가 들어 왔을 경우 page를 옴긴다는 상황에서 redux action 안에 로직이 들어가기 때문에 그안에 어떻게 react-router 의 history 같은것을 불러다가 페이지에 나타낼 수 있는지를 말하는 것이다)

a.얕은 integration 을 가져다가 사용하기 (redux-thunk 사용)

  • thunk 라이브러리에서 middleware 를 설정하기 전에 추가적으로 다른 argument 를 넣을 수 있습니다.

  • extraArgument() 함수를 이용해서 추가적으로 argument 를 추가 해서 그것을 middleware 로 활용하는 방법입니다.

// src 경로에 history.js 생성
import { createBrowserHistory } from "history";

// BrowserRouter 의 history 와 아래 변수 history 와 맞춰 줘야 함
const history = createBrowserHistory();

export default history;
// store.js
const store = createStore(
  todoApp,
  composeWithDevTools(
    applyMiddleware(thunk.withExtraArgument({ history }), promise)
  )
);
// App.js

import history from './history';

function App() {
  return (
    <BrowserRouter history={history}>
    ....
    </BrowserRouter>

b.redux 안에 reducer 를 사용해서 router 를 전체 연결하는 방식 (connected-react-router 사용)

connected-react-router 자세히 보기..

설치

yarn add connected-react-router
  • redux 와 react-router 를 강하게 연결시켜주는 역활을 합니다
// reducer.js 파일에서 router 이름의 reducer 를 추가해 줌

import { connectRouter } from "connected-react-router";
import history from "../../history";

const reducer = combineReducers({
  todos,
  filter,
  users,
  router: connectRouter(history),
});

export default reducer;
  • store 쪽에 middleware 로써 routerMiddleware 를 설정합니다
// in store.js
import { routerMiddleware } from "connected-react-router";

// react-router 의 history 를 가져와서 thunk의 extraArgument() 를 사용하여 새로운 middleware

const store = createStore(
  todoApp,
  composeWithDevTools(
    applyMiddleware(
      thunk.withExtraArgument({ history }),
      promise,
      routerMiddleware(history)
    )
  )
);

export default store;
  • app.js 에서 ConnectedRouter 로 경로를 감싸 줍니다.
// in App.js

import { ConnectedRouter } from "connected-react-router";

function App() {
  return (
    <ConnectedRouter history={history}>
      <Route exact path="/" component={Home} />
      <Route exact path="/todos" component={Todos} />
      <Route exact path="/users" component={Users} />
    </ConnectedRouter>
  • 테스트로 버튼을 누르면 /todos 페이지로 이동시키는 동작 만들기
// in Home.jsx

import { push } from "connected-react-router";
import { useDispatch } from "react-redux";

const Home = () => {
  // dispatch 가져오기
  const dispatch = useDispatch();

  // click 할 경우에 push /todos 로 설정된 action 이 실행되게 dispatch 되는 함수
  const click = () => {
    dispatch(push("/todos"));
  };

  return (
      <button onClick={click}>todos로 이동</button>
  )

export default Home;

🔶 🔷 📌 🔑

Reference

Categories:

Updated:

Leave a comment