[React] Redux Toolkit 이 그렇게 편해?

MINEW·2022년 10월 21일
0

Redux Toolkit 이란?

  1. Redux Toolkit
    - Redux Toolkit은 Redux를 더 쉽게 사용하기 위해 만들어졌습니다.
    - Redux에서 공식으로 제공하는 개발도구입니다.

  2. Redux Toolkit 사용 이유
    - 리덕스를 사용하면 보일러 플레이트 코드가 많다는 단점이 있습니다.
    - 리덕스 툴킷을 사용하면 보일러 플레이트 코드를 줄이고 액션 타입, 액션 생성함수, 리듀서를 하나의 함수로 선언할 수 있습니다.
    - 그리고 패키지 의존성을 줄여줍니다. 리덕스 툴킷에는 많은 라이브러리들이 내장되어 있기 때문에 redux-thunk 등을 따로 설치하지 않고 사용할 수 있습니다.
    - 참고) 보일러 플레이트 코드: 최소한의 변경으로 여러곳에서 재사용되며, 반복적으로 비슷한 형태를 띄는 코드.


설치 방법

// JS (CRA + redux-toolkit)
$ npx create-react-app 프로젝트명 --template redux

// TS (CRA + redux-toolkit)
$ npx create-react-app 프로젝트명 --template redux-typescript

// 기존 파일에 설치 (JS, TS 동일)
npm install @reduxjs/toolkit react-redux

// + logger 미들웨어 설치 (선택사항)
npm i -D redux-logger @types/redux-logger

redux-toolkit 기본 예시

  1. 개별 createSlice 파일 (action + reducer)
// src/store/counter.ts    ---> reducer 파일 1 (action + reducer)
import { createSlice } from '@reduxjs/toolkit';

const counter = createSlice({
  name: 'counter',
  initialState: { // 초기값 설정
    value: 0,
    arr: [] as number[],
    bool: false,
    str: ''
  },
  reducers: { // 개별 리듀서 (reducers는 액션 생성자를 자동으로 만들어준다)
    // 1번) action.type === up 이면 -> 이 함수를 실행시켜줘
  	up: (state, action) => { // 2번) state = initialState 를 안써도 처음에 initialState가 들어간다.
      const step = action.payload.step; // 3번) payload가 여러개 일 경우에는 다음과 같이 사용한다.

      state.value = state.value + step;
      state.arr.push(action.payload.step); // 4번) redux-toolkit에서는, 맨 처음에 ...state 로 불변성 전후비교 했던거 안해도된다.
      state.bool = !state.bool; // true -> false -> true -> ...
    },
    // 5번) action.type === down 이면 -> 이 함수를 실행시켜줘
  	down: (state, action) => {
      state.value = state.value - action.payload.step;
      state.arr.pop(); // 6번) 이외에 splice 등 원본훼손도 가능해졌다.
    },
    write: (state, action) => {
      state.str = action.payload; // 7번) payload가 1개일 경우에는 다음과 같이 사용한다.
    }
  }
});

export default counter;
export const { up, down, write } = counter.actions; // 8번) dispatch에서 간편히 사용하기 위해 구조분해할당으로 내보내어 사용할 수도 있다
  1. configureStore 파일 (통합 리듀서)
// src/store/index.ts    ---> store 파일
import { configureStore } from '@reduxjs/toolkit'; // 1번) 반드시 필요
import logger from 'redux-logger'; // 6번) logger를 사용하고 싶다면,
import counter from './counter'; // 2번) 개별 리듀서 가져오기

export type RootState = ReturnType<typeof store.getState>; // 5번) 타입스크립트에서 useSelector를 사용할 때 반드시 필요

const store =  configureStore({ // 3번) createStore + combineReducers
  reducer: {
  	counter: counter.reducer, // 4번) 개별 리듀서 넣기
  },
  middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger) // 7번) 다음과 같이 사용
});

export default store;
  1. redux 사용 컴포넌트 1
// src/App.tsx
import { useSelector, useDispatch } from "react-redux"; // 1번) 반드시 필요
import { RootState } from './store'; // 2번) 타입스크립트에서 useSelector를 사용할 때 반드시 필요
import counter, { up, down, write } from './store/counter'; // 3번) dispatch를 사용하기 위한 방법1 & 2

