내가 자꾸 까먹어서 작성해보는
redux-toolkit
기초 가이드라인 😅
(본 포스팅은 【한글자막】 React 완벽 가이드 with Redux, Next.js, TypeScript를 수강한 내용을 바탕으로 작성했습니다.)
redux-toolkit
의 핵심 개념은 기존의 Redux
와 큰 차이가 없으나, 사용법이 조금 다르기 때문에 처음에 배울 때 종종 어려움을 겪었습니다. 그 결과 주석을 달지 않고서는 돌아서면 까먹는 지경에 이르렀기에(...) 저는 코드에 하나씩 주석을 달아가며 이해해보려고 합니다. 비록 완벽하지는 않지만 redux-toolkit
을 활용할 때 가끔씩 찾아와 리마인드하고 익힐 수 있는 기초 가이드라인이 되었으면 좋겠습니다.😊
ex) 버튼을 눌러 장바구니 컴포넌트가 토글(보여지거나 사라지거나)되도록 하고 싶다!
ex) 버튼을 클릭할 때마다 true 또는 false로 업데이트되는 state가 필요하다.
-> 장바구니 컴포넌트가 보여지는지 여부를 잘 나타나도록 isCartVisible이라는 이름은 어떨까?
ex) 그리고 액션은 단순히 click이라는 이름보다는 toggle을 사용하는 게 더 적합할 것 같다.
// 📂 src/store/ui-slice.js
// 슬라이스를 생성하는 함수를 불러옵니다.
import { createSlice } from "@reduxjs/toolkit";
// 생성되는 슬라이스 객체를 uiSlice라는 이름으로 저장합니다.
const uiSlice = createSlice({
name: 'ui',
// state의 초기값을 지정해줍니다.
// 처음에는 카트 컴포넌트를 보여주고 싶지 않기 때문에 초기값을 false로 설정해주었습니다.
initialState: { isCartVisible: false },
// dispatch로 받아오는 액션에 따라 실행하는 리듀서 메서드입니다.
reducers: {
// toggle이라는 액션을 받아오면 아래와 같이 state를 변경하겠군요!
toggle(state) {
state.isCartVisible = !state.isCartVisible;
}
}
});
// 아까 createSlice로 슬라이스 객체를 생성해 uiSlice라는 변수에 저장한다고 했죠?
// 이 슬라이스 객체를 콘솔에 확인해보면 actions라는 프로퍼티가 있습니다.
// 여기에 바로 액션 생성자 함수가 담겨있습니다. 이걸 외부에서 쓸 수 있도록 export해줍니다.
export const cartActions = uiSlice.actions;
/* export const { toggle } = uiSlice.actions; */
// 👆 또는 이렇게 구조분해할당으로 action 이름을 바로 가져다쓸 수 있게 할 수도 있습니다.
// 나중에 스토어의 index.js에서 가져다 쓰기 때문에 슬라이스도 export 해줍니다.
export default cartSlice;
// 📂 src/store/index.js
// 스토어를 생성하는 함수
import { configureStore } from "@reduxjs/toolkit";
// export한 uiSlice를 불러옵니다.
import uiSlice from "./cart-slice.js";
// 스토어를 생성해서 store라는 변수에 저장합니다.
const store = configureStore({
// 여기 reducer 프로퍼티가 있습니다. 리덕스를 사용하면 하나의 애플리케이션에는
// 하나의 스토어만 존재해야 한다는 원칙이 있기 때문에, 전역 상태를 관리하는 하나의
// 리듀서를 정의합니다. 대신 하나의 객체에 여러 리듀서들을 담습니다.
reducer: { ui: uiSlice.reducer }
})
export default store;
// 📂 src/index.js
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import store from './store';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
// Provider 컴포넌트로 감싸서 스토어를 공급해줍니다.
root.render(<Provider store={store}><App /></Provider>);
앞서 버튼을 클릭했을 때 장바구니가 토글되는 기능을 구현한다고 가정했습니다.
따라서 state
의 변경을 일으키는 컴포넌트는 바로 버튼
입니다.
CartButton
이라는 컴포넌트에서 작업할 준비를 합니다.
// 📂 src/components.CartButton.js
const CartButton = () => {
return (
<button>
<span>장바구니 보기</span>
</button>
);
};
export default CartButton;
액션은 state
변경을 일으키는 액션과 정보를 나타내는 역할을 하고,
디스패치는 액션을 리듀서에 전달하는 역할을 합니다.
// 📂 src/components.CartButton.js
// 액션을 불러옵니다.
import { uiActions } from '../../store/ui-slice';
// 액션 객체를 전달할 dispatch를 생성하는 useDispatch 함수를 불러옵니다.
import { useDispatch } from 'react-redux';
const CartButton = () => {
// dispatch 함수를 생성해줍니다.
const dispath = useDispatch();
// '장바구니 보기' 버튼을 클릭하면(onClick) state가 변경되도록 하고 싶습니다.
// 따라서 클릭 이벤트를 처리하는 toggleCartHandler라는 이벤트 핸들러를 하나 정의합니다.
// 이벤트 핸들러를 만든 다음, 핸들러 안에서 액션 객체를 생성해 dispatch해줍니다!
const toggleCartHandler = ()=> {
// 액션 생성자 함수를 호출해 액션을 생성하고, 생성된 액션을 dispatch합니다.
dispatch(uiActions.toggle());
}
return (
<button onClick={toggleCartHandler}>
<span>장바구니 보기</span>
</button>
);
};
export default CartButton;
'장바구니 보기' 버튼을 클릭해 state
가 변경되면 일어나는 일에 대해 생각해봅니다.
여기서는 장바구니가 보여지고 사라지도록 하겠습니다.
따라서 장바구니 컴포넌트인 Cart
컴포넌트로 가서 업데이트된state
를 적용해줄 예정입니다.
import Cart from './components/Cart/Cart';
import Layout from './components/Layout/Layout';
function App() {
return (
<Layout>
{isCartVisible && <Cart />} // 논리 연산자 &&을 쓰는 이유가 궁금하다면 아래를 참고해주세요!
</Layout>
);
}
export default App;
🙋♀️
&&
가 여기서 왜 나오나요?
👉 논리 연산자를 활용해 표현식을 평가하는 단축 평가입니다. 논리 연산자라고 한다면||
와&&
,?
이 있죠? 그 중에서||
와&&
을 사용해 표현식을 전부 평가하지 않고 도중에 생략한다음, 연산 결과를 결정한 피연산자를 반환하는 것을 의미합니다.
🙋♀️ 조금 더 설명해주세요...
👉 예를 들어 OR 연산자인||
을 사용하면 하나만 true여도 식이 true로 평가되기 때문에, 첫 번째 피연산자 즉 앞에 있는 게 true면 뒤에 있는 건 평가하지 않습니다. 반면 AND 연산자인&&
을 사용하면 모든 피연산자가 true여야만 식이 true로 평가되기 때문에, 첫 번째 피연산자 즉 앞에 있는게 false면 뒤에 있는 건 평가하지 않고 식을 false로 평가합니다.&&
을 사용한 식이 true로 평가받기 위해서는 적어도 첫 번째 피연산자가 true여야 합니다.
🙋♀️ 여기서는 어떻게 쓰인건가요?
이를 위 코드에 적용해보면, 만약isCartVisible
이라는state
가 false면 하나라도 false이기 때문에 평가가 끝납니다. 따라서 논리 연산 결과를 결정한 앞에 것만 반환되고, 뒤에 있는Cart
컴포넌트가 반환되지 않습니다. (렌더링되지 않습니다.) 만약isCartVisible
이 true면 뒤에 있는 게 연산 결과를 결정합니다. 그리고Cart
컴포넌트가 존재하므로 평가식이 true로 평가될 것이며, 연산 결과를 결정한 뒤에 있는 피연산자, 즉Cart
컴포넌트가 반환됩니다. (렌더링됩니다.) 결론은 컴포넌트를 렌더링할 때 앞에 state를 조건으로 걸어주고 있는 것입니다. state가 true일 때만 컴포넌트가 보여지도록! 삼항연산자로 조건부 렌더링하는 것보다 훨씬 간편합니다.
useSelector
는 state
를 반환하는 함수입니다.
특정 state
만 관심있는 경우, 어떻게 하는 지 코드를 보며 설명하겠습니다.
// 📂 src/App.js
import Cart from './components/Cart/Cart';
import Layout from './components/Layout/Layout';
import { useSelector } from 'react-redux';
function App() {
// useSelector는 전체 state를 받습니다. 이 중에서 관심있는 state만 가져와 저장해야 합니다.
// 여기서는 isCartVisible이라는 state만을 가져오고 싶습니다. 어떻게 하면 될까요?
// 바로 스토어에서 우리가 관심있는 state가 저장되어있는 slice에 매칭되는 key를 적어주면 됩니다.
// 한 칸 더 아래 코드 블럭에서 좀 더 자세히 설명해보겠습니다.
const showCart = useSelector((state) => state.ui.isCartVisible)
return (
<Layout>
{isCartVisible && <Cart />}
</Layout>
);
}
export default App;
// 📂 src/store/index.js
import { configureStore } from "@reduxjs/toolkit";
import cartSlice from "./cart-slice,js";
// 여기 스토어가 있습니다.
const store = configureStore({
// reuducer 프로퍼티 안에 객체가 있는데, 우리가 관심있는 state가 저장되어 있는
// 슬라이스를 찾고, 그 슬라이스에 붙은 key명을 써주면 됩니다.
// 현재 우리는 isCartVisible이라는 state를 가져다 쓰고 싶고,
// 이는 uiSlice라는 슬라이스에 정의되어 있습니다.
// 그리고 그 슬라이스는 스토어에 ui라는 이름으로 지정되어있네요.
// 여기있는 key 즉 ui로 접근해 우리가 원하는 state를 변수에 담아서 쓰면 됩니다.
// 이제 바로 위에서 봤던 코드블럭으로 다시 올라가 이해해봅시다.
reducer: { ui: uiSlice.reducer }
})
export default store;
🤔 정말로 스토어의
reducer
객체 안에 있는key
와 슬라이스를 매칭하는건지 확인하고 싶으면,key
이름을 마음대로 바꾸어 봅시다. 둘 중 하나라도 이름이 다르면 제대로 실행되지 않는다는 것을 알 수 있었습니다.
정리해보자면, redux-toolkit
역시 기존의 Redux
와 마찬가지로 복잡하고 다양한 상태 변화를 예측 가능하도록 하나의 스토어에서 관리합니다. 사용법이 조금씩 다르지만, 저는 아래 3가지 항목이 redux-toolkit
, 나아가 상태를 관리할 때 꼭 고려할 만한 사항이라고 생각합니다.
이러한 명확한 체크리스트를 가지고 작업한다면, 안 그래도 복잡한 상태 관리를 조금 더 명확하게 할 수 있을 것 같습니다!