[TIL] 211006

Lee Syong·2021년 10월 6일
0

TIL

목록 보기
49/204
post-thumbnail

📝 오늘 한 것

  1. Object API 사용법 / Object 확장 / prototype / for in 문 / Object.hasOwnProperty

  2. 미니 게임 리팩토링


📚 배운 것

1. 객체(object)

생활코딩 2014 Object 참고

  • 자바스크립트의 모든 객체는 Object 객체를 상속 받는다. 따라서 모든 객체는 Object 객체의 프로퍼티를 가지고 있다. 그 중에는 prototype이란 특수한 프로퍼티도 있다.

  • Object의 prototype은, 모든 객체의 prototype이 된다. 따라서 모든 객체가 가지고 있어야 할 기능이 있다면, Object의 prototype으로 지정할 수 있다.

1) Object API 사용법

(1) 예시1 - Object.keys()

  • 생성자 함수 Object도 객체이므로 프로퍼티를 가질 수 있다. Object 객체에서 keys라는 메서드를 호출하면서 특정 인자를 넣어주는 방식으로 사용할 수 있다.
// Object.keys()
var arr = ['a', 'b', 'c'];
console.log(Object.keys(arr)); // ['0', '1', '2']
Object.keys = function (arg) {}

(2) 예시2 - Object.prototype.toString()

  • 생성자 함수를 통해 새로운 객체를 만든 후 이를 변수에 할당한다. 그 변수에서 해당 메서드를 호출한다.

  • 모든 객체는 Object 표준 내장 객체의 자식이다. 배열도 마찬가지이므로 똑같이 사용할 수 있다.

// Object.prototype.toString()
var o = new Object();
console.log(o.toString()); // 

var a = new Array(1, 2, 3);
console.log(a.toString()); // 1,2,3
Object.prototype.toString = function () {}

2) Object 확장

// '객체가 해당 프로퍼티를 포함하는지'를 나타내는 메서드를 직접 정의하려고 함
// 모든 객체에서 사용 가능한 contain이란 메서드를 직접 정의해줌
Object.prototype.contain = function (value) {
  for (let name in this) {
    if (this[name] === value) {
      return true;
    }
  }
  return false;
}

const obj = {A: 'a', B: 'b', C: 'c'};
console.log(obj.contain('d')); // false

const arr = ['a', 'b', 'c'];
console.log(arr.contain('a')); // true

3) Object 확장의 위험

  • Object 확장을 사용하는 이유는, 모든 객체에 영향을 줄 수 있기 때문이다. 그러나 이것이 위험한 이유 또한 모든 객체에 영향을 줄 수 있기 때문이다.
Object.prototype.contain = function (value) {
  for (let name in this) {
    if (this[name] === value) {
      return true;
    }
  }
  return false;
}

const obj = {A: 'a', B: 'b', C: 'c'}
for (let name in obj) {
  console.log(name); // A, B, C, contain
}

const arr = ['a', 'b', 'c'];
for (let name in arr) {
  console.log(name); // 0, 1, 2, contain
}
  • for in 문을 통해 obj 혹은 arr의 프로퍼티 값만 가져오고 싶은데 prototype에 contain() 메서드를 추가함으로써 원하는 값만 출력되지 않는 문제가 발생한다.

  • 따라서 Object 확장을 사용할 때는 구현하고자 하는 것이 무엇인지를 확실히 파악한 후, 그로 인해 발생할 문제점을 충분히 인지하고서 사용해야 한다. Object 확장은 어떤 기능이 필요한 공통 로직의 최소 단위에서 적용해야 한다.

한편,
💡 for in 구문을 통해 직접 정의한 프로퍼티만 가져오는 방법

  • 객체.hasOwnProperty(프로퍼티) 메서드 사용
    • 객체가 특정 프로퍼티를 가지고 있는지를 알려준다
    • true 혹은 false 값을 리턴한다
  • 이를 통해 부모 객체로부터 상속받은 프로퍼티와 자기 자신이 직접 정의한 프로퍼티를 구분할 수 있다
for (let name in obj) {
  if (obj.hasOwnProperty(name)) {
    console.log(name); // A, B, C
  }
}

2. 모듈(module)

  • 모듈화(부품화)를 통해 코드의 재활용성을 높이고, 유지·보수를 쉽게 할 수 있도록 도와준다.

  • 호스트 환경이란 자바스크립트가 구동되는 환경을 말한다. 그 종류로는 브라우저, node.js, Google Apps Script 등이 있다. 호스트 환경에 따라 서로 다른 모듈화 방법이 제공되고 있다.


