[React] redux-thunk (전역상태관리 비동기 처리) + redux-logger, Redux DevTools

MINEW·2022년 9월 19일
0

1. redux-thunk 란?

  • 비동기 처리를 위한 redux-middleware 라이브러리
  • dispatch에 action 생성자 함수 대신, thunk 함수를 넣어서 사용. ex) dispatch(thunk함수)

2. Ducks 패턴 이란?

  • 구조중심이 아니라 기능중심으로 파일을 나눔
  • action type & action 생성자함수, thunk 함수, reducer 함수를 1개의 파일 내에서 정의해 사용하는 방식
  • redux/modules 폴더 안에서 기능중심으로 파일을 만든 뒤 1개의 파일 내에서 관리하기 때문에, action, thunk, reducer 파일을 열어두고 왔다갔다하면서 작업을 하지 않아도 된다는 장점이 있음

3. redux-logger 란?

  • redux가 작동될때, 브라우저 개발자 도구에서 redux의 결과물을 깔끔한 형식으로 확인하고 추적할 수 있음


4. Redux DevTools 란?

  • redux가 작동될때, 브라우저 개발자 도구에서 redux의 결과물을 깔끔한 형식으로 확인하고 추적할 수 있음


5. 기본 예시 + 구조중심으로 파일분리 (Ducks 패턴 X)

1) action & thunk함수 파일

// src/store/actions/actions.js (action 파일)

import axios from 'axios';

// 액션 객체 (액션 타입 + 액션 생성자 함수)
export const THUNK = 'THUNK'; // 5번) 
export function thunkAction(data) { // 6번) 
  return  { // 7번) 
    type: THUNK, // type
    data // payload
  }
};

// thunk함수
export function getThunk() { // 1번) // App.js 9번)
  return async (dispatch) => { // 2번) 
    const { data } = await axios.get('https://fakestoreapi.com/products');
    console.log(data); // 20개의 데이터

    // 3번) // App.js 10번)
    dispatch(thunkAction(data)) // dispatch(액션생성자) // 4번) // App.js 11번)
  }
};
[ 주석 설명 ]
1. thunk함수를 만든다
2. return값은 객체가 아니라, '함수'여야한다.
3. reducer 함수를 호출한다
4. 이때, payload로 데이터를 넘긴다.
5. 액션의 type을 정의한다. (일반적으로, 액션의 type은 대문자 + _ 조합을 사용)
6. 액션을 생성하는 함수를 만든다. (액션 생성자)
7. 액션 객체를 return (항상 객체 형태로 return을 작성해야한다)

+ App.js 
9. 호출된 thunk함수가 실행되고
10. -
11. -

2) 개별 reducer 파일

// src/store/reducers/data.js (reducer 파일 1)

import { THUNK } from './actions' // 4번)

// 초기값 설정
const initialState = []; // 3번) 

// reducer 함수
// App.js 12번) 
export default function dataReducer(state = initialState, action) { // 1번) 
  if (action.type === THUNK) {
    console.log(action.data); // payload로 데이터를 받는다 // App.js 13번)
    return [ ...state, ...action.data ] // 2번) // App.js 14번)
  }

  return state // 5번) 
}
[ 주석 설명 ]
1. 1번째 인자는 현재state, 2번째 인자는 액션객체
2. 업데이트 할 새로운state (원본을 훼손하면 안되기때문에, 배열복사를 사용)
3. 초기값 설정 (undefined 방지) // 이 값이 화면에 출력되는 초기값!!!
4. -
5. 꼭 넣어야한다 (undefined 방지) // 이 값이 화면에 출력되는 초기값!!!

+ App.js
12. reducer 함수가 실행되고
13. -
14. -

3) 통합 reducer 파일

// src/store/reducers/index.js (reducer 파일 통합)

import { combineReducers } from 'redux'; // 1번) 
import addsubReducer from './addsub'
import countReducer from './count'
import dataReducer from './data';

const rootReducer = combineReducers({ // 2번) 
	value: addsubReducer, // const { value } = useSelector(state => state.value)
	count: countReducer, // const { count } = useSelector(state => state.count)
	data: dataReducer // const data = useSelector(state => state.data) // 3번) 
})

export default rootReducer; // 4번) 
[ 주석 설명 ]
1. combineReducers
2. 리듀서 함수들 통합
3. 단, 이때는 { data }로 하면 error 발생!
4. 내보내기

4) createStore 파일

// src/store/store.js (store 파일)

// 기본 예시
import { createStore, applyMiddleware } from "redux"; // 1번) 
import thunk from "redux-thunk"; // 2번) 
import rootReducer from './reducers'; // 3번) 

const store = createStore(rootReducer, applyMiddleware(thunk)); // 4번) 

export default store; // 5번) 

