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
-
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
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()));
- 주로 chrome 을 사용하는데 크롬 웹 스토어에 Redux DevTools 를 설치합니다.
- 주로 redux에 문제가 있을 경우에 devTools 를 사용해서 문제 해결을 하게 됩니다.
4.redux-thunk
- redux middleware 중에서 가장많이 사용되고 있는 라이브러리 인 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 입니다
설치
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
- 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 사용)
설치
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
-
React redux official site - https://react-redux.js.org/tutorials/quick-start
-
Redux middleware - https://redux.js.org/understanding/history-and-design/middleware
Leave a comment