PR링크 :https://github.com/woowacourse-precourse/javascript-racingcar-6/pull/14
미션링크: https://github.com/woowacourse-precourse/javascript-racingcar-6
프리코스를 진행하기 전 스스로 세운 목표가 있었는데 단순히 코딩테스트 문제처럼 문제해결이 아니라 다양한 방면을 적용해서 미션을 해결하는 것이였다.
1주차에는 객체지향적으로 미션을 해결했다면 이번에는 함수형 프로그래밍으로 미션을 진행하였다. 정리하자면 함수형 프로그래밍으로 미션을 해결하고 공통피드백, 미션 요구사항을 최대한 지키는 것이 이번 과제의 목표였다.
공통 피드백 중 이것은 꼭 지켜야 겠다고 한 것을 정리한다.
우테코 디스코드 채널에서 피어분이 공유하신 세팅방법을 참고하였다.
ESLint / Prettier 세팅
module.exports = {
env: {
node: true,
es2021: true,
jest: true,
},
extends: ['airbnb-base', 'plugin:prettier/recommended'],
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
rules: {
'max-depth': ['error', 2],
'import/extensions': ['off'],
'no-constructor-return': 'off',
'class-methods-use-this': 'off',
'no-return-assign': 'off',
},
};
미션을 진행하면서 ESLint에 맞지 않은 것을 예기치않게 사용할 필요가 있었다. 그래서 rules부분에 사용자 정의로 rules를 off시켰다.
그리고 code Formatter를 사용해보니 자동으로 변경되는 것은 좋지만 바뀌지 않는 것이 좋은데 계속 변경되어서 이 기능은 사용안하기로 했다.
이 또한 우테코 디스코드 채널에서 알게 되었다.
우테코 디스코드 채널 없었으면 이렇게 많은 성장을 못했을 것 같다.
😀 좋은 커밋 메시지 작성법
제목과 본문을 구분한다.
커밋 메시지는 제목과 본문으로 구분하는 것이 좋아요. 제목은 변경 사항의 요약을, 본문은 변경 사항의 세부 내용을 설명합니다.
제목은 50자 이내로 작성한다.
제목은 50자 이내로 작성하는 것이 좋아요. 제목이 너무 길면, 한눈에 파악하기 어려워집니다.
제목은 명령문으로 작성한다.
제목은 명령문으로 작성하는 것이 좋아요. 명령문으로 작성하면, 변경 사항을 명확하게 표현할 수 있습니다.
본문은 72자 이내로 작성한다.
본문은 72자 이내로 작성하는 것이 좋아요. 본문이 너무 길면, 읽기가 어려워집니다.
커밋 유형을 명시한다.
커밋 메시지에는 커밋 유형을 명시하는 것이 좋아요. 커밋 유형을 명시하면, 변경 사항의 종류를 쉽게 파악할 수 있습니다.
😀 커밋 유형
커밋 메시지에는 커밋 유형을 명시하는 것이 좋아요. 커밋 유형을 명시하면, 변경 사항의 종류를 쉽게 파악할 수 있습니다. 일반적으로 사용되는 커밋 유형은 다음과 같습니다.
feat: 새로운 기능 추가
fix: 버그 수정
docs: 문서 수정
style: 코드 스타일 수정
refactor: 코드 구조 개선
test: 테스트 추가 또는 수정
chore: 기타 변경 사항
이번 프리코스 동안 꼭 얻고 싶은 것은 클론 코딩 하는법이다. 내가 생각하는 클론 코딩은 다른 개발자가 나의 코드를 읽었을 때 주석문 없이도 코드만 보고서 쉽게 이해할 수 있는 코딩이라 생각한다. 아직은 서툴지만 주석 없이 변수명, 함수명, Airbnb 컨밴션 등으로 쉽게 이해할 수 있도록 연습하고 있다.
gitignore의 다음과 같이 git에 올리지 않을 것을 정리해두었다.
그런데도 git에 올라갔다. 정확한 이유는 모르겠지만 아직 git에 대해 지식도 부족하다는 것을 느꼈다.
미션을 진행하면서 가장 중요하게 생각한 부분이다.
👉 받은 인자 외에 다른 외부의 상태에 영향을 끼치않고 리턴값 외에는 외부와 소통이 없다.
1. 항상 동일한 인자를 주면 동일한 결과를 리턴function add(a,b) { return a+b; }
순수함수가 아닌 예
2. 동일한 인자를 주었을때 상황에 따라 결과가 달라지는 함수var c=20; function add2(a,b){ a+b+c; }
즉 외부의 변수값을 변경하지 않아야 한다. 이 말은 Class에서 필드 변수는 모든 함수에서 참조하는 것과는 반대로 함수형 프로그래밍에서는 최대한 필드함수 개념이 없어야 한다고 생각했고 함수 return값으로 변수를 해결해야 한다고 생각했다.
function getTurnOverResult(racingCarList) {
const turnOverRacingCarList = racingCarList.map(carObject =>
decideMoveOrStop() ? getCarNewObject(carObject.carName, carObject.moveCount + 1) : carObject,
);
return convertObjectListFreeze(turnOverRacingCarList);
}
내가 구현한 함수 중 일부인데 주로 고차함수를 사용하였다. 미션 요구 사항에서 랜덤넘버가 4이상일 때 차는 움직일 수 있는데 이 로직에 대한 함수이다. racingCarList에 있는 Car객체를 돌면서 dedcideMoveOrStop함수를 실행하여 랜덤넘버가 4이상인지 확인 후 맞다면 움직이고 그렇지 않으면 정지하는 것이다. 이렇게 한 후 나온 결과값을 새로운 배열로 리턴하고 객체 동결화하는 내용이다.
외부에 변수를 바꾸는 것이 아닌 로직 수행 후 해당 결과를 리턴하는 순수함수 느낌으로 구현하였다.
🧐 불변성이란?
원본 데이터 구조를 변경하는 대신 그 데이터의 구조의 복사본을 만들고, 그 중 일부를 변경한다
그리고 원본 대신 변경한 복사본을 사용해 필요한 작업을 진행한다.
1. 불변성을 만족하지 못하는 코드var addColor = function(title, colors){ colors.push({title: title}) return colors; }
2. 불변성을 만족하는 코드
///Array.concat 사용 const addColor = (title, array) => array.concat({title}) ///배열 스프레드 연산자 사용 const addColor = (title, list) => [...list, {title}]
즉 원본 배열 colors에 객체를 넣는 것이 아니라
새로운 배열에 spread연산자를 활용하여 list 원소와 객체를 넣는 개념이다.
export function getCarNewObject(paramCarName, paramMoveCount) {
return Object.freeze({
carName: paramCarName,
moveCount: paramMoveCount,
});
}
export function convertObjectListFreeze(objectList) {
return Object.freeze(
objectList.map(carObject => getCarNewObject(carObject.carName, carObject.moveCount)),
);
}
export function convertListFreeze(list) {
return Object.freeze([...list]);
}
내가 구현한 함수인데 이것은 불변성을 보장하는 함수이다. Object.freeze를 하면 객체가 동결된다. 하지만 내 데이터 구조상 중첩 Object가 나온다. 이럴 때에는 안에 있는 것 전부 동결해주고 밖에 있는 Object도 동결해주어야 한다.
마찬가지로 배열도 spread연산자를 활용하여 새로운 배열을 만들고 이것을 동결화 시켰다.
이로서 모든 데이터는 불변성을 가진다고 보장할 수 있었다.
아무래도 함수형 프로그래밍하기 위해서 필수적으로 필요한 고차함수를 사용하려고 노력하였다. 그 중에서 내장 고차함수 filter, map, reduce, forEach등을 사용하여 원본 데이터는 건들지 않고 새로운 데이터를 반환하도록 하였다.
일단 재귀를 왜 사용하는지 궁금하였다.
함수형 프로그래밍에서의 재귀 사용 이유
첫째, 함수형 프로그래밍은 불변성(immutability)을 지향합니다. 즉, 데이터의 상태를 변경하지 않고 새로운 데이터를 생성하여 반환하는 것을 선호합니다. 이러한 관점에서 보면, 반복문보다는 재귀가 더 자연스럽게 느껴질 수 있습니다. 왜냐하면 반복문은 내부 상태를 계속해서 변경하기 때문입니다.
둘째, 재귀는 문제 해결 방식이 직관적입니다. 어떤 문제를 더 작은 하위 문제로 나누어 해결하는 방식이기 때문에, 코드의 가독성을 높여줄 수 있습니다.
물론 재귀를 사용하면 Stack을 사용하기 때문에 대용량 데이터를 처리하기에는 좋지 않지만 함수형 프로그래밍 공부한다는 목적으로 반복문보단 재귀를 사용하였다.
function racingCarMove(racingCarList, gameCount) {
if (gameCount === 0) return racingCarList;
const turnOverRacingCarList = getTurnOverResult(racingCarList);
printRacingCar(turnOverRacingCarList);
return racingCarMove(turnOverRacingCarList, gameCount - 1);
}
나의 코드이다. 턴이 끝날 때마다 재귀함수를 실행시켜서 새로운 배열을 리턴받도록 하였다.
(내코드)
export const moveLimitNumber = 4;
export const CarNameLimitLength = 5;
export const randomMinNumber = 0;
export const randomMaxNumber = 9;
export const progressBar = '-';
(리뷰어 코드)
export const GAME_RULE = {
moveLimitNumber : 4,
...
}
상수를 쓸 때 안에 넣고 카멜케이스를 쓰는 것이 좋을 것 같다는 리뷰를 받았다.
기존에는 View 모델에서 유효성 검사 로직을 진행하였다. 하지만 이것은 서비스로직에 조금 더 가깝다는 리뷰를 받았다.
(기존 코드)
const GAME_MESSAGE = Object.freeze({
INPUT_CARNAME: '경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)\n',
INPUT_GAMECOUNT: '시도할 횟수는 몇 회인가요?\n',
OUTPUT_TITLE: '\n실행결과',
OUTPUT_CARRACING_MOVE: (carName, moveCount) => `${carName} : ${progressBar.repeat(moveCount)}`,
OUTPUT_WINNERCARNAME: carName => `최종 우승자 : ${carName.join(', ')}`,
});
inputCarName처럼 카멜케이스로 작성하는 것이 권장된다는 리뷰를 받았다.
export function getCarNewObject(paramCarName, paramMoveCount) {
return Object.freeze({
carName: paramCarName,
moveCount: paramMoveCount,
});
}
export function convertObjectListFreeze(objectList) {
return Object.freeze(
objectList.map(carObject => getCarNewObject(carObject.carName, carObject.moveCount)),
);
}
=>
(리뷰어 코드)
export function getCarNewObject({ carName, moveCount }) {
return Object.freeze({ carName, moveCount });
}
객체분할을 사용하면 더욱 깔끔해질 것 같다는 리뷰를 받았다.
함수형 프로그래밍을 이론은 어느 정도 알고는 있었지만 실습을 한 적은 없었다. 하지만 이번 프리코스 미션을 함수형 프로그래밍으로 실습을 함으로써 많이 배울 수 있었던 것 같다. 그리고 다른 분들의 코드를 보니 테스트코드를 정말 잘 짜신분들이 많았다.
이번 주차에는 테스트코드의 비중을 크게 두지 않은 것 같다. 다음 주차에는 테스트 코드의 비중을 높게 두어야 겠다고 생각했다.