본문 바로가기
💻CODING/react. vue

[react] Redux 예제 (ft. 미들웨어, Redux-thunk, Redux-devtools)

by 코딩하는 갓디노 2021. 7. 18.

react redux 

 

react에서 
Redux 사용 예제입니다.

 

예제는 유투버, code Scalper님의 강의를 들으면서 공부한 내용입니다.

Redux의 기본 원리

 

화면 결과

 

폴더 구조

 

폴더 안 파일 구조

 

react Redux 예제 구현 순서

컴포넌트 파일 구성

  • CRA로 프로젝트 생성, components 폴더 구성
  • components 폴더 내에 Subscribers.js, Display.js, Views.js 세 개의 파일 생성
  • App.js에서 위의 하위 components 삽입

 

리덕스

  • redux 폴더 안에, 하위 폴더 생성(comments, subscribers, views)
  • 각 하위 폴더 마다, types.js, actions.js, reducer.js 순서로 파일 생성
  • types.js : action type을 변수로 작성 후 export
  • actions.js: types.js에서 action type 변수를 import 해서 action 코드 작성 후 export
  • reducer.js: types.js에서 action type 변수를 import, reducer 함수를 switch문으로 작성 후 export
  • 코드 간결화를 위해 redux 폴더 안에 index.js 생성하여 모든 action을 다 import 한 후 export 

 

스토어

  • store.js에서 import {createStore} from 'redux';
  • createStore에 인자로 생성된 리듀서 함수를 넣어줌 //const store = createStore(rootReducer);
  • App.js에서 Provider를 react-redux에서 import 시킴
  • function App() { return ( 
    	<Provider store={store}> 
        	<div className="App"> ... </div> 
        </Provider> ); 
      }
  • store import 후, <Provider store={store}> 태그로 전체를 감싸줌
  • combineReducer를 통해서 각 reducer 함수를 하나로 합침
  • const rootReducer = combineReducers({
        views: viewsReducer, //state.views로 state값 바뀜
        subscribers: subscriberReducer, //state.subscribers
        comments: commmentsReducer
    })
  • rootReducer.js에서 각 reducer를 import 하여 하나로 합친 후 export
  • store.js에서 rootReducer import 후 createStore(rootReducer)로 변경

 

화면 출력

    • 각 컴포넌트에서 import { connect } from 'react-redux';
    • export default connect(mapStateToPropsmapDispatchToProps)(컴포넌트);
    • mapStateToProps: state 상태 값을 props로 넘김
    • mapDispatchToProps: state 상태 변경을 props로 disaptch

 

미들웨어 - Redux 확장 기능

  • import logger from 'redux-logger';
  • import {createStore, applyMiddleware} from 'redux';
  • createStore(rootReducer, applyMiddleware(logger)); 로 미들웨어 logger 적용
  • console.log를 통해 이벤트 history 확인

 

Redux Devtools 크롬 extension - Redux 확장 기능

    • 크롬 웹스토어에서 redux devtools 추가 설치
    • import { composeWithDevTools } from 'redux-devtools-extension';
    • const store = createStore(rootReducercomposeWithDevTools(applyMiddleware(...middleware)));
    • 개발자 화면 Redux에서 디버깅 툴 확인

 

Redux thunk (액션을 순차적으로 일어나도록 실행) - Redux 확장 기능

    • import thunk from 'redux-thunk'; 
    • const middleware = [logger, thunk]
    • json.placeholder의 더미 데이터를 이용해 비동기 호출
    • export const fetchComments = () => {
          //fetch 이벤트 작성
          //redux-thunk를 썼기때문에 return으로 dispatch를 인자로 넘겨받아 액션 함수를 사용함
          //redux-thunk를 이용하여 fetch 이벤트를 순차적으로 발생시킴 
          return(dispatch) => { 
              dispatch(fetchCommentRequest())
              fetch("https://jsonplaceholder.typicode.com/comments")
              .then( res => res.json())
              .then(commments => dispatch(fetchCommentSuccess(commments)) )
              .catch(err => dispatch(fetchCommentFailure(err)))
          }
      }
    • 나머지 fetchCommentSuccess, fetchCommentFailure, fetchCommentRequest action 코드, reducer 함수 작성

 

라이브러리 설치 코드

cra 설치

npm install creat react-app react-redux

 

redux 설치

npm install redux react-redux

 

middleware 설치

npm install redux-logger

 

redux devtools extionsion 설치

npm install --save redux-devtools-extension

 

redux thunk 설치

npm install redux-thunk

 

파일 코드

Subscribers.js

import React from 'react';
import {connect} from 'react-redux';
//import { addSubscriber } from '../redux/subscribers/actions'; //아래 index.js로 코드 간결하게
import { addSubscriber } from '../redux/index'; // '/index 삭제해도됨

// const Subscribers = (props) => {
//     return (
//         <div className='items'>
//           <h2>구독자수: {props.count}</h2>
//           <button onClick={()=>props.addSubscriber()}>구독하기</button>
//         </div>
//     );
// };

