오늘은 React의 상태 관리 라이브러리인 Redux에 대해서 배웠다. 이건 수업 시간에 따라가긴 했는데 제대로 이해하지는 못했어서, 수업에서 실험했던 코드들을 살펴보면서 완전한 내 것으로 만들려고 한다.

State의 종류
- Local State : useState()를 통해 각각의 컴포넌트가 소유하고 있는 상태. 해당 컴포넌트 내에서만 관리되고 사용된다.
- Cross-Component State : 두 개 이상의 컴포넌트 간에 props를 통해 공유되는 상태.
- App-Wide State : 애플리케이션의 전체 영역에서 사용되는 상태. 여러 컴포넌트, 혹은 앱의 전체 영역에서 공유해야하는 데이터나 상태에 사용됨.
React의 상태관리
npm으로 설치해야하는 라이브러리 사용, 또는 React의 기본 기능인 context API를 사용하는 방법이 있다.
라이브러리는 굉장히 다양한데, 가장 대표적인게 Redux와 Recoil이다.
Redux | - 현재 점유율 높음 - 코드가 길어짐, 대규모 프로젝트에 유리 |
Recoil | - 최근 많이 사용되는 추세 - 짧은 코드로 관리할 수 있음 |
Redux
JS의 상태관리 라이브러리.
React 외에 Angular, Vue에서도 사용할 수 있지만 React의 상태관리 라이브러리로 가장 많이 사용되고 있다.
💡 Redux를 사용하는 이유
컴포넌트 수가 많은 대형 프로젝트에서는 state를 전달하기 위해 props를 다방면으로 넘겨야하는 경우가 생기는데, 이 때 Redux를 사용하면 전역적으로 상태를 관리할 수 있어서, state를 props drilling하지 않고 store에서 언제든지 꺼내어 사용할 수 있다.
Redux에서는 Store, Action, Reducer, Dispatch 라는 개념을 사용한다.
Store | 상태가 관리되는 공간으로, 현재 애플리케이션 상태와 reducer 함수가 들어있다. 하나의 프로젝트는 하나의 store만 가질 수 있다. store에 있는 데이터는 컴포넌트에서 직접 조작하지 않는다. |
Action | 컴포넌트에서 store에 운반할 객체 형태의 데이터. reducer 함수가 수행할 작업을 설명한다. |
Reducer | action의 type에 따라 변화를 일으키는 함수, 상황에 따라 어떤 행동을 할지 작성하는 곳. 첫번째 매개변수는 현재 상태, 두번째 매개변수는 action을 받는다. 항상 새로운 상태의 객체를 반환하며, 데이터 저장은 하면 안된다. |
Dispatch | 실제로 action을 실행하는 곳으로, dispatch(action) 형태로 호출한다. |
Redux 동작원리와 원칙
동작원리 | 1. 디스패치가 액션을 스토어에 보낸다. 2. 스토어는 리듀서한테 상태와 액션을 전달한다. 3. 리듀서는 액션과 상태를 확인하고 반환한다. 4. 스토어는 새로 변경된 상태를 확인하고 컴포넌트 리렌더링 → Redux는 동기적이고 전역적으로 상태를 관리해주는 라이브러리! |
사용원칙 | - 하나의 애플리케이션에는 하나의 store만 존재해야 한다. - store에 저장된 state는 reducer만 변경할 수 있다. - reducer는 기존의 state를 변경하는 것이 아니고 새로운 state 객체를 반환한다. |
Redux 사용하기
사용하려면 터미널에서 아래 명령어로 redux, react-redux, @reduxjs/toolkit라는 3개의 모듈을 설치해야 한다.
@reduxjs/toolkit의 경우에는 Redux의 복잡성을 줄이고 기능을 효율적으로 구현하기 위해서 설치한다.
$ npm install redux react-redux @reduxjs/toolkit
Redux Toolkit에는 2개의 메서드가 있다.
configureStore | Redux store를 생성하기 위한 함수. 여러 미들웨어와 reducer를 쉽게 통합할 수 있다. Redux DevTools 확장 프로그램과의 통합도 제공한다. |
createSlice | reducer와 action을 함께 생성하는 함수. 액션 타입, 액션 생성함수, reducer를 한 번에 정의. |
이제 Redux를 실제로 사용한 예시 코드를 살펴보자.👀
1. Redux로 하나의 상태만 관리하는 방법
// 리액트 어플의 index.js
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
const root = ReactDOM.createRoot(document.getElementById('root'));
// 1. redux를 가장 쉽게 사용하는 방법 (하나의 상태만 관리, 코드 분리 x)
const store = configureStore({ reducer }); // 인자로 객체를 받고 있음
function reducer(state = 0, action) {
switch (action.type) {
case 'increment':
return state + 1;
case 'decrement':
return state - 1;
case 'reset':
return 0;
default:
return state;
}
}
root.render(
<Provider store={store}>
<React.StrictMode>
<App />
</React.StrictMode>
</Provider>
);
- 리액트 어플의 index.js는 브라우저에 보여지기 위한 기본 설정
- 우선 기능을 제대로 사용하기 위해 provider, configureStore를 import한다.
- configureStore()로 store를 만들고 인자로 reducer를 객체 형태로 보낸다.
- reducer 함수에 현재 상태와 액션을 인자로 보내고, 스코프에 switch문으로 상황마다 어떤 행동을 할지 설정한다.
- render 내부에 provider를 최상위 부모태그로 설정해서 감싼다.
// Counter.jsx
import { useSelector, useDispatch } from 'react-redux';
export default function Counter() {
const number = useSelector((state) => state);
console.log(number);
const dispatch = useDispatch();
return (
<>
<h2>Redux로 store에서 관리되는 state 가져오고 사용하기</h2>
<div>전역관리되는 state value: {number}</div>
<hr />
<div>전역관리되는 state값 변경하기</div>
<button onClick={() => dispatch({ type: 'increment' })}>+1</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-1</button>
<button onClick={() => dispatch({ type: 'reset' })}>리셋</button>
</>
);
}
- Counter.jsx는 dispatch()로 reducer에 있는 액션을 가져와서 실행하는 공간
- 일단 useSelector(), useDispatch() 기능을 사용하기 위해 import한다.
- 함수형 컴포넌트 선언문에서 useSelector()를 number라는 변수에 담고, 인자로 state를 보낸다. state는 스토어에 보관중인 상태값을 의미한다. useDispatch()도 변수에 담았다.
- return문 내부에 브라우저에서 보여질 부분을 작성한다. 버튼에 onClick으로 dispatch()를 연결하고, 인자로 객체 형태의 액션을 보낸다.
// app.js
import Counter from './components/Counter';
function App() {
return (
<div className="App">
<Counter />
</div>
);
}
export default App;
- 마지막으로 app.js에서 Counter 컴포넌트를 불러와서 출력한다.
2. Redux로 다수의 상태를 관리하고, 코드도 분리된 상태
// 리액트 어플의 index.js
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './store';
import { composeWithDevTools } from '@redux-devtools/extension';
const root = ReactDOM.createRoot(document.getElementById('root'));
// 2. redux를 사용해 다수의 상태 관리, 코드 분리한 상태
// -> reducer 따로 만들고 store 관리하는 폴더도 따로 만듦
const store = configureStore({ reducer: rootReducer }, composeWithDevTools());
root.render(
<Provider store={store}>
<React.StrictMode>
<App />
</React.StrictMode>
</Provider>
);
- 리액트 어플의 index.js는 브라우저에 보여지기 위한 기본 설정
- 우선 기능을 제대로 사용하기 위해 import를 한다.
rootReducer는 모든 상태가 관리되고 있는 store에서 가져온 것.
composeWithDevTools는 리액트의 관리자 도구. - configureStore()로 store를 만들고 인자로 reducer와 composeWithDevTools()를 보낸다.
reducer는 store에서 rootReducer를 가져와서 사용한다. - render 내부에 provider를 최상위 부모태그로 설정해서 감싼다.
// store/index.js
import { combineReducers } from '@reduxjs/toolkit';
import { countReducer } from './modules/CountReducer';
// import { isLoggedInReducer } from './modules/isLoggedInReducer';
// import { bankReducer } from './modules/BackReducer';
const rootReducer = combineReducers({
count: countReducer,
// isLoggedIn: isLoggedInReducer,
// money: bankReducer,
});
export default rootReducer;
- store/index.js는 reducer 함수가 있는 다수의 파일을 가져와서 rootReducer라는 이름으로 한꺼번에 관리하는 공간
- 다양한 reducer를 하나로 묶어야해서 combineReducers를 import한다.
여기서는 countReducer도 다른 것들이랑 같이 관리하려고 import했음. - combineReducers()로 다양한 reducer 함수들을 묶기 위해서, rootReducer라는 변수에 담고 인자로 다양한 reducer 함수들을 객체 형태로 보낸다.
- 다른 곳에서 가져다 써야하니까 export 시킨다.
// CountReducer.js
// 초기값
const initialState = 0;
// 액션 타입 정의
export const counterMinus = () => ({
type: 'count/decrement',
});
export const counterPlus = () => ({
type: 'count/increment',
});
export const counterReset = () => ({
type: 'count/reset',
});
// reducer
export const countReducer = (state = initialState, action) => {
switch (action.type) {
case 'count/increment':
return state + 1;
case 'count/decrement':
return state - 1;
case 'count/reset':
return 0;
default:
return state;
}
};
- CountReducer는 언제 어떤 행동을 할지 reducer()로 설정하는 공간
- 일단 초기값을 설정하고, 상황에 따른 액션타입을 정의하면서 함수들의 인자로 액션을 객체 형태로 보낸다.
count/~~로 작성하는 이유는 다른 state에서 type으로 사용될 수 있어서, 명확하게 특정하여 원하는 로직을 실행하기 위함이다. - reducer 함수의 인자로 '현재 상태'와 '액션'을 보내고, 스코프에 switch문으로 상황별로 어떤 행동을 할지 작성한다.
// UseAllState.jsx
import { useDispatch, useSelector } from 'react-redux';
import { counterMinus,
counterReset,
counterPlus } from './../store/modules/CountReducer';
export default function UseAllState() {
return (
<div style={{ border: '5px solid pink', padding: 10 }}>
<h2>부모 컴포넌트</h2>
<div>자식으로 Child 컴포넌트를 가짐</div>
<div>이 컴포넌트는 어떤 props도 전달하지 않음</div>
<Child />
</div>
);
}
function Child() {
// useSelector 사용해서 스토어에서 관리되는 전역 상태 가져오기
const count = useSelector((state) => state.count);
const dispatch = useDispatch();
return (
<div style={{ border: '5px solid skyblue', padding: 5 }}>
<div>count state: {count}</div>
<button onClick={() => dispatch({ type: 'count/increment' })}>+1</button>
<button onClick={() => dispatch({ type: 'count/decrement' })}>-1</button>
<button onClick={() => dispatch({ type: 'count/reset' })}>reset</button>
<hr />
<div>countReducer에서 정의한 액션 함수 사용</div>
<button onClick={() => dispatch(counterPlus())}>+1</button>
<button onClick={() => dispatch(counterMinus())}>-1</button>
<button onClick={() => dispatch(counterReset())}>reset</button>
</div>
);
}
- UseAllState는 dispatch()로 reducer에 있는 액션을 받아서 실행하는 공간
- 스토어에서 state를 불러와야 하니까 useSelector()를 import했고,
reducer 함수의 액션을 실행해야 하니까 useDispatch()를 import한다.
CountReducer에서 설정한 액션타임 함수도 불러온다. - 함수형 컴포넌트 선언문에 자식 컴포넌트로 불러와서 브라우저에 출력하겠다고 설정했다.
자식 컴포넌트에서 useSelector()에 인자로 state를 보내서 스토어에 있는 상태를 가져오고,
dispatch로 reducer의 액션도 가져온다. - return 내부에 브라우저에서 실제로 보여질 부분을 작성하는데,
첫번째로는 버튼의 onClick에 dispatch를 연결하고 인자로 객체 형태의 액션을 보낸다.
두번째로는 dispatch()에 인자로 CountReducer에서 설정한 액션함수를 넣어주는 방법이 있다.
// app.js
import UseAllState from './components/UseAllState';
function App() {
return (
<div className="App">
<UseAllState />
</div>
);
}
export default App;
- 마지막으로 app.js에서 UseAllState 컴포넌트를 불러와서 return에서 출력해준다.
createSlice
reducer와 액션 함수를 쉽게 생성하기 위해 react/toolkit에서 제공하는 기능.
앞선 예시에서는 다양한 파일들에서 데이터를 서로 주고 받느라 코드가 매우 길었지만, createSlice를 사용하면 reducer()와 액션을 한 곳에서 설정할 수 있어서 코드가 훨씬 간결해진다.
수업 시간에 실험했던 예시 코드를 살펴보자.👀
// CountSlice.js
import { createSlice } from '@reduxjs/toolkit';
const initialState = { count: 0 };
const countSlice = createSlice({
name: 'count',
initialState,
reducers: {
increment: (state) => {
state.count += 1;
},
decrement: (state) => {
state.count -= 1;
},
increase: (state, action) => {
state.count += action.payload;
},
decrease: (state, action) => {
state.count -= action.payload;
},
reset: (state) => {
state.count = 0;
},
},
});
export const { increment, decrement, increase, decrease, reset } =
countSlice.actions;
export default countSlice.reducer;
- CountSlice.js는 createSlice()로 슬라이스를 만들어서 reducer와 액션을 동시에 설정하는 공간
- 우선 초기값을 설정하고, countSlice 라는 변수에 담긴 createSlice()의 인자에 객체 형태로 슬라이스 이름, 초기값, reducer()를 보낸다.
- reducer()와 액션을 다른 곳에서 사용하기 위해 export한다.
// store/index.js
import { combineReducers } from '@reduxjs/toolkit';
// import { isLoggedInReducer } from './modules/isLoggedInReducer';
// import { bankReducer } from './modules/BackReducer';
import countReducer from './modules/CountSlice';
const rootReducer = combineReducers({
count: countReducer,
// isLoggedIn: isLoggedInReducer,
// money: bankReducer,
});
export default rootReducer;
- store/index.js는 다양한 파일에 있는 reducer를 전부 가져와서 rootReducer로 묶어서 한꺼번에 관리하는 공간
- 다양한 reducer를 하나로 묶어야해서 combineReducers를 import했고, CountSlice에서 만든 reducer를 import했다.
- combineReducers()를 rootReducer라는 변수에 담고, 인자로 reducer 함수들을 객체 형태로 묶어서 보낸다.
- 다른 곳에서 가져다 써야하니까 export 시킨다.
// UseToolkit.jsx
import { useSelector, useDispatch } from 'react-redux';
import { increment,
decrement,
increase,
decrease,
reset } from '../store/modules/CountSlice';
import { useRef } from 'react';
export default function UseToolkit() {
const count = useSelector((state) => state.count.count);
// 기본값으로 객체를 넣어놨기 때문에 한번 더 들어가야함
const dispatch = useDispatch();
const inputRef = useRef();
return (
<>
<h2>redux toolkit의 createSlice 사용하기</h2>
<div>{count}</div>
<button onClick={() => dispatch(increment())}>+1</button>
<button onClick={() => dispatch(decrement())}>-1</button>
<hr />
<input type="number" ref={inputRef} />
<button
onClick={() => dispatch(increase(Number(inputRef.current.value)))}
>더하기</button>
<button
onClick={() => dispatch(decrease(Number(inputRef.current.value)))}
>빼기</button>
<button onClick={() => dispatch(reset())}>리셋</button>
</>
);
}
- UseToolkit.jsx는 useSelector()로 state를 가져오고, dispatch()로 액션을 불러와서 실행하는 공간
- useSelector()는 rootReducer에서 묶어준 state를 가져오기 위해 import했고,
reducer 함수의 액션을 실행해야 하니까 useDispatch()를 import한다.
CountSlice에서 설정한 액션들도 import한다. - 함수형 컴포넌트 선언문 내부에서 useSelector()를 count라는 변수에 담고, 인자로 state를 보낸다.
※ 이때 두 번 들어가는 이유 → state를 rootReducer에서 가져오는데, rootReducer에는 인자에 countReducer가 객체 형태로 들어있고, countReducer는 CountSlice.js에서 객체 형태로 들어있는 reducer 함수이기 때문이다. - reducer의 액션을 가져와서 실행하기 위해 useDispatch()를 변수에 담았고,
DOM에 접근해서 input에 들어온 값을 브라우저에 출력하려고 useRef()를 사용했다. - return에서 브라우저에 실제로 출력할 부분을 작성한다.
// app.js
import UseToolkit from './components/UseToolkit';
function App() {
return (
<div className="App">
<UseToolkit />
</div>
);
}
export default App;
- 마지막으로 app.js에서 UseToolkit 컴포넌트 불러와서 페이지에 출력한다.
하나의 React 어플리케이션 안에서 다양한 경우를 실험하다보니, 어떤 파일들이 서로 연관이 있는건지 헷갈려서 리더님한테 확인받고 작성하느라 포스팅 업로드에 시간이 더 걸렸다. 이것도 이해하니까 그렇게 어려운건 아닌 것 같은데, 조금 복잡하다는 생각은 든다. 그래도 다른 언어들처럼 몇 차례 더 사용해보면 손에 익을 것 같기는 하다.
그나저나 Redux 작동원리 연구하느라 3시간 밖에 못 잤더니 아주 피곤하구만..😅
내일의 수업을 위해 오늘 잘 쉬어야지..

'💻 Frontend > Redux, Zustand' 카테고리의 다른 글
Zustand 실습 (코드 리팩토링) (0) | 2024.10.21 |
---|---|
Flux 패턴의 기본 개념 (0) | 2024.10.16 |
Zustand 기본 개념 정리 (2) | 2024.10.14 |
Redux-thunk, Redux-saga 개념 정리 (0) | 2024.08.16 |