기록하는 개발자

[Redux] React+TypeScript 환경에 Redux 적용하기 본문

Web/Redux

[Redux] React+TypeScript 환경에 Redux 적용하기

밍맹030 2022. 3. 7. 21:22
728x90

나는 지금 Springboot+react환경에서 프론트엔드를 맡아 졸업 프로젝트를 진행 중이다.

 

전부터 프로젝트 진행 시 react에서 useState hook을 사용할 때,

하위 컴포넌트가 state를 사용하기 위해서는

해당 state가 쓰이지 않는 중간 컴포넌트에도 state를 전달해야하는 방식이

정말 비효율적이라는 생각이 들었다.

 

이 점을 해결하고자 검색해봤을 때, 이름만 알고 있었던 'Redux'가 바로 그 역할을 한다는 것을 깨달았다.

개발을 직접 해보니 왜 써야하는지 이유를 스스로 체감하게 되는 것 같다.

 

Redux

1. store

   - 전역에서 state를 관리해주는 공간으로 객체 형태이다.

 

2. action

   - 상태 변화를 일으키는 주체이다.

   - 객체 형태이고 타입이라는 것이 필수로 들어간다. 액션 생성 함수로 발행한다.

 

3. reducer

   - 형태 :  reducer(prevProps,action) => newState   

   - reducer는 action을 실행시키는 순수함수로,

     현재 state와 들어온 action을 인자로 받아 새로운 state를 만들어 반환한다.

 

순수함수는 동일한 props가 들어오면 동일한 결과를 return해야 하며,
함수 내 존재하는 변수 외에 외부의 값을 참조, 의존하거나 변경하지 않아야 함.

 

4. dispatch

   - store의 내장함수 중 하나로, 호출 시 store는 reducer 함수를 실행시켜 새로운 함수를 생성한다.

      →'redux'대신 "react-redux"를 사용하면 dispatch대신

         useDispatch라는 hook을 사용하여 더욱 쉽게 action을 실행시킬 수 있다.

 

5. Provider

   - 생성된 app이 store를 사용할 수 있도록 wrapping한다.

   - App에 props로 store를 전달한다.

 

 

설치(app생성 후 과정만 기록)

// react
npm i redux react-redux

// typescript
npm i  --dev @types/react-redux

 

 

내가 만든 Redux 폴더 구조

내가 적용할 action은 게시판의 boardList를 수정하는 역할이다.

useState를 사용했을 때는 [boardList,setBoardList]였다.

 

게시판의 필터링이 변경되면 서버에 axios.get 요청을 보낸다.

서버로부터 변경된 필터링에 해당하는 BoardList를 정상적으로 받아오면 action을 통해서 boardList의 상태를 변경한다.

 

 

 

Type.tsx

import { setBoardList } from "./Actions/changeBoardListAction";

export type Image = {
  src: string;
};

export interface boardItem {
  id: number;
  type: string;
  name: string;
  image: string;
  reviews: Array<Object>;
}

export type BOARDLIST = {
  boardlist: Array<boardItem>;
};

export type changeBoardListAction = ReturnType<typeof setBoardList>;

- type 재사용을 위해 Types.tsx 파일을 만들어 필요한 type들을 export로 선언해준다. 

- action의 return type도 정해준다.

 

 

 

Actions>changeBoardListActions.tsx

import * as type from "../Types";

export const SET_BOARDLIST = "changeBoardList/SET_BOARDLIST" as const;

export const setBoardList = (boardItemList: type.boardItem[]) => ({
  type: SET_BOARDLIST,
  payload: boardItemList,
});

 

1)  "../Types" import

   - "../Types" 에서 type이라는 이름으로 미리 선언해 놓은 type들을 import 한다.

 

 

2) Action Type 정의 

export const SET_BOARDLIST = "changeBoardListAction/SET_BOARDLIST" as const;

  - 이때의 type은 typescript의 type이 아니다.

  - Action Type : redux action에 들어갈 Type을 정의해 주는 것이다.

  - 나는 일단 action을 한 가지만 만들기 위해 Action Type을 SET_BOARDLIST 하나만 선언했지만

    action이 수정, 삭제 두 개인 경우 CLEAR_BOARDLIST 등의 이름으로 action을 한 개 더 만들면 된다.

 

 

3) Action 생성 함수

