이번 글에선 이전 글의 회고를 이어갈 예정이며 세 번째 목표인 ES6+ 문법 사용하기에 대해 미션에서 적용한 코드를 살펴보며 어떤 이유에서 추가되었으며, 어떻게 사용해야 할지 살펴보려고 합니다.
⚠️ ES6+(ES6 ~ ES2022)까지의 모든 문법을 사용하는 것이 아닌 미션에 필요한 항목을 선정 후 작성한다는 점을 먼저 밝힙니다!
ES6 이후의 자바스크립트 버전을 통틀어 표현하는 용어
ES
는 자바스크립트 표준인 ECMAScript
를 지칭하는 용어이며, 즉 ES2015(ES6)
이후의 버전을 통틀어 표현하는 용어라고 생각하시면 됩니다.
ES6 이후 버전의 문법 들을 사용하는 것이 중요한 이유는 아래와 같습니다.
class
,async/await
,Promise
등은 ES5 이전의 문제점을 해결하고 효율적인 작업이 가능하다.arrow function
,template literal
,Destructuring Assignment
의 문법을 통해 가독성을 개선할 수 있다.reduce
,map
,filter
등의 고차 함수를 통해 간결한 표현이 가능하다.- 기본 모듈 시스템을 통해 코드를 모듈별로 분리하고 재사용성을 높일 수 있다.
- 최신 문법의 경우 호환성 이슈가 있지만,
Babel
과 같은 트랜스파일러를 사용하면 최신 JavaScript 문법을 구버전 브라우저에서도 호환되는 코드로 변환되기 때문에 호환성 문제도 발생할 이유가 거의 없다.
이외에도 정말 많은 장점들이 있기 때문에 사용하지 않을 이유가 없었습니다.
이번 미션에서 ES6+
문법을 적용해볼 항목들은 다음과 같습니다.
- arrow function
- class
- concise method
- template literal
- destructuring assignment
- spread operator
- let & const
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
에서 callback
의 function
키워드의 경우 return
문 생략이 불가능하며, this binding
이 다르게 적용되기 때문에 bind
를 추가적으로 사용하여 호출하는 것을 알 수 있습니다.
하지만, arrow function
을 사용하면 this binding
문제를 해결할 수 있을 뿐 아니라 코드 가독성이 더 좋아지는 것을 2번째 예제에서 확인할 수 있습니다.
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);
}
}
이번에 미션 진행하면서 만든 BaseballMaker
를 ES5
문법으로 만들어보았습니다.
자바스크립트
는 프로토타입
을 기반으로 한 객체지향 프로그래밍을 지원하는 언어였기 때문에 생성자 함수를 class 대신 사용해오고 있어 Java
나 C++
를 해오신 분들이라면 이전에 알고 있던 객체지향 프로그래밍 언어에서 사용해오던 문법과 형태가 많이 다른 것을 알 수 있습니다.
또한 생성자 함수의 문제점
은 아래와 같이 나열할 수 있습니다.
- static 키워드, private, public, constructor, method 모두 지원하지 않아 가독성 적으로 좋지 못하다.
- 생성자 함수에서 메소드를 인스턴스에 직접 추가할 경우, 각 인스턴스마다 메소드를 복사하게 되어 메모리 사용이 비효율적일 수 있다.
- 일반 함수와 구분하기 위해 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
를 사용하는 것이 더 좋습니다.
// 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
보다 더 직관적으로 사용할 수 있습니다.
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
를 파싱하여 사용할 수 있습니다.
#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),
};
}
구조 분해 할당
을 적용하지 않았을 때의 예시 입니다. params
가 depth가 2인 객체이다 보니 프로퍼티를 많이 겹쳐 표현한 것을 알 수 있습니다.
만약, strike
나 ball
, 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
와 함께 사용한다면 더 효과적으로 객체를 관리할 수 있습니다.
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;
}
size
가 3
이 될 때까지 중복되지 않는 숫자를 추가한 후 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주차도 화이팅하세요!