function App() {
  const dispatch = useDispatch();
  const { value, str } = useSelector((state: RootState) => state.counter); // 4번) state.개별리듀서명.state값

	const upButton = () => {
    // 5번) createSlice상수명.acionts.액션타입(payload) // 방법1) 액션 생성자 사용
		dispatch(counter.actions.up({step: 2})); // === dispatch(up({step: 2}));
    dispatch(write('up'));
	};
	const downButton = () => {
    // 6번) 액션타입(payload) // 방법2) 액션 생성자를 더욱 간단하게 사용
		dispatch(down({step: 2})); // === dispatch(counter.actions.down({step: 2}));
    dispatch(write('down'));
	};

  return (
    <div>
      <div>
        <button onClick={downButton}>-</button>
        <span>{ value }</span>
        <button onClick={upButton}>+</button>
      </div>
      <div>{ str }</div>
    </div>
  );
}

export default App;
  1. index 파일
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { Provider } from 'react-redux'; // 1번) store를 사용하기 위해서 필요한 단계 1
import store from './store'; // 2번) store를 사용하기 위해서 필요한 단계 2

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <Provider store={ store }>
      <App />
    </Provider>
  </React.StrictMode>
);

redux-toolkit 비동기 처리

  • createAsyncThunk는 비동기 작업을 처리하는 action을 만들어줍니다.
  • 기본적으로, reducers를 사용하면 redux-toolkit이 액션 생성자를 자동으로 만들어줍니다.
  • 그러나, creatAsyncThunk로 만든 비동기 작업은 액션 생성자를 자동으로 생성하지 못하기 때문에, createSlice의 extraReducers에 직접 액션 생성자를 정의해야합니다.

redux-toolkit 비동기 처리 기본 예시

  1. 개별 createSlice 파일 (action + reducer) + thunk
// src/store/goods.ts    ---> reducer 파일 1 (action + reducer) + thunk
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; // 1번) createAsyncThunk
import { productsApi, singleApi } from '../apis/goodsApi';

export interface ProductGuard {
  id: number,
  title: string,
  price: number,
  description: string,
  category: string,
  image: string,
  rating: { rate: number, count: number}
};

const getAll = createAsyncThunk( // 2번) 비동기 처리 액션 생성자 (thunk 함수)
  'goods/getAll', // 3번) 액션 타입
  async () => {
    const data = await productsApi();
    return data;
  }
);
const getSingle = createAsyncThunk( // 비동기 처리 액션 생성자 (thunk 함수)
  'goods/getSingle', // 액션 타입
  async (productId: string) => {
    const data = await singleApi(productId);
    return data;
  }
);

const goods = createSlice({
  name: 'goods',
  initialState: { // 초기값 설정
    all: {
      status: null as string | null,
      data: [] as ProductGuard[]
    },
    single: {
      status: null as string | null,
      data: {} as ProductGuard
    },
  },
  reducers: {}, // 4번) 동기적인 reducers를 사용하지 않더라도, 빈 객체라도 넣어줘야 오류가 발생하지 않는다.
  // 5번) creatAsyncThunk는 액션 생성자를 자동으로 생성 X, createSlice의 extraReducers에 직접 액션 생성자를 정의해야한다.
  extraReducers: (builder) => {
    builder.addCase(getAll.pending, (state) => { // pending(시작, 로딩중)일때 동작할 reducer를, 2번째 인자에 함수로 전달.
      state.all.status = 'loading';
    })
    builder.addCase(getAll.fulfilled, (state, action) => { // fulfilled(성공)일때 동작할 reducer를, 2번째 인자에 함수로 전달.
      state.all.data = action.payload; // getAll thunk함수의 return 값 === action.payload
      state.all.status = 'complete';
    })
    builder.addCase(getAll.rejected, (state) => { // rejected(실패)일때 동작할 reducer를, 2번째 인자에 함수로 전달.
      state.all.status = 'fail';
    })
    builder.addCase(getSingle.pending, (state) => {
      state.single.status = 'loading';
    })
    builder.addCase(getSingle.fulfilled, (state, action) => {
      state.single.data = action.payload;
      state.single.status = 'complete';
    })
    builder.addCase(getSingle.rejected, (state) => {
      state.single.status = 'fail';
    })
  }
});

export default goods;
export { getAll, getSingle }; // 6번) thunk함수 내보내기
  1. configureStore 파일 (통합 리듀서)
// src/store/index.ts    ---> store 파일
import { configureStore } from '@reduxjs/toolkit'; // 1번) 반드시 필요
import { useDispatch } from "react-redux";
import logger from 'redux-logger'; // 7번) logger를 사용하고 싶다면,
import counter from './counter'; // 2번) 개별 리듀서 가져오기
import goods from './goods';

export type RootState = ReturnType<typeof store.getState>; // 6번) 타입스크립트에서 useSelector를 사용할 때 반드시 필요
export const useAppDispatch = () => useDispatch<typeof store.dispatch>(); // 9번) thunk를 사용할 때는 여기서 useDispatch를 내보낸다!

