1. redux
1.1. 리덕스란?
- 단일 저장소(store): 리덕스는 하나의 중앙 저장소를 통해 애플리케이션의 상태를 관리 -> 상태의 일관성을 유지하고 디버깅이 쉬움
- 불변성(Immutability): 리덕스는 상태를 직접 변경하는 것을 금지하고, 상태가 갱신될 때는 새로운 객체를 반환 -> 직접 상태를 변경하는 것이 아니기 때문에 예측 가능성을 높이고 상태 관리의 안정성을 제공
- 액션(Actions) & 리듀서(Reducers): 상태를 변경할 때는 액션 객체를 디스패치하고, 리듀서는 이 액션에 따라 새로운 상태를 반환 -> 이를 통해 상태 변경 로직이 예측 가능하고 테스트하기 쉬움
- 리액트에서는 이미 state 랑 props 로 상태 관리 잘하고 있는데 (feat. 리덕스를 쓰는 이유)?
리액트에서 데이터는 단방향으로 흐른다. 부모 -> 자식 컴포넌트로만 데이터가 흐르기 때문에 불필요하게 여러 컴포넌트들을 거쳐서 전달 받아야 하는 번거로움이 있다. 애플리케이션의 규모가 작은 경우에는 괜찮지만, 규모가 큰 경우에는 state 관리가 어렵다.
(관리해야 하는 state 의 수가 많고, state 에 관련된 컴포넌트가 많을 때 관리가 어렵다. 리덕스는 모든 state 를 한 곳에 모아 store 이란 곳에서 관리하기 때문에 관리가 훨씬 수월하다.)
사진으로 보면 이해가 쉬운데,
state 와 props 로 하는 상태관리는 소문이라고 생각하면 된다. 보라색 컴포넌트가 바뀌었다는 것을 관련 컴포넌트에게 알리려면 컴포넌트를 타고 타고 가야한다.
redux 로 하는 상태관리는 대중매체라고 생각하면 된다. store 이라는 대충매체에서 보라색 컴포넌트가 바뀌었다는 걸 전역으로 알려준다. 한번에 소식을 알릴 수 있기 때문에 소문으로 타고타고 가서 알려주는 것보다 간편하다.
1.2. 리덕스의 주요 개념
- 액션 (Action)
액션은 애플리케이션의 상태를 변경하기 위해 디스패치되는 객체로, 상태가 변경되어야 하는 이유나 이벤트를 설명한다. 리듀스는 이 객체를 수신하여 새로운 상태를 반환한다. 리덕스 애플리케이션의 상태변경은 오직 액션을 통해서만 이루어진다. 객체 안에는 type 속성이 들어있고 필요시 payload 도 들어간다.
{
type: "ACTION_TYPE",
payload: { /* 데이터 */ }
}
- 리듀서 (Reducer)
리듀서는 애플리케이션의 상태를 변경하는 역할을 하는 순수 함수로, 액션이 디스패치될 때 리듀서는 현재 상태와 액션 객체를 인자로 받아 새로운 상태를 반환하는 기능을 한다. 데이터의 불변성을 보장하기 위해 리듀서는 현재 상태를 직접 수정하지 않고, 현재 상태를 기반으로 새로운 객체를 반환한다. (상태의 불변성을 보장 -> 상태관리 예측 가능)
※ 추가- 순수함수란?
주어진 입력값(현재 상태와 액션)에 대해 항상 동일한 출력값(새로운 상태)을 반환하는 함수를 말한다. 입력값 이외의 외부 요인에 의존하지 않는다.
- 스토어 (Store)
애플리케이션의 전체 상태를 저장/관리하는 장소로 상태 관리의 중심 역할을 한다.
스토어의 주요 역할로는 상태 저장 / 상태 읽기 / 액션 디스패치 / 구독 및 알림이 있다.
1.3. 리덕스 사용해보기
우선 리덕스 스토어를 사용하려면, main.tsx 로 가서 App 컴포넌트를 Provider 로 감싸주고 props 로 store 를 넣어줘야 한다. (아래 코드처럼 만들기)
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import { Provider } from 'react-redux'
import store from './store';
ReactDOM.createRoot(document.getElementById('root')!).render(
<Provider store={store}>
<App />
</Provider>
)
- 리덕스 스토어 만들기
리덕스에서는 보통 createStore 나 configureStore 을 사용해서 스토어를 생성하는데, 나는 리덕스 툴킷에서 제공하는 configureReducer 를 사용해서 스토어를 만들어보았다. (createStore 보다 더 많은 기능과 편리함을 제공한다고 함!)
import { configureStore } from "@reduxjs/toolkit";
import rootReducer from './rootReducer';
const store = configureStore({
reducer: rootReducer,
})
export default store;
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
코드 뜯어보기! (이해가 안돼서 한 줄 한 줄 뜯어보기로 함)
스토어를 생성하고, 스토어에서 상태를 관리하는 리듀서를 설정하는 부분이다. 여기서 rootReducer 는 리덕스 스토어에 연결될 최상위 리듀서를 의미한다.
const store = configureStore({
reducer: rootReducer,
})
최상위 리듀서는 이렇게 Slice들을 모아놓은 형태
const rootReducer = {
logger: loggerReducer,
boards: boardsReducer,
modal: modalReducer,
}
export type RootState = ReturnType<typeof store.getState>;
RootState 타입은 store 의 전체 상태의 타입을 정의한다.
typeof store.getState 를 사용해서 getState 메서드의 반환 타입을 추론하고 이를 RootState 타입으로 지정한다.
더 풀어서 설명해보면, getState 메서드는 현재 상태를 반환하는 메서드로 store 의 현재 상태를 반환해주고, typeof 연산자로 getState 가 반환한 store 의 상태의 타입을 추론한다. 그리고 추론된 타입은 ReturnType 의 타입이 된다.
(사실 나는 여기서 store 의 전체 상태를 추론해서 어디쓰지? 왜 필요한거지? 라는 궁금증이 또 생겼는데 밑에서 차차 풀어보도록 하겠다
-> useSelector 부분 참고)
export type AppDispatch = typeof store.dispatch;
AppDispatch 는 스토어에서 사용하는 디스패치 함수의 타입을 정의한다.
디스패치 함수는 리덕스 스토어에서 제공되는 함수로 액션 객체를 리듀서 함수에 전달하는 역할을 한다. 그래서 여기서 말하는 디스패치 함수의 타입을 정의한다는 말은 디스패치 함수가 어떤 액션 객체를 받을 수 있는지 타입스크립트가 추론하는 것을 말한다.
타입스크립트는 디스패치의 타입을 추론해서 받을 수 있는 액션이 들어오면 받고 받을 수 없는 액션이 들어오면 걸러낸다.
- createSlice
createSlice 는 Redux Toolkit 에서 제공하는 함수로, 액션 타입, 액션 생성자, 리듀서를 따로 작성할 필요없이 한 번에 정의해서 간단하게 사용할 수 있다. 초기값(initialState), 액션 타입, 리듀서 함수를 포함하는 객체를 정의하고, 이를 기반으로 자동으로 액션 생성자와 리듀서를 생성한다.
createSlice 요소들
- initialState: 리덕스 상태의 초기 값, 슬라이스가 관리할 상태가 무엇인지 설정하는 부분
- reducers: 상태를 변경하는 리듀서 함수들을 정의, 각 리듀서는 액션을 처리하여 상태를 어떻게 변경할지 결정함.
- 첫 번째 인자는 현재 상태
- 두 번째 인자는 액션 객체로 type, payload(선택적) 속성을 가짐. 상태변경에 필요한 데이터를 담고있음.
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
const initialState = { value: 1 };
const calcSlice = createSlice({
name: 'calc',
initialState,
reducers: {
increment: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});
export const { increment } = calcSlice.actions;
export const calcReducer = calcSlice.reducer;
- useSelector
useSelector 는 React-Redux 에서 제공하는 훅으로 리덕스 스토어의 상태를 리액트 컴포넌트에서 읽어오기 위해 사용된다.
이 훅을 사용하여 컴포넌트에서 리덕스의 상태를 쉽게 조회할 수 있다. 또 상태를 구독(subscribe)하여 상태가 변경되면 해당 컴포넌트는 자동으로 리렌더링이 된다. 추가로 useSelector 는 상태를 읽는 용도로만 사용되며 상태를 수정하거나 업데이트하는 것은 불가능하다.
import { useSelector } from 'react-redux';
import { RootState } from '../redux-store/store';
const DisplayNumber: React.FC = () => {
// 셀렉터 훅 가져오기
// state 를 파라미터로 받아 상태를 리턴하는 콜백함수를 인자로 받음
const num = useSelector((state: RootState) => state.calc.value);
return (
<div>
<h1>Display Number</h1>
<input type="text" value={num} readOnly></input>
</div>
);
}
export default DisplayNumber
여기서 위에서 궁금했던 리덕스 스토어의 현재 상태를 타입으로 만드는 이유를 알 수 있다.
state 의 타입을 스토어의 상태를 정의한 타입 RootState 로 지정하여 사용하고 있는데, 타입을 지정해주지 않으면 사진처럼 state 의 타입이 unknown 이라는 에러문구가 뜬다.
- 왜 타입을 지정해주지 않으면 에러가 나는거지?
state 의 타입을 명시해주지 않으면 타입스크립트가 state 의 구조를 알 수 없어서 오류가 난다고 한다.
리덕스 스토어에 저장된 state 가 어떻게 생긴지 알아야 잘 읽어올 수 있는데, 정보가 없어서 잘 읽어올 수 없는 것!
(위의 코드로 예를 들어보면, 상태에 접근할 때 어떻게 생긴지 모르면 state.calc.value 에 숫자가 들어가는지 문자열이 들어가는지를 알 수 없기 때문에 타입 오류가 발생할 위험이 커질 수 있음)
- useDispatch
useDispatch 는 React-Redux 에서 제공하는 훅으로, 액션을 상태를 변경할 수 있는 리듀서로 전달하여 상태를 변경하도록 하는 함수이다.
useDispatch 를 사용하면 그 자리에 dispatch 함수가 남는데, dispatch 함수를 사용하여 리듀서에 액션을 보낼 수 있다. 리듀서는 디스패치 함수가 보낸 액션을 받아서 상태를 변경(새로운 객체 반환함)한다.
import { useDispatch } from 'react-redux';
import { increment } from "../redux-store/calcSlice";
const AddNumber: React.FC = () => {
const dispatch = useDispatch(); // 이 자리에 dispatch 함수가 반환되어 남음
// ...
}
전체 코드
import { useState } from "react";
import { useDispatch } from 'react-redux';
import { increment } from "../redux-store/calcSlice";
const AddNumber: React.FC = () => {
const [size, setSize] = useState(1);
// useDispatch 는 타입스크립트에서 자동으로 타입이 정의됨, 리덕스에서 제공하는 훅임
const dispatch = useDispatch(); // 디스패치 함수 가져오기
return (
<div>
<h1>Add Number</h1>
<input
type="button"
value="+"
onClick={()=>{
dispatch(increment(size)); // 액션 디스패치
}}></input>
<input
type="text"
value={size}
onChange={(e) => {
setSize(Number(e.target.value));
}}></input>
</div>
);
}
export default AddNumber
'웹 > React' 카테고리의 다른 글
[React+JS/리액트] PWA 셋팅해서 앱으로 발행하기 (모바일앱인척하기) (0) | 2024.07.30 |
---|---|
[React/리액트] Vite 설치 및 사용 (0) | 2024.06.22 |