[원티드 프리온보딩 프론트엔드][2주차 1차 과제] - 요청 대시보드 구현: 리덕스 툴킷 사용기

GY·2022년 2월 9일
0

원티드 프리온보딩

목록 보기
4/12
post-thumbnail

프리온보딩 프론트엔드 코스 2주차 1차 과제에서 리덕스 툴킷을 사용한 전역 상태관리를 도전해보았다.
과제에서 제시한 조건은 아니었으나, 팀 내에서 리덕스를 활용한 상태관리를 활용해보고 익숙해지고자 선택한 사항이었다.

진행한 프로젝트 돌아보기 & 리덕스를 선택한 이유는 다음의 포스팅에서 보실 수 있습니다.
👉 포스팅 보러가기

리덕스 툴킷을 사용한 전역상태관리는 다음의 기능에 쓰였다.

  • 가공방식, 재료 2개의 필터링 조건 선택 시 해당하는 견적요청 카드 필터링해 노출하기
  • '상담 중' 토글 버튼 클릭 시 해당 상태에 부합하는 견적 요청 카드 필터링해 노출하기

📘 setting

npm install @types/react-redux react-redux @reduxjs/toolkit

store 생성 - configureStore

redux toolkit에서는 configureSTore을 사용해 store를 생성한다.
이 configureStore는 reducer들을 모아주는 역할을 한다.

//store/store.ts
import { configureStore } from '@reduxjs/toolkit';
import requestsSlice from './request';

export default configureStore({
  reducer: {
    requests: requestsSlice,
  },
});

생성한 store는 provider로 전역에서 접근할 수 있도록 했다.

import store from './store/store';

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <ThemeProvider theme={theme}>
        <GlobalStyle />
        <App />
      </ThemeProvider>
    </Provider>
  </React.StrictMode>,
  document.getElementById('root'),

초기값과 reducer 세팅

export const RequestsSlice = createSlice({
  name: 'requests', // 이름 정의
  initialState: {
    // 초기값 설정
    requests: [],
    isConsulting: false,
    methods: [],
    materials: [],
    filteredRequests: [], // 상담중, 체크박스 => view
  } as RequestsState,
  reducers: {
    //   state는 initialState객체와 동일
    fetchData: (state: RequestsState, action: PayloadAction<Request[]>) => {
      state.requests = action.payload;
      state.filteredRequests = action.payload;
    },
    filterStatus: (state: RequestsState) => {
      state.isConsulting = !state.isConsulting;
      state.filteredRequests = filterRequests(state);
    },
    filterMethod: (state: RequestsState, action: PayloadAction<string[]>) => {
      state.methods = action.payload;
      state.filteredRequests = filterRequests(state);
    },
    filterMaterial: (state: RequestsState, action: PayloadAction<string[]>) => {
      state.materials = action.payload;
      state.filteredRequests = filterRequests(state);
    },
  },
});
  • Ducks pattern을 사용하기 위해 createSlice를 사용했다.

    Ducks pattern: 구조가 아닌 기능 중심으로 나눈 패턴

  • initialState는 과제에서 제공받은 mock data의 항목을 그대로 적용했다.
  • reducer는 4가지를 선언했다.
    • fetchData: 데이터 받아오기
    • filterStatus: '상담중' 토글 버튼 클릭 시 status가 '상담중'인 요청만 필터링하기
    • filterMethod: 선택한 가공방식 (밀링, 선반) 에 해당하는 요청 필터링하기
    • filterMaterial: 선택한 재료 (알루미늄, 구리 등) 를 포함하는 요청 필터링하기

createSlice

slice?

slice는 action과 reducer를 합한 단위로 보면 적절할 것 같다.
더 정확히는 state 트리 구조에서의 리듀서 함수 1개를 가리킨다고 한다.
그런데 여기에 '조각'이라는 의미를 담은 이름을 붙인 이유는 무엇일까?
리듀서 액션을 하나의 단위로 앱의 로직을 분리하고자 함인 것 같다.

createSlice는 reducer 만 생성하여도 reducer의 key 값으로 액션까지 자동으로 생성해 주는 기능을 지원한다.
조금 더 구체적으로는, reducer를 만들면 자동으로 slice.action에 reducers에서 선언한 각 reducer에 대한 actionCretor함수가 생성한다. (예시 코드 출처)