export const setBoardList = (boardItemList: type.boardItem[]) => ({
  type: SET_BOARDLIST,
  payload: boardItemList,
});

  - type : action의 type
  - payload : Action 함수의 parameter(인자)

  - 위 action setBoardList는 boardItem[]이라는 type의 배열인 boardItemList를 인자로 받고

     action type이 SET_BOARDLIST action을 할 것이다.

 

 

 

 

Reducers>handleBoardList.tsx

  - reducer 에는 앞서 선언한 action들이 구체적으로 어떤 기능을 하는지 기술한다.

import * as type from "../Types";
import { SET_BOARDLIST } from "../Actions/changeBoardListAction";
import produce from "immer";
import { createReducer } from "typesafe-actions";

export const initialState: type.BOARDLIST = {
  boardlist: [],
};

//draft : 기존의 state, action : 새로운 action
const boardlist = createReducer<type.BOARDLIST, type.changeBoardListAction>(
  initialState,
  {
    [SET_BOARDLIST]: (state, action) =>
      produce(state, (draft) => {
        draft.boardlist = action.payload;
      }),
  }
);

export default boardlist;

 

 

1) import 

import * as type from "../Types";
import { SET_BOARDLIST } from "../Actions/changeBoardListAction";
import produce from "immer";
import { createReducer } from "typesafe-actions";

 

  - 가장 먼저 Types.tsx에 선언했던 type들을 import 한다.

 

  - typesafe-actions에서 createReducer를 import 한다.

             npm i typesafe-actions로 설치

      기존 : reducer를 switch/case문을 통해 작성

      createReducer : reducer를 object의 형식으로 작성 가능

 

  - immer에서 produce를 import

      기존 : reducer 함수 작성 시 불변성 유지를 위하여 ... 문법을 사용하였다.

        그러나, 큰 규모나 복잡한 함수 사용에서는 불변성 유지가 어렵다.

      produce : 2개의 인자만 설정해주면 불변성을 스스로 관리해준다.

 

 

2) initialState 정의

export const initialState: type.BOARDLIST = {
  boardlist: [],
};

필요한 state는 게시판에 띄울 boardlist 이므로 boardlist : [] 와 같이 초기값을 빈 배열로 선언했다.

 

 

3) Reducer 함수 구현

const boardlist = createReducer<type.BOARDLIST, type.changeBoardListAction>(
  initialState,
  {
    [SET_BOARDLIST]: (state, action) =>
      produce(state, (draft) => {
        draft.boardlist = action.payload;
      }),
  }
);

export default boardlist;

 .1 createReducer함수

   type : state에 대한 Type(type.BOARDLIST)과 Action에 대한 Type(type.changeBoardListAction)이 필요하다. 

   parameter: initialState와 object({ }안 내용)가 들어간다.

 

 

 .2 object

//Action Type : SET_BOARDLIST
[SET_BOARDLIST] : (state, action)

 

 action : new action(새로 호출된 action 자체)

→ action을 통해 앞선 changeBoardListActions.tsx의 Action 생성 함수에서 지정해줬던 payload를 사용할 수 있다.

 

 

 Action의 Type을 정의 후 아래와 같이 produce함수를 사용한다.

produce(state, (draft)) => { 함 수 작 성 }),

draft : 기존의 state

produce(state, (draft) => {
    draft.boardlist = action.payload;
}),

- 기존의 boardlist(draft.boardlist)를 action의 인자(payload)로 변경한다.

 

 

 

rootReducder.tsx

- RootState를 정의하고 구현한 reducer를 combineReducers를 통해 합쳐준다.

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

const rootReducer = combineReducers({ handleBoardList });
export type RootState = ReturnType<typeof rootReducer>;

export default rootReducer;

 

reducer가 여러 개인 경우 아래처럼 모두 combineReducers에 넣어준다.

const rootReducer = combineReducers({ 
	reducer1,
	reducer2,
	reducer3,
    ...
});

 

index.tsx

- redux는 기본적으로 store를 통해 state를 공유한다.

  보통 store는 가장 상위 폴더인 index.tsx에 선언하여 모든 하위 태그들이 공유할 수 있도록 한다.

import React from "react";
import ReactDOM from "react-dom";
import reportWebVitals from "./reportWebVitals";
import "./index.css";
import App from "./App";
import { Provider } from "react-redux";
import { createStore } from "redux";
import rootReducer from "./Redux/Reducers/rootReducer";

