`useContext`, `useMemo`를 다루기로 했는데, 헷갈려서 까먹기 전에 redux 기본 사용법을 정리하려고 한다.
Redux
왜 redux를 사용할까?
`useState`만 사용하여 state를 관리하려면 props를 통해 부모 컴포넌트에서 자식 컴포넌트로 공유해야 했다. 그러면 몇 단계 하위 컴포넌트에서 사용하려면 불필요한 props도 계속 전달해줘야 하는 불편함이 있다. 이처럼 props를 계속 아래로 아래로 내려보내는 것을 prop drilling이라고 한다. Prop drilling을 해결하는 하나의 방법으로 redux를 사용할 수 있다. Redux를 사용하면 state를 공유하기 위해 부모-자식 컴포넌트 관계를 유지하지 않아도 된다.
앞으로 `useState`를 사용해 특정 컴포넌트에서 생성된 state를 local state, 특정 컴포넌트에서 생성되지 않은 global state라고 하겠다. 또한 중앙에서 global state를 관리하는 곳을 store라고 부르겠다. 이 store에서 어느 위치에 있는 컴포넌트든 필요한 state를 불러올 수 있다. store가 이렇게 global state를 관리하는 것을 전역 상태 관리라고 부른다. Redux는 전역 상태 관리 라이브러리라고 많이 불린다.
아직 다루지 않았지만 `useContext`를 통해서 state를 관리할 수도 있다. 그러면 `useContext`와 `redux`의 차이점은 무엇일까?
1. 성능 최적화
`useContext`는 Provider 하위의 모든 컴포넌트를 리렌더링하게 만들 수 있다. 하지만 상태가 변경될 때마다 관련된 모든 컴포넌트가 불필요하게 리렌더링될 수 있어, redux를 통해 선택적으로 컴포넌트를 리렌더링해 최적화할 수 있다.
2. 상태 로직 중앙화와 일관성
`useContext`를 사용해 일관적으로 하위 모든 컴포넌트에 상태를 연결할 수 있지만, context가 복잡하게 얽히면 관리가 어려워진다. redux의 store를 통해 중앙에서 일관성 있고 예측 가능하도록 상태를 변경할 수 있다. 또한 모든 상태 변경 로직이 reducer에 의해 처리되어 디버깅과 테스팅이 용이하다.
3. 강력한 미들웨어와 개발 도구
Redux는 다양한 미들웨어를 지원해 비동기 작업, 로깅, 상태 변경에 대한 추가 처리 등 복잡한 기능을 구현할 수 있다. 또한 'Redux DevTools'와 같은 개발 도구를 통해 상태 변화를 시각적으로 모니터링하고 이전 상태로 롤백하는 등의 기능도 사용할 수 있다.
Redux 설정
아래 코드를 실행하자. `react-redux`는 react에서 redux를 실행할 수 있게 해주는 패키지이다.
yarn add redux react-redux
폴더 구조
react+vite로 생성된 폴더 중 src 폴더에 redux 포더를 만들고 하위에 'config' 폴더와 'modules' 폴더를 만들자.
- `redux` 폴더 : 리덕스와 관련된 파일을 모두 모아놓은 폴더
- `config` 폴더 : 리덕스 설정과 관련된 파일을 모아놓은 폴더
- `configStores.js` : 중앙 state 관리소인 store를 만드는 설정 코드들이 있는 파일
- `modules` : 앞으로 만들 state들의 그룹. `.js` 파일들로 구성
Redux 사용 기초
기본 설정
이번 포스팅은 기초적인 내용만 다룬다. 이해보다는 사용 방법을 익히는 것을 중점으로 한다.
// scr/config/configStore.js
import { createStore } from "redux";
import { combineReducers } from "redux";
const rootReducer = combineReducers({});
const store = createStore(rootReducer);
export default store;
1. `createStore()`
Redux의 가장 핵심이 되는 스토어를 만드는 메소드(함수).
Redux는 중앙 store로 모든 상태 트리를 관리한다. Redux를 사용할 때 `creatorStore()`를 호출할 일은 한 번밖에 없을 것이다. 나중에 redux toolkit하게 될 경우 `configureStore()`로 바꿔 사용한다.
2. `combineReducers()`
Redux는 action > dispatch > reducer 순으로 동작한다. 이때 애플리케이션이 복잡해지면 reducer 부분을 여러 개로 나눠야 하는 경우가 발생할 수도 있다. combineReducers은 여러 개의 독립적인 reducer의 반환 값을 하나의 상태 객체로 만들어준다.
이제 `main.jsx`의 App 컴포넌트를 `<Provider>`로 감싸주자.
// src/main.jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
import './index.css'
import {Provider} from "react-redux"
import store from './redux/config/configStore.js'
createRoot(document.getElementById('root')).render(
<StrictMode>
<Provider store={store}>
{/* 모든 하위 컴포넌트에서는 store를 사용할 수 있음 */}
<App />
</Provider>
</StrictMode>,
)
우리는 버튼을 누르면 입력한 값을 더하거나 빼주는 프로그램을 만들 것이다. 이를 위해 `counter.js` module을 하나 만들자.
// src/modules/counter.js
// initial state
const initialState = {
number: 0,
};
// action value : 사람 실수 방지
const PLUS_ONE = 'PLUS_ONE';
const MINUS_ONE = 'MINUS_ONE';
// action creator
export const plusOne = () => {
return {
type: PLUS_ONE
}
}
export const minusOne = () => {
return {
type: MINUS_ONE
}
}
// reducer 함수
const counter = (state = initialState, action) => {
switch (action.type) {
default:
return state;
}
};
export default counter;
- `initialState` : 초기 상태값
- `const [counter, setCounter] = useState(initialState)`와 동일한 내용
- 초기 상태값은 꼭 객채 `{}`로 지정하지 않아도 됨.
- `reducer` : 변화를 주는 함수
- `setCounter()`처럼 함수임.
- action value에 따라 state를 변화시켜 줌. (action value, action creator를 저장해 여러번 같은 코드를 작성해 실수가 나지 않도록 해줌)
- 따라서 state와 action이 인자로 필요함.
- 이때 `state = initialState`로 초기화해줘야 함.
Reducer 연결하기
이제 `configStore.js`에 reducer를 연결해주자.
// src/config/configStore.js
import { combineReducers, createStore } from "redux";
import counter from "../modules/counter"
// 1. rootReducer 만들기
const rootReducer = combineReducers({
counter, // counter: counter 와 동일한 코드 (key, value의 모양이 같아 생략 가능)
});
// 2. store 조합
const store = createStore(rootReducer);
// 3. 만든 store 내보내기
export default store;
`rootReducer`를 모듈로 만들어 놓은 여러 reducer들을 모아 놓은 것이다. 이를 `createStore()`를 통해 store로 만들어준다.
`useSelector`
이어서 store에서 counter state를 가져와보자. 이때 `useSelector`라는 `react-redux`의 hook이 필요하다.
// src/App.jsx
import './App.css'
import { useSelector } from 'react-redux'
function App() {
const counterReducer = useSelector((state) => {
return state.counter;
})
console.log('counterReducer :', counterReducer); // {number:0}
return (
<>
{/* your codes */}
</>
)
}
export default App
위와 같이 코드를 실행하면 콘솔창에서 initialState로 설정한 `{number: 0}`가 출력되는 것을 확인할 수 있다.
Reducer에게 보낼 명령 만들기
위에 작성해둔 `counter.js`를 다시 살펴보자.
// src/modules/counter.js
// initial state
const initialState = {
number: 0,
};
// action value : 사람 실수 방지
const PLUS_ONE = 'PLUS_ONE';
const MINUS_ONE = 'MINUS_ONE';
// action creator
export const plusOne = () => {
return {
type: PLUS_ONE
}
}
export const minusOne = () => {
return {
type: MINUS_ONE
}
}
// reducer 함수 : 수정됨
const counter = (state = initialState, action) =>
switch (action.type) {
case PLUS_ONE:
return { number: state.number + 1 };
case MINUS_ONE:
return { number: state.number - 1 };
default:
return state;
}
};
export default counter;
`action` 객체는 반드시 `type`이라는 key를 가져야한다. Reducer는 이 key의 value를 통해 state를 변경해준다. 이때 action을 자동으로 만들어주는 action creator도 같이 정의해 사용 편의를 더해주자.
`useDispatch()`
이제 더하기/빼기 버튼을 만들어 위에서 만들어준 action을 적용해보자. 이를 위해 `useDispatch()`라는 `react-redux`의 hook을 사용해야 한다.
// src/App.jsx
import './App.css'
import { useDispatch, useSelector } from 'react-redux'
import { minusOne, plusOne } from './redux/modules/counter'
function App() {
const counterReducer = useSelector((state) => {
return state.counter;
})
console.log('counterReducer :', counterReducer);
const dispatch = useDispatch();
// button의 action을 reducer에게 전달해줌
return (
<>
{counterReducer.number}
<button onClick={() => {
dispatch(minusOne())
}}>
빼기
</button>
<button onClick={() => {
dispatch(plusOne())
}}>
더하기
</button>
</>
)
}
export default App
빼기/더하기 버튼을 누르면 `dispatch()`를 통해 action value를 전해 받은 reducer가 그에 맞는 action을 취해준다.
Payload
이제 입력한 값을 더하거나 빼주는 코드로 바꿔주자. Reducer에는 `payload`를 통해 값을 전달해줄 수 있다. 위에는 정해진 특정한 하나의 액션(1을 더하기/빼기)만 할 수 있었다면 `payload`를 통해 액션에 조금의 variation을 줄 수 있다.
먼저 `App.jsx`를 다음과 같이 수정하자.
// src/App.jsx
import { useState } from 'react'
import './App.css'
import { useDispatch, useSelector } from 'react-redux'
import { minusOne, plusOne } from './redux/modules/counter'
function App() {
const [count, setCount] = useState(0);
const counterReducer = useSelector((state) => {
return state.counter;
})
console.log('counterReducer :', counterReducer);
const dispatch = useDispatch();
// button의 action을 reducer에게 전달해줌
return (
<>
{counterReducer.number}
<input type='number' onChange={(e) => {
setCount(+e.target.value);
}} />
<button onClick={() => {
dispatch(minusOne(count))
}}>
빼기
</button>
<button onClick={() => {
dispatch(plusOne(count))
}}>
더하기
</button>
</>
)
}
export default App
`useState()`를 통해 `<input>`의 입력값에 대한 상태변화를 감지해 `dispatch()`를 통해 reducer에 전달해주자.
이제 입력받은 count라는 `payload`를 다룰 수 있도록 `counter.js`를 수정하자.
const initialState = {
number: 0,
};
// action value
const PLUS_ONE = 'PLUS_ONE';
const MINUS_ONE = 'MINUS_ONE';
// action creator
export const plusOne = (payload) => {
return {
type: PLUS_ONE,
payload,
}
}
export const minusOne = (payload) => {
return {
type: MINUS_ONE,
payload,
}
}
// reducer 함수
const counter = (state = initialState, action) => {
console.log("action :", action);
switch (action.type) {
case PLUS_ONE:
return { number: state.number + action.payload };
case MINUS_ONE:
return { number: state.number - action.payload };
default:
return state;
}
};
export default counter;
사실 꼭 `payload`라고 부르지 않아도 된다. 공식 문서에 제시되어 있거나 개발자들이 따르는 하나의 convention이 정해져있지 않아 원하는대로 불러도 된다.
이제 input 창에 입력한 값을 더하거나 빼주는 기능하는 페이지를 완성했다.
지금까지 작성한 코드는 'Duck 패턴'으로 작성된 것이다. 'Duck 패턴'은 아래 규칙을 지킨다.
1. Reducer 함수를 export default 함.
2. Action creator를 export함.
3. Action type은 app/reducer/ACTION_TYPE 형태로 작성함. (외부 라이브러리로서 사용되거나 외부 라이브러리가 필요로 할 경우에는 UPPER_SNAKE_CASE 로만 작성해도 됨.)
'Frontend > Today I Learned' 카테고리의 다른 글
[JS, redux] Redux Toolkit 기본 사용법 (0) | 2024.08.26 |
---|---|
[JS] 이벤트 겹쳤을 때 막는 방법 (+ 이벤트 캡처링, 버블링) (0) | 2024.08.23 |
[React] Hooks - useState, useEffect, useRef (0) | 2024.08.16 |
[CSS] 스크롤 바 스타일 (0) | 2024.08.13 |
[JS] Module 기초 정리 (0) | 2024.08.08 |