// 추가 예시: redux-thunk (with redux-logger, Redux DevTools) 방법
import { createStore, applyMiddleware } from "redux"; // 1번) 
import thunk from "redux-thunk"; // 2번) 
import rootReducer from './reducers'; // 3번) 
import logger from 'redux-logger'; // 4번) 
import { composeWithDevTools } from 'redux-devtools-extension'; // 5번) 

const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(thunk, logger))); // 6번) 

export default store; // 7번) 
[ 주석 설명 ]
* 기본 예시
1. 반드시 필요
2. 반드시 필요
3. reducer 통합함수를 가져온다
4. createStore(reducer함수)는 스토어를 만들고 후, 리듀서를 등록한다
5. store 안에는 dispatch(action), getState 등이 자동으로 들어있음

* 추가 예시: redux-thunk (with redux-logger, Redux DevTools) 방법
1. 반드시 필요
2. 반드시 필요
3. reducer 통합함수를 가져온다
4. 반드시 필요
5. 반드시 필요
6. logger는 가장 마지막에 들어가야 한다
7. store 안에는 dispatch(action), getState 등이 자동으로 들어있음

5) redux 사용 컴포넌트

// src/App.js (redux 사용, 컴포넌트 1)

import React from 'react';
import { useSelector, useDispatch } from "react-redux"; // 1번)
import { addAction, subAction, resetAction, pushAction } from './store/actions'; // 4번) 
import { getThunk } from './store/actions'; // 6번) 

function App() {
	const dispatch = useDispatch(); // 2번) 

  // 3번) 
	const { value } = useSelector(state => state.value) // addsubReducer // 처음 렌더링 될때는 초기값이 출력된다
	const { count } = useSelector(state => state.count) // countReducer

	const addButton = () => {
		dispatch(addAction(`action.type === 'INCREMENT' 인 경우`)) // 5번) 
	}
	const subButton = () => {
		dispatch(subAction())
	}
	const resetButton = () => {
		dispatch(resetAction())
	}
	const pushButton = () => {
		dispatch(pushAction())
	}

  // 8번) 
	const test = () => {
		dispatch(getThunk())
	}
	
  // state가 rootReducer
	const data = useSelector(state => state.data) // App.js 15번) 
	const result = data.map((item) => { // App.js 16번) 
		return (
			<p key={ item.id }>{ item.id }</p>
		)
	})

  
	return (
		<div className="App">
			<div>
				value: { value }
			</div>
			<button onClick={ addButton }> + </button>
			<button onClick={ subButton }> - </button>
			<button onClick={ resetButton }> reset </button>
			<div>
				count: {count}
			</div>
			<button onClick={ pushButton }> click </button>

			<button onClick={ test }> 비동기 처리 </button> // 7번) 
			<div>{ result }</div> // App.js 17번) 
		</div>
	);
}

export default App;
[ 주석 설명 ]
1. -
2. 꼭 할당해서 사용해야한다
3. state가 rootReducer
4. 액션 생성자 함수를 불러온다
5. reducer함수(액션생성자(payload))
6. thunk함수를 가져와서
7. 클릭하면
8. dispatch(thunk함수)를 호출한다

+ App.js
15. data = [ 20개의 객체 데이터 {}, {} ... ]
16. map으로 풀어줘야만 사용가능
17. div태그 안에, p태그가 20개 나타난다 (자동 리렌더링)

6) index 파일

// src/main.jsx (Vite 버전)
// src/index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';

import { Provider } from 'react-redux'; // store를 사용하기 위해서 필요한 단계 1
import store from './store/store' // store.js (./폴더명/파일명)

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Provider store={ store }> // store를 사용하기 위해서 필요한 단계 2
      <App />
    </Provider>
  </React.StrictMode>
);

6. 심화 예시 (로딩 중, 성공, 실패 처리) + 기능중심으로 파일분리 (Ducks 패턴 O)

1) action & thunk & reducer 파일

// src/redux/modules/comments.js (action & thunk & reducer 파일 1)

import { getAllCommentsApi, postCommentApi } from '../../apis/axios';

const GET_ALLCOMMENTS_START = 'comments/GET_ALLCOMMENTS_START';
function getAllStart() {
  return {
    type: GET_ALLCOMMENTS_START
  };
};

const GET_ALLCOMMENTS_SUCCESS = 'comments/GET_ALLCOMMENTS_SUCCESS';
function getAllSuccess(data) {
  return {
    type: GET_ALLCOMMENTS_SUCCESS,
    data
  };
};

const GET_ALLCOMMENTS_FAIL = 'comments/GET_ALLCOMMENTS_FAIL';
function getAllFail(error) {
  return {
    type: GET_ALLCOMMENTS_FAIL,
    error
  };
};

const DELETE_COMMENT_START = 'comments/DELETE_COMMENT_START';
function deleteCommentStart() {
  return {
    type: DELETE_COMMENT_START
  };
};

