2024.06.25 프로그래머스 - React Task Management 2
React 로 만드는 Task Management App 2
오늘 중점적으로 집중했던 부분은, 컴포넌트의 작성이나 스타일링이 아니라
Redux-Toolkit 의 원리와 그 사용법에 집중했다.
현재 강의에서는 Redux-Toolkit을 사용하여 상태관리를 진행하는데,
기존 JS로만 다뤄보았던 Redux를 TS를 통해 다루게되니까, 신경쓰거나 처음 경험해보는 부분이 많았다.
이에 대해 더 찾아 보고 공부한 내용을 정리하는게 유익할 것 같다.
기존 Redux Library의 문제점
- 너무 많은 코드를 작성해야 했다.
- 하나의 상태 관리를 위해 다양한 파일 분산을 했어야 했다.
- Redux Devtools 사용을 위한 단계가 필요했다.
React-Redux의 코어 기능
- createStore
- 실질적인 store 생성
- combineReducers
- 하나의 큰 reducer에 여러 작은 싱글 리듀서를 결합하는 기능
- applyMiddleware
- 스토어를 보강하기 위한 여러 미들웨어를 결합
- compose
- 여러 싱글 스토어를 하나의 큰 스토어로 결합하는 기능
Redux-Toolkit
We specifically created Redux Toolkit to eliminate the “boilerplate” from hand-written Redux logic, prevent common mistakes, and provide APIs that simplify standard Redux tasks.
Redux-Toolkit 은 Redux 팀에서 사용자가 손으로 직접 작성하는 Redux 로직이 들어간 ‘보일러플레이트’를 최대한 제거하고, 이에 따라 실수를 방지하며 (정해진 방식대로만 구성하면 되기 떄문) Redux로 할 수 있는 일을 더욱 간단하게 할 수 있게 만들기 위해 개발 했다고 한다.
Redux-Toolkit 의 핵심정인 API
configureStore
- Reducer 결합, Chunk 미들웨어 추가, Redux Devtools 통합 설정 등을 단일 함수로 구현가능한 API
- 단일 함수의 호출 한번으로 Redux Store를 세팅할 수 있고 이전보다 더 쉽다.
createSlice
lets you write reducers that use the Immer library to enable writing immutable updates using “mutating” JS syntax like
state.value = 123
, with no spreads needed. It also automatically generates action creator functions for each reducer, and generates action type strings internally based on your reducer’s names. Finally, it works great with TypeScript.immer
라이브러리를 사용하여, 기존의 전개구문을 통한 state값의 변경 없이 불변성을 유지하면서 일반 JS 구문으로 state 값을 변경할 수 있다.- 각 reducer에 대한 생성자 함수를 자동으로 생성하고 내부적으로 해당 reducer함수를 식별하는 문자열 데이터를 생성
createSlice 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { createSlice } from '@reduxjs/toolkit'
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
todoAdded(state, action) {
state.push({
id: action.payload.id,
text: action.payload.text,
completed: false,
})
},
todoToggled(state, action) {
const todo = state.find((todo) => todo.id === action.payload)
todo.completed = !todo.completed
},
},
})
export const { todoAdded, todoToggled } = todosSlice.actions
export default todosSlice.reducer
공식문서상의 createSlice
API 에 대한 예제코드이다.
한가지 주목할 점은, 기존 Redux를 써본 사람들은 알겠지만,
reducer내의 return 구문이 보이지 않는다.
이는 앞서 설명한 것처럼 immer
를 사용하여 불변성을 유지하며,
보통의 JS구문만으로 상태를 변경할 수 있도록 만들어진 API이기 때문에,
state와 action (payload가 포함된) 을 인자로 받는 reducer함수를 통해
상태를 변경할 떄
굳이 return 이나, 배열복사, 이전 객체 복사로 불변성을 사용자가 유지해줄 필요가 없어졌다.
configureStore를 통한 간단한 예제
1
2
3
4
5
6
7
8
9
10
import { configureStore } from '@reduxjs/toolkit'
import todosReducer from '../features/todos/todosSlice' // slice1
import filtersReducer from '../features/filters/filtersSlice' // slice2
export const store = configureStore({
reducer: {
todos: todosReducer,
filters: filtersReducer,
},
})
- 공식문서가 말하는 cofigureStore를 호출해서 reducer를 결합했을 때의 작동방식
- The slice reducers were automatically passed to
combineReducers()
- The
redux-thunk
middleware was automatically added - Dev-mode middleware was added to catch accidental mutations
- The Redux DevTools Extension was automatically set up
- The middleware and DevTools enhancers were composed together and added to the store
- The slice reducers were automatically passed to
현재 JS에서는 위의 코드정도로 useSelector 와 useDispatch를 통해 Redux Store를 충분히 사용할 수 있지만,
찾아보니 TS에서는 그 방법이 조금 다르다, 다르다기보다는 일련의 과정이 추가로 더 필요하다!
TS 와 Redux-Toolkit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { configureStore } from '@reduxjs/toolkit'
// ...
export const store = configureStore({
reducer: {
posts: postsReducer,
comments: commentsReducer,
users: usersReducer,
},
})
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch
RootState
와AppDispatch
TS로 RTK를 이용시, 반복되는 타입 지정을 방지하고자 작성하는 방식인 것 같다.- 해당 타입을 통해 useTypedDispatch, useTypedSeletor 와 같은 커스텀훅을 만들어 편하게 사용자가 TS 환경에서 useSelector, useDispatch 를 사용할 수 있게 이러한 방법을 권장하는것 같다.
공식문서상 RootState 와 AppDispatch 를 따로 추출하는 이유
While it’s possible to import the RootState
and AppDispatch
types into each component, it’s better to create typed versions of the useDispatch
and useSelector
hooks for usage in your application. This is important for a couple reasons:
- For
useSelector
, it saves you the need to type(state: RootState)
every time - For
useDispatch
, the defaultDispatch
type does not know about thunks. In order to correctly dispatch thunks, you need to use the specific customizedAppDispatch
type from the store that includes the thunk middleware types, and use that withuseDispatch
. Adding a pre-typeduseDispatch
hook keeps you from forgetting to importAppDispatch
where it’s needed.
Since these are actual variables, not types, it’s important to define them in a separate file such as app/hooks.ts
, not the store setup file. This allows you to import them into any component file that needs to use the hooks, and avoids potential circular import dependency issues.
강의에서의 커스텀 dispatch, selector Hook
1
2
3
4
5
6
7
8
9
import { TypedUseSelectorHook, useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import { AppDispatch, RootState } from "../store";
export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;
export const useTypedDispatch = () => useDispatch<AppDispatch>();
// const logger = useSelector((state: RootState) => state.logger);
강의에서는 TypedUseSelectorHook 과 TypeScript Generic으로
useDispatch와 useSelector에 대한 커스텀 훅을 만들어 사용한다.
이제 공식문서와 비교 해보자
공식문서의 커스텀 dispatch, selector hook
1
2
3
4
5
6
import { useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>()
공식문서에서는 .withType
문법으로 Type Annotation을 지정한 것을 알 수 있는데,
해당 문법은 React-Redux V9.1.0 에 추가된 버전이다.
강의시점상 9.1.0 버전이 나온지 얼마 안되었거나, 이전에 녹화된 강의이기 때문에
해당 .withType
문법을 사용하지 않았지만, 공식문서의 기능과 같은 기능을 수행한다.
버전에 따라 사용할 수 있는 문법과 사용할 수 없는 문법의 차이를 알고, 사용하는 것이 좋겠다.