우아한테크코스 6기 프리코스 - 숫자 야구 미션 회고(3)(with ES6+ 문법 사용하기)

jiny·2023년 10월 25일
1
post-thumbnail

이번 글에선 이전 글의 회고를 이어갈 예정이며 세 번째 목표인 ES6+ 문법 사용하기에 대해 미션에서 적용한 코드를 살펴보며 어떤 이유에서 추가되었으며, 어떻게 사용해야 할지 살펴보려고 합니다.

⚠️ ES6+(ES6 ~ ES2022)까지의 모든 문법을 사용하는 것이 아닌 미션에 필요한 항목을 선정 후 작성한다는 점을 먼저 밝힙니다!

ES6+란?

ES6 이후의 자바스크립트 버전을 통틀어 표현하는 용어

ES는 자바스크립트 표준인 ECMAScript를 지칭하는 용어이며, 즉 ES2015(ES6) 이후의 버전을 통틀어 표현하는 용어라고 생각하시면 됩니다.

ES6 이후 버전의 문법 들을 사용하는 것이 중요한 이유는 아래와 같습니다.

ES6 이후가 중요한 이유

  1. class, async/await, Promise 등은 ES5 이전의 문제점을 해결하고 효율적인 작업이 가능하다.
  2. arrow function, template literal, Destructuring Assignment의 문법을 통해 가독성을 개선할 수 있다.
  3. reduce, map, filter등의 고차 함수를 통해 간결한 표현이 가능하다.
  4. 기본 모듈 시스템을 통해 코드를 모듈별로 분리하고 재사용성을 높일 수 있다.
  5. 최신 문법의 경우 호환성 이슈가 있지만, Babel과 같은 트랜스파일러를 사용하면 최신 JavaScript 문법을 구버전 브라우저에서도 호환되는 코드로 변환되기 때문에 호환성 문제도 발생할 이유가 거의 없다.

이외에도 정말 많은 장점들이 있기 때문에 사용하지 않을 이유가 없었습니다.

이번 미션에서 ES6+ 문법을 적용해볼 항목들은 다음과 같습니다.

  1. arrow function
  2. class
  3. concise method
  4. template literal
  5. destructuring assignment
  6. spread operator
  7. let & const

Arrow function

function pickRandomNumberInRange (minNumber, maxNumber) {
  return Random.pickNumberInRange(minNumber, maxNumber);
}

export const pickRandomNumberInRange = (minNumber, maxNumber) =>
  Random.pickNumberInRange(minNumber, maxNumber);

function와 달리 arrow function은 함수 표현식으로만 사용이 가능하며 return 문 생략 및 this binding이 상위 스코프를 따른다는 특징이 있습니다.

// bad
comparePlayerBaseball (playerBaseball) {
  return playerBaseball.reduce(function(prevCompareResult, playerBaseballNumber, digit) {
      return this.#calculateCompareResult({
          prevCompareResult: prevCompareResult,
          playerBaseballNumber: playerBaseballNumber,
          digit: digit
      });
  }.bind(this), { strike: 0, ball: 0 });
}

// good
comparePlayerBaseball(playerBaseball) {
  return playerBaseball.reduce(
    (prevCompareResult, playerBaseballNumber, digit) =>
      this.#calculateCompareResult({ prevCompareResult, playerBaseballNumber, digit }),
    { strike: 0, ball: 0 },
  );
}

더 많은 차이를 느껴보기 위해 2번째 예시를 가져왔습니다.

bad case에서 callbackfunction 키워드의 경우 return 문 생략이 불가능하며, this binding이 다르게 적용되기 때문에 bind를 추가적으로 사용하여 호출하는 것을 알 수 있습니다.

하지만, arrow function을 사용하면 this binding 문제를 해결할 수 있을 뿐 아니라 코드 가독성이 더 좋아지는 것을 2번째 예제에서 확인할 수 있습니다.

Class

ES5(생성자 함수)

function BaseballMaker() {
  BaseballMaker.BASEBALL_SHAPE = Object.freeze({
    minNumber: 1,
    maxNumber: 9,
    size: 3,
  });
  
  var baseballShape = BaseballMaker.BASEBALL_SHAPE;
  
  BaseballMaker.create = function() {
    return new BaseballMaker();
  };
  
  BaseballMaker.prototype.createBaseball = function() {
    var baseball = new Set();
    var minNumber = baseballShape.minNumber;
    var maxNumber = baseballShape.maxNumber;
    while (baseball.size < baseballShape.size) {
        var baseballDigit = Math.floor(Math.random()*10) // 예시 
        baseball.add(baseballDigit);
    }
    return Array.from(baseball);
  }
}

