본문 바로가기
react

[react]redux-middleware

by 남민섭 2023. 2. 24.
728x90
반응형

미들웨어란

미들웨어는 소프트웨어 각 분야에서 세부적으로 다르게 뜻하는데 위키백과에서는 OS(운영체제)와 소프트웨어 중간에서 조정과 중개 역할을 하는 중간 소프트웨어라고 말함.

 

 

리덕스 + 미들웨어

리덕스 미들웨어는 리덕스가 지니는 핵심 기능
contextAPI와 차별화되는 부분

 

서버에있는 값을 받아올때까지 기다렸다가 리듀서 호출

(dispatch({type: "add_todo"}))

 

 

 

리덕스 미들웨어를 사용하면 액션이 디스패치된 다음, 리듀서에서 해당 액션을 받아서

업데이트 하기전에 추가적인 작업을 할수 있음!!!!!!!

 

추가적인 작업의 예!!!

1. 특정 조건에 따라 액션이 무시되게 만들수 있음
2. 액션을 콘솔에 출력하거나, 서버쪽에 로깅을 할 수 있음
3. 액션이 디스패치 됐을 때 이를 수정해서 리듀서에게 전달할 수 있음
4. 특정 액션이 발생했을 때 이에 기반하여 다른 액션이 발생되도록 할 수 잇음
5. 특정 액션이 발생했을 때 특정 자바스크립트 함수를 실행 시킬수도 있음



제일 중요한 용도임!!!!!

리덕스에서 미들웨어를 사용하는 주된 용도는 비동기 작업을 처리할 때
앱에서 백엔드api(ex. axios로 서버 연결할때 )를 연동해야 할 때 리덕스 미들웨어로 처리한다

 

비동기 작업 관련된 리덕스 미들웨어 라이브러리

redux-thunk
redux-saga
redux-opservable
redux-promise-middleware

 

리덕스 미들웨어!!!!
미들웨어는 함수입니다. 함수를 연달아서 두번 리턴하는 함수입니다.

const middleware = store => next => action => {
	//하고 싶은 작업
}

함수 선언문 변경

function middleware(store){
	return function(next){
		return function(action){
			//하고 싶은 작업....
		}
	}
}

 

 

각 함수에서 받아오는 파라미터

store 리덕스 스토어 store dispatch()/ getState()/ subscribe() 내장함수들이 있음
next 액션을 다음 미들웨어에게 전달하는 함수
ex) next(action)
action은 현재 처리하고 있는 액션 객체

리덕스 스토어에는 여러 개의 미들웨어를 등록할 수 있음!!!

실습

myLogger.js

const myLogger = store => next => action => {
    //액션 출력하기
    console.log(action); //리듀서 가기전에 먼저 출력
    //next는 다음미들웨어에게 액션 전달
    const result = next(action);
    console.log(store.getState())
    return result;
}
//역할은 콘솔에 뭐가 왔는지 출력하는 용도
export default myLogger;

 

미들웨어 스토어에 적용

import { applyMiddleware, legacy_createStore as createStore } from 'redux';
import myLogger from './middleware'

const store = createStore(rootReducer, applyMiddleware(myLogger));

데브툴즈도 함께 사용 하려면

const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(myLogger))

 


redux-logger 미들웨어 

설치

npm install redux-logger

 

미들웨어 스토어에 적용

import { applyMiddleware, legacy_createStore as createStore } from 'redux';
import myLogger from './middleware'
import logger from 'redux-logger';


const store = createStore(rootReducer, applyMiddleware(myLogger, logger));

데브툴즈도 함께 사용 하려면

const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(myLogger, logger))

 

로거 콘솔 출력


redux-thunk 미들웨어

대표적으로 자주 사용함

리덕스에서 비동기 작업을 처리할 때 사용
액션 객체가 아닌 함수를 디스패치

 

설치

npm install redux-thunk

 

 

 

todolist에 redux-thunk 실습

modules 폴더

todos.js

thunk 실습을 위해 Toggle, Delete 클릭했을때 dispatch 호출하는 과정에 setTimeout 1초 설정했다

//1.액션타입
const ADD_TODO = "todos/ADD_TODO";
const TOGGLE_TODO = "todos/TOGGLE_TODO";
const DEL_TODO = "todos/DEL_TODO"
//2.액션 생성함수
let nextId = 1;
//[{id: 1, text: "할일", isDone: false},
//{id: 2, text: "할일" ,isDone: false},
//{id: 3, text: "할일" ,isDone: false}]
//{ type: ADD_TODO, todo: {id: 새로운 번호, text: "새로운할일"}}
//addTodo("리액트 공부하기")
//{type: ADD_TODO, todo: {id:1, text: "리액트 공부하기"}}
//액션객체는 dispatch()할때 사용됨
//addTodo 호출하는 값을 받을거 text로
export const addTodo = (text) => ({  //객체 리턴은 항상 소괄호 씌워줘야함
    type: ADD_TODO,  
    todo:{
        id: nextId++, 
        text: text,
        idDone:false
    }
})

