React 프로젝트에 있어 '상태관리'는 중요한 개념이다. 앞서 설명했던 Context-API도 상태관리를 위한 유용한 방법이지만, 이번에는 'Redux-Toolkit'에 대해 알아보고자 한다. 이번 포스팅에서는 slice의 기본 사용방법을 확인하기 위해 counter 프로젝트를 만들어 보는 것을 목표로 한다.
이번 Redux-toolkit(=RTK) 포스팅은 'slice'에 초점을 맞춘다. RTK에는 createAction
, createReducer
등도 존재하지만 이것들을 합친 것이 createSlice
이고 훨씬 더 간편하다. 다음은 creatSlice를 이용하여 counter 프로젝트를 만들기 위한 과정의 개괄이다.
1. set up(Redux Toolkit library)
2. counterSlice 생성
👉 2.1. initial state type 지정
👉 2.2. createSlice -> action.payload type 지정
👉 2.3. export : actions, reducer of slice
3. store 생성
👉 3.1. configureStore -> store 생성
👉 3.2. RootState, AppDispatch 생성 및 export
4. store 연결 : index.ts에서 Provider를 통해 store 연결
5. [사용 단계] hooks 생성 : useSelector, useDispatch를 개별 컴포넌트에서 편하게 사용하기 위해 따로 만들어 준다.
6. [사용 단계] view 구현 : View에서 위의 5에서 생성한 hook을 통해 store 내의 값을 불러온다.
프로젝트 생성 및 redux-toolkit
npx create-react-app slice_practice --template typescript
첫번째는 Typescript를 이용한 react-redux 관련 라이브러리 설치 명령어이다. 두번째는 redux-toolkit을 사용하기 위한 라이브러리 설치이며, 이것이 설치되면 createSlice 메서드를 이용가능하다. 마지막으로 세번째와 네번째 라이브러리를 차례로 설치한다.
yarn add @types/react-redux
yarn add @reduxjs/toolkit
yarn add redux-typescript
yarn add react-redux
src 디렉토리 하단에 features - counter - counter.ts 파일을 생성한다.
전체 스크립트부터 확인하고, 위의 개괄에서 살펴본 바와 같이 코딩 순서 3단계로 나누어 살펴보겠다.
// counter.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit"
// 1. initial state type 생성
export type counterState = {
value: number
}
// 1-1. initial state 객체 생성
const initialState: counterState = {
value: 0,
}
// 2. slice 생성 : createSlice
const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment: (state) => {
state.value += 1
},
decrement: (state) => {
state.value -= 1
},
incrementByValue: (state, action: PayloadAction<number>) => {
state.value += action.payload
},
},
})
// 3. export
// 3-1. export actions
export const { increment, decrement, incrementByValue } = counterSlice.actions
// 3-2. export default slice.reducer
export default counterSlice.reducer
initial state의 type을 미리 정해놓고, initial state 객체까지 생성한다.
// 1. initial state type 생성
export type counterState = {
value: number
}
// 1-1. initial state 객체 생성
const initialState: counterState = {
value: 0,
}
const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment: (state) => {
state.value += 1
},
decrement: (state) => {
state.value -= 1
},
incrementByValue: (state, action: PayloadAction<number>) => {
state.value += action.payload
},
},
})
createSlice는 name, initialState, reducers 세 요소를 요구한다.
name
은 action 앞에 첨가되어 다른 slice의 action들과 이름 중복을 피하는 역할을 한다. initialState
에는 미리 생성한 initialState를 넣는다. 그리고 reducers
의 각각은 action 역할을 하며 동시에 state의 변화까지 구현한다. 그리고 immer.js를 내장하고 있기 때문에 state 값을 return하지 않아도 된다.
slice 내의 actions들과 reducer를 export한다.
// 3-1. export actions
export const { increment, decrement, incrementByValue } = counterSlice.actions
// 3-2. export default slice.reducer
export default counterSlice.reducer
store은 redux에서 state와 dispatch할 함수들을 가지는 저장소 역할을 한다. typeScript 환경 내에서는 store 내에서 state, dispatch의 type을 지정해줘야 한다.
src - app - store.ts 생성한다.
Redux-toolkit에서는 createStore 대신으로 configureStore을 사용하여 store을 손쉽게 생성가능하다.
import { configureStore } from "@reduxjs/toolkit"
import CounterReducer from "../features/counter/counter"
export const store = configureStore({
reducer: {
counter: CounterReducer,
},
})
앞서 설명한 바와 같이 store는 state와 dispatch를 보관하는데 가장 큰 의미가 있다. 따라서 typeScript 환경 내에서는 이 둘의 type을 밝혀준다.
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
지금까지 만들어놓은 store와 컴포넌트들을 연결함으로써, 컴포넌트들이 store 내의 state나 dispatch 함수들을 자유롭게 사용할 수 있도록 한다. 이 때, 가장 상위 컴포넌트에 store을 연결하면 하단의 컴포넌트 모두 자동 연결되기 때문에 index.tsx에서 Provider를 생성하여 store을 연결한다.
// index.tsx
import {Provider} from "react-redux"
import {store} from './app/store'
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
이것으로 counterSlice를 어떤 컴포넌트에서도 쓸 수 있도록 준비가 끝났다.
store내의 state와 dispatch를 쓰기 위해서는 useSelector와 useDispatch를 사용한다. 이를 사용하는 매 컴포넌트마다 계속 적기 힘들기 때문에 따로 custom hook을 만들어 관리한다.
src - app -hooks.ts 생성한다.
import { useDispatch, useSelector, TypedUseSelectorHook } from "react-redux"
import { AppDispatch, RootState } from "./store"
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
각 컴포넌트에서 useAppSelector와 useAppDispatch를 이용하여 store의 값들을 사용한다. 해당 코드는 간단하니 어떻게 사용하면 되는 지 한 번 훑어보는 것으로도 충분할 것이다.
import React, { useState } from 'react'
import { useAppDispatch, useAppSelector } from '../../app/hooks'
import {increment, decrement, incrementByValue} from './counter'
function CounterView() {
const count = useAppSelector(state => state.counter.value)
const dispatch = useAppDispatch()
const [num, setNum] = useState<number>(0);
const handleChange = (e:React.ChangeEvent<HTMLInputElement>) =>{
setNum(parseInt(e.currentTarget.value))
}
return (
<>
<div style ={{display : 'flex', justifyContent :"center", alignContent : "center"}}>
<button onClick ={()=>dispatch(decrement())}>-1</button>
<h1 style = {{margin : "3em"}}>{count}</h1>
<button onClick ={()=>dispatch(increment())}>+1</button>
</div>
<div>
<input type="number" onChange = {handleChange}/>
<button onClick={()=>dispatch(incrementByValue(num))}>+{num}</button>
</div>
</>
)
}
export default CounterView
이번 주는 집중력이 부족하다. createSlice를 몇 번이나 하고, 같은 예제를 몇 번 반복했음에도 이번 포스팅을 하는 도중에도 몇 분동안 막혀서 답답했었다. 그러니까 첫 술에 배부르려 하지말고 찬찬히 보고 지속적으로 이용해서 꼭 내 것으로 만들고 싶다. 상태관리 라이브러리를 여러개 이용해봤는데, 이번 redux-toolkit이 가장 쉽게 이해되면서 기존의 redux와 궤를 같이 하기 때문에 접근성도 좋다고 생각한다. 당분간은 redux-slice를 쓸 생각이다!
로그인 로직을 짤때 redux-toolkit 를 썼는데 쓰면서도 이해가 잘 되질않아 검색하던중 이 글을 발견했는데 발견해서 너무 다행이네요 !! 덕분에 조금더 개념 정리가 되었습니다! 감사합니다 :)