
++ 리액트를 더 잘 쓰기 위한 정리 시리즈
앞선 시리즈에서 보듯 Context API를 사용해 얻을 수 있는 큰 장점이 있다.
App 컴포넌트에서 자식의 자식의 자식의 자식 컴포넌트에게 대물림하는 Props Drilling을 방지해 전역적인 상태관리가 필요한 데이터를 한 저장소에서 관리하고 사용할 수 있다는 것이다.
그런데 Context API와 비슷한 역할을 하는 Redux는 왜 쓰는걸까?
만약 전역으로 상태관리할 데이터가 많다고 가정해보자. 하나의 Context로 관리를 하기엔 너무 양이 많고 복잡해진다.
Context를 여러개로 쪼개면? 아래처럼 첩첩산중으로 Provider가 쌓이게 된다.
<FirstProvider>
<SecondProvider>
<ThirdProvider>
<App />
</ThirdProvider>
</SecondProvider>
</FirstProvider>
Context API를 사용하는 것도 좋은 선택지 중 하나이지만 전역으로 관리할 상태변수가 많다면 Redux를 사용하는 것이 더 나은 선택지가 될 수 있다. 상황에 맞게 사용하면 된다.
리덕스 공식문서를 보면 리덕스를 아래와 같이 설명하고 있다.
리덕스는 JS 앱을 위한 예측 가능한 상태 컨테이너입니다.
https://ko.redux.js.org/introduction/getting-started/
즉, 리덕스는 리액트 뿐만아니라 JS으로 만든 앱이라면 클라이언트, 서버, 네이티브 다양한 환경에서 사용할 수 있다는 것이다.
리덕스는 3가지 요소를 바탕으로 동작한다.
Store, Reducer, Action
아래 사진을 참고해 전체적인 흐름을 파악하면 이해하는데 도움이 된다.