function createSlice({
    reducers : Object<string, ReducerFunction | ReducerAndPrepareObject>,
    initialState: any,
    name: string,
    extraReducers?:
    | Object<string, ReducerFunction>
    | ((builder: ActionReducerMapBuilder<State>) => void)
})

name : 해당 모듈의 이름을 작성한다.
initialState : 해당 모듈의 초기값을 세팅한다.
reducers : 리듀서를 작성한다. 이때 해당 리듀서의 키값으로 액션함수가 자동으로 생성된다.

작성한 slice에서 action과 reducer export하기

export const { fetchData, filterStatus, filterMethod, filterMaterial } =
  RequestsSlice.actions;

export default RequestsSlice.reducer;
//export const { reducer } = RequestsSlice.reducer; 와 같이 destructuring도 가능하다.

payload

액션에 필요한 추가 데이터는 payload라는 이름을 사용한다. (예시 코드 출처)

const MY_ACTION = 'sample/MY_ACTION';
const myAction = createAction(MY_ACTION);
const action = myAction('hello world');
/* 결과:
	{type: MY_ACTION, payload: 'hello world'}
*/

액션 생성 함수에서 받아온 파라미터를 그대로 payload를 받아오지 않고 변형으로 주어 넣고 싶다면, payload를 정의하는 함수를 따로 선언해서 넣어주면 된다.

액션 생성함수는 액션에 필요한 추가 데이터를 모두 payload라는 이름으로 사용하기 때문에 모두 action.payload값을 조회하여 업데이트 하도록 구현하면 된다.

payloadAction type

slice에서는 리듀서의 리턴 타입으로 payload에 어떤 데이터를 넣어야 하는지 지정할 수 있다.
이때는 payloadAction 타입을 사용한다.

팁: 객체 비구조화 할당 문법으로 가독성 높이기

팀 프로젝트에서 코드를 작성할 때 모든 리듀서를 정의할 때 action.payload를 사용하기 때문에 한 눈에 파악하기 다소 어려운 느낌이 있었다.
그리고 프로젝트가 끝난뒤 리덕스 툴킷에 대해 다시 정리해보면서 좋은 방법을 찾아 함께 적어두려한다.

아래와 같이 비구조화 할당 문법으로 이름을 각각 새로 지정해주면 코드를 파악하기 더 쉽다.
예시 코드

