Home playField,finsishBanner 리팩토링
Post
Cancel

playField,finsishBanner 리팩토링

1. 함수하나당 하나의 기능만 수행

지금 기존의 함수를 보면은 displayAll에서 carrotcount가 들어가 있고 startGame 함수에서도 게임을 시작하는것 뿐만 아니라 일시중지, 다시시작하는 기능까지 들어있다.

또한 data.json을 이용해서 다량의 아이콘을 만들었는데 그럴필요없이 for문을 사용하면 된다. 참!! 본격적으로 리팩토링을 시작하기 전에 기존의 함수를 다듬어야 한다. 바로 1함수1기능으로 다 만들고, 비효율적인 코드는 수정한다음 리팩토링을 진행해야한다는 뜻이다.

2. 리팩토링

2-1. popup

먼저 popup부터 리팩토링해보도록 하겠다. 우선 main.js에서 popup에 관련된 함수만을 다 빼와서 finishBanner.js에 모아두자. 그리고 class안에 보관!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
export default class Banner {
  constructor() {
    this.finishBanner = document.querySelector(".popup");
    this.gameResult = this.finishBanner.querySelector("span");
    this.replayBtn = document.querySelector(".replay");
    this.nextStageBtn = document.querySelector(".next_stage");

    this.replayBtn.addEventListener("click", () => {
      this.OnReplayClick && this.OnReplayClick();
      this.hide();
    });
  }

  setOnReplayClick(func) {
    this.OnReplayClick = func;
  }

  hide() {
    this.finishBanner.classList.add("non_display");
  }

  show(result) {
    this.finishBanner.classList.remove("non_display");
    let message;
    switch (result) {
      case "win":
        message = "you won!  replay?🎉";
        break;
      case "bug":
        message = "You clicked the bug😝 wanna play again?";
        break;
      case "timeout":
        message = `Time out..😅 Wanna play again?`;
        break;
      case "restart":
        message = `Restart?`;
        break;
      case "finally":
        message = "Completed all stages🎉 going back to stage 1?";
        break;
      default:
        throw new Error("unvalid mesage");
    }
    this.gameResult.innerHTML = message;
  }
}

우선 constructor함수는 class Banner가 만들어지면 자동으로 실행된다. 그리고 그 외에 함수는 invoke할때만 실행된다.

리플레이버튼을 클릭했을때 게임이 실행되려면 startGame 함수가 필요한데 finishBanner안에는 popup관련 함수만 있어야 하므로 startGame은 game module에서 넘겨받기로 했다. 그래서 setOnReplayClick을 이용해서 startGame함수를 넘겨받고 OnReplayClick에 저장한다음 있으면(&&) 그걸 실행하도록 했다.

또한 네이밍도 showBanner가 아닌 show여야 한다. 왜냐하면, 예를 들어 showBanner라고 이름을 지었다고 해보자. 나중에 클래스 안에 있는 showbanner메소드를 호출할때 banner.showBanner 라고하면 중복이 발생하기 때문. banner.show라고 이름짓는게 훨씬 깔끔.

2-2. play_field

playField.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import * as sound from "./sound.js";

const CARROT_SIZE = 60;

export default class Field {
  constructor() {
    // this.IconCount = IconCount;
    this.playField = document.querySelector(".play_field");
    this.carrotCount = this.playField.getElementsByClassName("carrot");
  }

  setIconClickEvent(func) {
    this.onClick = func;
    // this.onClick && this.onClick.bind(this);
  }

  onIconButton = (event) => {
    const whichIcon = event.target;
    if (whichIcon.matches(".carrot")) {
      whichIcon.remove();
      this.onClick && this.onClick("carrot");
      sound.playCarrotSound();
    }
    if (whichIcon.matches(".bug")) {
      this.onClick && this.onClick("bug");
      sound.playBugSound();
    }
  };

  _creatIcon(iconName, iconPath) {
    // private 한 함수는 underscore를 붙여줌. typescript에선 private이라고 지정하는 기능이 있는데 바닐라 JS는 아직 없다ㅠ
    iconName.src = `./img/${iconPath}.png`;
    iconName.classList.add(`${iconPath}`);
    const playCoor = this.playField.getBoundingClientRect();
    const x = 0;
    const y = 0;
    const x1 = playCoor.width;
    const y1 = playCoor.height;
    // CARROT_SIZE 를 빼는 이유는?
    // icon이 출력되는 범위를 화면에 꽉 채우게 될때, 만약 좌표가 화면 끝에 위치하게 된다고 가정해보면, 당근 왼쪽 끝부분이 그 좌표에 맞춰지게 된다.
    // 따라서 화면 밖으로 벗어나게 됨. 그래서 화면 안에서 출력되게 하려면 애초에 화면의 가로, 세로 길이가 당근의 가로 세로만큼 줄어들어야 당근이 삐져나오지 않게 됨.(논리적이다)
    var translateX = getRandomCoor(x, x1 - CARROT_SIZE);
    var translateY = getRandomCoor(y, y1 - CARROT_SIZE);

    iconName.style.transform = `translate(${translateX}px,${translateY}px)`;
    return iconName;
  }

  createCarrotIcon() {
    const imgCarrot = new Image();
    const carrotIcon = this._creatIcon(imgCarrot, "carrot");
    this.playField.append(carrotIcon);
  }

  createBugIcon() {
    const imgBug = new Image();
    const bugIcon = this._creatIcon(imgBug, "bug");
    this.playField.append(bugIcon);
  }

  displayAllIcon(count) {
    // 콜백으로 넘겨줄때 this 바인딩함! callback function binded with this.
    // 이거때문에 좀 헤맴 ㅋ 버그있을때는 똑같은 stage에 머무르지말고 알고리즘내에서 버그가 발생했다고 생각하는 지점에서
    // 차근차근 한단계씩 거슬러올라가보길
    this.playField.addEventListener("click", this.onIconButton);
    this.playField.innerHTML = "";
    var i = 0;
    for (i = 0; i < count; i++) {
      this.createCarrotIcon();
      this.createBugIcon();
    }
  }
}

function getRandomCoor(min, max) {
  return Math.random() * (max - min) + min;
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random 참고
}

보면 그전에 만든 sound를 import해서 잘 사용하고 있는걸 볼 수 있다.

또한 아이콘을 클릭하면 나타나는 이벤트를 넘겨 받기 위해서 setIconClickEvent 메소드를 만들었다.

getRandomCoor는 class바깥에 있다. constructor안에 있는 정보를 사용할 필요가 없는 함수. 완전 독립적으로 존재해도 이상하지 않을 함수라서 빼는게 낫다. 안그럼 class를 불러낼때마다 쓸데없이 저장되서 메모리 손실 발생.

또한 여기서 눈여겨봐야하는 건 binding이다. displayAllIcon함수를 보면 class안에있는 onIconButton 메소드를 넘겨주는걸 볼 수 있다. 이때 onIconButton안에 있는 this까지 넘겨주는건 아니다. 따라서 this를 메소드와 binding해야한다(묶어줘야한다) binding하는 방법엔 3가지가 있다

  1. this.onIconButton = this.onIconButton.bind(this)
  2. this.playField.addEventListener(“click”,(event) => this.onIconButton(event));
  3. onIconButton = (event) => {…}

참고로 arrow function로 만들면 저번에 배웠던것 처럼 arrow funciton 안에는 this가 없으니, 상위스코프로 넘어가서 this를 찾으려고 할거기 때문에 자동으로 this가 명시되는 결과로 이어진다. 그럼 다음글에서는 top_ui를 리팩토링해보도록 하자

This post is licensed under CC BY 4.0 by the author.