이번 시간에는 대망의 점수 알고리즘 짜기!
이번 프로젝트의 핵심 기능이 될 것이다.
DFS 방식으로 알고리즘을 짜보려고 한다. 인접한 노드들의 개수를 합산하여 return하는 함수를 만들어서 합산값의 제곱을 finalScore 객체에 누적해줄 것이다.
그런데 문제가 생겼다. dfs 함수를 만들면 2중 for문을 만들어서 원소를 하나씩 순회하는데, 순회하면서 원본 배열의 숫자를 싹 다 0으로 만들어버리는 로직을 짰더니 화면에 0만 뜬다..
이럴 때 2차원 배열은 깊은 복사를 해야 한다. 알고리즘 풀면서 학습한 적이 있긴 한데.. 그렇긴한데... 나 개념 제대로 아는 거 맞아? 정리하고 로직을 다시 짜보자.
2개 이상의 참조자료형(객체, 배열 등)이 중첩되었다고 가정하고 설명하는 맥락이다.
내부에 중첩된 참조자료형(이하 '중첩된 객체'로 표현하겠음)이 있는 참조자료형 arr와 obj가 있다고 하자.
let arr = [1, 2, [3, 4], 5];
let obj = [{ coffee1 : "MegaCoffee"},{coffee2 : "Starbucks"}];
중첩된 객체를 가진 객체를 복사하면 한 단계의 복사본만 만든다.
배열: slice(), spread 연산자
const copiedArr1 = arr.slice();
console.log(arr === copiedArr1) // false
console.log(arr[2] === copiedArr1[2]) // true
const copiedArr2 = [...arr];
console.log(arr === copiedArr2) // false
console.log(arr[2] === copiedArr2[2] // true
arr의 복사본이 만들어졌지만(1단계만 복사), 중첩된 객체(배열)인 arr[2] = [3, 4]는 복사본이 만들어지지 않았다. arr[2], copiedArr1[2], copiedArr2[2]는 복사된 적이 없어 유일한 중첩된 객체를 가리키고 있다.
const copiedObj1 = Object.assign({},obj);
const copiedObj2 = {...obj};
console.log(obj === copiedObj1) // false
console.log(obj === copiedObj2) // false
console.log(obj[1] === copiedObj1[1]) // true
console.log(obj[1] === copiedObj2[1] // true
obj의 복사본이 만들어졌지만(1단계만 복사), 중첩된 객체인 { coffee1 : "MegaCoffee"}와 {coffee2 : "Starbucks"}는 복사본이 만들어지지 않았다.
arr[1], copiedObj1[1], copiedObj2[1]는 복사된 적 없어 유일한 중첩된 객체를 가리키고 있다.
객체가 중첩되어 있을 때 중첩된 객체의 복사본까지 전부 만듦.
const copiedArr_JSON = JSON.parse(JSON.stringify(arr));
console.log(arr === copiedArr_JSON); // false
console.log(arr[2] === copiedArr1[2]); // false
❗️ 단 이 방법을 함수가 중첩되어 있는 객체에 사용하면 함수는 복사되지 않고 null로 바뀐다.
const copiedArrContainsFn_JSON = JSON.parse(JSON.stringify(["1", function(){}]))
console.log(copiedArrContainsFn_JSON) // ["1", null]
const copiedArr_JSON = JSON.parse(JSON.stringify(arr));
console.log(arr === copiedArr_JSON); // false
console.log(arr[2] === copiedArr1[2]); // false
중첩된 객체인 arr[2]까지 모두 복사되었다.
깊은 복사본이 필요하다....! 그래서 lodash library를 사용해보기로 선택!
근데 require로 lodash library를 가져왔는데 밑에 ...이 뜨네? 뭔가 권고되고 있는 사항이 있나보다.
import로 바꿔라? 왜 바꾸라고 권장할까? 궁금해서 찾아보았다.
https://stackdiary.com/require-vs-import-in-javascript/
require는 node.js의 내장 함수이고, import는 es6 문법이며 더욱 모던하고 권장되는 방식이라고 한다. 그래서 import로 바꿈~
import cloneDeep from "lodash/cloneDeep";
이렇게 cloneDeep을 가져올 때는 상대경로를 지정해주어야 용량이 줄어든다는 흥미로운 블로그 글을 봐서 나도 상대경로를 잘 지정해주었다.
이제 lodash libary도 import해왔으니 지인짜로 알고리즘을 짜보자.
dfs 방식으로 짜보았다.
우선 저번 시간에 클릭한 유저의 상태에 따라 클릭한 숫자가 달라지게끔 구현해놓았으므로, 점수 계산 예시를 위해 내 맘대로 클릭해놓았다.
// 한 번 탐색할 때마다 user1인지 user2인지에 따라 score1 또는 score2를 count해서 반환할 것.
let score1 = 0;
let score2 = 0;
// 처음 함수를 호출할 때는 userNumber를 인자로 전달하지 않음. 재귀 호출일 때만 userNumber 인자로 전달.
const getUsersScoreByDFS = (i: number, j: number, userNumber?: number) => {
// out of range 처리
if (i < 0 || i >= 9 || j < 0 || j >= 9) return false;
// 빈 칸이면 함수 종료
if (deepCopiedTable[i][j] === 0) return false;
// 재귀 호출인데 맨 처음 할당한 userNumber가 아니고 상대편 숫자일 경우 함수 종료
if (userNumber && deepCopiedTable[i][j] !== userNumber) return false;
// 현재 칸의 숫자가 1이면 score1 1만큼 증가
if (deepCopiedTable[i][j] === 1) {
score1 += 1;
} else {
// 현재 칸의 숫자가 2이면 score2 1만큼 증가
score2 += 1;
}
// 재귀 호출이 아니라 신규 호출인 상태이고 현재 칸의 숫자가 1일 경우, userNumber에 1 할당.
if (!userNumber && deepCopiedTable[i][j] === 1) {
userNumber = 1;
} else if (!userNumber && deepCopiedTable[i][j] === 2) {
// 2일 경우 userNumber에 2 할당
userNumber = 2;
}
// 재귀 호출 전 방금 탐색한 칸의 숫자를 다시 방문하지 않도록 0으로 변경 (방문 처리)
deepCopiedTable[i][j] = 0;
// 상하좌우 탐색. 이 때 재귀호출이므로 현재 userNumber를 인자로 넘긴다.
getUsersScoreByDFS(i - 1, j, userNumber);
getUsersScoreByDFS(i + 1, j, userNumber);
getUsersScoreByDFS(i, j - 1, userNumber);
getUsersScoreByDFS(i, j + 1, userNumber);
// score1과 score2 둘 중 하나는 0일 것이다.
return [score1, score2];
};
// 최종 score를 저장하는 객체
const finalScore = { user1: 0, user2: 0 };
// 2중 for문을 이용하여 모든 칸의 인덱스를 순회하며 dfs 함수 호출
for (let i = 0; i < 9; i++) {
for (let j = 0; j < 9; j++) {
const result = getUsersScoreByDFS(i, j);
// result가 false일 때는 진입하지 않음.
if (result) {
console.log(i, j, result);
let [score1, score2] = result;
// 최종 점수에 누적해주는 부분
finalScore.user1 += score1 ** 2;
finalScore.user2 += score2 ** 2;
}
// 최종 점수에 누적이 끝났으므로 다시 0으로 만들어준다.
score1 = 0;
score2 = 0;
}
}
console.log("finalScore", finalScore);
와우 점수가 나왔따!
user1은 115점이고 user2는 135점이란다.
손으로 계산해봐도 맞구나!
나 핵심 기능 구현 성공한거야?!!
는 아직 할 게 많이 남았다 ㅎㅎㅎ..
그래도 뿌듯~!~!
이제 useGetScore라는 custum hook을 사용해서 점수 계산 로직을 숨기려고 한다.
import cloneDeep from "lodash/cloneDeep";
// useGetScore라는 custom hook을 만들어서 로직을 따로 숨겨놓았다. 이 때 매개변수는 user가 클릭한 말들이 숫자로 나타나 있는 table이다.
export const useGetScore = (
table: number[][]
): { user1: number; user2: number } => {
const deepCopiedTable = cloneDeep(table);
let score1 = 0;
let score2 = 0;
const getUsersScoreByDFS = (i: number, j: number, userNumber?: number) => {
if (i < 0 || i >= 9 || j < 0 || j >= 9) return false;
if (deepCopiedTable[i][j] === 0) return false;
if (userNumber && deepCopiedTable[i][j] !== userNumber) return false;
if (deepCopiedTable[i][j] === 1) {
score1 += 1;
} else {
score2 += 1;
}
if (!userNumber && deepCopiedTable[i][j] === 1) {
userNumber = 1;
} else if (!userNumber && deepCopiedTable[i][j] === 2) {
userNumber = 2;
}
deepCopiedTable[i][j] = 0;
getUsersScoreByDFS(i - 1, j, userNumber);
getUsersScoreByDFS(i + 1, j, userNumber);
getUsersScoreByDFS(i, j - 1, userNumber);
getUsersScoreByDFS(i, j + 1, userNumber);
return [score1, score2];
};
const finalScore = { user1: 0, user2: 0 };
for (let i = 0; i < 9; i++) {
for (let j = 0; j < 9; j++) {
const result = getUsersScoreByDFS(i, j);
if (result) {
console.log(i, j, result);
let [score1, score2] = result;
finalScore.user1 += score1 ** 2;
finalScore.user2 += score2 ** 2;
}
score1 = 0;
score2 = 0;
}
}
// 마지막에는 최종 점수 객체만 return한다.
return finalScore;
};
const finalScore = useGetScore(table);
console.log("custom hook으로 받은 finalScore", finalScore);
// -----------------------------------------------------bye👋
// let score1 = 0;
// let score2 = 0;
// const getUsersScoreByDFS = (i: number, j: number, userNumber?: number) => {
// if (i < 0 || i >= 9 || j < 0 || j >= 9) return false;
// if (deepCopiedTable[i][j] === 0) return false;
// if (userNumber && deepCopiedTable[i][j] !== userNumber) return false;
// if (deepCopiedTable[i][j] === 1) {
// score1 += 1;
// } else {
// score2 += 1;
// }
// if (!userNumber && deepCopiedTable[i][j] === 1) {
// userNumber = 1;
// } else if (!userNumber && deepCopiedTable[i][j] === 2) {
// userNumber = 2;
// }
// deepCopiedTable[i][j] = 0;
// getUsersScoreByDFS(i - 1, j, userNumber);
// getUsersScoreByDFS(i + 1, j, userNumber);
// getUsersScoreByDFS(i, j - 1, userNumber);
// getUsersScoreByDFS(i, j + 1, userNumber);
// return [score1, score2];
// };
// const finalScore = { user1: 0, user2: 0 };
// for (let i = 0; i < 9; i++) {
// for (let j = 0; j < 9; j++) {
// const result = getUsersScoreByDFS(i, j);
// if (result) {
// console.log(i, j, result);
// let [score1, score2] = result;
// finalScore.user1 += score1 ** 2;
// finalScore.user2 += score2 ** 2;
// }
// score1 = 0;
// score2 = 0;
// }
// }
// console.log("finalScore", finalScore);
아래 주석 처리된 코드들은 전부 custom hook으로 이사 갔고, 이제 GameBoardContainer 컴포넌트에서는 한 줄로 결과를 받아올 수 있다.
이제 Get Score 버튼에 onClickShowResult 함수를 생성하여 연결해주자.
Get Score 버튼이 있는 GameResult 컴포넌트에서 gameBoardStore에서 useSelector로 받아온 table을 useGetScore에 인자로 전달하여 finalScore에 접근이 가능하다.
//...import 생략
function GameResult() {
const userNames = useSelector((store: RootState) => store.userNames);
const table = useSelector((store: RootState) => store.gameBoardStore);
// custom hook인 useGetScore 호출
const finalScore = useGetScore(table);
const onClickShowResult = () => {
console.log("최종 점수", finalScore);
};
// 최종 점수 {user1: 73, user2: 84}
return (
<>
<User
userNumber={1}
src={GameResultImgURL.user1}
userName={userNames.userName1}
/>
<GameBoardContainer />
<User
userNumber={2}
src={GameResultImgURL.user2}
userName={userNames.userName2}
/>
<GetScoreBtnWrapper>
// 버튼에 onClick 연결
<GetScoreBtn onClick={onClickShowResult}>
{GET_SCORE_BUTTON_PHRASE}
</GetScoreBtn>
</GetScoreBtnWrapper>
</>
);
}
export default GameResult;
드디어 점수 알고리즘을 짜서 버튼에 onClick 연결하는 것까지 마무리했다!!