const DELETE_COMMENT_SUCCESS = 'comments/DELETE_COMMENT_SUCCESS';
function deleteCommentSuccess(commentId) {
  return {
    type: DELETE_COMMENT_SUCCESS,
    commentId
  };
};

const DELETE_COMMENT_FAIL = 'comments/DELETE_COMMENT_FAIL';
function deleteCommentFail(error) {
  return {
    type: DELETE_COMMENT_FAIL,
    error
  };
};

export function getAllThunk() {
  return async (dispatch) => {
    try {
      dispatch(getAllStart());
      const data = await getAllCommentsApi();
      dispatch(getAllSuccess(data));
    } catch (error) {
      dispatch(getAllFail(error));
    }
  };
};

export function deleteCommentThunk(commentId) {
  return async (dispatch) => {
    try {
      dispatch(deleteCommentStart());
      await deleteCommentApi(commentId);
      dispatch(deleteCommentSuccess(commentId));
    } catch (error) {
      dispatch(deleteCommentFail(error));
    }
  };
};

const initialState = {
  loading: false,
  data: [],
  error: null
};
export function commentsReducer(state = initialState, action) {
  switch (action.type) {
		case GET_ALLCOMMENTS_START:
			return {
        ...state,
        loading: true,
        error: null
      };
		case GET_ALLCOMMENTS_SUCCESS:
      return {
        ...state,
        loading: false,
        data: action.data
      };
    case GET_ALLCOMMENTS_FAIL:
      return {
        ...state,
        loading: false,
        error: action.error
      };
		case DELETE_COMMENT_START:
			return {
        ...state,
        loading: true,
        error: null
      };
		case DELETE_COMMENT_SUCCESS: {
      const actionData = state.data.filter(comment => comment.id !== action.commentId);
      return {
        ...state,
        loading: false,
        data: actionData
      };
    }
    case DELETE_COMMENT_FAIL:
      return {
        ...state,
        loading: false,
        error: action.error
      };
		default:
			return state
	}
};

2) 통합 reducer 파일

// src/redux/modules/index.js (reducer 파일 통합)

import { combineReducers } from 'redux';
import { commentsReducer } from './comments';
import { singleCommentReducer } from './singleComment';
import { paginationReducer } from './pagination';

const rootReducer = combineReducers({
  comments: commentsReducer,
  singleComment: singleCommentReducer,
  pagination: paginationReducer
})

export default rootReducer;

3) createStore 파일

// src/redux/store.js

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

const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(thunk, logger)));

export default store;

4) redux 사용 컴포넌트

// src/components/CommentList/index.js (redux 사용, 컴포넌트 1)

import * as S from './style';
import { useEffect } from 'react';
import { useSelector, useDispatch } from "react-redux";
import { getAllThunk, deleteCommentThunk } from '../../redux/modules/comments';
import { getCommentThunk } from '../../redux/modules/singleComment';
import { paginationAction, LIMIT } from '../../redux/modules/pagination';

function CommentList() {
  const dispatch = useDispatch();
  const comments = useSelector(state => state.comments.data);
  const singleComment = useSelector(state => state.singleComment.data);
  const pagination = useSelector(state => state.pagination);
  const offset = (pagination - 1) * LIMIT;

  useEffect(() => {
    dispatch(getAllThunk());
  }, []);

  const deleteData = (id) => {
    if (confirm('삭제하시겠습니까?') === true) {
      dispatch(deleteCommentThunk(id));
      dispatch(paginationAction(1));
    }
  };
  const getData = (id) => {
    if (Object.keys(singleComment).length !== 0) alert('기존 폼을 *수정 완료* 혹은 *수정 취소* 후, 수정 할 댓글을 다시 선택해주세요');
    else dispatch(getCommentThunk(id));
  };

  const commentLists = comments.slice(offset, offset + LIMIT).map((comment, index) => {
    return (
      <S.CommentBox key={index}>
        <img src={comment.profile_url} alt="프로필 이미지" />
        <span>{comment.author}</span>
        <S.CreatedAtBox>{comment.createdAt}</S.CreatedAtBox>
        <S.ContentBox>{comment.content}</S.ContentBox>
        <S.ButtonBox>
          <button onClick={() => getData(comment.id)}>수정</button>
          <button onClick={() => deleteData(comment.id)}>삭제</button>
        </S.ButtonBox>
        <hr />
      </S.CommentBox>
    );
  });


  return commentLists;
};

export default CommentList;

5) index 파일

// src/index.js

import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import { Provider } from 'react-redux';
import store from './redux/store';

const container = document.getElementById('root');
const root = createRoot(container);

root.render(
  <React.StrictMode>
    <Provider store={ store }>
      <App />
    </Provider>
  </React.StrictMode>
);
profile
JS, TS, React, Vue, Node.js, Express, SQL 공부한 내용을 기록하는 장소입니다

0개의 댓글