<step80>'redux(react)_리덕스 미들웨어'
1)
npm install redux
npm install react-redux
카운터 만들기
1. 리덕스 모듈
액션타입, 초기값, 액션생성함수, 리듀서
modules 폴더
① counter.js - 카운터를 관리하는 리듀서
modules/counter.js
// 액션타입, 액션 생성함수, 초깃값, 리듀서
// 초깃값
const initialState = 0;
// 액션타입
const INCREASE = "INCREASE";
const DECREASE = "DECREASE";
// 액션생성함수 - 액션을 리턴해주는 함수
// dispatch({ type: INCREASE }) === dispatch(increase())
export const increase = () => ({ type : INCREASE });
export const decrease = () => ({ type : DECREASE });
// 리듀서
export default function counter(state=initialState, action){
switch(action.type){
case INCREASE:
return state + 1;
case DECREASE:
return state - 1;
default:
return state;
}
}
② index.js - 여러개의 리듀서를 하나로 합치기 rootReducer
modules/index.js
import { combineReducers } from "redux";
import counter from "./counter";
const rootReducer = combineReducers({ counter });
export default rootReducer;
2. 리액트 프로젝트에 리덕스 사용하기
① index.js 파일에서 store생성하기
createStore(rootReducer)
② <Provider store={store}><App/></Provider>
3. 컴포넌트 만들기
① 프리젠테이셔널 컴포넌트 - props
components/Counter.js
import React from 'react';
const Counter = ({ number, onIncrease, onDecrease }) => {
return (
<div>
<h1>{number}</h1>
<button onClick={onIncrease}>+1</button>
<button onClick={onDecrease}>-1</button>
</div>
);
};
export default Counter;
② 컨테이너 컴포넌트 - 스토어에 접근
components/CounterContainer.js
import React from 'react';
import Counter from './Counter';
import { useSelector, useDispatch } from 'react-redux';
import { decrease, increase } from '../modules/counter';
const CounterContainer = () => {
const number = useSelector(state=> state.counter);
const dispatch = useDispatch();
const onIncrease = () => {
dispatch(increase());
}
const onDecrease = () => {
dispatch(decrease());
}
return (
<Counter number={number} onIncrease={onIncrease} onDecrease={onDecrease} />
);
};
export default CounterContainer;
4. 미들웨어 만들기
① middleware폴더 생성 myLogger.js만들기
② 미들웨어 사용하기
index.js
applyMiddleware 함수 불러오기
createStore의 인수로 추가
createStore(rootReducer, applyMiddleware(myLogger))
middlewares/myLogger
// 전달받은 액션을 출력하고 다음으로 넘기기
const myLogger = store => next => action => {
console.log(action); // 액션을 출력
const result = next(action); // 다음 미들웨어 (또는 리듀서)에게 액션을 전달함
console.log('/t', store.getState());
return result; // 여기서 반환하는 값은 dispatch(액션)의 결과물이 됩니다. 기본: undefined
};
export default myLogger;
5. DevTools 사용하기
composeWithDevTools 불러오기
createStore(rootReducer, composeWithDevTools(applyMiddleware(myLogger)))
yarn add redux-devtools-extension
index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { applyMiddleware, legacy_createStore as createStore } from 'redux';
import rootReducer from './modules';
import { Provider } from 'react-redux';
import myLogger from './middlewares/myLogger';
import { composeWithDevTools } from 'redux-devtools-extension';
// 미들웨어 적용하기 applyMiddleware(미들웨어 이름)
const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(myLogger)));
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
6. redux-thunk 사용하기
리덕스에서 비동기 작업 처리할 때 가장 많이 사용하는 미들웨어입니다.
액션객체가 아닌 함수를 디스패치할 수 있습니다.
라이브러리 설치
npm install redux-thunk
미들웨어 추가
createStore(rootReducer, composeWithDevTools(applyMiddleware(ReduxThunk,myLogger)))
const thunk = store => next => action => {
typeof action === "function"
? action(store.dispatch, store.getState)
: next(action)
}
7. 카운트 딜레이하기
modules/counter.js
// 액션타입, 액션 생성함수, 초깃값, 리듀서
// 초깃값
const initialState = 0;
// 액션타입
const INCREASE = "INCREASE";
const DECREASE = "DECREASE";
// 액션생성함수 - 액션을 리턴해주는 함수
// dispatch({ type: INCREASE }) === dispatch(increase())
export const increase = () => ({ type : INCREASE });
export const decrease = () => ({ type : DECREASE });
export const increaseAsync = () => dispatch => {
setTimeout(() => dispatch(increase()),1000)
}
export const decreaseAsync = () => dispatch=> {
setTimeout(() => dispatch(decrease()),1000)
}
// 리듀서
export default function counter(state=initialState, action){
switch(action.type){
case INCREASE:
return state + 1;
case DECREASE:
return state - 1;
default:
return state;
}
}
components/CounterContainer.js
import React from 'react';
import Counter from './Counter';
import { useSelector, useDispatch } from 'react-redux';
import { decreaseAsync, increaseAsync } from '../modules/counter';
const CounterContainer = () => {
const number = useSelector(state=> state.counter);
const dispatch = useDispatch();
const onIncrease = () => {
dispatch(increaseAsync());
}
const onDecrease = () => {
dispatch(decreaseAsync());
}
return (
<Counter number={number} onIncrease={onIncrease} onDecrease={onDecrease} />
);
};
export default CounterContainer;
2)
api/posts.js
// n 밀리세컨즈동안 기다리는 프로미스를 만들어주는 함수
const sleep = n => new Promise(resolve => setTimeout(resolve, n));
//
const posts = [
{
id: 1,
title: "리덕스 미들웨어를 공부합시다",
body: "리덕스 미들웨어를 만들어 봅시다"
},
{
id: 2,
title: "redux-thunk를 사용해봅시다",
body: "redux-thunk를 사용해서 비동기 작업을 처리해봅시다"
},
{
id: 3,
title: "redux-saga도 공부해봅시다",
body: "redux-saga를 사용해서 비동기 작업을 처리해보세요"
}
]
// 포스트 목록을 가져오는 비동기 함수
export const getPosts = async () => {
await sleep(500); // 0.5초 쉬기
return posts;
}
// ID로 포스트를 조회하는 비동기 함수
export const getPostById = async id => {
await sleep(500); // 0.5초 쉬기
return posts.find(post => post.id === id); // id로 찾아서 반환
}
components/
PostList.js
import React from 'react';
const PostList = ({ posts }) => {
return (
<ul>
{posts.map(post=>(
<li key={post.id}>
{post.title}
</li>
))}
</ul>
);
};
export default PostList;
PostListContainer.js
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { getPosts } from '../modules/posts';
import PostList from './PostList';
const PostListContainer = () => {
const { loading, data, error } = useSelector(state=> state.posts.posts)
const dispatch = useDispatch();
// 컴포넌트 마운트 후 포스트 목록 요청하기
useEffect(()=>{
dispatch(getPosts())
},[dispatch])
if(loading) return <div>로딩중....</div>
if(error) return <div>에러발생</div>
if(!data) return null
return (
<div>
<PostList posts={data} />
</div>
);
};
export default PostListContainer;
modules/posts.js
// 액션타입, 액션생성함수 => thunk함수, 초깃값, 리듀서
// 프로미스가 시작, 성공, 실패했을 때 다른 액션을 디스패치해야한다.
// 각 프로미스마다 thunk함수를 만들어주어야 합니다.
// 리듀서에서 액션에 따라 로딩중, 결과, 에러상태를 변경
import * as postAPI from '../api/posts'; // api.posts안의 함수 모두 불러오기
// 초깃값
const initialState = {
posts: {
loading: false,
data: null,
error: null
},
post: {
loading: false,
data: null,
error: null
}
}
// 액션타입
// 포스트 여러개 조회하기
const GET_POSTS = "GET_POSTS"; // 요청시작
const GET_POSTS_SUCCESS = "GET_POSTS_SUCCESS"; // 요청성공
const GET_POSTS_ERROR = "GET_POSTS_ERROR"; // 요청실패
// 포스트 하나 조회하기
const GET_POST = "GET_POST"; // 요청시작
const GET_POST_SUCCESS = "GET_POST_SUCCESS"; // 요청성공
const GET_POST_ERROR = "GET_POST_ERROR"; // 요청실패
// thunk 함수
export const getPosts = () => async dispatch => {
dispatch({ type: GET_POSTS }) // 요청을 시작
try{
const posts = await postAPI.getPosts();
dispatch({ type: GET_POSTS_SUCCESS, posts }); // 성공
// dispatch({ type: GET_POSTS_SUCCESS, posts: posts }); // 윗줄과 같은 의미
}
catch(e) {
dispatch({ type: GET_POSTS_ERROR, error: e }) // 실패
}
}
// 하나만 조회하는 thunk 함수
export const getPost = id => async dispatch => {
dispatch({ type: GET_POST }) // 요청을 시작
try{
const post = await postAPI.getPostById(id); // api호출
dispatch({ type: GET_POST_SUCCESS, post }); // 성공
// dispatch({ type: GET_POST_SUCCESS, post: post }); // 윗줄과 같은 의미
}
catch(e) {
dispatch({ type: GET_POST_ERROR, error: e }) // 실패
}
}
export default function posts(state = initialState, action){
switch(action.type){
case GET_POSTS:
return {
...state,
posts: {
loading: true,
data: null,
error: null
}
}
case GET_POSTS_SUCCESS:
return {
...state,
posts: {
loading: false,
data: action.posts,
error: null
}
}
case GET_POSTS_ERROR:
return {
...state,
posts: {
loading: false,
data: null,
error: action.error
}
}
case GET_POST:
return {
...state,
post: {
loading: true,
data: null,
error: null
}
}
case GET_POST_SUCCESS:
return {
...state,
post: {
loading: false,
data: action.post,
error: null
}
}
case GET_POST_ERROR:
return {
...state,
post: {
loading: false,
data: null,
error: action.error
}
}
default:
return state;
}
}
* 정리하기
lib/asyncUtils.js
// thunk 함수
// GET_POSTS
export const createPromiseThunk = (type, promiseCreator) => {
// GET_POSTS_SUCCESS, GET_POSTS_ERROR
const [SUCCESS, ERROR] = [`${type}_SUCCESS`, `${type}_ERROR`];
return param => async dispatch => {
dispatch({ type, param }) // 요청을 시작
try{
const payload = await promiseCreator(param); // api호출
dispatch({ type: SUCCESS, payload }); // 성공
// dispatch({ type: GET_POST_SUCCESS, post: post }); // 윗줄과 같은 의미
}
catch(e) {
dispatch({ type: ERROR, payload: e, error: true }) // 실패
}
}
}
// 리듀서에서 사용할 수 있는 유틸함수
export const reducerUtils = {
initial: (initialData = null)=>({
loading: false,
data: initialData,
error: null
}),
loading: (prevState = null)=> ({
loading: true,
data: prevState,
error: null
}),
success: payload => ({
loading: false,
data: payload,
error: null
}),
error: error => ({
loading: false,
data: null,
error: error
})
}
modules/posts.js
// 액션타입, 액션생성함수 => thunk함수, 초깃값, 리듀서
// 프로미스가 시작, 성공, 실패했을 때 다른 액션을 디스패치해야한다.
// 각 프로미스마다 thunk함수를 만들어주어야 합니다.
// 리듀서에서 액션에 따라 로딩중, 결과, 에러상태를 변경
import * as postAPI from '../api/posts'; // api.posts안의 함수 모두 불러오기
import { createPromiseThunk, reducerUtils } from '../lib/asyncUtils';
// 초깃값
// 반복되는 초기값 {}를 initial()를 실행하여 리턴받음
const initialState = {
posts: reducerUtils.initial(),
post: reducerUtils.initial()
}
// 액션타입
// 포스트 여러개 조회하기
const GET_POSTS = "GET_POSTS"; // 요청시작
const GET_POSTS_SUCCESS = "GET_POSTS_SUCCESS"; // 요청성공
const GET_POSTS_ERROR = "GET_POSTS_ERROR"; // 요청실패
// 포스트 하나 조회하기
const GET_POST = "GET_POST"; // 요청시작
const GET_POST_SUCCESS = "GET_POST_SUCCESS"; // 요청성공
const GET_POST_ERROR = "GET_POST_ERROR"; // 요청실패
// thunk 함수
export const getPosts = createPromiseThunk(GET_POSTS, postAPI.getPosts)
// 하나만 조회하는 thunk 함수
export const getPost = createPromiseThunk(GET_POST, postAPI.getPostById)
export default function posts(state = initialState, action){
switch(action.type){
case GET_POSTS:
return {
...state,
posts: reducerUtils.loading()
}
case GET_POSTS_SUCCESS:
return {
...state,
posts: reducerUtils.success(action.payload)
}
case GET_POSTS_ERROR:
return {
...state,
posts: reducerUtils.error(action.error)
}
case GET_POST:
return {
...state,
post: reducerUtils.loading()
}
case GET_POST_SUCCESS:
return {
...state,
post: reducerUtils.success(action.payload)
}
case GET_POST_ERROR:
return {
...state,
post: reducerUtils.error(action.error)
}
default:
return state;
}
}
components/
Post.js
import React from 'react';
const Post = ({ post }) => {
return (
<div>
<h1>{post.title}</h1>
<p>{post.body}</p>
</div>
);
};
export default Post;
PostContainer.js
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { getPost } from '../modules/posts';
import Post from './Post';
const PostContainer = ({ postId }) => {
const { data, loading, error } = useSelector(state => state.posts.post);
const dispatch = useDispatch();
useEffect(()=>{
dispatch(getPost(postId))
},[dispatch, postId])
if(loading) return <div>로딩중....</div>
if(error) return <div>에러발생....</div>
if(!data) return null
return (
<Post post={data}/>
);
};
export default PostContainer;
page/
PostListPage.js
import React from 'react';
import PostContainer from '../components/PostContainer';
const PostListPage = () => {
return (
<PostContainer />
);
};
export default PostListPage;
PostPage.js
import React from 'react';
import PostContainer from '../components/PostContainer';
import { useParams } from 'react-router-dom';
const PostPage = () => {
const { id } = useParams();
return (
<PostContainer postId={parseInt(id)} />
);
};
export default PostPage;
App.js
import './App.css';
import CounterContainer from './components/CounterContainer';
import PostListContainer from './components/PostListContainer';
import { Routes, Route } from 'react-router-dom';
import PostListPage from './page/PostListPage';
import PostPage from './page/PostPage';
function App() {
return (
<div className="App">
<CounterContainer/>
<Routes>
<Route path="/" element={<PostListPage/>} />
<Route path="/:id" element={<PostPage/>} />
</Routes>
<PostListContainer/>
</div>
);
}
export default App;
index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { applyMiddleware, legacy_createStore as createStore } from 'redux';
import rootReducer from './modules';
import { Provider } from 'react-redux';
import myLogger from './middlewares/myLogger';
import { composeWithDevTools } from 'redux-devtools-extension';
import ReduxThunk from 'redux-thunk';
import { BrowserRouter } from 'react-router-dom';
// 미들웨어 적용하기 applyMiddleware(미들웨어 이름)
const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(ReduxThunk,myLogger)));
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<BrowserRouter>
<Provider store={store}>
<App />
</Provider>
</BrowserRouter>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();