4주차를 시작하기 전에
3주차 공통 피드백을 공부하고 넘어가려고 한다. 피드백에 어떤 의도가 숨어있을지 나름대로 생각해보자.
함수(메서드) 라인에 대한 기준
3주차 과제에는 라인제한이 15줄이였는데 이번 4주차 과제는 라인제한이 10줄이었다. 함수 또는 메소드로 쪼개는 것을 더 엄격하게 보려는 것으로 파악된다.
아래 코드는 프리코스를 진행하시는 분의 코드를 발췌한 것이다. 나는 이 코드가 좋다고 생각했다. 위에서 아래로 코드를 막히는 부분없이 읽을 수 있었고, 관련 메소드 끼리 붙어있어 코드를 파악하기 편했다. 컨트롤러 부분은 이렇게 흐름을 쉽게 파악 할 수 있게 작성할 생각이다.
비즈니스 로직과 UI 로직을 분리한다.
비즈니스 로직과 UI 로직을 분리하게되면 관심사가 분리되어 유지보수에 용이하다. 예를 들면 UI 부분의 에러가 날 경우 분리해둔 UI 로직만 검사하면 되기때문이다.
객체의 상태 접근을 제한한다
아래와 같이 필드를 private 로 선언하여 외부에서 접근하는 것을 제한할 수 있다.
class WinningLotto {
#lotto
#bonusNumber
constructor(lotto, bonusNumber) {
this.#lotto = lotto
this.#bonusNumber = bonusNumber
}
}
그렇다면 왜 외부에서 class 의 필드에 접근하는 것을 제한하는게 좋을까?
아래 처럼 winningLotto 인스턴스를 생성한 후 lotto 필드를 직접적으로 수정할 수 있다. 이렇게 접근해버리면 어디에서나 필드의 데이터가 변경될 수 있기 때문에 이를 막기 위해 private로 접근을 제한하는 것이다.
const winningLotto = new WinningLotto()
winningLotto.lotto = [1,2,3,4,5,6]
그러면 만약 필드데이터를 변경해야하는 상황에는 어떻게 해야하나?
setter를 만들어주는 것이다. 아래 처럼 말이다. 이렇게 되면 데이터를 변경하되 프로그래머가 의도한대로 값이 할당되게 할 수 있다.
아래 예제에서는 value 가 0 일때는 값이 할당되지 않도록 early return 을 해준 모습이다.
class Test {
#number
constructor(number) {
this.#number = number
}
getNumber(){
return this.#number
}
setNumber(value){
if(value===0) return
this.#number = value
}
}
그런데 또 궁금증이 생겼다. 외부에서 직접할당 되는 것은 문제를 야기할 수 있어 OK, 그래서 set 예약어 또는 setter 메소드를 사용하는데
set 예약어와 setter 메소드중에서 선호되거나 더 나은 방법이 뭘까? 라는 생각을 했다.
아래는 같은 기능을 하지만 set 과 setter 메소드를 사용하여 class 를 구현한 코드이다.
class Test {
#number
constructor(number) {
this.#number = number
}
getNumber(){
return this.#number
}
setNumber(value){
this.#number = value
}
}
class Test2 {
#number
constructor(number) {
this.#number = number
}
getNumber(){
return this.#number
}
set number(value){
this.#number = value
}
}
const test = new Test(7)
test.setNumber(10)
console.log(test.getNumber()) // 10
const test2 = new Test2(7)
test2.number = 10
console.log(test2.getNumber()) // 10
프리코스 커뮤니티에 고민을 공유했다. 고마운 분들의 답변으로 내 생각을 정리할 수 있었다.
우선 airbnb 에서는 setter 메소드를 사용하는 것을 set 보다 권장하고 있었다.
그리고 set 은 직접할당 처럼 보이기도 해서 setter 메소드를 만들어주는 것이 명확한 표현이 된다는 근거를 가지게 되었다.
객체는 객체스럽게 사용한다
아래의 BadLotto는 #number의 데이터를 get만 하고 실제 조작은 외부에서 이루어진다.
class BadLotto{
#numbers
constructor(numbers) {
this.#numbers = numbers
}
getLotto(){
return this.#numbers
}
}
const catchEvenNumber = (prev,number) => {
if(number%2 === 0) prev.push(number)
return prev
}
const badLotto = new BadLotto([1,2,3,4,5,6])
const lotto = badLotto.getLotto()
const EvenNumberLotto) = lotto.reduce(catchEvenNumber,[])
console.log(EvenNumberLotto) // [2,4,6]
피드백에서는 이러한 모델의 데이터 가공은 해당 객체 내에서 이루어지게 하길 원한다고 이해했다.
아래 코드는 GoodLotto 라는 객체 내부에서 짝수 배열을 리턴하게 된다.
class GoodLotto{
#numbers
constructor(numbers) {
this.#numbers = numbers
}
getLotto(){
return this.#numbers
}
getEvenNumberLotto(){
return lotto.reduce(catchEvenNumber,[])
}
catchEvenNumber (prev,number) {
if(number%2 === 0) prev.push(number)
return prev
}
}
const goodLotto = new GoodLotto([1,2,3,4,5,6])
console.log(goodLotto.getEvenNumberLotto())
구현 과정
도메인 분리
구현이 완료된 코드를 보니 Bridge라는 도메인에 많은 정보가 저장되고 있다는 것을 파악했다.
const { Console } = require('@woowacourse/mission-utils');
const init = {
blueprint: [],
length: 0,
upperBridge: '',
lowerBridge: '',
turn: 0,
};
class Bridge {
#bridge;
constructor() {
this.#bridge = { ...init };
}
get data() {
return this.#bridge;
}
setData(key, value) {
this.#bridge = { ...this.#bridge, [key]: value };
}
retry() {
this.#bridge = { ...init, blueprint: this.#bridge.blueprint, length: this.#bridge.length };
}
}
module.exports = Bridge;
다리 게임의 정답배열, 결과를 저장하는 필드, 몇번째 다리를 건너는지, 재시도횟수는 몇번인지와 같은 모든 데이터가 Bridge에 저장되어 있었고 이를 분리해야겠다고 생각했다. 그 이유는 너무 많은 정보를 담고있어 해당 도메인이 무엇과 관련된 객체인지 알 수가 없었고 분리하는 것이 일관성 부분이나 코드를 처음 보는 3자의 입장에서 볼때 쉽게 분석이 가능할 것이라고 생각했기때문이다. 함수가 하나의 기능만 하도록 나누듯, 클래스 또한 관련된 데이터에 맞게 분리하자고 생각했다.
어떤 데이터를 저장하고 가공하는 객체인가 라는 생각에 맞게 도메인을 크게 3개의 파일로 분리하였고, 분리된 도메인은 다리 게임의 정답을 저장하는 도메인 Bridge와 현재 다리의 index와 재시도 횟수를 저장하는 도메인 BridgeGame, 출력 결과 데이터를 저장하는 StepResult 였다. 하나의 class에 집중되어있던 메소드를 각각의 도메인으로 분리하고 테스트를 실행시켜 다시 정상적으로 동작하는 것을 확인할 수 있었다.
캡슐화
도메인 객체에 대한 캡슐화를 수행했다. 특히 bridgeGame의 turn, tryCount 데이터가 바뀌어버리면 다른 결괏값이 출력이 되기 때문에 private 필드로 선언을 바꾸어주고 외부에서는 메소드를 통해서만 수정이 가능하도록 변경하였다.
JSDoc
리팩토링을 하면서 가장 고민했던 것은 JsDoc을 사용하여 주석을 남길 것인지 아닌지였다. 하지만 결과적으로는 JsDoc을 사용하지 않기로 하였다. 과제의 템플릿 코드를 통해 JsDoc을 알게 되었고 메소드의 기능과 파라미터, 리턴의 타입을 명시하면 가독성이 좋아지겠다고 생각이 들었다. 그러나 프리코스를 진행하면서 변수명과 함수명 그리고 클린 코드를 통해 어떠한 동작이 이루어지는지 알 수 있게끔 하는 것이 더 중요하다고 생각하여 주석에 의존하지 않고 가독성이 좋은 코드를 짜기 위해 노력했다. 시간 들여 작성했던 JsDoc 코드가 아깝기는 하였지만 제 코드를 믿고 과감하게 삭제하였다. 그래도 이번 기회를 통해 배우게 된 JsDoc을 사용하면 코드만을 통해서 설명하기 어려운 로직이 있을 때 코드의 가독성을 높일 수 있게 되었다.
소감
끝으로 4주간의 과정을 거치면서 얻은 큰 수확은 코드로 소통하는 방법을 배운 것이다. 내 코드를 타인이 봐도 쉽게 이해할 수 있게, 코드에 대한 자신감을 얻을 수 있었던 과정이었다. 과제를 수행하면서 아쉬운 점도 많았고 해결이 안될 때는 속상하기도 했지만 그러한 시행착오들이 저를 더 성장시켜준 것 같다. 앞으로도 부지런히 배워 좋은 개발자가 될것이다.
'공부기록 > 웹 개발' 카테고리의 다른 글
[React Query] staleTime vs cacheTime (2) | 2023.01.26 |
---|---|
[우아한 테크코스 프리코스] 1차 탈락 (4) | 2022.12.15 |
[우아한 테크코스 프리코스] 3주차 (0) | 2022.11.15 |
[우아한 테크코스 프리코스] 2주차 (1) | 2022.11.09 |
html dataset 을 사용한 css switch case (1) | 2022.10.11 |