과제 수행에 앞서, 다음 자료들을 읽으며 자바스크립트와 코딩 기본기를 다졌다.
자바스크립트 기본서를 읽으며 알게된 내용은 다른 문서를 이해하는 데에도 도움이 되었다.
4.4.2.2 함수를 호출할 때 this 바인딩
자바스크립트에서는 함수를 호출하면, 해당 함수 내부 코드에서 사용된 this는 전역 객체에 바인딩된다. 브라우저에서 자바스크립트를 실행하는 경우 전역 객체는 window 객체가 된다. (중략)
하지만 이러한 함수 호출에서의 this 바인딩 특성은 내부함수를 호출했을 경우에도 그대로 적용되므로, 내부 함수에서 this를 이용할 때는 주의해야 한다. (중략)
자바스크립트에서는 내부 함수 호출 패턴을 정의해 놓지 않기 때문이다. 내부 함수도 결국 함수이므로 이를 호출할 때는 함수 호출로 취급된다. 따라서 함수 호출 패턴 규칙에 따라서 내부 함수의 this는 전역객체 (window)에 바인딩된다. 때문에 this.value 값에 1을 더한 것은 결국 원래 의도했던 것과는 다르게 window.value 값에 1을 더한 결과가 나온다.
인사이드 자바스크립트 Inside JavaScript 중에서
// 전역 변수 value 정의
var value = 100;
// myObject 객체 생성
var myObject = {
value: 1,
func1: function () {
this.value += 1;
console.log('func1() called. this.value: ' + this.value);
// func2() 내부 함수
func2 = function () {
this.value += 1;
console.log('func2() called. this.value: ' + this.value);
// func3() 내부 함수
func3 = function () {
this.value += 1;
console.log('func3() called. this.value: ' + this.value);
}
func3(); // func3() 내부 함수 호출
}
func2(); // func2() 내부 함수 호출
}
};
myObject.func1(); // func1() 메서드 호출
/**
* [출력 결과]
* func1() called. this.value: 2
* func2() called. this.value: 101
* func3() called. this.value: 102
*/
프로토타입 및 정적 메서드를 사용한 this 바인딩
메서드를 변수에 할당한 다음 호출하는 것과 같이, 정적 메서드나 프로토타입 메서드가 this 값 없이 호출될 때, this 값은 메서드 안에서 undefined가 됩니다.
Classes mdn 문서 중에서
class Animal {
speak() {
return this;
}
}
let obj = new Animal();
obj.speak(); // the Animal object
let speak = obj.speak;
speak(); // undefined
자바스크립트 기본서와 Classes mdn 문서에서 서로 다른 방식으로 this 바인딩에 대해 다루고 있다. 기본서를 읽으며 this 바인딩 개념에 친숙해진 덕에, Classes mdn 문서를 더 쉽게 이해할 수 있었다. 또한 같은 개념이 class에서는 어떤 방식으로 적용되는지 배우며 지식을 심화할 수 있었다.
기능 목록을 처음 작성해보는 데다가, 보편적으로 어떤 방식으로 기능 목록이 작성되는지에 대한 데이터베이스가 없어서 고민이 많았다. 부족한 것보단 과한 게 낫겠다는 판단 하에, 기능 요구 사항과 프로그래밍 요구 사항 없이 기능 목록만 보고도 코딩이 가능할 정도로 디테일하게 작성했다. 출력 메시지는 물론, 사용할 API, 에러처리할 예외 사항 등을 포함한 디테일한 기능목록이 작성되었다. 다음주 피드백이 궁금하다
커밋 메시지에 해당 커밋에서 작업한 내용에 대한 이해가 가능하도록 작성하라는 피드백이 있었다. 기능 목록을 작성할 때 각 기능에 번호를 매기고, 커밋할 때 scope란에도 구현한 기능의 번호를 기입하였다. 그런데, 이 방식은 기능 목록에 변화가 생길 경우 각 기능의 번호가 하나씩 밀린다는 치명적인 문제가 생긴다는 사실을 인지하였다.. (3주 차 부터는 다른 방식으로 커밋 메시지를 작성할 예정)
이름을 짓는데 시간을 투자했다. 좋은 변수명을 짓는 것에 대하여 (feat. 읽기 좋은 코드가 좋은 코드다)에서 많은 영감을 받았다.
computer → threeDigitNumber
컴퓨터가 선택한 3자리 수를 담는 배열의 이름을 지어야했다. 처음에는 프로그래밍 요구사항의 예시처럼 컴퓨터가 생성한 숫자라는 의미에서 computer로 지었다.
const computer = [];
while (computer.length < 3) {
const number = MissionUtils.Random.pickNumberInRange(1, 9);
if (!computer.includes(number)) {
computer.push(number);
}
}
컴퓨터가 선택한 3자리 수 vs 컴퓨터가 선택한 3자리 수
그러나, 다시 생각해보니 3자리 수라는 의미를 전달하는 데에는 threeDigitNumber라는 이름이 더 적합해 보였다. 글을 쓰는 지금 다시 생각해보니 변수 명을 computer로 하면 맞춰야하는 숫자의 자리 수가 바뀌어도 코드를 재활용할 수 있어 좋을 것 같다. 괜히 바꿨나..
generateThreeDigitNumber() {
const threeDigitNumber = [];
while (threeDigitNumber.length < 3) {
const number = Random.pickNumberInRange(1, 9);
if (!threeDigitNumber.includes(number)) {
threeDigitNumber.push(number);
}
}
return threeDigitNumber;
}
guessNumber → pitchNumber
숫자를 입력받고, 입력한 숫자에 대한 결과를 출력하고, 3개의 숫자를 맞출 때까지 질답을 반복하고, 3개의 숫자를 모두 맞추면 게임 종료 또는 재시작하는 게임 전반의 과정을 처리하는 함수의 이름을 지어야 했다. 처음에는 막연하게 guessNumber라는 이름을 지었다. 그러나, 숫자를 입력받는 기능 외에 게임 전반을 처리하는 함수의 기능을 설명하기엔 부족하다는 생각이 들었다.
그때 ‘야구 게임’이라는 프로그램의 특성을 반영하면 좋겠다는 생각이 들었다. 야구에서 한 번의 투구pitch가 발생하면, 심판이 strike인지 ball인지 판정judge한다. 3 strike이면 이번 이닝에서의 수비가 중단된다.
숫자를 입력하는 행위를 투구pitch에 비유하면 이를 판정judgeBallStrike하고, 판정 결과를 바탕으로 또다른 투구를 할 것인지 결정하는 일련의 과정을 한 단어로 표현할 수 있겠다는 생각이 들었다.
pitchNumber() {
Console.readLine('숫자를 입력해주세요 : ', (inputStr) => {
const guess = this.isValidGuess(inputStr);
const { ball, strike } = this.judgeBallStrike(guess, this.answer);
this.printHint(ball, strike);
if (strike !== 3) this.pitchNumber();
else this.askReplay();
});
}
inputStr, inputArr, inputSet
콘솔에서 입력은 문자열 형식으로 들어온다. 같은 수인지, 같은 자리인지를 동시에 판정해야 하기에 배열 형태로 값을 변환하는 게 유리하고, 플레이어가 중복된 수를 입력한 경우 에러처리해야하기에 집합 형태로도 값을 변환해야 했다. 같은 입력값을 String, Array, Set 세 가지 형식으로 다루고 있기에, inputStr, inputArr, inputSet이라는 변수 명을 통해 같은 값이 다른 형식으로 이용되고 있음을 드러내어 나 자신과 다른 개발자가 코드를 이해하기 편하도록 하였다.
isValidGuess(inputStr) {
const inputArr = [...inputStr].map(Number);
const inputSet = new Set(inputArr);
if (!/^[1-9]{3}$/.test(inputStr) || inputSet.size !== 3) {
throw new Error(
'각 자리가 1부터 9까지 서로 다른 수로 이루어진 3자리 수를 입력하세요.'
);
}
return inputArr;
}
원래는 모든 함수에 주석을 다는 걸 의무처럼 여겼다. 그러나 피드백을 보고 생각이 바뀌는 동시에, 이름을 더 잘 지어야겠다는 동기부여가 되었다.
변수 이름, 함수(메서드) 이름을 통해 어떤 의도인지가 드러난다면 굳이 주석을 달지 않는다. 모든 변수와 함수에 주석을 달기보다 가능하면 이름을 통해 의도를 드러내고, 의도를 드러내기 힘든 경우 주석을 다는 연습을 한다.
테스트 케이스를 작성할 때처럼, 의도를 드러내는 이름을 짓기 힘들 때만 주석을 활용하였다.
play
게임 시작
init
게임 시작 문구 출력, 정답(컴퓨터가 생성한 3자리 수) 초기화
generateThreeDigitNumber
컴퓨터의 3자리 수 생성
pitchNumber
플레이어로부터 숫자 입력 받음, 게임 과정 전반(입력한 숫자에 대한 결과 출력, 3개의 숫자를 맞출 때까지 질답 반복, 3개의 숫자를 모두 맞추면 게임 종료 또는 재시작) 처리
isValidGuess
사용자가 잘못된 값을 입력한 경우 에러 처리
judgeBallStrike
컴퓨터가 생성한 숫자와 플레이어가 입력한 숫자 비교
printHint
숫자 비교 결과(힌트) 출력
askReplay
게임이 끝난 경우 1과 2 중 하나의 수 입력받아 게임 재시작/종료
isValidAnswer
사용자가 잘못된 값을 입력한 경우 에러 처리
init
게임 시작 문구 출력
generateThreeDigitNumber
컴퓨터의 3자리 수 생성
isValidGuess
잘못된 값을 입력한 경우 에러처리
judgeBallStrike
볼, 스트라이크 개수 구하기
printHint
입력한 숫자에 대한 결과 출력
isValidAnswer
게임 종료 후 1, 2 외의 숫자 입력 시 에러처리
askReplay
게임 종료 후 재시작
jest는 처음 이용해보는데, 새로 배운 자바스크립트 문법을 활용해 테스트 케이스를 작성하는 일이 나름 재미있었다. 기능 목록을 작성할 때 예외 사항까지 디테일하게 작성한 점이 도움이 되었다. 주석을 활용해 각 테스트 케이스가 잡고자하는 예외사항을 표현하였다.
test('잘못된 값을 입력한 경우 에러처리', () => {
const game = new Game();
const guesses = [
// 3자리 수가 아닐 경우
'1234',
'12',
'-12',
'0.1',
// 중복된 수가 있는 경우
'121',
// 0을 포함할 경우
'012',
// 숫자가 아닐 경우
'abc',
'일이삼',
'!@#',
];
guesses.forEach((guess) => {
expect(() => {
game.isValidGuess(guess);
}).toThrow(
'각 자리가 1부터 9까지 서로 다른 수로 이루어진 3자리 수를 입력하세요.'
);
});
});
우테코가 아니었다면 시간 내에 돌아가는 코드를 짜기에 급급했지, 위의 것들에 시간을 들이며 코딩하는 연습을 해보지 못했을 것이다. 내가 짜고있는 코드가 소위 말해 예쁜 코드가 맞는지 항상 의심하고 했는데, 코드 컨벤션을 정독하고 나서는 그 걱정이 조금은 줄어들었다.