const Subscribers = ({count, addSubscriber}) => { //destructuring
    return (
        <div className='items'>
          <h2>구독자수: {count}</h2>
          <button onClick={()=>addSubscriber()}>구독하기</button>
        </div>
    );
};

const mapStateToProps = (state) => { //props.count
    console.log(state);
    return {
        count: state.subscribers.count //count가 props로 전달됨
    }
}

// const mapDispatchToProps = (dispatch) => { //함수로 전달
//  return {
//      addSubscriber: () => dispatch(addSubscriber())//addSubscriber가 props로 전달됨
//  }
// }

const mapDispatchToProps = {
    addSubscriber //객체로 전달, addSubscriber: addSubscriber
}

export default connect(mapStateToProps, mapDispatchToProps)(Subscribers);

 

view.js

import React, { useState } from 'react';
import {connect} from 'react-redux';
import { addView, removeView } from '../redux/index';

const Views = ({count, addView, removeView}) => { //destructuring
const [number, setNumber] = useState(1)

    return (
        <div className='items'>
          <h2>조회 수: {count}</h2>
        {/* input value가 onChange될때마다 number값에 저장 */}
        <input type='text' value={number} onChange={(e) => setNumber(e.target.value)} />    
        <button onClick={()=>addView(number)}>증가</button>
        <button onClick={()=>removeView(number)}>감소</button>
         
        
        </div>
    );
};

const mapStateToProps = (state) => { //props.coun
    return {
        count: state.views.count //count가 props로 전달됨
    }
}

const mapDispatchToProps = { 
    addView: (number) => addView(number), //메서드로 전달
    removeView, //객체 전달
}

export default connect(mapStateToProps, mapDispatchToProps)(Views);

 

Display.js

import React from 'react';
import {connect} from 'react-redux';

const Display = (props) => {
    return (
        <div>
            <p>구독자수{props.count}</p>
        </div>
    );
};

const mapStateToProps = (state) => { //props.count
    return {
        count: state.subscribers.count //count가 props로 전달됨
    }
}

export default connect(mapStateToProps)(Display);

 

Commpents.js

import React, {useEffect} from 'react';
import { connect } from 'react-redux';
import {fetchComments} from '../redux';

const Comments = ({fetchComments, loading, comments}) => {
    useEffect(() => {
        fetchComments()
    }, []) 

    const commentItems = loading? (<div>is loading...</div>) : (
        comments.map(comment => (
            <div key={comment.id}>
                <h3>{comment.name}</h3>
                <p>{comment.email}</p>
                <p>{comment.body}</p>
            </div>
        ))
    )

    return (
        <div className='comments'>
            {commentItems}
        </div>
    );
};

const mapStateToProps = ({comments}) =>{
    return {
        comments: comments.items.slice(0,8) //8개만 출력
    }
}

const mapDispatchToProps = {
    fetchComments
}

export default connect(mapStateToProps, mapDispatchToProps)(Comments);

 

App.js

import './App.css';
import Subscribers from './components/Subscribers';
// npm install redux react-redux
import {Provider} from 'react-redux';
import store from './redux/store'
import Display from './components/Display';
import Views from './components/Views';
import Comments from './components/Comments';

function App() {
  return (
    <Provider store={store}>
      <div className="App">
        <Comments />
        <Subscribers/>
        <Views/>
        <Display />
      </div>
    </Provider>
  );
}

export default App;

 

App.css

.App {
  text-align: center;
}

.items {
  border-bottom: 1px solid #333;
  margin-bottom: 1rem;
  padding-bottom: 1rem;
}

.comments {
  display: grid;
  grid-template-columns: repeat(4,1fr);
  grid-gap: 1rem;
}
.comments > div{
  border: 1px solid #dedede;
}

 

Redux 폴더

subscribers 폴더 > types.js

export const ADD_SUBSCRIBER = 'ADD_SUBSCRIBER';
export const REMOVE_SUBSCRIBER = 'REMOVE_SUBSCRIBER';

 

subscribers 폴더 > actions.js

import {ADD_SUBSCRIBER,REMOVE_SUBSCRIBER} from './types';

export const addSubscriber = () => {
    return {
        type: ADD_SUBSCRIBER
    }
}

export const removeSubscriber = () => {
    return {
        type: REMOVE_SUBSCRIBER
    }
}

 

subscribers 폴더 > reducer.js

import {ADD_SUBSCRIBER,REMOVE_SUBSCRIBER} from './types';

const initialState = { //state 초깃값
    count : 100
}

const subscriberReducer = (state = initialState, action) => {
    switch(action.type) {
        case ADD_SUBSCRIBER: 
        return {
            ...state, 
            count: state.count + 1 
        }
    
        case REMOVE_SUBSCRIBER: 
        return {
            ...state, 
            count: state.count - 1 
        }
        default: return state; 
    }
}

export default subscriberReducer;

 

views 폴더 > types.js

export const ADD_VIEW = 'ADD_VIEW';
export const REMOVE_VIEW = 'REMOVE_VIEW';

 

views 폴더 > actions.js

import {ADD_VIEW, REMOVE_VIEW} from './types';

