리액트의 상태 관리 관련 많은 라이브러리가 있지만 리덕스가 가장 많이 사용되고 있습니다.
물론 리덕스는 리액트가 없어도 사용할 수 있고 리액트도 리덕스 없이 Context API를 통해서 글로벌 상태 관리를 충분히 할 수 있습니다.
따라서 리덕스를 사용한다면 내 "애플리케이션에 반드시 리덕스가 필요한가?"를 고민해볼 필요가 있습니다.
내 애플리케이션에 앞서 말한 전역 상태가 필요할 경우, 빈번한 상태 업데이트 등.. 리덕스가 꼭 필요한 애플리케이션에 잘 사용한다면 유용하게 사용할 수 있지만 그렇지 않다면 오히려 독이 될 수 있습니다.
리덕스 기본에서 간단하게 정리를 해놨는데 마음에 안들어서 다시 작성
컴포넌트에서 컴포넌트로 상태(state)를 보내기 위해서 반드시 부/모 관계가 되어야 한다.
상위[1] 컴포넌트에서 하위[4] 컴포넌트로 값을 보내고자 할때 값을 전달 받지 않아도 되는 불필요한 하위 컴포넌트들[2, 3]도 값을 필수로 전달받아야 한다.
하위 컴포넌트에서 상위 컴포넌트로 값을 보낼 수 없다.
사실 상태 관리 라이브러리인 리덕스를 사용하는 이유는 리액트로 개발을 해본 경험이 있다면 굳이 언급할 필요가 없을 듯.
리덕스와 같은 상태 관리 라이브러리들은 위 문제점들을 해결하고자 나왔습니다.
Local State (지역상태) : 컴포넌트에서 useState 훅을 이용해서 생성한 state로 좁은 범위 안에서 생성된 state
Global State (전역상태) : 중앙화
된 위치에서 생성된 state
리덕스 팀에서 이야기하는 중앙화된 상태관리는 흔히 전역 상태관리라고도 불려집니다.
전역에서 관리되는 변수는 어디서든 불러올 수 있듯 전역에서 관리되는 상태들은 모든 컴포넌트에서 사용할 수 있다는 의미입니다.
본 내용은 Redux Toolkik / Thunk / React-Query에 대한 내용을 담고있지 않은 기본적인 리덕스 사용방법에 대한 글입니다.
yarn add redux react-redux
리액트에서 리덕스를 사용하기 위해 패키지를 설치
- src
- redux
- modules
- config
- configStore.js
폴더/파일 구조에 정답은 없지만 리덕스를 관리하는 폴더를 만들어 사용하면 개발/유지/보수에 유리합니다.
redux : 리덕스 관련 코드를 보관하기 위한 폴더
modules : state들을 관리하기 위한 폴더
config : 리덕스 관련 설정 파일을 위한 폴더
configStore.js : 중앙 상태 관리소의 느낌으로 상태들을 전역적으로 관리해줄 파일
해당 블로그에는 리덕스라는 도구의 사용방법을 작성하며 동작 원리에 대한 내용은 언급하지 않습니다.
// 중앙 데이터 관리소(store)를 설정하는 부분
// 앱의 상태를 보관하는 Redux 저장소
import { createStore } from "redux";
// combineReducers : Reducer들을 하나로 합쳐주는 api
import { combineReducers } from "redux";
// rootReducer : 만들어진 리듀서들을 한곳으로 모으는 역할
const rootReducer = combineReducers({
// 내부에는 /redux/modules 폴더에 만들어질 리듀서들이 위치
});
const store = createStore(rootReducer);
export default store;
...
import { Provider } from "react-redux";
import store from "./redux/config/configStore";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<Provider store={store}>
<App />
</Provider>
);
보통의 Redux 앱에는 하나의 루트 리듀서 함수를 가진 단 하나의 저장소가 있습니다. 앱이 커짐에 따라 루트 리듀서를 상태 트리의 서로 다른 부분에서 개별적으로 동작하는 작은 리듀서들로 나눌 수 있습니다. React 앱을 하나의 루트 컴포넌트에서 시작해서 여러 작은 컴포넌트의 조합으로 바꾸는 것과 동일합니다. |
Redux 공식문서
최상위 컴포넌트로 사용되는 App
컴포넌트 상위에 Provider를 이용해서 전역으로 상태를 관리할 수 있게 합니다.
기본적인 카운터로 사용법을 익힙니다.
// 초기 상태값(state)
const initialState = {
number: 0,
};
// 1. state를 action의 type에 따라 변경하는 함수
const counter = (state = initialState, action) => {
switch (action.type) {
default:
return state;
}
};
export default counter;
// 초기 상태값(state)
const initialState = {
number: 0,
};
// 리덕스 없이 일반적으로 상태를 만들때 사용하던 방식 및 초기값
const [counter, setCounter] = useState(0);
counter라는 *리듀서를 위와 같은 형식으로 구성하며 리덕스에서 리듀서 함수를 만들 때 인자로 state와 action을 가집니다.
위에서 생성된 counter 리듀서를 중앙 관리소 개념인 store에 저장
import counter from "../modules/counter";
const rootReducer = combineReducers({
counter,
});
const store = createStore(rootReducer);
리액트 앱 전체에서 counter라는 리듀서를 사용할 수 있도록 작성합니다.
store에 저장된 리듀서 사용
import { useSelector } from "react-redux";
function App() {
// store에 접근해서 counter에 접근하고 싶다
// useSelector
const counter = useSelector(({ counter }) => {
return counter;
});
console.log("counter: ", counter.number);
return (
<>
<div>현재 카운트 : {counter.number}</div>
</>
);
}
export default App;
useSelector 훅을 사용해서 store에 저장된 리듀서들을 불러올 수 있습니다.
리덕스 공식문서에 나와있는 이미지로 위에서 만들어 놓은 store(중앙 데이터 관리소)에는 Reducer와 State가 들어가 있는 모습입니다.
여기서 State는 로컬이 아닌 전역적인 상태이고 리듀서는 해당 전역 상태를 제어하는 역할을 수행합니다.
UI에서 어떠한 이벤트를 통해서 상태가 변경되어야 한다는 요청이 들어오면 *Dispatch 가 해당 요청을 수행합니다.
*Dispatch는 이때 action 객체를 가지고 스토어에 접근합니다.
이제 만들어 둔 카운터에 버튼을 클릭하면 + 1 이될 수 있도록 작성해봅니다.
먼저 버튼 클릭이라는 이벤트가 발생했을 때 스토어에 저장되어 있는 리듀서가 어떠한 동작을 하는지 작성해줍니다.
// action value
const PLUS_ONE = "counter/PLUS_ONE";
// action creator : action value를 reture하는 함수
export const plusOne = () => {
return {
type: PLUS_ONE,
};
};
const counter = (state = initialState, action) => {
switch (action.type) {
case plusOne:
return {
number: state.number + 1,
};
default:
return state;
}
};
코드는 간단합니다. action의 type으로 어떤 값이 오는지 어떤 동작을 수행할지 작성합니다.
import { useDispatch, useSelector } from "react-redux";
import { plusOne } from "./redux/modules/counter";
function App() {
...
const dispatch = useDispatch();
return (
<>
<div>현재 카운트 : {counter.number}</div>
<button
onClick={() => {
dispatch(plusOne());
}}
>
+
</button>
</>
);
}
이제 상태 변경을 요청하는 UI에서 어떤 요청인지 리덕스 훅인 useDispatch를 통해서 스토어에 전달해줍니다.
위에 코드 UI에서 이벤트를 감지해서 전달하는 디스패치와 어떠한 액션인지 확인하는 counter.js 리듀서의 이름을 "counter/PLUS_ONE" 와 같이 직접 하드코딩 하는것이 아니라 다른 방법을 사용하고 있는데 이를 Action Creator | Action Value라고 합니다.
리덕스 공식문서에는 직접적으로 작성하는 하드코딩을 지양하고 Action Creator | Action Value 방식을 권장한다고 나와 있는데 이는 많은 이유가 있겠지만 변수 형태로 관리를 해서 재사용성 및 휴먼에러를 최소화 하자는 의미가 가장 큽니다.
Ducks 패턴 👈 키워드로 검색
// action value
const PLUS_ONE = "counter/PLUS_ONE";
const MINUS_ONE = "counter/MINUS_ONE";
이때 상수로 작성하되, 네이밍 기법은 모듈명/액션타입을 권장하고 있습니다.
또한 액션 벨류 뿐만 아니라 dispatch를 사용하는 코드에서도 권장하는 방법이 있습니다.
<button
onClick={() => {
dispatch({
type: "counter/PLUS_ONE"
});
}}
>
+
</button>
위 코드에서는 어떠한 액션 타입인지 명시해주기만 할 뿐 다른 로직은 없지만 스코프가 커지면 해당 부분도 많은 로직이 들어갈 수 있습니다.
// action creator : action value를 reture하는 함수
export const plusOne = () => {
return {
type: PLUS_ONE,
};
};
export const minusOne = () => {
return {
type: MINUS_ONE,
};
};
따라서 위와 같이 액션 밸류 객체를 반환해주는 함수로 관리해주면 휴먼 에러를 최소화 할 수 있습니다.
때문에 이러한 액션 객체를 반환해주는 코드를 export 해줘서 사용하고자 하는 컴포넌트에서 불러와 로직을 작성할 필요없이 사용하기만 하면 됩니다.
<button
onClick={() => {
dispatch(plusOne());
}}
>
+
</button>
위 간단한 카운터 예에서 인풋값을 받아서 받은 만큼 + 시켜줄 수 있는 방법인 리덕스의 payload에 대해서 실습합니다.
우선 평소에 리액트를 사용하던 방법으로 useState를 이용해서 사용자의 입력값을 받아올 수 있도록 훅을 작성합니다.
const [number, setNumber] = useState(0);
<input
type="number"
value={number}
onChange={(e) => {
const { value } = e.target;
setNumber(+value);
}}
/>
...
const PLUS_N = "counter/PLUS_N";
...
export const plusN = (payload) => {
return {
type: PLUS_N,
payload,
};
};
const counter = (state = initialState, action) => {
switch (action.type) {
case plusOne:
return {
number: state.number + 1,
};
case plusN:
return {
number: state.number + action.payload,
};
default:
return state;
}
};
사용자의 인풋값을 받아오면 어떤 액션 벨류로 어떤 동작을 수행할지 작성하기 위해서 액션 밸류와 액션 크리에이터를 추가해줬습니다.
이때 액션 크레이어터에 type 뿐만 아니라 실제 인풋값을 받아오기 위해 payload를 추가해줍니다.
<button
onClick={() => {
dispatch(plusN(number));
}}
>
+
</button>
이제 사용자의 입력값을 payload로 넘겨줄 수 있습니다.