리액트로 화면을 만들다보니 테마 설정, 스크롤 위치 등 전역적인 상태가 필요해져, 상태 관리 라이브러리인 Redux를 시작하게 되었습니다.
이번 포스팅에서는 상태 관리란 무엇인지, 상태 관리 라이브러리인 Redux의 개념, 원칙, Redux를 이용한 상태관리 프로세스를 소개해보고자 합니다.
상태 관리 도구는 왜 필요한 것인가?
상태와 상태관리 도구의 필요성, 상태 관리 라이브러리를 소개합니다.
React
에서는 컴포넌트(Component) 안에서 여러 상태들이 관리되고 있습니다.
부모로 받은 상태인 Props, 컴포넌트가 독자적으로 가질 수 있는 상태인 State가 있습니다.
컴포넌트는 상태가 변경됨에 따라 화면을 렌더링해줍니다. 아래의 코드는 컴포넌트의 상태 관리 예시입니다.
import React, {useState} from 'react';
const Clock = () => {
const [date, setDate] = useState(new Date());
const refreshDate = () => {
setDate(new Date());
}
return (
<div>
<h1>Hello World</h1>
<h2>It's {date.toLocaleTimeString()}.</h2>
<button onClick={refreshDate}>Refresh</button>
</div>
);
};
export default Clock;
일반적인 컴포넌트에서는 상태관리 도구가 따로 필요하지 않습니다. 상태관리 도구가 필요한 때에는 컴포넌트에서 사용되는 하나의 상태가 다른 컴포넌트에서도 사용될 때 입니다.
이 때, 상태를 주고 받는 가장 직관적이고 고전적인 방법은 props를 통해 상태를 내려주는 방법입니다.
그런데 자식이 많아지면 상태 관리가 매우 복잡해지고, 상태를 관리하는 상위 컴포넌트에서 계속 내려받아야 합니다. 이러한 문제를 Props Drilling 이라고 합니다.
Props Drilling 문제를 해결하는 방법은 상태를 전역적으로 관리하는 것입니다.
전역 상태 관리는 Content API
를 사용하거나 상태 관리 라이브러리(redux
, recoil
, mobx
)를 이용하여 관리합니다.
상태 관리 라이브러리는 여러 종류가 있으며, 각 라이브러리에는 장단점이 존재하기에 목적에 맞게 사용하는 것이 중요할 것 같습니다.
NPM Trend 사이트에서 라이브러리 순위를 볼 수 있습니다.
리덕스(Redux)
는 가장 인기 있는 JavaScript 상태관리 라이브러리입니다.
"어플리케이션 상태는 모두 한곳에서 집중관리된다."
- Store에서 집중 관리
- 컴포넌트마다 동기화가 필요없이 store에서 각각에 연결되어 있는 컴포넌트에게 일괄적으로 처리
"상태는 불변하며, 오직 Action 만이 상태교체를 요청할 수 있다."
- 상태는 불변데이터
- 만약 상태를 교체해야된다면, 액션(Action)을 통해서 변경 가능
"변화는 순수함수(Reducer)로 작성되어야 한다."
- Action에 의해 상태가 어떻게 변화하는지 지정하기 위해 Reducer를 작성
- Store(스토어) – Action(액션) – Reducer(리듀서)
Redux의 핵심 구성 요소는 Store, Action, Reducer 입니다.
스토어(Store)는 애플리케이션의 상태를 전역적으로 관리하는 하나의 공간입니다. 상태 정보가 필요할 때, 스토어에 접근하여 상태를 조회합니다.
useSelector()
메소드를 통해 스토어에 접근할 수 있습니다.
액션(Action)은 컴포넌트가 상태를 변경할 때, 전달할 데이터를 의미합니다. 일종의 주문서와 같은 역할입니다.
Action은 자바스크립트 객체 형식으로 표현됩니다.
{
type: 'ACTION_CHANGE_THEME',
payload: {
value: 'light',
}
}
액션(Action)을 스토어(Store)에 전달하기 위해서는 전달자가 필요합니다. 그 역할을 수행하는 것이 리듀서(Reducer)입니다.
dispatch()
메소드를 통해 액션을 전달하고 리듀서를 호출합니다.
Redux Toolkit은 Redux에서 공식적으로 제공하는 가이드라인입니다. Redux는 비교적 러닝 커브(Learning Curve)가 높은 것으로 알려져 있는데, 너무 높은 자율성으로 설정하는 것이 복잡하기 때문입니다.
Redux Toolkit은 아래 3개의 문제점을 해결하기 위해 만들어졌습니다
계속해서 Redux Toolkit을 활용한 상태 관리 프로세스를 소개합니다.
프로젝트에 redux
와 redux-toolkit
을 추가하여 상태를 관리하는 프로세스를 소개해보고자합니다.
먼저 프로젝트에 redux 모듈을 설치해줍니다.
# redux core 설치
yarn add redux react-redux
# redux type 설치
yarn add -D @types/react-redux
# redux toolkit 설치
yarn add @reduxjs/toolkit
store
로 사용할 index.ts
파일을 생성합니다.
// 파일 경로: src/store/index.ts
import {configureStore, createSlice} from '@reduxjs/toolkit'
export default configureStore({
reducer: {}
})
루트 파일에 react-redux 모듈의 Provider
컴포넌트를 이용하여 store를 등록합니다.
💡 Provider
Provider 하면 무언가 떠오르지 않나요?
react-redux 모듈은 내부적으로 Context API를 사용합니다.
// 파일 경로: src/index.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { App } from './App'
import {store} from "./store/index";
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<Provider store={store}>
<App />
</Provider>,
)
Slice는 일종의 Store에서 떨어져 나온 조각이란 의미로, slice를 하나의 상태 값을 관리합니다.
import {createSlice} from "@reduxjs/toolkit";
// 1. createSlice를 이용하여 Slice 생성
const slice = createSlice({
// 2. slice 이름 지정
name: 'name',
// 3. 초기 상태 지정
initialState: {},
// 4. 상태를 변경할 reducer 구현
reducers: {}
})
export default slice;
대표적인 Counter Slice를 만들어 보았습니다.
// 파일 위치: src/store/slice/counter.ts
import {createSlice, PayloadAction} from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0
},
reducers: {
// Payload가 없을 경우
plus: (state) => {
state.value ++;
},
// Payload가 있을 경우
plus: (state, action: PayloadAction<number>) => {
state.value += action.payload
},
minus: (state, action: PayloadAction<number>) => {
state.value -= action.payload
},
}
})
export const {plus, minus} = counterSlice.actions
export default counterSlice;
store에 slice를 등록합니다.
// 파일 위치: src/store/index.ts
import {configureStore} from '@reduxjs/toolkit'
import counterSlice from "./slice/counter";
export const store = configureStore({
reducer: {
counter: counterSlice.reducer,
}
})
export type RootState = ReturnType<typeof store.getState>
// 파일 위치: src/App.tsx
import React from 'react';
import {useDispatch, useSelector} from "react-redux";
import {RootState} from "./store/index";
import {minus, plus} from "./Store/slice/counter";
import {Button} from "@mui/material";
const App = () => {
const dispatch = useDispatch();
const count = useSelector((state: RootState) => state.counter.value)
return (
<div>
<div>
value: {count}
</div>
<div>
<Button onClick={() => dispatch(plus(1))}>Add</Button>
<Button onClick={() => dispatch(minus(1))}>Minus</Button>
</div>
</div>
);
};
export default App;
아래와 같은 화면이 만들어집니다.
useSelector
Hooks를 이용해 store에 저장된 state를 가져오고,
useDispatch
를 사용해 변경할 값을 reducer에 전달해줍니다.
import { useDispatch, useSelector } from 'react-redux';
const dispatch = useDispatch();
const count = useSelector((state: RootState) => state.counter.value)
redux
를 처음 접했을 때는 라이브러리를 이해하기가 너무 어렵다고만 생각이 들었습니다.😇
지금와서 생각해보면 가장 큰 문제는 상태
에 대한 이해 부족입니다. 라이브러리를 무작정 쓰기 전에 컴포넌트 상태와 전역 상태가 왜 필요한지를 먼저 살펴볼 필요가 있습니다
프론트를 개발하면서 컴포넌트의 상태 관리
에 대해 이해도가 생겨났고, redux에서 가이드라인을 잡아주는 redux-toolkit
모듈을 추가로 개발해주는 덕택에 리덕스를 빠르게 활용할 수 있게 되었습니다 🙏🙏
결론. 모두 툴킷쓰세요~~~🥺