본문 바로가기
react

[React 상태 관리] useReducer 사용해 리팩토링 하기 (+ custom hook)

by HomieKim 2022. 3. 30.

상태 관리 라이브러리 없이 다른 색깔 찾기 게임을 구현 한적 있습니다!

numble에서 간단한 피드백을 주셨고 리액트 상태 관리에 대해 공부하던 중 당시 읽고 넘어 가기만 했던 프로젝트를 개선해 보았습니다.

기존 코드는 다음 글에서 확인할 수 있습니다. 

https://hoime.tistory.com/38?category=534509 

 

[Numble] 다른 색깔 찾기 게임

넘블에서 진행한 다른 색깔 찾기 게임 챌린지 참여! https://www.numble.it/45cee9d3-49ad-4f67-9d2a-14607c2eeba7 [React] 상태관리 라이브러리를 사용하지 않고 다른 색깔 찾기 게임 제작 시간은 금! 챌린지를..

hoime.tistory.com


✔ 개선 사항

1. 불필요한 상태가 정의 되어있습니다.

  - 스테이지가 올라 갈 때 마다 사각형 갯수를 다르게 해야하는데, stage에 따라 itemCnt를 계산하여 반복문 통해 순차적인 배열을 생성해 렌더링 했었습니다.

  - stage값이 이미 state로 정의 되었고 stage따라 계산만 하면 되는 단순 배열이기 때문에 굳이 배열로 따로 상태 관리 하지 않아도 되게 바꾸었습니다.

// stage 통해 itemCnt 계산 후
[...Array(itemCnt)].map((_, index)=> (...))
// 이런 패턴으로 개선

 

2. 상위 컴포넌트에서 모든 상태와 handler 함수를 정의 하고 하위 컴포넌트로 전달 하는 패턴

  - 당장의 가독성이 좋지 않고, 추후 변경 사항이 생겼을 때 손대기 어려운 컴포넌트가 될 수 있습니다.

  - customHook 과 useReducer()를 사용해 개선 하였습니다.

  - random 값을 생성하는 단순 함수는 utils라는 폴더를 만들어 분리 하였습니다.

✔ 폴더 구조

 

✔ useReducer, useGame

  - 개선 사항의 핵심은 두 컴포넌트라고 생각합니다.

  - useReducer의 경우 리덕스를 공부했다면 쉽게 이해할 수 있습니다. 

  - useReducer 함수를 통해 state와 dispatch 함수를 받아 올 수 있습니다.

// reducer.js
import getRandomState from "../utils/random";

export const initialState = {
  stage: 1,
  score: 0,
  remainSecond: 0,
  isPlay: true,
  baseColor: "",
  answerColor: "",
  answerIndex: 0,
};

export function reducer(state, action) {
  switch (action.type) {
    case "INIT_GAME":
      return {
        ...state,
        ...getRandomState(1),
        stage: state.stage,
        score: state.remainSecond,
        remainSecond: 15,
        isPlay: true,
      };
    case "SELECT_CORRECT":
      return {
        ...state,
        ...getRandomState(state.stage + 1),
        remainSecond: 15,
        stage: state.stage + 1,
        score: state.score + Math.pow(state.stage, 3) * state.remainSecond,
      };
    case "SELECT_WORONG":
      return {
        ...state,
        remainSecond: Math.max(state.remainSecond - 3, 0),
      };
    case "COUNT_TIMER":
      if (!state.isPlay) {
        return state;
      }
      if(state.remainSecond <= 0) {
        return {
          ...state,
          isPlay : false,
        };
      }else{
        return {
          ...state,
          remainSecond: state.remainSecond -1,
        };
      }
    default:
      throw state;
  }
}

 

- useReducer 함수의 인자로 initialState를 넘겨 줘야 되는데 꼭 여기서 안해도 되긴 합니다. export로 정의 하여 useGame에서 import하여 사용하였습니다.

// useGame.js
const useGame = () => {
	const [gameState, dispatch] = useReducer(reducer, initialState);
    // 중략...
}

export default useGame;

 

✔ 느낀점

  • 상태 관리에 대해 공부 할수록 리액트의 핵심적인 컨셉이 이게 아닐까 라는 생각이 듭니다.
  • 단순 구현이 아니라 효율적인 상태관리가 필요하다는 것을 절실히 느낍니다.
  • 리덕스를 공부하며 재사용되거나 자주 쓰이는 상태들을 전역적으로 store에서 관리를 하면 좋겠구나 생각했는데 custom Hook을 에서 첫 로딩시 상태 (effect)와 이벤트 핸들러를 정의 해서 리턴해주는 식으로 하는 것은 혼자 했으면 생각도 못했을 것 같다. 
  • 특히 custom Hook을 앞으로 최대한 활용해봐야 겠다고 느낌, 처음에 내가 했던 방식은 App.js에 useState랑 온갖 함수를 잔뜩 써놨는데 random 함수를 분리하고 custom hook까지 쓰니까 App.js 비교도 안되게 간결 해짐...
// App.js
import React from "react";
import Board from "./components/board";
import Header from "./components/header";
import useGame from "./hooks/useGame";

const App = () => {
  const {gameState, action} = useGame();
  console.log("App state : ", gameState);
  return (
    <>
      <Header {...gameState}  />
      <Board {...gameState} onSelect={action.clickHandler} />
    </>
  );
};

export default App;

✔ 전체 코드

https://github.com/HomieKim/React_PlayGround/tree/main/diff-color-refactoring

✔ 참조

https://sumini.dev/retrospective/002-numble-thinking-in-react/

댓글