이번에 미션 진행하면서 만든 BaseballMakerES5 문법으로 만들어보았습니다.

자바스크립트프로토타입기반으로 한 객체지향 프로그래밍을 지원하는 언어였기 때문생성자 함수를 class 대신 사용해오고 있어 JavaC++를 해오신 분들이라면 이전에 알고 있던 객체지향 프로그래밍 언어에서 사용해오던 문법과 형태가 많이 다른 것을 알 수 있습니다.

또한 생성자 함수의 문제점은 아래와 같이 나열할 수 있습니다.

  1. static 키워드, private, public, constructor, method 모두 지원하지 않아 가독성 적으로 좋지 못하다.
  2. 생성자 함수에서 메소드를 인스턴스에 직접 추가할 경우, 각 인스턴스마다 메소드를 복사하게 되어 메모리 사용이 비효율적일 수 있다.
  3. 일반 함수와 구분하기 위해 Pascal Case로 나타내야하지만, 그렇지 못한 경우도 존재할 수 있다.
class BaseballMaker {
  static BASEBALL_SHAPE = Object.freeze({
    minNumber: 1,
    maxNumber: 9,
    size: 3,
  });

  #baseballShape;

  constructor() {
    this.#baseballShape = BaseballMaker.BASEBALL_SHAPE;
  }

  static create() {
    return new BaseballMaker();
  }

  createBaseball() {
    const baseball = new Set();
    const { minNumber, maxNumber } = this.#baseballShape;
    while (baseball.size < this.#baseballShape.size) {
      const baseballDigit = pickRandomNumberInRange(minNumber, maxNumber);
      baseball.add(baseballDigit);
    }
    return [...baseball];
  }
}

class 경우 static, constructor, private field 등을 제공하기 때문에 객체지향 언어의 class와 거의 유사한 형태를 띄는 것을 확인할 수 있습니다.

또한, class를 사용하기 때문에 생성자 함수와도 잘 구별되는 것을 확인할 수 있습니다.

// class를 사용한 경우
const a = BaseballMaker() // TypeError: Class constructor BaseballMaker cannot be invoked without 'new'

// 생성자 함수를 사용한 경우
const a = BaseballMaker() // undefined

또한 생성자 함수의 경우 new 키워드를 사용하지 않으면 undefined를 반환하는 이슈가 존재하기 때문에 class를 사용하는 것이 더 좋습니다.

concise method

// arrow function
export const OUTPUT_MESSAGE_METHOD = Object.freeze({
  compareResult: ({ strike, ball }) =>
    [
      [ball, COMPARE_RESULT_FORMAT_TYPES.ball],
      [strike, COMPARE_RESULT_FORMAT_TYPES.strike],
    ]
      .filter(([count]) => count > 0)
      .map(([count, suffix]) => `${count}${suffix}`)
      .join(SYMBOLS.space) || COMPARE_RESULT_FORMAT_TYPES.nothing,
});

// concise method
export const OUTPUT_MESSAGE_METHOD = Object.freeze({
  compareResult({ strike, ball }) {
    return (
      [
        [ball, COMPARE_RESULT_FORMAT_TYPES.ball],
        [strike, COMPARE_RESULT_FORMAT_TYPES.strike],
      ]
        .filter(([count]) => count > 0)
        .map(([count, suffix]) => `${count}${suffix}`)
        .join(SYMBOLS.space) || COMPARE_RESULT_FORMAT_TYPES.nothing
    );
  },
});

concise method축약된 메서드라는 의미로, 사실 위 코드에서 arrow function을 사용하는 것이 더 간결한 것을 알 수 있습니다.

하지만, class의 경우 concise method의 형태이며, class를 주로 사용하고 있기 때문일관성을 위해 concise method를 사용할 수 있습니다.

또한, arrow function와 달리 자신만의 this 바인딩을 갖기 때문메서드 내에서 객체의 다른 속성에 접근할 때 arrow function보다 더 직관적으로 사용할 수 있습니다.

template literal

export const OUTPUT_MESSAGE_TEXT = Object.freeze({
  exitGame: BaseballMaker.BASEBALL_SHAPE.size + '개의 숫자를 모두 맞히셨습니다! 게임 종료'
});