const store =  configureStore({ // 3번) createStore + combineReducers
  reducer: {
  	counter: counter.reducer, // 4번) 개별 리듀서 넣기
  	goods: goods.reducer, // 5번) thunk를 사용할 때도 같은 방식으로 넣어준다
  },
  middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger) // 8번) 다음과 같이 사용
});

export default store;
  1. redux 사용 컴포넌트 1
// src/App.tsx
import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux"; // 1번) thunk를 사용할 때는, 여기서 useDispatch를 가져와서
import { RootState, useAppDispatch } from './store'; // 3번) store에서 정의한 useAppDispatch를 가져와서
import { up, down } from './store/counter';
import { getAll, getSingle } from './store/goods';

function App() {
  // const dispatch = useDispatch(); // 2번) 사용하는 것이 아니라,
  const dispatch = useAppDispatch(); // 4번) useDispatch처럼 사용한다! (thunk 뿐만 아니라, 동기적인 reducers도 당연히 사용가능하다!)
  const { value, arr } = useSelector((state: RootState) => state.counter);
  const { all, single } = useSelector((state: RootState) => state.goods);

	const upButton = () => dispatch(up({step: 2}));
	const downButton = () => dispatch(down({step: 2}));

  useEffect(() => { // 5번) Redux를 쓸 때와 다르게, redux-toolkit에서는 로딩중에 tag렌더링 error가 발생하지 않는다!
    dispatch(getAll());
  }, []);

  const arrLists = arr.map((num, index) => <span key={index}>{num}</span>);

  const allLists = all.data.map((item, index) => {
    return (
      <div key={index}>
        <span>{item.id}</span> / 
        <span>{item.title}</span>
      </div>
    )
  });

  const getSingleBtn = (productId: string) => dispatch(getSingle(productId));
  const singleLists = () => {
    return (
      <div>
        <span>{single.data.id}</span> / 
        <span>{single.data.title}</span>
      </div>
    )
  };


  return (
    <div>
      <div>
        <button onClick={downButton}>-</button>
        <span>{ value }</span>
        <button onClick={upButton}>+</button>
        <div>{arrLists}</div>
      </div>
      <br/>
      <div>
        <button onClick={() => getSingleBtn('1')}>싱글</button>
        <div>{allLists}</div>
        <div>{singleLists()}</div>
      </div>
    </div>
  );
}

export default App;
  1. index 파일 (동일)
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { Provider } from 'react-redux'; // 1번) store를 사용하기 위해서 필요한 단계 1
import store from './store'; // 2번) store를 사용하기 위해서 필요한 단계 2

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <Provider store={ store }>
      <App />
    </Provider>
  </React.StrictMode>
);

dispatch가 완료된 후 state가져오는 법 (2가지)

useEffect mounted 에서 데이터를 요청하면, state가 undefined일때 tag가 렌더링되어 error가 발생하는 문제를 해결하기 위한 방법 2가지

  1. 방법1: useState
function Game() {
  const dispatch = useDispatch();
  const [isLoaded, setIsLoaded] = useState(false);

  useEffect(() => {
    dispatch(resetBoard({height: 16, width: 16})); // 1번) dispatch를 실행하고
    setIsLoaded(true); // 2번) useState로 로딩이 완료되었다는 것을 알린다
  }, []); // 4번) 단, 초기에 리렌더링이 발생한다는 단점이 있다.

  return (
    <div>
      {isLoaded ? cleanBoard(height, width): <div>Loading...</div>} // 3번) useState의 업데이트에 따라 tag를 보여주면 문제 해결!
    </div>
  )
}
  1. 방법2: useRef
function Game() {
  const dispatch = useDispatch();
  const isLoaded = useRef(false);

  useEffect(() => {
    dispatch(resetBoard({height: 16, width: 16})); // 1번) dispatch를 실행하고
    isLoaded.current = true; // 2번) useRef로 로딩이 완료되었다는 것을 알린다
  }, []); // 4번) useState와 다르게, 초기에 리렌더링이 발생하지 않는다는 장점이 있다. (리렌더링 개선 가능)

  return (
    <div>
      {isLoaded.current ? cleanBoard(height, width): <div>Loading...</div>} // 3번) useRef의 업데이트에 따라 tag를 보여주면 문제 해결!
    </div>
  )
}

profile
JS, TS, React, Vue, Node.js, Express, SQL 공부한 내용을 기록하는 장소입니다

0개의 댓글