본문 바로가기

사이드 프로젝트

[React 프로젝트] redux-toolkit 도입기... with typescript

도입 배경

 

프로젝트 기능 중에 

다른 유저들이 본 식물들을 띄우는 기능이 있다.

북마크를 클릭하면 내 도감 목록들 중에 하나를 선택하여 도감에 식물을 넣을 수 있다.

서버에서, 도감에 이미 있는 아이템을 확인해서 구분해 주면 좋겠지만

도감이 여러개이기 때문에 구분하는 것이 여러모로 복잡하기 때문에 

기능을 단순화 시키기로 했다.

 

다른 유저들이 본 식물들을 띄워주고 

사용자가 특정 식물을 선택해서 도감에 넣으면

그 식물은 목록에서 필터링된다.

이 과정을 클라이언트에서 맡게 됐고, 그렇기 때문에 상태값은 로그인 ~ 다음 로그인 전까지 유지 된다.

 

로직이.. 따로 어려울 건 없다.

플로우차트가 익숙하진 않지만 그냥 끄적여 봤다..

 

1. 추천 식물 정보 목록을 받고 

2. 추천 식물 정보를 하나 선택해서 도감에 넣으면 

3. 다른 유저들이 본 식물 목록에선 도감에 넣은 식물만 필터링해서 보여준다. 

단순하다. 

 

사실 이렇게 단순한 상태관리에 리덕스를 넣을 필요는 없었다. 

왜 리덕스를 도입할 필요가 없었는지에 대한 이유를 살펴보자면,

 

1. 내가 구현하고자 하는 데이터는 복잡하지 않다 - 여러 개의 상태가 서로 의존하거나 상호작용하는경우 리덕스가 중앙에서 관리를 해주기 때문에 상호작용을 예측 할 수 있는데 내가 구현하고자 하는 데이터는 다른 상태값과 상호 의존적이지 않다. 

 

2. 상태의 전역 관리 - 여러 컴포넌트에서 데이터를 공유하고 있지 않다. 굳이 중앙에서 관리할 필요가 없다는 이야기다.

 

3. 단순한 로직에비해 코드의 양이 방대하다 - 단순히 도감에 식물을 집어넣고, 식물 목록을 조회해오는데에만 많은 코드를 작성한다. 계속 써오던 Recoil 에 비하면 코드양이 방대하게 느껴졌다.

 

그럼에도 도입을 한 이유는

평소에 리덕스를 쓰는게 어렵다고 느껴져서 배우고 싶었고

쓰면서 어떤 환경에서 사용하면 좋을지 궁금해서 쓴게 크다.

그리고 redux는 디버깅이 용이하다고 하는데 Redux DevTools를 이용해서 상태관리 추적을 어떻게 할 수 있는지 궁금하기도 했다.

 

결론 : 복잡한 리덕스를 학습하기 위해 

 

index.tsx

import ReactDOM from 'react-dom/client'
import './style/index.css'
import './style/style.css'
import './utils/common/scripts/configure.js'
import App from './App'
import { BrowserRouter } from 'react-router-dom'

import { RecoilRoot } from 'recoil'
import { Provider } from 'react-redux'
import store from './utils/store'

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)

window.Kakao.init(`${process.env.REACT_APP_KAKAO_SHARE}`) // 내 웹 키를 할당하면 된다

root.render(
  <BrowserRouter>
    <RecoilRoot>
      <Provider store={store}>
        <App />
      </Provider>
    </RecoilRoot>
  </BrowserRouter>,
)

 

provider 를 통해서 store를 전달

 

슬라이스 파일 

import { createSlice } from '@reduxjs/toolkit'
import { tokenApi } from 'src/utils/common/api/useAxios'
import { castRecPlantListType } from 'src/utils/common/scripts/checkType'
import { RcntVwdItem } from 'src/utils/common/type/type'
import { AppDispatch } from '..'

const initialPlantRecommendedState: { list: RcntVwdItem[]; isPending: boolean; isError: boolean } = {
  list: [],
  isPending: true,
  isError: false,
}

const plantRecommendedSlice = createSlice({
  name: 'plantRecommended',
  initialState: initialPlantRecommendedState,
  reducers: {
    // state : 최신 상태
    // action : 컴포넌트가 보낸 액션

    decrement(state, action) {
      if (state.list !== null) {
        state.list = state.list.filter((item: RcntVwdItem) => item.plantSpeciesId !== action.payload)
      }
    },

    getData(state, action) {
      state.list = action.payload
      state.isPending = false
      state.isError = false
    },
    setError(state, action) {
      state.isError = action.payload
    },
  },
})

export const fetchPlantRecommendedData = () => {
  return async (dispatch: AppDispatch) => {
    try {
      const res = await tokenApi.get(
        `${process.env.REACT_APP_API_DOMAIN}encyclo-service/stat/recent-plant-detail?페이지=1&사이즈=6`,
      )

      if (res && res.data) {
        const castObj = castRecPlantListType(res.data)

        if (castObj) {
          const newArr: RcntVwdItem[] = []
          for (let i = 0; i < castObj.data.results.length; i++) {
            const elem = castObj.data.results[i]
            newArr.push({
              commonName: elem.plantBriefInfo.commonName,
              imageUrl: elem.plantBriefInfo.imageUrl,
              scientificName: elem.plantBriefInfo.scientificName,
              plantSpeciesId: elem.plantBriefInfo.plantSpeciesId,
            })
          }

          dispatch(plantRecommendedActions.getData(newArr))
        }
      }
    } catch (error) {
      dispatch(plantRecommendedActions.setError(true))
    }
  }
}

// slice에서 설정한 리듀서에 접근 가능
export default plantRecommendedSlice.reducer
export const plantRecommendedActions = plantRecommendedSlice.actions

 

index.ts

import { configureStore } from '@reduxjs/toolkit'

import plantRecommendedSliceReducer from './plant/plantRecommended'

const store = configureStore({
  reducer: {
    plantRecommended: plantRecommendedSliceReducer,
  },
})

export type AppDispatch = typeof store.dispatch
export type RootState = ReturnType<typeof store.getState>
export default store

 

컴포넌트에 사용할 커스텀 훅 .ts

// 유저들이 둘러본 목록
export function useGetPlantList() {
  const dispatch = useDispatch<AppDispatch>()
  const collectionList = useSelector((state: RootState) => state.plantRecommended.list)
  const isPending = useSelector((state: RootState) => state.plantRecommended.isPending)
  const isError = useSelector((state: RootState) => state.plantRecommended.isError)

  useEffect(() => {
    if (collectionList?.length === 0 || collectionList === null || !collectionList) {
      dispatch(fetchPlantRecommendedData())
    }
  }, [])

  const setCollectionList = (selectId: number | string) => {
    dispatch(plantRecommendedActions.decrement(selectId))
  }

  return {
    collectionList,
    setCollectionList,
    isPending,
    isError,
  }
}