1) Store는 모든 전역 변수들을 관리하는 중앙 저장소 역할을 한다. Store를 구독하는 컴포넌트들은 전역 변수들을 불러와 사용할 수 있다. 단, 상태를 변경시킬수는 없다.
2) Reducer는 Store의 전역 변수들을 변경할 수 있는 유일한 역할을 담당하는 함수이다.
3) 컴포넌트는 Reducer에 정의된 Action 함수를 사용해 Reducer가 전역변수를 업데이트할 수 있도록 요청한다.
Redux에서는
redux-toolkit사용을 적극 권장하고 있다.
리덕스의 동작 원리를 잘 이해할 수 있도록 redux를 사용하는 방법을 정리했다. 또한 redux-toolkit을 왜 사용하는지 알 수 있기 때문에 아래 내용들은 참고용으로 봐주세요 :)
Redux의 동작원리를 이해했다면 그 흐름에 따라 설계하듯 코드를 짜면 도움이 된다. 예시로 카운팅한 횟수를 관리하는 리덕스로 만들어보자.
// 1. Store를 생성하도록 redux에서 불러오기
import { createStore } from "redux";
// 2. Counter Reducer 함수 정의하기
// state는 상태관리할 변수이고, action은 리듀서를 호출하는 역할
// state의 초기값을 꼭 설정해줘야 함
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;
};
// 3. Store의 상태를 변경시킬 Reducer를 포인팅
const store = createStore(CounterReducer);
// 4. Store를 다른 컴포넌트에서 사용할 수 있도록 해주기
export default store
// 5. App 컴포넌트 전체에 Store를 구독할 수 있도록 함 (필요한 컴포넌트들만 구독할 수 있게 부분적으로 해도 됨)
import { Provider } from "react-redux";
import store from "./Store";
<Provider store={store}>
<App />
</Provider>
// Counter.js 컴포넌트
import { useSelector, useDispatch } from "react-redux";
// 6. store에 counter 변수를 useSelector 훅을 사용해 불러오기
const counter = useSelector((state) => state.counter);
// 7. Action을 실행할 수 있게 해주는 useDispatch 훅 불러오기
const dispatch = useDispatch();
// 8. Action 타입에 따라 실행할 함수를 호출하기
// counter 1 증가
const handleIncrement = () => {
dispatch({ type: "increment" });
}
// counter 1 감소
const handleDecrement = () => {
dispatch({ type: "decrement" });
}
return {
<>
<h1>{ counter }</h1>
<button onClick={handleIncrement}>Increment Counter</button>
<button onClick={handleDecrement}>Decrement Counter</button>
</>
}
리덕스를 사용할 때 중요한 몇가지 사항들이 있다.
이를 바탕으로 redux-toolkit을 사용하는 것이 왜 좋은지 알 수 있다.
CounterReducer를 보면 항상 새로운 객체를 생성해 리턴해주고 있다.
아래 코드처럼 state가 참조하는 값을 사용해 직접 +1 을 해주도록 해서는 안된다.
물론 아래 코드처럼 해도 동작은 한다. 하지만 객체나 배열 등 다양한 데이터 형식을 사용할때, 저렇게 직접적으로 참조해 변경할 경우 state가 최신 업데이트된 값이라고 보장할 수 없다. 이는 잠재적인 버그의 원인이 된다.
const CounterReducer = (state = { counter: 0 }, action) => {
if (action.type === "increment") {
state.counter += 1; // 문제의 코드
return state
}
return state;
};
위에서는 counter 변수 하나만 있었지만, 복잡한 앱일수록 많은 변수들을 관리해야 한다면? 철자 안틀리고, 관리하기 정말 어려워 진다.
state 초기값으로 많은 변수들이 있으면,,, a만 업데이트하면 되는데 다른 변수들도 전부 관리해줘야 한다..
또한 action type 함수명들도 철자를 틀리면 안되기 때문에 상수 값으로 정의해 사용할 수 있지만 잠재적으로 틀릴 가능성이 높다.
const initialState = {
a: "~~",
b: "!!",
c: "%%",
d: "**",
... 이하 생략
}
if (action.type === "any function") {
return {
a: state.a + "any state",
b: state.b,
c: state.c,
... 이하생략
}
}
위에서 본 Redux를 사용했을 때 생길 수 있는 버그와 이슈들을 예방할 수 있는 방법을 Redux-Toolkit이 제공하고 있다!!
어떤 장점이 있는지, 어떻게 사용하는지 알아보기 위해 위에서 사용했던 것과 같은 예제로 비교해 보자.
// 1. createStore 대신 configureStore
import { configureStore } from "@reduxjs/toolkit";
// 2. 다양한 state를 관리하는 리듀서로 쪼개고,
// 리듀서를 객체로 관리해 액션을 사용할 수 있는 redux-toolkit의 slice를 사용
import { createSlice } from "@reduxjs/toolkit";
const initialCounterState = { counter: 0 };
// 3. slice를 사용할 때는 name, initailState, reducers 3가지 속성 값을 정의해주어야 한다. 3가지 형식 꼭 지키기!
const CounterSlice = createSlice({
name: "counter",
initialState: initialCounterState,
reducers: { // action type을 따로 정의해줄 필요 없다. 곧 action 함수 이름이 된다. reducers 복수임.
increment(state) {
state.counter++;
},
decrement(state) {
state.counter--;
}
},
});
// 4. 하나의 store에는 하나의 reducer만이 포인팅된다.
// 따라서 slice.reducer 단수임!
export const store = configureStore({
reducer: CounterSlice.reducer
});
configureStore는 여러 리듀서를 key-value 값으로 가져와 하나의 리듀서로 통합해 사용한다.
export const store = configureStore({
reducer: {
counter: CounterSlice.reducer,
otherCounter: OtherSlice.reducer,
... 이하생략
}
});
위의 reducers를 정의된 action 함수들을 보면 state를 직접적으로 변경하고 있다. 하지만 redux-toolkit은 immer라는 내장 패키지를 통해 state값이 항상 불변성을 유지하도록 해준다. 이게 toolkit의 가장 큰 장점이 아닐까!
reducers: { // action type을 따로 정의해줄 필요 없다. 곧 action 함수 이름이 된다. reducers 복수임.
increment(state) {
state.counter++;
},
decrement(state) {
state.counter--;
}
}
또한 action 함수를 호출할 때, slice의 action 함수들을 생성해 사용할 수 있기 때문에 실수할 위험도 줄어든다.
redux를 사용할때만 좀 알고, 나중에 쓰려고 하면 까먹어서 다시 코드를 찾아보고 이런식으로 사용하다보니 두루뭉실하게만 알고 있었다. 하지만 이번 기회에 확실하게 흐름과 개념을 파악하고 싶었다.
또한 이번에 진행할 사이드 프로젝트에 어떤 상태관리 라이브러리를 쓸 것인지 고민중이기도 했다. 가장 많이 사용하는 redux & redux-toolkit을 정확하게 이해하면 어쩌면 그로부터 파생된, 비슷한 상태관리 라이브러리들을 직접 깊게 써보지 않아도 선택에 도움이 될 것이라고 생각했다.
글을 쓰는데 생각보다 오래걸렸지만 redux를 사용하는 이유, redux-toolkit을 적극적으로 권장하는 이유에 대해 이해할 수 있었던 시간이었다.