[개인과제] 포켓몬 도감 만들기3

안현희·2024년 11월 11일
0

React를 배워보자!

목록 보기
16/20

남은 선택사항을 모두 구현해보자.

  • 리덕스 툴킷으로 프로젝트 리팩토링
  • 디테일 페이지 구현

1. Redux Toolkit

🤔 왜 Redux Toolkit인가?

Context API로도 전역상태관리가 되는데
Redux Toolkit를 사용해야하는지 대한 의문이 들었다.

아래의 그림을 살펴보자.

  • 그림으로 어느정도 유추 할 수 있겠지만,
    Context API는 데이터가 변경되면 해당 Context를 사용하는 모든 자식 컴포넌트가 리렌더링된다.

  • 그에 반해 redux는 전역상태관리가 아예 따로 되기 때문에, 불필요한 리렌더링이 자연스레 방지된다.
    그렇기에 우리는 전역상태관리로 redux를 사용한다.

Tip. redux와 같은 전역상태관리는 규모가 큰 애플리케이션에서 효과적입니다.


그래서 Redux Toolkit이란 무엇인가?

Redux Toolkit은 현재 Redux로직을 작성할 때 권장되는 방법으로, Redux의 복잡성을 줄이고 개발 효율성을 높이기 위해 만들어졌습니다.


Redux Toolkit의 사용법

  1. Redux Slice 생성하기
  2. Store 설정하기
  3. App에 Store 적용하기
  4. 컴포넌트에서 Redux 사용하기

Redux Toolkit으로 리팩토링 시작

먼저, 패키지를 설치해준다.

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 : pokemonReducerpokemon이라는 key값으로 스토어에 연결해준다.
    이를 통해 pokemonSlice의 상태와 액션에 접근하고 관리 할 수 있다.


AppStore를 적용시킨다.

//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;
  • 간단하다.

이제 필요한 컴포넌트 내에서 useSelectoruseDispatch를 이용해서 로직을 완성시키면 된다.

//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. useSelectorRedux에서 상태를 가져올 때 사용하는 React Hook입니다.
따라서, Redux를 사용하지 않는다면 작동하지 않습니다.

const selectedPokemon = useSelector((state) => state.pokemon); : 이 로직에서 pokemonstore.jsx파일 안에 내에 정의되어 있는 reducerkey값이다.


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. useDispatchRedux에서 상태를 업데이트하기 위해 액션디스패치할 때 사용하는 이므로 Redux를 사용하지 않는 경우에는 useDispatch를 사용할 필요가 없다.

이렇게 리팩토링이 끝났다.


2. 디테일 페이지에 버튼만들기

디테일 페이지에서 포켓몬을 대시보드에 추가할 수 있는 '추가' 버튼을 구현하여 페이지 전환 간 상태를 유지할 수 있도록 합니다.

//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;
  • 상태와 함수가 둘 다 필요하기 때문에
    useSelectoruseDispatch 모두 불러온 뒤,
    위와 같이 로직을 완성해준다.

이로서 프로젝트의 필수 구현사항과 선택구현사항을 모두 완료했다.


회고

숙련주차 과제가 주어지기전까지
참 많이 긴장하고 초조에 했던 내 자신이 생각난다.
지나고 나니까 그럴 필요는 없었던것 같다.

생각보다 잘해냈고, 생각보다 쉬웠다.

그렇게까지 긴장할 필요가 없었던것 같고,
앞으로도 이렇게만 하면 될것같다.
물론 항상 최선을 다할것이라는것은 변함이 없다.

앞으로 더욱 어려운 과정이 기다리고 있을테지만
"내게 능력 주시는 자 안에서 내가 모든 것을 할 수 있느니라."

그럼이만

0개의 댓글