export const OUTPUT_MESSAGE_TEXT = Object.freeze({
  exitGame: `${BaseballMaker.BASEBALL_SHAPE.size}개의 숫자를 모두 맞히셨습니다! 게임 종료`,
});

template literal은 ``를 통해 사용이 가능하며 위와 같이 변수와 문자열을 함께 사용해야 할 때, 문자열 연결 연산자에 비해 간결하게 사용이 가능합니다.

const dom = `
	<li>
		<a>1</a>
		<a>2</a>
		<a>3</a>
	<li>
`

만약 여러 행을 사용해야 하는 경우가 발생한다면, template literal을 통해 이스케이프 시퀀스 없이 표현이 가능합니다.

const highlight = (strings, ...values) => {
  console.log(strings, ...values)
  return strings.map((string, index) => {
    return `${string}${values[index] ? `<strong>${values[index]}</strong>` : ''}`;
  }).join('');
}

const user = 'Alice';
const amount = 10;

const taggedOutput = highlight`안녕하세요, ${user}님! 현재 포인트는 ${amount}포인트입니다.`;

console.log(taggedOutput)

더 발전된 형태로 Tagged templates을 통해 template literal를 파싱하여 사용할 수 있습니다.

destructuring assignment

#calculateCompareResult(params) {
  return {
    strike: params.prevCompareResult.strike + (this.#isStrike(playerBaseballNumber, params.digit) ? 1 : 0),
    ball: params.prevCompareResult.ball + (this.#isBall(playerBaseballNumber, params.digit) ? 1 : 0),
  };
}

구조 분해 할당적용하지 않았을 때의 예시 입니다. paramsdepth가 2인 객체이다 보니 프로퍼티를 많이 겹쳐 표현한 것을 알 수 있습니다.

만약, strikeball, digit이 변경되거나 아예 prevCompareResult가 변경된다면 많은 부분을 수정해야 하기 때문에 꽤나 골치가 아플 것 같습니다.

#calculateCompareResult({ prevCompareResult: { strike, ball }, playerBaseballNumber, digit }) {
  return {
    strike: strike + (this.#isStrike(playerBaseballNumber, digit) ? 1 : 0),
    ball: ball + (this.#isBall(playerBaseballNumber, digit) ? 1 : 0),
  };
}

하지만 구조 분해 할당을 통해 최대 depth까지 적용함으로써 가독성을 개선한 것을 알 수 있습니다.

#calculateCompareResult(params) {
  return { 
    params.prevCompareResult.strike,
    params.prevCompareResult.ball 
  };
}

#calculateCompareResult({ prevCompareResult: { strike, ball }, playerBaseballNumber, digit }) {
  return { strike, ball };
}

위 예제 처럼 ES6에서 추가 된 shorthand property와 함께 사용한다면 더 효과적으로 객체를 관리할 수 있습니다.

spread operator

createBaseball() {
  const baseball = new Set();
  const minNumber = this.#baseballShape.minNumber;
  const maxNumber = this.#baseballShape.maxNumber;
  
  while (baseball.size < this.#baseballShape.size) {
    const baseballDigit = pickRandomNumberInRange(minNumber, maxNumber);
    baseball.add(baseballDigit);
  }
  
  const result = [];
  baseball.forEach(value => {
    result.push(value);
  });
  
  return result;
}

size3이 될 때까지 중복되지 않는 숫자를 추가한 후 result라는 새로운 배열을 만들어 forEach를 통해 값을 옮긴 후 result를 반환하고 있습니다.

이는, spread operator를 통해 더 개선할 수 있습니다.

createBaseball() {
  const baseball = new Set();
  const { minNumber, maxNumber } = this.#baseballShape;
  while (baseball.size < this.#baseballShape.size) {
    const baseballDigit = pickRandomNumberInRange(minNumber, maxNumber);
    baseball.add(baseballDigit);
  }
  return [...baseball];
}

spread operator의 경우 새로운 배열을 만든 후 baseball과 같은 iterable한 값을 복사하도록 도와주기 때문에 5줄의 코드가 1줄로 줄어들어 가독성이 좋아지는 것을 알 수 있습니다.

레퍼런스

2개의 댓글

comment-user-thumbnail
2023년 10월 26일

좋은 글 너무 잘봤습니다 ~ 🙂
2주차도 화이팅하세요!

1개의 답글