ReactDOM.render(
  <React.StrictMode>
    <Provider store={createStore(rootReducer)}>
      <App />
    </Provider>
    ,
  </React.StrictMode>,
  document.getElementById("root")
);

reportWebVitals();

 

1) import

    createStore : store를 생성하는 함수

    Provider : 생성된 store를 모든 하위 태그가 공유할 수 있도록 하는 class

 

2) store 공유

    - 1. store에 parameter가 rootReducer인 createStore함수의 return값을 넣어준다.

    - 2. Provider 태그의 parameter로 store를 넣어준다.

 

    위 두 단계 만으로 모든 하위 태그들이 이 store를 공유하게 된다.

 

 

 

component에서의 redux 사용 모습

 

1. 상태관리 대상인 상태(boardlist) 가져오기

import React, { useState, useEffect } from "react";
import "./board.css";
import axios from "axios";

import * as type from "../Redux/Types";
import { useSelector, useDispatch } from "react-redux";
import { setBoardList } from "../Redux/Actions/changeBoardListAction";
import { RootState } from "../Redux/Reducers/rootReducer";

const Board = () => {
  const boardList = useSelector((state: RootState) => state.handleBoardList.boardlist);
  
  ...
  
  return <div>...</div>
};

export default Board;

  - useSelector hook을 이용하여 Rootstate중 handleBoardList의 boardlist를 가져온다.

 

useSelector() : 리덕스 스토어에 저장된 데이터를 추출하는 Hook

 

 

2. action 사용하기

import React, { useCallback } from "react";
import axios from "axios";

import { useDispatch } from "react-redux";
import * as type from "../Redux/Types";
import { setBoardList } from "../Redux/Actions/changeBoardListAction";

function useGetAlcoholList(filterObj: filterState) {
  //dispatch를 최상단 함수에서만 사용
  const dispatch = useDispatch();
  const setBoard = useCallback(
    (boardItemList: type.boardItem[]) => dispatch(setBoardList(boardItemList)),
    [dispatch]
  );

  const GetAlcoholList = () => {
    axios({
      method: "GET",
      url: `[생략]`,
    })
      .then((res) => {
        setBoard(res.data);
      })
      .catch((err) => {
        console.log("리스트 가져오기 에러", err);
      });
  };

  return { GetAlcoholList };
}

export default useGetAlcoholList;

 

 1) import

import * as type from "../Redux/Types";
import { setBoardList } from "../Redux/Actions/changeBoardListAction";

- type와 action으로 작성한 setBoardList를 import 해온다.

 

 

  2) action 사용

  const dispatch = useDispatch();
  const setBoard = useCallback(
    (boardItemList: type.boardItem[]) => dispatch(setBoardList(boardItemList)),
    [dispatch]
  );

 - useCallback과 useDispatch를 이용하여 setBoard에 action으로 지정했던 setBoardList를 저장한다. 

 

 useCallback(function(), array[])
      
: useMemo처럼 함수를 memoization하기 위해서 사용되는 hook 이다.

     첫번째 인자(함수)를 두번째 인자(배열) 내의 값이 변경될 때까지 저장해놓고 재사용할 수 있게 해준다.
useDispatch() : 리덕스 스토어에 설정된 action에 대한 dispatch를 연결하는 Hook

 

 

 

공식 문서 : https://redux.js.org/usage/usage-with-typescript

 

Usage With TypeScript | Redux

- Standard patterns for setting up a Redux app with TypeScript

redux.js.org

redux 적용에 도움 받은 곳 : https://devlog-h.tistory.com/33

 

Typescript에서 Redux를 사용해보자

안녕하세요 휴몬랩 초보 개발자 차암새입니다. 이번 시간에는 Typescript 환경에서 Redux를 사용해보겠습니다. Redux는 React 상에서 state관리가 힘들 수 있는 부분을 보완하기 위해 만들어졌습니다. ※

devlog-h.tistory.com

useCallback : https://www.daleseo.com/react-hooks-use-callback/

 

React Hooks: useCallback 사용법

Engineering Blog by Dale Seo

www.daleseo.com

 

728x90

'Web > Redux' 카테고리의 다른 글

[Redux] useDispatch hook 에러 해결기(Error: Invalid hook call)  (0) 2022.03.07