Redux는 "action"이라는 이벤트를 사용하여 애플리케이션 상태를 관리하고 업데이트하기 위한 패턴이자 라이브러리입니다. 전체 애플리케이션에서 사용해야 하는 상태를 위한 중앙 집중식 저장소 역할을 하며, 예측 가능한 방식으로만 상태를 업데이트할 수 있도록 하는 규칙을 사용합니다.
애플리케이션 전역 또는 컴포넌트 간 공유하고 있는 상태가 있다고 무조건 Redux가 필요한 것은 아닙니다. 상황에 따라 Context를 사용해 충분히 해결할 수 있을 수 있습니다.
공식 문서에서는 다음과 같은 상황에서 Redux 사용을 고려해 보라고 말하고 있습니다.
- 앱의 여러 위치에 필요한 대량의 애플리케이션 상태가 있는 경우
- 앱 상태가 시간이 지남에 따라 자주 업데이트되는 경우
- 해당 상태를 업데이트하는 로직이 복잡할 수 있습니다.
- 앱에 중간 크기 또는 큰 규모의 코드베이스가 있고 많은 사람이 작업할 수 있는 경우
Context를 사용할 때 렌더링 성능을 최적화하려면 Provider를 분리해야 하는데 이렇게 하면 규모가 큰 애플리케이션에서는 코드가 엄청나게 복잡해질 것입니다.
바닐라 자바스크립트에서 Redux 사용하기
Redux는 리액트와 함께 자주 사용될 뿐 모든 UI 프레임워크와 통합할 수 있습니다. 또한, 바닐라 자바스크립트와 사용될 수도 있습니다. 즉, Redux는 리액트에만 제한되지 않습니다.
바닐라 자바스크립트에서 Redux를 사용해보면서 관련 용어들을 학습하겠습니다.
npm init -y
npm install redux
store
리덕스 상태를 저장하고 있는 객체로 createStore에 리듀서를 전달하여 생성합니다.
스토어의 getState 메서드를 통해 현재 상태 값을 확인할 수 있습니다.
const redux = require('redux');
const counterReducer = (state, action) => {}
// 리듀서 함수를 전달하여 스토어 생성
const store = redux.createStore(counterReducer);
// 현재 상태 값을 반환
const latestState = store.getState();
console.log(latestState);
Reducers
현재 상태와 액션 객체를 수신하고, 필요한 경우 상태를 업데이트하는 방법을 결정한 후 새 상태를 반환하는 함수입니다. 기존 상태를 변형하는게 아니라 기존 상태와 액션 객체를 기반으로 새로운 상태를 반환한다는 것을 유의해야 합니다.
리듀서를 스토어에 전달하여 스토어를 생성하면 리듀서가 한번 실행됩니다. 이때 반환할 초기값을 지정할 수도 있습니다.
// 리듀서 함수는 기본 자바스크립트 함수이지만 리덕스가 실행시켜 인수로 state와 action을 갖는다.
const counterReducer = (state = { counter: 0 }, action) => {
if (action.type === 'increment') {
return {
counter: state.counter + 1,
};
}
if (action.type === 'decrement') {
return {
counter: state.counter - 1,
};
}
return state;
};
Actions
리듀서 함수는 2번째 인수로 전달되는 action은 일반 자바스크립트 객체입니다. 액션 객체는 관례적으로 type와 payload 필드를 사용합니다.
type 필드는 리듀서 함수에서 어떤 작업을 처리해야 할지 알려주는 역할을 하고 payload 필드에는 추가 정보를 제공하는 역할을 합니다.
const action = { type: 'increment' }
const actionWithPayload = { type: 'increment', payload: { step: 5 } }
const actionWithPayload2 = { type: 'increment', payload: 5 }
Dispatch
Redux에서는 상태를 직접 수정할 수 없습니다. 상태를 업데이트하는 유일한 방법은 액션 객체를 전달하여 디스패치를 호출하는 것입니다.
디스패치가 호출되면 스토어는 리듀서 함수를 실행하고 새 상태값을 저장합니다.
// 액션을 발송하는 메소드이다.
store.dispatch({ type: 'increment' });
store.dispatch({ type: 'decrement' });
Subscribe
Redux는 구독을 통해 상태가 변경될 때 특정 함수를 실행(트리거)합니다. 리액트 환경이라면 특정 함수는 대부분 컴포넌트가 될 것입니다.
store 객체의 subscribe 메서드로 counterSubscriber함수를 구독했고 상태가 변경되면 해당 함수가 호출되어 현재 상태 값을 출력합니다.
// 구독된 함수
const counterSubscriber = () => {
const latestState = store.getState();
console.log(latestState);
};
// 구독 등록
store.subscribe(counterSubscriber);
리액트에서 Redux 사용하기
Redux를 리액트 컴포넌트와 통합하기 위해서 react-redux 라는 공식 패키지를 사용합니다.
npm install react-redux
store 객체 생성
바닐라 자바스크립트 환경과 동일하게 createStore함수에 리듀서 함수를 전달하여 store 객체를 생성합니다.
// store/index.js
import { createStore } from 'redux';
const initialState = { counter: 0, showCounter: true };
const counterReducer = (state = initialState, action) => {
if (action.type === 'increment') {
// 기존 상태와 병합되는 것이 아닌 덮어쓰는 것이다.
return {
...state,
counter: state.counter + 1,
};
}
if (action.type === 'increase') {
return {
...state,
counter: state.counter + action.amount,
};
}
if (action.type === 'decrement') {
return {
...state,
counter: state.counter - 1,
};
}
if (action.type === 'toggle') {
return {
...state,
showCounter: !state.showCounter,
};
}
return state;
};
const store = createStore(counterReducer);
// 리액트와 연결해야 한다.(스토어를 제공한다.)
export default store;
리액트 컴포넌트에 store 제공
react-redux의 <Provider />를 사용하여 Redux store를 전달하면 리액트 컴포넌트에서 store에 엑세스할 수 있습니다.
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import App from './App';
import store from './store';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
useSelector
react-redux에서 제공하는 훅으로 Redux store에 있는 상태를 가져오고 자동으로 구독합니다. 바닐라 자바스크립트 환경과 다르게 react-redux가 알아서 구독을 관리합니다.
import { useSelector } from 'react-redux';
const Counter = () => {
const counter = useSelector((state) => state.counter);
const show = useSelector((state) => state.showCounter);
...
return (
<main className={classes.counter}>
<h1>Redux Counter</h1>
{show && <div className={classes.value}>{counter}</div>}
...
</main>
);
};
export default Counter;
useDispatch
디스패치를 제공하는 훅입니다. 이를 통해 액션 객체를 디스패치하여 상태를 변경합니다.
import { useSelector, useDispatch } from 'react-redux';
const Counter = () => {
const dispatch = useDispatch();
const counter = useSelector((state) => state.counter);
const show = useSelector((state) => state.showCounter);
const incrementHandler = () => {
dispatch({ type: 'increment' });
};
const increaseHandler = () => {
dispatch({ type: 'increase', amount: 5 });
};
const decrementHandler = () => {
dispatch({ type: 'decrement' });
};
const toggleCounterHandler = () => {
dispatch({ type: 'toggle' });
};
return (
<main className={classes.counter}>
<h1>Redux Counter</h1>
{show && <div className={classes.value}>{counter}</div>}
<div>
<button onClick={incrementHandler}>Increment</button>
<button onClick={increaseHandler}>Increase by 5</button>
<button onClick={decrementHandler}>Decrement</button>
</div>
<button onClick={toggleCounterHandler}>Toggle Counter</button>
</main>
);
};
export default Counter;
클래스형 컴포넌트
connect 라는 고차 컴포넌트를 사용하여 리액트 컴포넌트의 props로 상태와 디스패치 함수를 전달합니다.
import { connect } from 'react-redux';
class Counter extends Component {
incrementHandler() {
this.props.increment();
}
decrementHandler() {
this.props.decrement();
}
toggleCounterHandler() {}
render() {
return (
<main className={classes.counter}>
<h1>Redux Counter</h1>
<div className={classes.value}>{this.props.counter}</div>
<div>
<button onClick={this.incrementHandler.bind(this)}>Increment</button>
<button onClick={this.decrementHandler.bind(this)}>Decrement</button>
</div>
<button onClick={this.toggleCounterHandler}>Toggle Counter</button>
</main>
);
}
}
const mapStateToProps = (state) => {
return {
counter: state.counter,
};
};
const mapDispatchToProps = (dispatch) => {
return {
increment: () => dispatch({ type: 'increment' }),
decrement: () => dispatch({ type: 'decrement' }),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
RTK with Typescript
구글 설문조사를 클론 코딩했을 때 작성했던 일부 코드입니다. 리덕스 공식 문서를 참고했습니다.
위에서 리덕스의 기본적인 내용을 설명했고 Redux Tookit도 크게 다르지 않기 때문에 코드에 대한 설명은 하지 않고 아래의 링크로 대신하겠습니다.
설치
npm i @reduxjs/toolkit @types/react-redux
store 생성
import { configureStore } from '@reduxjs/toolkit';
import surveyReducer from './slices/surveySlice';
import previewReducer from './slices/previewSlice';
const store = configureStore({
reducer: {
survey: surveyReducer,
preview: previewReducer,
},
});
export default store;
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
커스텀 Hook 생성
import { useDispatch, useSelector } from 'react-redux';
import type { AppDispatch, RootState } from './store';
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();
리액트에 store 통합
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import GlobalStyle from './styles/GlobalStyle.ts';
import store from './redux/store.ts';
import App from './App.tsx';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<Provider store={store}>
<GlobalStyle />
<App />
</Provider>
</React.StrictMode>,
);
Slice 생성
import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit';
import type { RootState } from '../store';
import { QuestionType } from '../../types/question';
export interface SurveyState {
title: string;
description: string;
questions: QuestionType[];
focusId: number;
}
const initialState: SurveyState = {
title: '',
description: '',
questions: [],
focusId: -1,
};
export const surveySlice = createSlice({
name: 'survey',
initialState,
reducers: {
changeTitle: (state, action: PayloadAction<string>) => {
state.title = action.payload;
},
changeDescription: (state, action: PayloadAction<string>) => {
state.description = action.payload;
},
....
},
});
export const {
changeTitle,
changeDescription,
...
} = surveySlice.actions;
export const selectTitle = (state: RootState) => state.survey.title;
export const selectDesc = (state: RootState) => state.survey.description;
...
export const selectIsFocus = createSelector(
selectFocusId,
(_, questionId: number) => questionId,
(focusId: number, questionId: number) => focusId === questionId,
);
export default surveySlice.reducer;
컴포넌트 사용 예시
import { useAppDispatch, useAppSelector } from '../../redux/hooks';
import {
changeDescription,
changeTitle,
selectDesc,
selectTitle,
} from '../../redux/slices/surveySlice';
import CardWrapper from '../CardWrapper';
import FocusMarker from '../FocusMarker';
function SurveyHeader() {
const dispatch = useAppDispatch();
const title = useAppSelector(selectTitle);
const description = useAppSelector(selectDesc);
const handleChangeTitle = (e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(changeTitle(e.target.value));
};
const handleChangeDesc = (e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(changeDescription(e.target.value));
};
return (
<CardWrapper>
<div>
{isFocus && <FocusMarker />}
<InputTitle
fullWidth
variant="standard"
placeholder="설문지 제목"
value={title}
onChange={handleChangeTitle}
/>
<InputDescription
fullWidth
multiline
variant="standard"
placeholder="설문지 설명"
value={description}
onChange={handleChangeDesc}
/>
</div>
</CardWrapper>
);
}
export default SurveyHeader;
'React' 카테고리의 다른 글
Storybook 알아보기 (1) | 2024.06.11 |
---|---|
Vite 기반 리액트 프로젝트와 Next.js 프로젝트에서 폰트 Preload 구현 (1) | 2024.06.05 |
핵심 웹 지표(Core Web Vitals) 정리 (0) | 2024.05.28 |
폰트 최적화 알아보기 (0) | 2024.05.25 |
React-Hook-Form와 Zod를 활용한 스키마 유효성 검사 (0) | 2024.02.15 |