4주 동안의 프리코스 대장정이 끝났다. 원래라면 4주 차를 진행하는 동안에 3주 차 회고를 작성했어야 하지만 4주 차의 벽에 부딪혀 정신없이 구현하느라 3주 차 회고는 꿈에도 꾸지 못했다. 순서가 밀렸지만 천천히 3주 차부터 다시 돌아보는 글을 작성해 본다.
2주 차 공통 피드백을 받고 아차 싶었다. 기능 목록을 작성하는 방법에 대한 내용이 있었는데 내가 미션에 접근하는 방식을 다르게 해야 한다는 생각을 하게 됐다.
기능 목록을 재검토한다
기능 목록을 작성할 때 클래스 설계와 구현, 메서드 설계와 구현 같은 상세한 내용은 포함하지 않는다. 클래스 이름이나 메서드 시그니처, 반환값 등은 언제든지 변경될 수 있기 때문이다. 구현해야 할 기능 목록을 중심으로 작성하되, 정상적인 경우뿐만 아니라 예외 상황도 함께 정리한다. 예외 상황은 시작 단계에서 파악하기 어려우므로, 기능을 구현하면서 지속적으로 업데이트하는 것이 좋다.
기능 목록을 업데이트한다
README.md 파일의 기능 목록은 구현 과정에서 변경될 수 있다. 시작부터 모든 기능을 완벽하게 정리해야 한다는 부담을 갖기보다는, 기능을 구현하면서 문서를 지속적으로 업데이트하는 것을 목표로 한다. 이를 통해 죽은 문서가 아닌 살아있는 문서로 유지될 수 있도록 노력해 보자.
실제로 1,2주 차 미션을 진행하면서 기능 목록을 작성하는 일을 가장 먼저 고려했고, 조금은 완벽히 작성해야 한다는 생각이 있었다. 물론 나중에 변경이 될 수 있다는 점은 이해하고 있으면서도 어느 정도는 무엇을, 어떻게 구현할 것인지를 알아야 설계를 할 수 있었기 때문에 더 신경을 쓰기도 했다. 하지만 이 과정에서 놓친 점은 열심히 작성을 해두고 `Git의 커밋 단위는 앞 단계에서 README.md 정리한 기능 목록 단위로 추가한다.`라는 요구사항을 깊이 생각하지 못했다는 것이다.
프로젝트를 진행할 때는 이슈를 최대한 분리해서 그에 따라 작업했기 때문에 이 과정이 자연스럽게 이루어졌는데 README에만 작성해 두고 구현에 집중하느라 단위를 지켜서 커밋하는 것을 신경 쓰지 못했다. 3주 차 로또 미션에서는 이점을 인지해서 노션에 각 기능의 세부 구현사항을 정리하고 구현하면서 커밋도 묶어서 하도록 의식적으로 노력했다. 이렇게하니 좋았던 점은 서로 의존성 있는 모듈을 실행 순서나 구현 순서를 고려하지 않아도 되어 고민할 시간이 많이 줄었다. 마치 스쿼시 머지를 커밋으로 하는 듯한 방식처럼 느껴졌다. 이 방법이 잘 맞았던 것인지 순차적으로 무리없이 구현할 수 있었다.
class Lotto {
#numbers;
constructor(numbers) {
this.#validate(numbers);
this.#numbers = numbers;
}
#validate(numbers) {
if (numbers.length !== 6) {
throw new Error("[ERROR] 로또 번호는 6개여야 합니다.");
}
}
// TODO: 추가 기능 구현
}
export default Lotto;
이번에는 새로운 조건이 추가되었다. Lotto 클래스가 이미 제공되고, 프라이빗 필드를 건들지 않으면서 다른 필드 추가 없이 활용해야 했다. 처음엔 좋은데?라고 생각했다가 막상 기능을 구현하면서 이 클래스를 어떻게 활용할지 떠오르지 않았다. 바로 활용이 어려웠던 이유를 생각해보면 1,2주 차 미션을 진행하면서 벌써 어떤 루틴처럼 재사용하듯이 로직을 짜버려서 오히려 이 제공된 클래스가 자유도를 제한하는 상황이 되었다. 이전까지 유효성 검사를 Validator 클래스로 모두 처리하고 있었기때문에 내부 메서드로 validate가 있으니 순간 머리가 굳었던 것 같다. 별거 아닌 문제였는데 기존 로직을 익숙한대로 습관처럼 쓰려고하니 멈칫하게 되었다. 직접 리뷰를 받은 것도 아닌데 클래스 하나로 혼난 기분이 들었다.
#history() {
Array.from({ length: this.quantity }).forEach(() => {
this.#draw();
const lotto = new Lotto(this.#numbers);
this.#lottos.push(lotto);
this.#numbers = [];
});
}
프리코스를 진행하며 반복문이 필요한 경우가 많았다. 1주 차에는 `for...of문`을 사용했으나 airbnb 컨벤션에서는 피한다는 것을 알게되어 2주 차에서는 기본 `for문`을 사용했다. 그런데 여러 리뷰들을 보다가 기본 for문은 불필요한 변수 생성이다라며 Array.from을 쓰는 방법에 대한 피드백을 보아 3주 차에서는 `Array.from`을 활용했다. 하지만 다시 생각해보니 Array.from도 배열을 생성하기때문에 quantity가 많아질수록 메모리 효율성이 떨어질 수 있다는 생각이 들었다.
그리고 for문에 대해서도 의문이 들었다. for문은 주로 `for(let i=0; i<10; i++){}` 형식으로 이루어진다. `i`도 변수이고, `i`값이 증가할수록 매번 새로운 값을 할당하기때문에 불필요한 변수 생성이라고 하는 것이다. 아니 그럼 지금까지 사용한 모든 경우의 for문은 불필요한 변수를 만들어서 순회를 했던 것인가?라는 생각을 할 수도 있겠다. 사실 순간적으로 내가 했던 생각이다.
for (let i = 0; i < this.#totalRounds; i++) {
this.#playRound();
this.#printRoundResult();
}
2주 차에서 사용했던 for문이다. 해당 로직으로 보았을 때, for문이 불필요한 변수 선언이라는 말은 내부 로직에서 선언된 변수 `i`를 사용하지 않는다는 것이다. 정말 특정 횟수만큼 순회가 필요하다는 것 외엔 for문의 특징을 활용하지 않는다. `Array.from` 또한 위에서 말한대로 순회만을 위해 배열을 생성하는 것과 같다. 다만 forEach를 쓰기 위한 불필요한 배열 생성이라고 할 수도 있겠다.
결론적으로 정말 순회만을 위한 로직을 고려한다면 `while문`이 적합하지 않을까라는 생각이 든다. 주관적으로 while문은 의도한 바가 직관적이지 않다고 느낀적이 많기때문에 필요한 상황이 있을때마다 다른 방법이 있다면 쓰지 않는 편이었는데 성능면에서 다시 생각해보게 되었다. 가독성은 내가 풀어야 할 숙제인 것 같다.
이전에 작성했던 `좋은 코드란 무엇일까?`글에 반복문에 대한 예시가 있어 함께 보면 좋을 것 같다.
좋은 코드란 무엇일까?
자바스크립트 객체 순환으로 알아보는 코드에 맥락 부여하기-나만의 좋은 코드의 기준을 세우기 위한 코드 작성법을 연습하고, 다른 사람을 설득할 수 있도록 의미를 생각해본다. 들어가며개
heeheehoho.tistory.com
로또 미션은 숫자로 시작해서 숫자로 끝났다. 그래서 유효성 검사 로직이 중복되는 경우가 많았고, 처음에는 일단 돌아가도록만 해보기 위해 중복을 신경쓰지 않고 모두 작성한 다음 리팩토링을 시작했다.
static defaultNumber(value) {
// 빈 값, 공백
Validator.isEmpty(value);
// 숫자가 아닌 값
Validator.isInvalid(value);
// 음수
Validator.isNegative(value);
// 정수가 아닌 값
Validator.isInteger(value);
}
검증이 필요한 값은 구매 금액 / 로또 번호 / 보너스 번호 이렇게 3가지로 분류할 수 있다. 먼저 모든 케이스가 공통적으로 적용되는 검사를 1개의 메서드로 묶었다.
하지만 문제가 있다. 로또 번호의 경우 배열 자료구조로 처리되기 때문에 유효성 검사를 위한 타입 구분이 필요했다. 각 함수 내부에 타입별 처리를 해줄 수 있지만 불필요한 중복이 많기때문에 이를 한번에 처리할 수 있는 유틸함수를 추가했다.
export default function applyToValueOrArray(value, callback) {
if (Array.isArray(value)) {
return value.some(callback);
}
return callback(value);
}
로또 번호에 대한 검사는 특정 조건이 1개라도 맞지 않으면 예외 처리가 필요했기때문에 `some`을 이용했다. 평가할 값과 조건 함수를 전달해서 내부적으로 타입에 따라 다르게 처리한다.
export default function isNegativeNumber(value) {
return applyToValueOrArray(value, (v) => +v < 0);
}
예를 들어, 음수 값을 평가하는 함수에 이렇게 적용할 수 있다.
이쯤되니 이상하게 느껴지는 부분이 있었다. 재사용을 위해 유틸 함수로 분리하고, 유효성 검사 로직을 한 곳에서 관리하기 위해 Validator 클래스를 두었는데 유틸 함수를 그대로 가져다 매핑해서 쓰게 되어 재사용이 재사용처럼 느껴지지 않았다. 그래서 공백 검사, 음수 검사 등 어떤 기능에도 사용할 수 있는 것들만 유틸 함수로 분리하고, 구매 금액 범위나 로또 번호와 같은 로또 미션에서만 사용되는 검사는 Validator의 메서드에 바로 예외 조건을 평가하는 방식으로 적용했다.
2주 차 공통 피드백에 테스트 코드에 대한 내용이 많았던만큼 테스트 코드에 익숙하지 않은 상태에서 걱정이 많이 되었다. 어찌저찌 해볼 수 있는 테스트 코드는 다 작성했지만, 제출 시 실행되는 테스트를 통과할 수 있을까?라는 생각이 들었다. 그래서 1,2주 차는 월요일에 제출했다면 3주 차에는 일요일에 먼저 제출하고 확인해보았다.
걱정한대로 2개의 테스트 중 1개를 통과하지 못했다. 이 문제는 분명 처음 제공된 테스트 코드와 관련이 있을 것이라 생각하여 테스트 코드를 처음 제공 받은대로 되돌려 디버깅해가며 원인을 찾았다. 예외를 던지는 부분에서 문제가 있음을 확인했는데 나의 코드를 바꾸면 로컬 테스트를 통과하지 못하고, 테스트 코드를 수정하면 제출 시 테스트에 실패하는 상황이었다. 아무리 뒤져도 이유를 찾지 못하다가 이전 미션들의 테스트 코드와 비교해보면서 달라진 점을 찾아보았고, 달라진 부분과 함께 요구사항 중 놓친 부분을 발견했다.
이번 미션에서는 예외가 발생하면 종료 되는 것이 아니었다. 예외에 대한 안내를 한 뒤에 바로 그 단계에서 다시 재입력을 받을 수 있도록 해야했다. `throw new Error`를 이용해서 바로 에러를 던지도록 되어 있어 메시지만을 확인하는 테스트 코드를 통과하지 못했던 것이었다.
try {
const input = await InputProcessor.get(PROMPT.BONUS_NUMBER_INPUT);
Validator.bonusNumber(winningNumbers, input);
return input;
} catch (error) {
Console.print(error.message);
return InputProcessor.purchasePrice();
}
입력값을 받을 때마다 try ~ catch문으로 감싸서 예외 발생 시 메시지를 출력하고 재귀 호출로 재입력을 받을 수 있도록 수정했다. 입력값을 받기 위한 제공된 API인 `readLineAsyn`는 비동기 함수라 await으로 값을 처리했기때문에 try ~ catch문을 사용할 수 있다.
마지막 에러를 해결해 깔끔히 2/2로 테스트를 통과하여 제출한 뒤에 커뮤니티를 통해 TDD에 대한 글을 보았다. 이전까지 테스트는 나에게 너무 먼 이야기였고, TDD에 대해 제대로 알지 못했었다. 하지만 마지막 문제의 에러를 해결한 뒤에 TDD에 대해 알고나니 이래서 필요하구나라는 생각이 들었다.
처음 미션을 포크 받으면 기본 테스트 코드가 작성 되어 있다. 당연히 처음엔 구현 로직이 완성되어 있지 않기때문에 에러가 난다. 나는 여기에서 에러를 당연하게 생각하고 항상 구현을 한 뒤에 테스트가 통과하는지 보는 식으로 진행했다. 하지만 TDD로 접근을 한다면 이미 제공된 테스트 코드를 참고하여 이 코드가 통과하도록 로직을 고민해볼 수 있다. 이 순서로 접근했다면 나는 재입력 받기에 대한 문제를 처음부터 해결할 수 있었을 것이다. 그리고 우테코에서 3주 차 미션을 통해 알려주려고 한 것도 이와 비슷한 느낌이지 않을까 싶다. 2주 차 공통 피드백에 TDD에 대한 영상을 보여줬기때문에 더더욱 그렇게 생각하게 되었다.
현재 내가 테스트 코드를 제대로 활용할 수 있다고는 대답하지 못하겠다. 아직 나에게 너무 어렵고, 이를 모두 고려하기엔 일주일이 부족했다. 하지만 이번 기회를 통해 어떤 식으로 작성하고, 왜 필요한지에 대해서만 인지할 수 있었다. 한번 경험하고 나니 애매하게 다룰 줄 알게 된 이 느낌이 싫어서 빨리 활용하는 방법에 익숙해지고 싶다는 욕심이 생긴다. 무엇보다 모두 통과했을때의 그 초록색들이 너무 기분이 좋아서 또 경험하고 싶다. 😄
짜여진 공식처럼 코드를 작성하는 습관을 고쳐야겠다는 생각이 든다. 매번 그 순간엔 고민하고 또 고민했다고 하지만 돌이켜보면 "이건 해본거다. 그때 이렇게 하면 되었지"라는 생각을 많이 하는 것 같다. 매주 다른 미션을 받을때마다 이 부분을 확인해볼 수 있도록 요구사항이 제공된 것은 아닐까 싶다. 물론 나는 그 미끼를 덥썩 물고 뼈저리게 후회를 하고 있는 중이다. 습득한 지식은 활용 방법 중 하나일뿐 언제나 정답일리는 없다는 것을 항상 염두해야겠다. 4주 차에도 비슷한 실수를 했기때문에 이 글을 쓰고 있는 지금도 후회가 된다. 하지만 이제 알았으니 미션들에 대해 복습할 때는 이전의 기억들을 최대한 내려두고 백지에서 시작한다는 생각으로 접근해보며 지금까지 공부해온 것들을 정말 활용할 수 있는 방법을 찾아야겠다.
[우아한 테크 코스 7기] 프리코스 4주 차 회고 (1) | 2024.11.19 |
---|---|
[우아한 테크 코스 7기] 프리코스 2주 차 회고 (2) | 2024.10.30 |
[우아한 테크 코스 7기] 프리코스 1주 차 회고 (3) | 2024.10.24 |