미션 페이지 & 코드 리뷰 페이지
2단계 코드 리뷰의 경우, 제가 제출기한을 어겨서 리뷰가 없었습니다. 지금 생각해도 너무 아쉽습니다. 시간을 지키는 거 정말로, 중요하군요...
이번 미션에서 배운 점
우테코의 첫 번째 미션인만큼, 새로운 것을 배우는 것보다는 프리코스에서 학습했던 내용들을 돌아보는 미션이었습니다. 프리코스에서 배운 내용들은 이미 적어두었으므로, 링크를 첨부하는 것으로 대신하겠습니다.
그 대신 다른 이야기를 해 볼까요?
for문 배틀 (vs. 리뷰어 파노)
Q. for문을 쓰는 게 대체 뭐가 문제라는 거죠?
자동차 미션을 진행하면서 가장 충격먹었던 사실 중 하나는 바로 지금까지 밥 먹듯이 사용해왔던 for문이 지양될 수 있다는 사실이었습니다. 이유는 바로, for문으로 반복문을 구현할 경우 실수하기 쉽고, 직관적이지 못 할수 있기 때문입니다.
- for문의 i 값을 실수로 잘못 적는다면? 종료 조건은?
- for문의 조건식/증감식만 보는 것으로는 코드를 이해하기가 쉽지 않을 수 있습니다. 결국 이해하기 위해 내부의 코드를 봐야 한다는 의미가 되죠.
위의 코드의 경우에는 배열의 처음부터 끝까지 순회하는 로직입니다. 이런 경우에는, Array.forEach() 를 사용한다면 for문의 변수를 직접 값을 올려가면서 실수할 여지를 주지 않아도 되며, 배열의 처음부터 끝까지 순회한다는 것을 내부의 코드를 보지 않고도 알 수 있습니다. 와!
carNames.forEach((carName, index) => {
console.log(`${carName} : ${'-'.repeat(carDistances[index])}`);
});
물론 이걸로 제 의문이 풀렸다면 좋았겠지만... 이 이후에도 for문 사용에 대한 고민은 여전히 있었습니다. 아래에 내용을 더 적어보겠습니다.
Q. flag 변수를 두고 중간에 break를 써야 하는 경우에는 for, let 안 쓰고 어떻게 판단하죠?
여기 배열의 모든 값이 짝수인지를 판별하는 함수를 구현했다고 해 볼게요.
const isAllNumbersEven = (numbers) => {
let flag = true;
numbers.forEach((number) => {
if (number % 2 !== 0) {
flag = false;
}
});
return flag;
};
forEach() 의 경우, 에러를 직접 발생시켜서 처리하지 않는 한 도중에 break 등을 써서 반복문의 바깥으로 나올 수 없습니다. 그렇기 때문에, flag라는 변수를 let을 사용하여 선언하고, 배열을 순회하는 도중 홀수가 나올 경우 flag 값을 false 로 재할당을 하는 것으로 구현이 가능합니다.
하지만, let 은 재할당이 가능하기 때문에 언제든 예상치 못 한 이유로 값이 바뀔 수 있습니다. 항상 비추천되는 것은 아니지만, 적어도 값이 바뀌어야 하는 명확한 이유가 없다면 let 대신 const 를 사용하는 것이 좀 더 권장될 것입니다.
자동차 미션이 끝난 뒤에도, 저는 위의 함수를 let을 사용하지 않고 어떻게 구현할 수 있을지 고민해 보았습니다. 그러다가, Array.every() 를 찾았습니다.
const isAllNumbersEven = (numbers) => {
return numbers.every((number) => number % 2 === 0);
};
Array.every() 는 배열의 모든 값에 대해 특정 조건을 만족해야만 true, 아니면 false 를 반환합니다. 이렇게 하면, let을 선언하지 않고, for문도 사용하지 않고 충분히 구현이 가능하겠네요! 여담으로, 모든 값이 아닌 하나의 값이라도 만족할 경우 true를 반환하도록 구현하고 싶으시다면 Array.some() 을 사용할 수 있습니다.
Q. 배열 순회하는 거 말고, 단순히 여러 번 반복만 하는 경우에는 for 써야 하지 않나요?
단순 일정 횟수를 반복하는 로직은 자동차 미션에서도 수행해야 하는 기능이었고, 도저히 for문을 안 쓰고는 해결할 방법이 없어 보였습니다. 고민해 보았지만, 답을 찾지 못 해 파노에게 질문을 한 결과, 아래의 코드를 저한테 주셨습니다.
Array.from({ length: 10 }).forEach((_, index) => {
console.log(index);
});
바로 배열을 직접 선언해서 생성한 배열로 반복문을 돌리는 방법이죠. for문으로 구현할 때보다 가독성도 훨씬 좋아진 것 같군요! forEach를 사용할 때의 장점을 모두 챙길 수 있었습니다. 다만 여전히 하나의 의문이 남았는데...
Q. 엄청나게 많은 횟수를 반복하면 어떡하죠?
배열을 활용하여 반복문을 돌리고, 반복 횟수가 극단적으로 큰 값이라면 for문을 사용했을 때와 비교했을 때 메모리를 엄청나게 많이 잡아먹을 수 있다는 생각이 들었습니다. 그래서 이번에는 두 번째 미션인 로또 미션을 제출하면서 리뷰어 헤인티에게 질문했습니다.
답변은 간단했습니다. 바로 본래대로 for문을 사용하는 것입니다. 배열을 사용하고 Array.prototype의 메서드들을 사용하는 것은 대부분의 상황에서 가독성과 유지보수에서 우위를 점할 수 있지만, 반복문의 횟수가 극단적으로 많은 일부의 상황에서는 메모리에서 더 우위를 점할 수 있는 for문을 사용하는 것이 유리할 수도 있다는 것입니다.
for문 사용에 대해 리뷰어와 배틀(?) 을 하는 과정에서, 개발자들이 왜 이렇게 코드를 짜는지, 장점과 단점은 무엇일지에 대해 많이 고민해 볼 수 있는 시간이 되었던 것 같습니다. 그리고 역시 정답은 없다는 것을 다시 한 번 느꼈습니다. 상황에 따라 각 방법의 장점과 단점을 생각해 가면서 자신한테, 그리고 팀한테 적절하다고 생각하는 방법을 채택하면 되는 것이죠.
현명하게 입력받기, 그리고 현명하게 에러 처리하기
노드에서 콘솔로 입력을 받는 과정은 사용자의 입력을 기다려야 하므로 비동기 처리가 필요합니다. 프리코스에서는 이 비동기 처리를 구현할 수 있도록 MissionUtils 라이브러리가 제공되었지만, 본과정에는 그런 거 없고 직접 비동기 처리를 구현해야 했습니다.
프리코스에서는 테스트가 정해져 있어 async/await 를 사용하는 등 비동기 코드를 사용할 수 없었는데, 이 기회에 사용해 보게 됐습니다. 페어인 루루가 빠르게 학습하고 알려준 덕분에 async/await를 사용한 깔끔한 코드를 짤 수 있었습니다.
// 사용자로부터 입력을 받는 부분의 코드 (View)
readCarNames(messages) {
return new Promise((resolve, reject) => {
readlineInterface.question(messages, (carNames) => {
if (validator.isCarNamesInvalid(carNames)) {
reject(new Error(MESSAGES.carTextError));
}
const trimmedCars = trimmer.trimCarNames(carNames);
resolve(trimmedCars);
});
});
},
// View로부터 사용자의 입력값을 전달받으면 처리하는 코드 (Controller)
async #readCarText() {
try {
const carNames = await InputView.readCarNames(MESSAGES.carText);
this.#carRaceGame.createRaceUsingCarNames(carNames);
this.#readRepeatNumber();
} catch (error) {
OutputView.printMessage(error.message);
this.#readCarText();
}
}
대략적인 코드의 작동 원리를 여기에 적고자 합니다.
- 사용자가 값을 입력하는 부분의 기능은 비동기로 처리해야 하므로 결과값을 리턴할 때는 Promise 객체를 사용합니다.
- 사용자가 올바른 값을 입력했다면 resolve() 를 사용하고, 그렇지 않다면 reject() 를 사용하여 결과를 전달합니다. 이는 비동기 작업이 각각 성공/실패했음을 구분하기 위함입니다.
- Controller의 함수에 async가 붙어 있는데, 이 경우 함수의 리턴값은 Promise 객체가 됩니다. 또한, 이 함수 내에서 await를 사용하여 우리가 앞서 구현했던 입력받는 함수를 사용하면, 결과값을 받을 때까지 다음 코드가 실행되지 않고 기다리게 됩니다. 이를 이용하여 비동기 처리가 가능합니다.
- 비동기 작업이 실패하여 reject() 가 반환되면 catch문 내부의 코드가 실행되어 에러 메시지를 사용자에게 보여주고 입력을 다시 받게 됩니다.
에러 처리의 경우, 입력 중 에러가 발생할 경우 에러는 View의 각 위치에서 발생시키고, 처리를 error 객체를 통해 Controller에서 모아서 처리하도록 구현했습니다. 이렇게 구현한다면, 에러가 발생했을 때 조건을 여러 개 적을 필요 없이 어떤 에러가 발생하더라도, 어떤 에러가 추후에 추가되더라도 동일하게 처리할 수 있게 될 것입니다. 유지보수가 용이해지겠죠.
여담으로, 우리가 필요한 것은 message 뿐이므로 구조 분해를 이용해 message만 가져와서 처리하는 것도 가능합니다. 그렇다면 아래와 같이 코드를 작성할 수 있겠죠.
async #readCarText() {
try {
const carNames = await InputView.readCarNames(MESSAGES.carText);
this.#carRaceGame.createRaceUsingCarNames(carNames);
this.#readRepeatNumber();
} catch ({ message }) { // <--
OutputView.printMessage(message);
this.#readCarText();
}
}
느낀 점
프리코스 때의 미션과 비슷하면서도 신기하게 새로운 점들을 배워갈 수 있었습니다. 오프라인인데다 페어와 같이 의논하면서 미션을 진행하고, 리뷰어분들께 직접 질문을 할 수 있다보니 궁금한 점에 대해 좀 더 깊게 생각해 볼 수 있었고, 다른 크루의 생각도 들어볼 수 있었습니다. 신기한 게 미션은 분명 같은데 구현 방식이 정말 다양해요.
그리고 정해진 시간은 꼭 지켜야겠습니다. 같은 실수를 반복하지 않도록 조심해야겠습니다. 미리 준비하는 것도 좋은 방법이 될 수 있겠죠.
'우아한테크코스' 카테고리의 다른 글
우아한테크코스 5기 - Lv. 2 "페이먼츠" 미션 (0) | 2023.05.15 |
---|---|
우아한테크코스 5기 - Lv. 2 "다시, 점심 뭐먹지" 미션 (3) | 2023.04.24 |
온보딩 미션 - 연극 후기 (6) | 2023.02.27 |
우아한테크코스 5기 지원 후기 (2) | 2023.01.12 |
우아한테크코스 5기 - 최종 코딩테스트, 그리고 결과 (0) | 2023.01.12 |