export const toggleTodo = (id) => ({
    type: TOGGLE_TODO,
    id: id
})
export const delTodo = (id) => ({
    type: DEL_TODO,
    id: id
})
//리덕스 thunk실습 사용 
export const toggleTodoAsync = (id) => dispatch => {
    setTimeout(()=>{
        dispatch(toggleTodo(id))
    },1000)
}
export const delTodoAsync = (id) => dispatch => {
    setTimeout(()=>{
        dispatch(delTodo(id))
    }, 1000)
}
//3.리듀서함수
export default function todos(state=[], action){
    switch(action.type){
        case ADD_TODO:
            return[
                ...state,
                action.todo
            ];
        case DEL_TODO:
            return state.filter(todo => todo.id !== action.id)
        case TOGGLE_TODO:
            return state.map(todo=> todo.id === action.id ? //아이디가 일치하는지?
                {...todo, isDone: !todo.isDone}: todo)        //일치하면 isDone반전 시키기
        default:                                              //일치하지 않으면 원래값 리턴
            return state;    
    
    }
}

 

modules 폴더

index.js

import { combineReducers } from "redux";
import todos from "./todos";


//한푸로젝트에 여러개의 리듀서가 있을 경우
//하나의 리듀서로 합쳐서 사용
//합쳐진 리듀서를 루트리듀서
//combineReducers({key: value})
//모듈과 모듈 합치는 파일 rootreducer 만들면 스토어 만들 준비 완료
const rootReducer = combineReducers({todos})
export default rootReducer;

 

src 폴더

index.js

스토어 만들기

rootReducer, dev-tools, middleware(thunk, logger) 추가

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 { composeWithDevTools } from '@redux-devtools/extension';
import Reduxthunk from 'redux-thunk'
import logger from 'redux-logger';



const root = ReactDOM.createRoot(document.getElementById('root'));
const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(Reduxthunk, logger)
)) 
//미들웨어 사용하려면 데브툴즈 안에 넣어줄것 데브툴즈 사용 안할꺼면 compose 없이 적어주면됨 
//myLogger는 내가 만든 미들웨어 원래는 안만들고 라이브러리로 사용함  
//createStore(rootReducer,applyMiddleware(myLogger)) 데브툴즈 사용 안할시 미들웨어만 사용 
// 미들웨어는 여러개 사용 가능
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();

 

components 폴더

InsertTodo.js

 

import React, { useState } from 'react';

const InsertTodo = ({onAddTodo}) => {
    const [text, setText] = useState("")
    const onChange = (e) => {
        const inputtext = e.target.value;
        setText(inputtext)
    }
    const onClick = () => {
        onAddTodo(text);
        setText("")
    }
    return (
        <div>
            <input name="text" value={text} onChange={onChange}/>
            <button onClick={onClick}>등록</button>
        </div>
    );
};

export default InsertTodo;

 

 

components 폴더

TodoLists.js

import React from 'react';

const TodoLists = ({todos, onToggleTodo, onDelTodo}) => {
    return (
        <div>
            <ul>
                {todos.map(todo=><li key={todo.id} style={{background : todo.isDone ? "pink" : null}}>
                    <span onClick={()=>{onToggleTodo(todo.id)}}>{todo.text}</span>
                <button onClick={()=>{onDelTodo(todo.id)}}>삭제</button></li>)}
            </ul>
        </div>
    );
};

export default TodoLists;

 

container 폴더

TodoContainer.js

thunk 실습용 함수 사용

import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import InsertTodo from '../components/InsertTodo';
import TodoLists from '../components/TodoLists';
import { addTodo, delTodo, delTodoAsync, toggleTodo, toggleTodoAsync } from '../modules/todos';

const TodoContainer = () => {
    const todos = useSelector(state=>state.todos)
    const dispatch = useDispatch()
    const onAddTodo = (text) => {
        dispatch(addTodo(text))
    }
    /* const onDelTodo = (id) => {
        dispatch(delTodo(id))
    } */
    //thunk 실습 
    const onDelTodo = (id) => {
        dispatch(delTodoAsync(id))
    }
    /* const onToggleTodo = (id) => {
        dispatch(toggleTodo(id))
    } */
    //thunk 실습
    const onToggleTodo = (id) => {
        dispatch(toggleTodoAsync(id))
    }
    return (
        <div>
            <InsertTodo onAddTodo={onAddTodo}/>
            <TodoLists todos={todos} onDelTodo={onDelTodo} onToggleTodo={onToggleTodo} />
        </div>
    );
};