3. 미니 게임 리팩토링

1) game.js 작성

  • this.gameFinishBanner.show()를 지우고, 게임이 끝났음을 main.js에 알려줌에 따라 main.js에서 게임 진행 상황에 따라 원하는 기능이 실행될 수 있도록 setStopGameListener() 메서드를 통해 콜백 함수를 지정해준다.

  • stop()과 finish() 메서드에 전달된 callBack 함수가 있다면 그것을 실행하도록 코드를 작성한 후 각각의 상황을 구별할 수 있도록 인자를 전해주었다. (cancel, win, lose)

'use strict';

import Field from './field.js';
import * as Sound from './sound.js';

export default class Game {
  constructor (gameDurationTime, carrotCount, bugCount) {
    this.gameDurationTime = gameDurationTime;
    this.carrotCount = carrotCount;
    this.bugCount = bugCount;

    this.startBtn = document.querySelector('.setting-startBtn');
    this.gameTimer = document.querySelector('.setting-timer');
    this.gameLimit = document.querySelector('.setting-limit');

    this.started = false;
    this.timer = undefined;
    this.limit = carrotCount;

    this.startBtn.addEventListener('click', () => {
      if (this.started) {
        this.stop();
      } else {
        this.start();
      }
    });

    this.gameField = new Field(carrotCount, bugCount);
    // 💡 콜백 함수로 onItemClick 전달
    this.gameField.setFieldClickListener((item) => this.onItemClick(item));
  }

  setStopGameListener (callBack) {
    this.callBack = callBack;
  }

  // 💡 onItemClick 함수의 일부만 field.js로 옮겼으므로 main.js에서 onItemClick 함수를 제거하면 안됨
  // 💡 field.js로 옮겨 적은 후 호출까지 되는 함수를 제외하고, 나머지 코드를 써줄 것
  onItemClick (item) {
    if (!this.started) {
      return;
    }

    if (item === 'carrot') {
      this.showGameLimit(--this.limit);
      if (this.limit === 0) {
        this.finish(true);
      }
    } else if (item === 'bug') {
      this.finish(false);
    }
  }

  showGameLimit (num) {
    this.gameLimit.innerHTML = num;
  }

  stop () {
    this.started = false;
    this.hideStartBtn();
    this.stopTimer();
    Sound.stopBackground();
    Sound.playAlert();
    /* this.gameFinishBanner.show('Replay ❓'); (삭제) */
    this.callBack && this.callBack('cancel'); /* 추가 */
  }

  finish (win) {
    this.started = false;
    this.hideStartBtn();
    this.stopTimer();

    /* gameFinishBanner.show(win? 'YOU WON 🎉' :'YOU LOST 💩'); (삭제) */
    this.callBack && this.callBack(win ? 'win' : 'lose'); /* 추가 */

    Sound.stopBackground();
    if (win) {
      Sound.playWin();
    } else {
      Sound.playBug();
    }
  }

  start () {
    this.started = true;
    this.initGame();
    this.startTimer();
    this.showStopIcon();
    Sound.playBackground();
  }

  hideStartBtn () {
    this.startBtn.style.visibility = 'hidden';
  }

  startTimer () {
    let remainingSec = this.gameDurationTime;
    this.displayTime(remainingSec);
    this.timer = setInterval(() => {
      if (remainingSec <= 0) {
        clearInterval(this.timer);
        this.finish(this.limit === 0);
        return;
      }
      this.displayTime(--remainingSec);
    }, 1000);
  }

  stopTimer () {
    clearInterval(this.timer);
  }

  displayTime (time) {
    let minutes = Math.floor(time / 60);
    let seconds = time % 60;
    this.gameTimer.innerHTML = `${minutes}:${seconds}`;
  }

  initGame () {
    this.limit = this.carrotCount;
    this.gameLimit.innerHTML = this.carrotCount;

    this.gameField.init();
  }

  showStopIcon () {
    const icon = document.querySelector('.fas');
    icon.classList.add('fa-stop');
    icon.classList.remove('fa-play');
    this.startBtn.style.visibility = 'visible';
  }
}

2) main.js 수정

  • 코드들을 game.js로 대거 이동시켰다.

  • gameFinishBaneer의 setClickListener() 메서드 인자로 주어졌던 startGame을 () => game.start()로 바꿨다.

  • throw new Error(‘에러 문구’)

'use strict';

