상태관리 라이브러리
❓상황
가장 많이 쓰이는 상태관리 라이브러리인 Redux의 최신버전인 Redux Toolkit을 정리하기 위함
📖 Redux Toolkit 이란?
상태를 저장하는 파일을 따로 분리하여 관리할 수 있다.
여러 컴포넌트를 거치지 않아도, 상태가 필요한 컴포넌트에 곧바로 상태를 줄 수 있다.
Redux Toolkit 패키지는 Redux 로직을 작성하는 표준 방법이 되도록 고안되었습니다. 원래 reducour에 대한 세 가지 일반적인 우려를 해결하기 위해 만들어졌다.
"Redux 스토어 구성이 너무 복잡합니다."
"Redux가 유용한 작업을 수행하기 위해서는 많은 패키지를 추가해야 합니다."
"리듀스에는 너무 많은 보일러 플레이트 코드가 필요합니다."
🐸 설치
CRA + Redux
# Redux + Plain JS template
yarn create react-app <프로젝트 파일명> --template redux
# Redux + TypeScript template
yarn create react-app my-app --template redux-typescript
Redux Toolkit
react-redux와 확장버전의 리덕스 툴킷, 2개의 라이브러리를 설치해야한다.
# yarn
yarn add react-redux @reduxjs/toolkit
🧮 정리
Store 생성
state들을 가질 저장소인 store를 생성해줘야한다.
일반적으로 src/app/store.ts 라는 경로에 생성하며,
configureStore라는 함수를 @reduxjs/toolki에서 불러와 store를 생성한다.
import { configureStore } from '@reduxjs/toolkit'
export const store = configureStore({
reducer: {
# slice들이 넣어져야한다.
},
})
// useSelector의 파라미터인 state의 type을 지정할때 사용한다.
// state는 위에서 정의한, store의 reducer들이다.
export type RootState = ReturnType<typeof store.getState>
// 나중에 사용할 useDispatch의 type을 지정할때 사용한다.
export type AppDispatch = typeof store.dispatch
Provider
store가 생성되었다면, 해당 store를 모든 컴포넌트에서 사용할 수 있도록 제공해줘야한다.
일반적으로 <Provider> 컴포넌트를 불러와서, src/index.tsx에 <App /> 컴포넌트를 감싼다.
그리고 속성으로 store를 작성해주면 된다.
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import { store } from './app/store'
import { Provider } from 'react-redux'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
Slice 생성
store의 구성요소 중 하나인, slice를 생성해줘야한다.
일반적으로 src/features 라는 경로에 사용될 유형의 폴더를 생성하고, 해당 폴더안에 ts파일을 생성한다.
예시로 숫자가 증가하는 state를 갖는 slice를 생성하려고 한다면,
src/features/counter/counter.ts 라는 경로로 파일을 생성하면 된다.
import { createSlice } from '@reduxjs/toolkit'
# payload type을 지정하기 위해 불러온다.
import type { PayloadAction } from '@reduxjs/toolkit'
interface CounterState {
value: number
}
# state 초기값을 설정한다.
const initialState: CounterState = {
value: 0,
}
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
# state는 counterSlice의 state를 뜻한다.
increment: (state) => {
state.value += 1
},
decrement: (state) => {
state.value -= 1
},
# 메소드는 2개의 파라미터를 갖는데, 첫번째는 해당 slice의 state이고,
# 두번째는 해당 메소드가 사용되는 곳에서 인자를 받을때,action이 해당 인자를 객체값으로 받는다.
# action은 인자를 받기때문에, type을 지정해줘야한다.
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload
},
},
})
// 내보낸 함수는 counterSlice의 state를 변경할 때 사용한다.
# dispatch(increment());
// counterSlice.actions는 counterSlice의 reducers들을 의미한다.
export const { increment, decrement, incrementByAmount } = counterSlice.actions
# counterSlice.reducer는 store에 slice를 등록할때 사용된다.
export default counterSlice.reducer
Store에 Slice 등록하기
생성된 slice를 store에 등록해야한다.
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'
export const store = configureStore({
reducer: {
counter: counterReducer,
},
})
사용하기
# useSelector의 type을 지정할때 사용한다.
import type { RootState } from '../../app/store'
# useDispatch의 type을 지정할때 사용한다.
import type { AppDispatch } from '../../app/store'
# useSelector는 store에 속한 slice의 value를 가져올때 사용한다.
# useDispatch는 slice의 action들을 시행할때 고차함수로 사용한다.
import { useSelector, useDispatch } from 'react-redux'
# slice의 action들을 불러온다.
import { decrement, increment, incrementByAmount } from './counterSlice'
export function Counter() {
# state는 store의 reducer에 적힌 slice들을 불러온다.
# 실습에서는 counter만 적었기때문에, counter만 있다.
# 확인하고 싶다면, console.log(state)를 이용하면 확인할 수 있다.
const count = useSelector((state: RootState) => state.counter.value)
const dispatch = useDispatch<AppDispatch>()
return (
<div>
<div>
<button
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
Increment
</button>
<span>{count}</span>
<button
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
Decrement
</button>
<button
aria-label="Decrement value"
onClick={() => dispatch(incrementByAmount(2))}
>
Decrement
</button>
</div>
</div>
)
}
hook으로 만들기
사용하기에서 조금 아쉬운 부분이 있다.
const count = useSelector((state: RootState) => state.counter.value)
const dispatch = useDispatch<AppDispatch>()
typescript를 사용하는 궁극적인 이유는, 컴파일 전에 미리 오류를 감지하여 쉽게 디버깅을 할 수 있기때문에 typescript를 사용한다.
typescript는 type을 필수적으로 지정해줘야하는데, type을 지정해주기때문에 코드가 조금 읽기가 어렵다.
이를 해결하기 위해, 별도의 hook으로 분리하고, 해당 파일안에 type을 지정하고 hook을 불러오면 코드가 깔끔해진다.
import { useDispatch, useSelector } from 'react-redux'
import type { TypedUseSelectorHook } from 'react-redux'
import type { RootState, AppDispatch } from './store'
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
물론 이것이 필수가아닌 선택사항이기 때문에, 지금 배우기에 복잡하다면, 나중에 redux-toolkit이 익숙해진 다음에 해도 늦지 않을 것이다.
데이터 패칭 : 비동기
이번에는 API 호출을 통해, 비동기적으로 데이터를 가져와보자
# 데이터 패칭 비동기 함수를 생성하는 createAsyncThunk를 불러온다.
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
# 데이터 패칭 함수를 생성하는데, 일반적인 방식이 아닌, createAsyncThunk 함수를 이용해야한다.
# createSyncThunk는 2개의 파라미터를 갖는데,
# 첫번째 파라미터는 고유한 key값이고,
# 두번째 파라미터는 콜백함수이다.
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (id: number) => {
const response = await fetch(`url/${id}`)
return response.json();
}
)
interface UsersState {
entities: []
loading: 'idle' | 'pending' | 'succeeded' | 'failed'
}
const initialState: UsersState = {
entities: [],
loading: 'idle',
}
const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {
# 일반적인 action을 정의할때 사용한다.
},
# 비동기 함수의 상태에 따라, action을 정의할 수 있다.
# extraReducers는 builder 콜백함수를 첫번째 파라미터로 갖는다.
extraReducers: (builder) => {
# builder는 addCase 메소드를 갖고, 2개의 파라미터를 가진다.
# 첫번째는 createAsyncThunk로 생성된 fetchUserById의 상태를 받고,
# 두번째는 fetchUserById의 상태일때의 action 함수를 정의한다.
# 즉, state는 useSlice의 state이고,
# action은 fetchUserById에서 return된 값을 가진다.
# 물론 action.payload를 이용하여 값을 이용할 수 있다.
builder.addCase(fetchUserById.pending, (state, action) => {
state.loading = 'pending';
}),
builder.addCase(fetchUserById.fulfilled, (state, action) => {
state.loading = 'succeeded';
state.entities.push(action.payload)
})
builder.addCase(fetchUserById.rejected, (state, action) => {
state.loading = 'failed';
})
},
})
extraReducers의 'builder' 콜백이 사용되는 이유는, 올바른 타입의 reducer를 제공하기 때문입니다...라고 한다.
'공부 > 프론트엔드' 카테고리의 다른 글
공부 | HTML, CSS 말 줄임(단일 행, 다중 행) 스타일 정리 (0) | 2022.10.12 |
---|---|
공부 | HTML, CSS 웹 접근성(스크린리더) 정리 (0) | 2022.10.11 |
공부 | swr 정리(ft. react) (0) | 2022.08.04 |
공부 | recoil 정리(ft. react) (0) | 2022.07.30 |
공부 | next.js 정리 (0) | 2022.07.20 |