Context API
로도 전역상태관리가 되는데
왜 Redux Toolkit
를 사용해야하는지 대한 의문이 들었다.
아래의 그림을 살펴보자.
그림으로 어느정도 유추 할 수 있겠지만,
Context API
는 데이터가 변경되면 해당 Context
를 사용하는 모든 자식 컴포넌트가 리렌더링된다.
그에 반해 redux
는 전역상태관리가 아예 따로 되기 때문에, 불필요한 리렌더링이 자연스레 방지된다.
그렇기에 우리는 전역상태관리로 redux
를 사용한다.
Tip. redux
와 같은 전역상태관리는 규모가 큰 애플리케이션에서 효과적입니다.
Redux Toolkit
은 현재Redux
로직을 작성할 때 권장되는 방법으로,Redux
의 복잡성을 줄이고 개발 효율성을 높이기 위해 만들어졌습니다.
npm install @reduxjs/toolkit react-redux
@reduxjs/toolkit
패키지로 제공되며, 리액트 프로젝트에서 리덕스를 사용하려면 react-redux
도 함께 설치해야 한다.Redux Slice
를 생성한다.//pokemonSlice.jsx import { createSlice } from "@reduxjs/toolkit"; import { toast } from "react-toastify"; const pokemonSlice = createSlice({ name: "pokemon", initialState: [], reducers: { handelAddPokemon: (state, action) => { const existingPokemon = state.find((p) => p.id === action.payload.id); if (existingPokemon) { toast.info("이미 추가된 포켓몬입니다."); return; } if (state.length >= 6) { toast.info("최대 6개까지만 추가할 수 있습니다."); return; } state.push(action.payload); }, handleRemovePokemon: (state, action) => { return state.filter((p) => p.id !== action.payload); }, }, }); export const { handelAddPokemon, handleRemovePokemon } = pokemonSlice.actions; export default pokemonSlice.reducer;
먼저 createSlice
로 슬라이스를 생성한다.
name
: pokemon
으로 지정되어, 액션 타입이 pokemon/handelAddPokemon
, pokemon/handleRemovePokemon
같은 형태로 자동 생성된다.
슬라이스를 식별하는 용도로 사용된다.
initialState
: selectedPokemon
을 빈 배열로 초기화하여, 선택된 포켓몬 목록을 관리한다.
reducers
: 상태를 업데이트 하는 함수를 정의한다.
store
설정을 해준다.//PokemonStore.jsx import { configureStore } from "@reduxjs/toolkit"; import pokemonReducer from "./PokemonSlice"; export const PokemonStore = configureStore({ reducer: { pokemon: pokemonReducer, }, }); export default PokemonStore;
configureStore
: Redux
스토어를 설정하고 생성하는 함수.
reducer
: pokemonReducer
를 pokemon
이라는 key
값으로 스토어에 연결해준다.
이를 통해 pokemonSlice
의 상태와 액션에 접근하고 관리 할 수 있다.
App
에 Store
를 적용시킨다.//App.jsx import { Provider } from "react-redux"; import AppRouter from "./Router"; import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; import PokemonStore from "./redux/PokemonStore"; function App() { return ( <Provider store={PokemonStore}> <AppRouter /> <ToastContainer /> </Provider> ); } export default App;
이제 필요한 컴포넌트 내에서 useSelector
와 useDispatch
를 이용해서 로직을 완성시키면 된다.
//Dashboard.jsx import { useSelector } from "react-redux"; import { DashboardImg, DashboardItem, DashboardList, DashboardMain, DashboardTitle, } from "../styles/DashboardStyles"; import PokemonCard from "./PokemonCard"; const Dashboard = () => { const selectedPokemon = useSelector((state) => state.pokemon); return ( <DashboardMain> <DashboardTitle>나만의 포켓몬</DashboardTitle> <DashboardList> {selectedPokemon.map((pokemon) => ( <PokemonCard key={pokemon.id} id={pokemon.id} korean_name={pokemon.korean_name} img_url={pokemon.img_url} onDashboard={true} /> ))} {Array.from({ length: 6 - selectedPokemon.length }).map((_, index) => ( <DashboardItem key={`empty-${index}`}> <DashboardImg src={"/pokeball-13iwdk7Y.png"} /> </DashboardItem> ))} </DashboardList> </DashboardMain> ); }; export default Dashboard;
useSelector
를 이용하여 state
에 접근한 뒤, pokemon
을 가져온다.Tip. useSelector
는 Redux
에서 상태를 가져올 때 사용하는 React Hook
입니다.
따라서, Redux
를 사용하지 않는다면 작동하지 않습니다.
※ const selectedPokemon = useSelector((state) => state.pokemon);
: 이 로직에서 pokemon
은 store.jsx
파일 안에 내에 정의되어 있는 reducer
의 key
값이다.
import { useNavigate } from "react-router-dom"; import { useDispatch } from "react-redux"; import { handelAddPokemon, handleRemovePokemon } from "../redux/PokemonSlice"; import { PokemonCardButton, PokemonCardImg, PokemonCardInfoWrap, PokemonCardItem, PokemonCardName, PokemonCardNumber, } from "../styles/PokemonCardStyles"; const PokemonCard = ({ id, korean_name, img_url, onDashboard }) => { const nav = useNavigate(); const dispatch = useDispatch(); const formattedId = id.toString().padStart(3, "0"); const handleButton = (e) => { e.stopPropagation(); if (onDashboard) { dispatch(handleRemovePokemon(id)); } else { dispatch(handelAddPokemon({ id, korean_name, img_url })); } }; return ( <PokemonCardItem onClick={() => { nav(`/pokemon-detail/${id}`); }} <PokemonCardImg src={img_url} /> <PokemonCardInfoWrap> <PokemonCardName>{korean_name}</PokemonCardName> <PokemonCardNumber>No. {formattedId}</PokemonCardNumber> </PokemonCardInfoWrap> <PokemonCardButton onClick={handleButton}> {onDashboard ? "삭제" : "추가"} </PokemonCardButton> </PokemonCardItem> ); }; export default PokemonCard;
useDispatch
를 이용하여, 함수실행 로직을 완성시켜준다.Tip. useDispatch
는 Redux
에서 상태를 업데이트하기 위해 액션을 디스패치할 때 사용하는 훅이므로 Redux
를 사용하지 않는 경우에는 useDispatch
를 사용할 필요가 없다.
디테일 페이지에서 포켓몬을 대시보드에 추가할 수 있는 '추가' 버튼을 구현하여 페이지 전환 간 상태를 유지할 수 있도록 합니다.
//PokemonDetail.jsx import { useParams, useNavigate } from "react-router-dom"; import { useSelector, useDispatch } from "react-redux"; import { handelAddPokemon, handleRemovePokemon } from "../redux/PokemonSlice"; import MOCK_DATA from "../data/MOCK_DATA"; import { DetailButton, DetailHandleButton, DetailImg, DetailInfo, DetailMain, DetailTitle, } from "../styles/PokemonDetailStyles"; const PokemonDetail = () => { const nav = useNavigate(); const { id } = useParams(); const dispatch = useDispatch(); const selectedPokemon = useSelector((state) => state.pokemon); const currentPokemon = MOCK_DATA.find((pokemon) => pokemon.id === Number(id)); const isAdded = selectedPokemon.some( (pokemon) => pokemon.id === currentPokemon.id ); const handleButtonClick = () => { if (isAdded) { dispatch(handleRemovePokemon(currentPokemon.id)); } else { dispatch(handelAddPokemon(currentPokemon)); } }; return ( <DetailMain> <DetailImg src={currentPokemon.img_url} /> <DetailTitle>{currentPokemon.korean_name}</DetailTitle> <DetailInfo>타입 : {currentPokemon.types.join(", ")}</DetailInfo> <DetailInfo>{currentPokemon.description}</DetailInfo> <DetailHandleButton onClick={handleButtonClick}> {isAdded ? "삭제" : "추가"} </DetailHandleButton> <DetailButton onClick={() => { nav(-1); }} > 뒤로 가기 </DetailButton> </DetailMain> ); }; export default PokemonDetail;
useSelector
와 useDispatch
모두 불러온 뒤,숙련주차 과제가 주어지기전까지
참 많이 긴장하고 초조에 했던 내 자신이 생각난다.
지나고 나니까 그럴 필요는 없었던것 같다.
생각보다 잘해냈고, 생각보다 쉬웠다.
그렇게까지 긴장할 필요가 없었던것 같고,
앞으로도 이렇게만 하면 될것같다.
물론 항상 최선을 다할것이라는것은 변함이 없다.
앞으로 더욱 어려운 과정이 기다리고 있을테지만
"내게 능력 주시는 자 안에서 내가 모든 것을 할 수 있느니라."