본문 바로가기
TIL&WIL

241218 TIL - reduce(), concat(), 개인과제03

by 나노다 2024. 12. 18.

reduce()

배열의 모든 요소를 모아서 하나의 값으로 만들 때 사용!! 

모든 요소를 더해서 누적합을 구할 수도 있고, 모든 요소를 모두 스캔한 후 가장 작은 값을 구할 수도 있음

서로 다른 요소들의 각 개수를 세볼 수도 있고, "하나의 값"을 만든다는 것만 기억하면 활용도 매우 높은 메서드!!


reducer

reduce()의 콜백함수를 reducer라 부른다! reducer는 배열의 모든 요소당 한 번 씩 실행됨!!

Array.reduce((acc,cur,idx,arr) => {}, initialValue);

reducer의 매개변수는 순서대로 누적된 값 acc, 현재 순회중인 요소 cur, 현재 요소의 인덱스 index, 순회중인 배열 arr

initialValue

acc의 초기값을 설정하는 매개변수!

초기값을 설정하지 않으면, 배열의 첫 요소가 acc의 첫 값이 되고, 두 번째 요소가 cur의 첫 값이 된다!!

  • 빈 배열인데 초기값도 설정 안 하면 TypeError를 만날 수 있다 ^_^
  • 빈 배열이어도 초기값은 설정했으면 reducer 실행 없이 바로 설정한 초기값을 반환한다!!
  • 요소가 하나 있는 배열인데 초기값 설정 안 했으면 reducer 실행 없이 바로  그 요소를 반환한다!!

reducer 작동 방식

reducer 내부에서 return 해준 값은 다음 요소 차례의 acc가 됨!

모든 요소 순회가 끝나면 마지막 return 값이 반환됨!


추가 정보들

  • 배열 요소들이 객체라면, 초기값 설정을 해주는 것이 안전하다!! 첫 acc가 객체 통째로 들어가기 때문에 연산에서 높은 확률로 TypeError를 만날 수 있다 ^_^
  • 배열 요소들이 배열이라면(=중첩배열), 전개구문을 활용하든, concat()을 쓰든 배열을 다 합치는 데 reduce()를 활용 가능

배열 요소 별 중복 개수 세보기

var names = ["Alice", "Bob", "Tiff", "Bruce", "Alice"];

var countedNames = names.reduce(function (allNames, name) {
  if (name in allNames) {
    allNames[name]++;
  } else {
    allNames[name] = 1;
  }
  return allNames;
}, {});
// countedNames is:
// { 'Alice': 2, 'Bob': 1, 'Tiff': 1, 'Bruce': 1 }

누산기를 객체화하고, 각 key를 이름들로 설계한다!

reducer에서는 만약 순회하는 요소가 누산기 객체에 이미 있는 key라면 1을 더하고,

없는 key라면 새로 1 값을 넣어 만들어주도록 하고, 최종적으론 각 이름의 개수를 표현하는 객체를 반환한다!!

배열 내 중복 요소 제거해보기

let arr = [1, 2, 1, 2, 3, 5, 4, 5, 3, 4, 4, 4, 4];
let result = arr.sort().reduce((accumulator, current) => {
  const length = accumulator.length;
  if (length === 0 || accumulator[length - 1] !== current) {
    accumulator.push(current);
  }
  return accumulator;
}, []);
console.log(result); // [1,2,3,4,5]

우선 sort()로 정렬을 해주고, reduce()를 실행한다! 

만약 누산기 배열에 아무 요소가 없거나(=첫 순회거나),

마지막으로 들어간 요소가 현재 순회중인 요소가 아니라면(=중복값이 아니라면)

누산기 배열에 값을 push해서 모으고, 최종적으로 고유한 값들만 가진 배열로 재탄생시킨다!!

Set과 Array.from()을 사용할 수 있는 환경이라면, Array.from(new Set(처리할배열))로도 중복 없앨 수 있다!!
Set은 배열과 유사하지만 중복값을 허용하지 않기 때문에,
배열을 Set으로 만들었다가 다시 배열로 만들면 중복값이 "내가 사라져볼게~"한다!

concat()

배열에서의 concat()

const array1 = ['a', 'b', 'c'];
const array2 = ['d', 'e', 'f'];
const array3 = array1.concat(array2);

