노력이 좋아서

<step80>'redux(react)_리덕스 미들웨어'

zoaseo 2022. 7. 14. 09:13

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();