export default TodoContainer;

src 폴더

App.js

조립!!!

import logo from './logo.svg';
import './App.css';
import TodoContainer from './container/TodoContainer';

function App() {
  return (
    <div className="App">
        <TodoContainer/>
    </div>
  );
}

export default App;

끝!!!!!

 


 

다음실습은 가짜 서버를 생성하고 thunk를 이용하여

데이터를 불러오기전 loading 상태,

데이터를 불러오기 실패했을때 error발생

데이터를 불러왔을때 success 화면에 데이터 구현해보기

 

가짜 서버 생성

api폴더 생성

posts.js

//가짜 서버 만들기
const sleep = n => new Promise(resolve => setTimeout(resolve, n))
//프로미스는 성공하면 resolve , 실패하면 reject
const posts = [
    {
        id:1,
        title: "리덕스",
        desc: "상태관리는 리덕스"
    },
    {
        id:2,
        title: "리덕스 미들웨어 redux-thunk",
        desc: "상태관리는 리덕스"
    },
    {
        id:3,
        title: "리덕스 미들웨어 redux-saga",
        desc: "상태관리는 리덕스"
    },
]

//가짜 비동기 함수
export const getPosts =  async () => {
    await sleep(500);  //0.t초 쉬고
    return posts   //배열 리턴
}

export const getPostbyId = async (id) => {
    await sleep(500);
    return posts.find(post => post.id === id) //id
}

리덕스 모듈 생성

moduls폴더

post.js

import * as postApi from '../api/posts'
//액션타입, 액션생성함수, 리듀서 
//1.액션타입
//포스터 여러개 조회하기
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문안에 작성
    //try 문안에서 에러가 발생되면 자바스크립트가 중지되지 않고
    //그 에러처리를 catch문에게 맞김
    try{
        const posts = await postApi.getPosts() 
        console.log(posts)
        dispatch({type: GET_POSTS_SUCCESS , payload: posts}); //성공
    }
    catch(e){
        dispatch({
            type: GET_POSTS_ERROR, payload: e
        })//실패
    }
}

//초기상태
const initialState = {
    posts: {
        loading: false,
        data: null,
        error: null
    },
    post: {
        loading: false,
        data: null,
        error: null
    }
}
//리듀서 생성
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.payload,
                        error: null
                    }
                }
                case GET_POSTS_ERROR:
                    return {
                        ...state,
                        posts: {
                            loading: false,
                            data: null,
                            error: action.payload
                        }
                    }        
        default: 
            return state    
    }
}

 

모듈 합치기

modules폴더

index.js

import { combineReducers } from "redux";
import posts from "./posts";
import todos from "./todos";


//한푸로젝트에 여러개의 리듀서가 있을 경우
//하나의 리듀서로 합쳐서 사용
//합쳐진 리듀서를 루트리듀서
//combineReducers({key: value})
//모듈과 모듈 합치는 파일 rootreducer 만들면 스토어 만들 준비 완료
const rootReducer = combineReducers({todos, posts})
export default rootReducer;

 

데이터 화면에 구현 시켜줄 프레지테이셔널컴포넌트 생성

components 폴더

PostLists.js

import React from 'react';

const PostLists = ({posts}) => {
    console.log(posts)
    return (
        <ul>
            {posts.map(post=><li key={post.id}>{post.title}</li>)}
        </ul>
    );
};

export default PostLists;

 

스토어에 저장된 데이터 조회, 호출시켜줄 컨테이너컴포넌트 생성 후 프레지테이셔널컴포넌트에 props로 전달

container 폴더

PostContainer.js

import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import PostLists from '../components/PostLists';
import { getPosts } from '../modules/posts';

const PostContainer = () => {
    const {data, loading, 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 <div>데이터없습니다.</div>
    console.log(data)
    return (
        <PostLists posts={data}/>
    );
};

export default PostContainer;

 

데이터를 받아오기 전은 loading 상태,

데이터를 못 받아왔을 때는 error 상태,

데이터를 받아왔을 때는 화면 구현

 

src폴더

App.js

조립!!!!

import logo from './logo.svg';
import './App.css';
import TodoContainer from './container/TodoContainer';
import PostContainer from './container/PostContainer';

function App() {
  return (
    <div className="App">
        <TodoContainer/>
        <PostContainer/>
    </div>
  );
}

export default App;

 

728x90
반응형

'react' 카테고리의 다른 글

스켈레톤 사용법  (0) 2023.03.23
[react]react-Heroku 서버 배포하기/vercel 웹 배포하기  (0) 2023.03.07
[React]todolist(redux사용)  (0) 2023.02.20
[React]todolist(reducer 사용)  (0) 2023.02.20
[React]Todolist(useState사용)  (0) 2023.01.31

댓글