console.log(array3);
// Expected output: Array ["a", "b", "c", "d", "e", "f"]

두 개 이상의 배열을 병합할 때 활용한다!!

합칠 배열에서 concat() 메서드를 붙이고, 매개변수로 붙일 배열들을 넣는 방식

기존 배열 건드리지 않고, 새 배열 반환함!! (얕은 복사본)


문자열에서의 concat()

const str1 = 'Hello';
const str2 = 'World';

console.log(str1.concat(' ', str2));
// Expected output: "Hello World"

console.log(str2.concat(', ', str1));
// Expected output: "World, Hello"

매개변수가 문자열이 아니면 문자열로 변환시켜 합침

문자열에는 이거 쓸 바에 +나 += 쓰는 게 몇 배는 빠름!! ← 고...공식문서에서 이럴 정도면 쓰지 말아야 한다고 생각해요...


개인과제) 오늘 한 작업

[fix] gameEnd 검증로직 개편

  • 점수 획득 방법이 강의 코드와 달라졌기 때문에 이에 대한 최적화

[update] 이벤트 요청응답 비동기화

  • sendEvent 함수 내에 socket.once 로직 추가
  • getItem 메서드에서 then을 통해 이벤트에 대한 응답 data로 전달받아 점수 획득 처리

[del] item-unlock 관련 코드 제거

  • item-unlock.json 파일 제거 (item 테이블에서 stage 필드를 추가해 기능 대체)
  • 파일 load하던 로직에서 unlock 부분 제거

[add] 기록 갱신 Broadcast

  • 서버에서 하이스코어 관리하도록 high-score-model.js 구현
  • gameEnd 핸들러에서 기록 갱신시 broadcast
  • Connection 메세지에 highScore 현황 담아 보냄
  • 클라의 소켓에서 받은 후 export
  • Score.js 및 index.js 에서 기존 highScore 관련 로직 제거 (localstroage 관련 부분)
  • Score 클래스의 draw에 import한 highScore 활용하도록 변경

개인과제) 오늘 만난 문제

점수 검증에서 다시 만난 어제의 원수와 얘를 무찌르니 새로 만난 두 적수...

관련 개념들 : Promise / Socket.IO listner methods / reduce()

발단 : 문제를 덮어두면 언젠가 터진다

 어제 조언도 들었겠다 호기롭게 재도전을 해보려다가, 우선 과제 요구사항을 먼저 구현하는 게 맞지 않을까? 하는 생각이 들어서, 덮어두기로 결정했다... 검증이 안 되는 건 아니었기에, 클라에서도 적용하고자하는 거는 내 욕심이었으니까!! 그렇게 gameEnd 핸들러를 수정하던 와중, 검증이 너무 잘 돼서 자꾸 fail 메세지를 만나는 상황에 봉착했다...

myScore는 클라 기준으로 계산한 점수, 즉 아이템 획득에 대한 정보였고, totalScore는 서버에 기록된 획득 아이템들의 id를 바탕으로 데이터 테이블에서 계산한 점수다!! 그런데 보다시피 점수가 90점 가량 차이가 나고 있었고, 마지막 스테이지의 코인 하나가 주는 점수가 50점인 걸 생각하면, 뭔가 단단히 잘못된 상황인 게 분명했다!!

 차이의 원인은 이 부분이었는데, 서버에선 검증 절차가 잘 작동하고 있었기 때문에, 부적절한 획득인 경우 당연히 기록을 하지 않았고, 즉 setItem 함수를 실행하지 않았고, 당연하게도 아이템 획득에 대한 정보는 서버와 클라가 서로 다를 수밖에 없었던 것이다! 우연히 모든 획득이 검증을 통과하거나 사소하게 몇 개 걸릴 때를 제외하고는 무조건 점수 검증이 실패할 수밖에 없었던 구조!!

 여기저기 발품을 팔고 다닌 덕에 동료 분의 Socket.js를 엿볼 수 있었는데, 특이했던 점이 이벤트 메세지를 보내는 함수에서 동시에 그에 대한 응답을 받고 있던 것이었다! 또 on이 아닌 once라는 메서드를 사용하고 계시길래, 무슨 차인가 하고 봤더니 once는 말 그대로 딱 한 번의 응답만 받아주는 리스너였다! 

 또한, emit과 once를 하나의 Promise 안에서 처리해줌으로써, 요청에 대한 명확한 응답을 바로바로 받을 수 있었고, 특히 내가 필요했던 아이템 획득에 대한 검증 결과를 받아오기 위해서 21번 핸들러의 응답인 경우 resolve를 통해 값을 넘기도록 설계해보았다!!

 그리곤 애증의 getItem 메서드로 돌아와, 이제는 Promise를 반환하게 된 sendEvent 실행 부분 뒤에 then을 붙여 응답 결과를 넘겨오도록 했고, 여기서 만약 status가 success면 점수를 올려주도록 구현해본 것이다!!!

 사진이 잘 안 보이지만 ㅠㅠㅠ 아주 감격적인 순간!!! 로그를 보면 첫 스테이지에서 아이템 획득을 다섯 번 했고, 한 번은 검증을 실패했다!! 첫 스테이지의 아이템은 10점을 주는데, 어제 같은 상황이었다면 서버는 40점, 클라는 50점이 되었겠지만, 드디어!!!!!!!!!! 클라에서도 검증 실패 점수는 올려주지 않는 감격적인 순간!!!!! 너무 신나서 잠시 여운을 즐기다가, 우선 이 문제를 당면하게 됐던 시작으로 돌아가, gameEnd 함수를 마무리하기로 했다!!