export const addView = (num) => {
    return {
        type: ADD_VIEW,
        payload: Number(num)//num을 숫자로 변환
    }
}

export const removeView = (num) => {
    return {
        type: REMOVE_VIEW,
        payload: Number(num)
    }
}

 

views 폴더 > reducer.js

import {ADD_VIEW, REMOVE_VIEW} from './types';

const initialState = { //state 초깃값
    count : 0
}

const viewsReducer = (state = initialState, action) => {
    switch(action.type) {
        case ADD_VIEW: 
        return {
            ...state, 
            count: state.count + action.payload
        }
      
        case REMOVE_VIEW: 
        return {
            ...state, 
            count: state.count - action.payload
        }
        default: return state; 
    }
}

export default viewsReducer;

 

comments 폴더 > types.js

export const FETCH_COMMENTS = 'FETCH_COMMENTS';
export const FETCH_COMMENTS_REQUEST = 'FETCH_COMMENTS_REQUEST';
export const FETCH_COMMENTS_SUCCESS = 'FETCH_COMMENTS_SUCCESS';
export const FETCH_COMMENTS_FAILURE = 'FETCH_COMMENTS_FAILURE';

 

comments 폴더 > actions.js

import {FETCH_COMMENTS, FETCH_COMMENTS_REQUEST, FETCH_COMMENTS_SUCCESS, FETCH_COMMENTS_FAILURE} from './types';

export const fetchComments = () => {
    //fetch 이벤트 작성
    //redux-thunk를 썼기때문에 return으로 dispatch를 인자로 넘겨받아 액션 함수를 사용함
    //redux-thunk를 이용하여 fetch 이벤트를 순차적으로 발생시킴 
    return(dispatch) => { 
        dispatch(fetchCommentRequest())
        fetch("https://jsonplaceholder.typicode.com/comments")
        .then( res => res.json())
        .then(commments => dispatch(fetchCommentSuccess(commments)) )
        .catch(err => dispatch(fetchCommentFailure(err)))
    }
}

const fetchCommentSuccess = (comments) => {
    return {
        type: FETCH_COMMENTS_SUCCESS,
        payload: comments
    }
}

const fetchCommentFailure = (err) => {
    return {
        type: FETCH_COMMENTS_FAILURE,
        payload: err  
    }
}

const fetchCommentRequest = () => {
    return {
        type: FETCH_COMMENTS_REQUEST
    }
}

 

comments 폴더 > reducer.js

import {FETCH_COMMENTS, FETCH_COMMENTS_REQUEST, FETCH_COMMENTS_SUCCESS, FETCH_COMMENTS_FAILURE} from './types';

const initialState = {
    items: [],
    loading: false,
    err: null
}

const commmentsReducer = (state=initialState, action) => {
    switch(action.type){
        case FETCH_COMMENTS_REQUEST: 
            return {
                ...state,
                loading: true,
            }

        case FETCH_COMMENTS_SUCCESS: 
            return {
                ...state,
                items: action.payload,
                loading: false
            }

        case FETCH_COMMENTS_FAILURE: 
            return {
                ...state,
                err: action.payload,
                loading: true
            }
        default: return state;
    }
}

export default commmentsReducer;

 

redux 폴더 > index.js

//코드의 간결화를 위해 하나로 합침
//가져오자마다 export 시킴
export { addSubscriber, removeSubscriber } from  './subscribers/actions';
export { addView, removeView } from  './views/actions';
export {fetchComments} from './comments/actions';

 

rootReducer.js

import { combineReducers } from "redux";
import subscriberReducer from "./subscribers/reducer";
import viewsReducer from "./views/reducer";
import commmentsReducer from "./comments/reducer";

const rootReducer = combineReducers({
    views: viewsReducer, //state.views로 state값 바뀜
    subscribers: subscriberReducer, //state.subscribers
    comments: commmentsReducer
})

export default rootReducer;

 

store.js

import {createStore, applyMiddleware} from 'redux';
import rootReducer from './rootReducer';
import logger from 'redux-logger';
import { composeWithDevTools } from 'redux-devtools-extension';
import thunk from 'redux-thunk'; //npm install redux-thunk

//middleware가 여러개가 될수 있기 때문에 변수를 만들고, logger를 담아줌
const middleware = [logger, thunk]

//const store = createStore(rootReducer, applyMiddleware(logger));
//spread operator로 [logger] 배열(껍데기)안의 내용물만 전달됨 -위와 똑같이 작동
const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(...middleware)));


export default store;

 

App.js

import {createStore, applyMiddleware} from 'redux';
import rootReducer from './rootReducer';
import logger from 'redux-logger';
import { composeWithDevTools } from 'redux-devtools-extension';
import thunk from 'redux-thunk'; //npm install redux-thunk

//middleware가 여러개가 될수 있기 때문에 변수를 만들고, logger를 담아줌
const middleware = [logger, thunk]

//const store = createStore(rootReducer, applyMiddleware(logger));
//spread operator로 [logger] 배열(껍데기)안의 내용물만 전달됨 -위와 똑같이 작동
const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(...middleware)));


export default store;

 

반응형

댓글