//#1
const todos = handleActions({
  [CHANGE_INPUT]: (state, action) => ({ ...state, input: action.payload }),

//#2  
const todos = handleActions(
  {
  [CHANGE_INPUT]: (state, { payload: input }) => ({ ...state, input }),


📘 세팅한 리덕스 사용하기

프로젝트 코드

각 기능별로 프로젝트에서 사용한 코드이다.
사용한 메서드에 대한 설명은 아래 사용한 메서드 정리에 정리해두었다.

데이터 불러오기


import { useDispatch, useSelector } from 'react-redux';
import { filterMethod, filterMaterial } from 'store/request';
import store from 'store/store';

type RootState = ReturnType<typeof store.getState>;

function App() {
  const { filteredRequests, methods, materials } = useSelector(
    (state: RootState) => state.requests,
  );
  const dispatch = useDispatch();

  async function getData() {
    const { requests: dataRequests } = await fetch(API_URL).then((res) =>
      res.json(),
    );
    dispatch(fetchData(dataRequests as Request[]));
  }

  useEffect(() => {
    getData();
  }, []);

재료/가공방식 필터링하기

import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import { IoMdArrowDropdown } from 'react-icons/io';
import { filterMethod, filterMaterial } from 'store/request';
import store from 'store/store';

type Props = {
  name: string;
  options: string[];
};

type RootState = ReturnType<typeof store.getState>;

function FilterButton({ name, options }: Props) {
  const { methods, materials } = useSelector(
    (state: RootState) => state.requests,
  );
  const dispatch = useDispatch();
  const [isClicked, setIsClicked] = useState<boolean>(false);

  const handleClick = () => {
    setIsClicked(!isClicked);
  };

  const handleCheck = (e: React.ChangeEvent<HTMLInputElement>) => {
    if (e.target.checked) {
      if (name === '가공방식') {
        dispatch(filterMethod([...methods, e.target.value]));
      } else if (name === '재료') {
        dispatch(filterMaterial([...materials, e.target.value]));
      }
    } else if (!e.target.checked) {
      if (name === '가공방식') {
        const newOptionList = methods.filter(
          (option: string) => option !== e.target.value,
        );
        dispatch(filterMethod(newOptionList));
      } else if (name === '재료') {
        const newOptionList = materials.filter(
          (option: string) => option !== e.target.value,
        );
        dispatch(filterMaterial(newOptionList));
      }
    }
  };

아쉬운점
같은 스트링을 2번 이상 사용하게 되었는데, 상수화를 시켜주는 것이 더 좋았을 것 같다.


재료/가공방식 필터 선택 리셋하기


function ResetButton() {
  const dispatch = useDispatch();

  const handleReset = () => {
    dispatch(filterMethod([]));
    dispatch(filterMaterial([]));
  };

상담중 카드 필터링하기

import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import ToggleBtn from 'components/atoms/ToggleBtn';
import styled from 'styled-components';
import store from '../../../store/store';
import { filterStatus } from '../../../store/request';

type RootState = ReturnType<typeof store.getState>;

function IsConsult() {
  const { isConsulting } = useSelector((state: RootState) => state.requests);
  const dispatch = useDispatch();
  const onChangeToggle = (): void => {
    dispatch(filterStatus());
  };


📘 사용한 메서드 정리

프로젝트에 직접 사용하진 않았지만 사용한 메서드와 관련된 내용도 함께 정리했습니다.

connect로 컨테이너 컴포넌트 만들기

리덕스 스토어와 연동된 컨테이너 컴포넌트를 만들 때는 connect함수를 사용한다.


useSelector로 상태조회하기

useSelector Hook을 사용하면 connect함수를 사용하지 않고도 리덕스 상태를 조회할 수 있다.


useDispatch

다른 컴포넌트에서 전역 상태를 가져오기 위한 메서드이다.
useDispatch 훅은 컴포넌트 내부에서 스토어의 내장함수인 dispatch를 사용할 수 있도록 해준다. 컨테이너 컴포넌트에서 액션을 디스패치해야할 때 useDispatch를 사용하면 된다.

connect함수를 사용해 컨테이너 컴포넌트를 만들면 해당 컴포넌트의 부모 컴포넌트가 리렌더링 될 때 props가 바뀌지 않는한 리렌더링이 자동으로 방지되어 성능이 최적화된다.

반대로 useSelector를 사용했을 때는 성능최적화가 자동으로 적용되지 않기 때문에 React.memo를 사용해주어야 한다.(예시 코드 출처)

const TodosContainer = () => {
  (...)
};
export default
React.memo(TodosContainer);

useStore

useStore 훅을 사용하면 컴포넌트 내부에서 리덕스 스토어 객체를 직접 사용할 수 있다.
물론 스토어에 직접 접근해야 하는상황은 흔치않기 때문에 자주 쓰이진 않는다.


immer

이번 과제에서 리듀서에서 상태를 업데이트할 때는 불변성을 지키기 위해 spread 연산자를 사용했다.
이번 과제에서는 무리가 없었으나, 데이터의 뎁스가 깊고 복잡할 경우 사용할 수 있는 immer라는 라이브러리에 대해 추가적으로 알게되었다.

immer는 불변성을 지키면서 별도로 원본을 복사하지 않아도 업데이트 할 수 있도록 해주는 라이브러리이다.

아래와 같이 사용한다. (예시 코드 출처)

import {createAction, handleActions} from 'redux-actions';
import produce from 'immer';

const todos = handleActions({
  [CHANGE_INPUT]: (state, { payload: input }) =>
    produce(state, (draft) => {
      draft.input = input;
    }),

간단한 구조를 가진 state를 업데이트할 때는 immer를 사용하는 것이 가독성을 오히려 떨어뜨릴 수 있으므로 꼭 필요할 때 사용하는 것이 중요할 것 같다.
이후에 깊은 뎁스의 state를 업데이트해야할 일이 생긴다면 immer를 사용해보려 한다.



Reference

profile
Why?에서 시작해 How를 찾는 과정을 좋아합니다. 그 고민과 성장의 과정을 꾸준히 기록하고자 합니다.

0개의 댓글