위기와 절정 : 클라이언트와 서버에서 각각 알고 있는 플레이 타임이 다르다?!

 서버에서도 정상적으로 점수가 일치하는 순간!!!

게임 종료 검증도 이제 success를 뱉어주는, 잘 된다고 그렇게만 믿었던 바로 그 순간....

 그렇게 새로운 문제와 만난다!!!! ㅋㅋㅋㅋㅋㅋㅋ

바로 게임 오버와 클리어의 차이에서 오는 문제였는데, 게임 오버인 경우 어차피 최대 플레이 시간을 채우지 않기 때문에 별다른 검증이 없지만, 클리어인 경우 실제 플레이 시간과 이론상 가능한 최대 플레이 시간을 비교하는 절차가 있었다! 그러니 그 둘을 비교해보아야겠지요....

 클라의 실제 플레이 시간에는 문제가 없었고, 원인은 서버에, 정확히는 reduce 메서드 활용에 있었다! 첫 원인은 reducer에 초기값 설정을 안 해줬던 것!! 저 stageTable은 데이터 테이블을 load한 것으로, 요소가 전부 객체인 배열이다... 그러니 순회하는 요소들의 자료형은 다 객체일 것이고, 당연히 객체에다가 숫자형을 더하고 앉았으니, 숫자가 차곡차곡 붙고, 우리 자바스크립트의 자동형변환으로 인해 문자열이 뱉어졌던 것!! 그리고 두 플탐의 비교도 당연히 형변환돼서 문자열 간 비교가 됐을 것이고, 저 [object Object]가 늘 이겼던 게지...

 초기값을 설정해주고 다시 체크를 해보니 정상적으로 숫자로 돌아오긴 했는데, 열심히 클리어를 테스트해보는데도 여전히 같은 오류가 뜨길래 뭘까 했는데......

 조건이 OR 조건이라 애초에 클리어 시 무조건 fail이 뜨는 상황이었기에 고쳐주었다... 나는 감자에요 그냥...

결말 : 기초 탄탄!!!!

 그렇게 해피 엔딩!! 우선 비동기 문제가 해결돼서 너무 기뻤고!! then을 제대로 활용해본 적이 없었는데 이번에 좀 연습이 된 것 같아 좋았다. 또 이외의 문제는 역시나 기본적인 곳들에서 온다는 거 다시금 깨닫고..... 자료형, 기본 문법 늘 신경쓰자!!


추가로 알게 된 것

  • [object Object]는 객체를 문자열로 변환시켰을 때 나오는 겨!!

'TIL&WIL' 카테고리의 다른 글

241224 TIL - JSON.parse()  (0) 2024.12.24
241219 TIL - 개인과제04  (2) 2024.12.20
241217 TIL - 개인과제02, shift()  (1) 2024.12.17
241213 TIL - 개인과제01, 음수인덱스  (0) 2024.12.13
241212 TIL - 개인과제 기획  (0) 2024.12.12