피카츄 카드게임 리팩토링 후기

한호수 (The Lake)·2023년 4월 20일
12
post-thumbnail

이력서를 넣으면서 프로젝트를 진행하고 싶어 찾아보던중 완전 초기에 팀원들과 페어프로그래밍만으로 만들었던 프로젝트를 열어본 결과 충격에 빠지게 되었고 리팩토링하기를 결정하게 되었다.

문제점


  1. 처음 개발할때는 재미로 만들생각만 했지 SPA로 만들생각은 하지 않았었다. 하지만 리팩토링하려고 하니 페이지별로 pushState를 사용해서 관리하는게 효율적이라고 생각되어 SPA로 리팩토링하게 되었다.
  2. 기존 코드가 기능별로 분리되지 않아 가독성이 떨어지는 문제가 생겼다.
  3. 코드의 흐름을 생각하려면 한참 걸리기 때문에 유지보수성이 떨어졌다.

혼자서 대부분의 기능을 가지고있던 모듈

어려웠던 점


  • 바닐라 JS로 SPA를 만들어본적이 없어서 pushState를 통해서 라우팅하는 방법을 공부해야했다.
  • 렌더링 시기를 결정 짓는 것과 렌더링 이 후 한번만 동작하는 함수를 따로 분리하는것이 어려웠다.

동일 레벨 컴포넌트로 데이터를 전달하는 과정

같은 부모를 둔 CardGame 컴포넌트에서 Result 페이지로 점수 정보를 넘겨줘야하는 경우가 생겼다. 여러가지 해결책을 생각했었는데 점수와 관련된 부분이라 자바스크립트 객체로 관리하는것이 좋을 것 같다는 생각이 들었다. 점수를 담당하는 객체를 만들고 인스턴스를 내려줌으로써 같은 데이터를 공유하고 메소드를 통해 조작할 수 있도록 하였다.

export default function App({ $target }) {
  const scoreManager = new ScoreManager();

  this.route = () => {
    // Home 페이지 렌더링
    if (pathname === "/") {
      new Home({ $target: $page, props: { scoreManager } }).setup(); // props로 전달
      
    // CardGame 페이지 렌더링
    } else if (pathname.indexOf("/cardGame/") === 0) {
      const [, , level] = pathname.split("/");

      new CardGame({
        $target: $page,
        props: { level, scoreManager },
      }).setup();
      
    // Result 페이지 렌더링
    } else if (pathname === "/result") {
      new Result({ $target: $page, props: { scoreManager } }).setup(); // 동일 객체를 props로 전달
    }
  };

개선 사항


카드 관련 동작을 CardManager 생성자 함수를 통해 분리

기존 파일

리팩토링 후

// 카드 관련 관심사를 처리하는 CardManager 생성자 함수
export default function CardManager() {
  this.setCardList = async (initCount) => {
	...중략
  };

  this.cardFlip = ($target) => {
	...중략
  };

  this.initCard = ($target) => {
	...중략
  };

  this.getCardPosition = ($target) => {
	...중략
  };

  ...중략
}

타이머 관련 기능 TimerManager 생성자 함수로 분리

export default function TimerManager(scoreManager) {
  this.scoreManager = scoreManager;
  this.timerId;
  this.clearTime;

  this.setTimer = (setState, limitTime) => {
    this.timerId = setInterval(() => {
      if (!scoreManager.getScoreData().winOrLose) {
        this.clearTime = --limitTime;
        setState({ value: limitTime });

        if (this.isClear()) this.endGame("defeat");
      } else {
        scoreManager.setClearTime(this.clearTime);
        this.endGame("win");
      }
    }, 1000);
  };

  ...중략
}

트러블 슈팅


이벤트 리스너 중복 문제

게임을 재시작 할때 ProgressBar, CardList 객체가 새로 생성되고 타이머가 계속 반복되는 현상을 발견했다.

원인은 재시도 버튼을 누르면 페이지를 생성하는 함수를 다시 실행하게 되고 setup 함수가 반복되면서 이벤트 리스너를 중복해서 설치하게 되어 메모리 누수와 함께 타이머도 재시도 숫자만큼 생성하게 된것이었다.

root 요소에 모든 이벤트 리스너를 추가해서 이벤트 위임으로 동작하게 했는데 처음에는 언마운트 될때 이벤트 리스너를 지우고 싶었지만 다수의 이벤트 리스너를 제거하는 방법은 생각보다 까다로웠다.

여러 방법을 생각하던 중 DOM 요소를 지우면 참조되지않는 이벤트 리스너는 가비지 컬렉션 당할것으로 생각했고 root 아래 <div class="page"></div> 요소를 생성해서 이벤트 리스너를 부착하고 페이지 이동 시 교체하는 방식으로 변경하여 해결되었다.


  this.route = () => {
    const { pathname } = location;

    $target.innerHTML = "";
	
    // 주소가 변경될때마다 페이지 요소를 만들어 $target(root)에 갈아 끼우게된다.
    const $page = document.createElement("div");
    $page.className = "page";
    $target.appendChild($page);

    if (pathname === "/") {
      new Home({ $target: $page, props: { scoreManager } }).setup();
    } 
    
   	...중략 
  }

배포 후 새로고침 문제

이전 React 프로젝트 같은 경우 netlify 통해서 배포하고 다른경로에서 새로고침이 생기면 _redirects 파일을 추가해 모든 경로에서 index.html을 다시 주는 방법을 사용했지만 해당 프로젝트에서는 새로고침이 일어나고 index.html에서 리소스 요청 시 모든 파일에 html파일이 응답으로 오는것을 확인했다.

그로인해서 index.js는 "text/html" 응답 받은것으로 Error를 뿜었다.

간단하게 생각하고 만든 프로젝트라서 정적페이지로 index.html 만 배포하려고 했었는데 배포 후에 발생하는 문제들을 생각하니 express.js로 간단한 서버를 만들어서 문제를 해결하기로 마음먹었다.

const express = require("express");
const path = require("path");
const app = express();
const port = process.env.PORT || 3000;

app.use("/static", express.static(path.resolve(__dirname, "static")));

app.get("/*", (req, res) => {
  res.sendFile(path.resolve(__dirname, "index.html"));
});

app.listen(port, () => {
  console.log(`Server running ....`);
});

모든 경로에 index.html을 반환하되 리소스 파일들이 있는 /static 경로로 요청이 오면 파일들을 반환하게 함으로 해결하였다.

간단하게 쉬어가는 생각으로 리팩토링을 진행했지만 문제도 많았고 배운것도 많은 리팩토링이 되었다.

2023-08-16
netlify 를 사용해서 배포할때 redirect 경로의 우선순위를 정할 수 있었습니다. express.js 를 사용하는것보다는 배포한 서비스의 기능을 사용하는것이 편리할 것 같습니다.

profile
항상 근거를 찾는 사람이 되자

3개의 댓글

comment-user-thumbnail
2023년 4월 26일

이 게임 재밌더라구요?🤭
호수님 글 잘 봤습니다! 술술 읽히도록 잘 쓰시네요😁!

1개의 답글
comment-user-thumbnail
2023년 4월 30일

Pikachu and smash karts are my favorite games right now.

답글 달기