import Result from './result.js';
import Game from './game.js';

// 다른 js 파일의 class 안으로 옮긴 코드들은, 그때그때 main.js에서 지워줘도 됨

const gameFinishBanner = new Result();
gameFinishBanner.setClickListener(() => game.start());

const game = new Game(10, 10, 7);
game.setStopGameListener((reason) => {
  let message;
  switch (reason) {
    case 'win':
      message = 'YOU WON 🎉';
      break;
    case 'lose':
      message = 'YOU LOST 💩';
      break;
    case 'cancel':
      message = 'Replay ❓';
      break;
    default:
      throw new Error('not valid reason');
  }
  gameFinishBanner.show(message);
});

3) 빌더 패턴 이용하기

const game = new Game(10, 10, 7)
  • 위의 코드처럼 생성자 함수의 인자가 그 의미를 알 수 없는 숫자로 되어 있거나 인자의 개수가 3개가 넘어가는 것은 좋지 않다.

  • game 클래스를 외부에 노출시키기 위해 사용했던 export default 키워드를 삭제하고, 대신에 gameBulider라는 클래스를 새로 만들어 외부에 노출시켰다.

game.js 수정

// 빌더 패턴
export default class GameBuilder {
  withGameDurationTime (duration) {
    this.gameDurationTime = duration;
    return this;
  }

  withCarrotCount (num) {
    this.carrotCount = num;
    return this;
  }

  withBugCount (num) {
    this.bugCount = num;
    return this;
  }

  build () {
    return new Game(
      this.gameDurationTime,
      this.carrotCount,
      this.bugCount
    );
  }
}

main.js 수정

import gameBuilder from './game.js';

const game = new GameBuilder()
  .withGameDurationTime(10)
  .withCarrotCount(10)
  .withBugCount(7)
  .build();

4) 자바스크립트로 타입 보장하기

  • 기존의 main.js의 switch 구문에서 case 다음에 문자열이 오기 때문에 오타가 발생할 확률이 높다. 이를 보완하기 위해 freeze() 메서드를 사용할 수 있다.

  • game.js 수정

    Object.freeze() 메서드는 해당 객체를 동결시켜 더 이상 변경할 수 없도록 한다.

export const Reason = Object.freeze({
  win = 'win',
  lose = 'lose',
  cancel = 'cancel'
});
  • main.js 수정
import { gameBuilder, Reason } from './game.js';

game.setStopGameListener((reason) => {
  let message;
  switch (reason) {
    case Reason.win:
      message = 'YOU WON 🎉';
      break;
    case Reason.lose:
      message = 'YOU LOST 💩';
      break;
    case Reason.cancel:
      message = 'Replay ❓';
      break;
    default:
      throw new Error('not valid reason');
  }
  gameFinishBanner.show(message);
});

5) stop()과 finish() 중복 제거 및 병합

  • 원래 코드 (game.js)
  stop () {
    this.started = false;
    this.hideStartBtn();
    this.stopTimer();
    Sound.stopBackground();
    Sound.playAlert();
    this.callBack && this.callBack('cancel'); /* 추가 */
  }

  finish (win) {
    this.started = false;
    this.hideStartBtn();
    this.stopTimer();
    this.callBack && this.callBack(win ? 'win' : 'lose');

    Sound.stopBackground();
    if (win) {
      Sound.playWin();
    } else {
      Sound.playBug();
    }
  }
  • 수정한 코드 (game.js)

    stop()과 많은 부분이 중복되던 finish()를 없애고, 구별하기 위해 인자를 썼다.

stop (reason) {
  this.started = false;
  this.hideStartBtn();
  this.stopTimer();
  Sound.stopBackground();
  this.callBack && this.callBack(reason);
}
  • 수정한 코드 (main.js)
game.setStopGameListener((reason) => {
  let message;
  switch (reason) {
    case Reason.win:
      message = 'YOU WON 🎉';
      Sound.playWin();
      break;
    case Reason.lose:
      message = 'YOU LOST 💩';
      Sound.playBug();
      break;
    case Reason.cancel:
      message = 'Replay ❓';
      Sound.playAlert();
      break;
    default:
      throw new Error('not valid reason');
  }
  gameFinishBanner.show(message);
});
  • 이후에 main.js에서 stop(), finish()와 관련된 부분을 수정해줬다. ex) stop(reason.win) 등

✨ 내일 할 것

  1. 강의 계속 듣기
profile
능동적으로 살자, 행복하게😁

0개의 댓글