[TIL] 24.10.05 SAT

GDORI·2024년 10월 5일
0

TIL

목록 보기
62/79
post-thumbnail

지난 게시글 때 해결하지 못한 점수검증

(Feat. 비동기 처리는 중요해요)

씁... 슬했다. 왜 계산식도 맞고 문제 없는 것 같은데 왜 안되는지 5시간을 헤맸다.

결론만 말하자면, 서버의 문제가 아닌 클라이언트측 코드에서 비동기 처리를 해주지 않아 생긴 문제였다. 왜 안되지 하고 서버시간으로 계산하던 것도 클라이언트에서 보낼 때 timestamp로 계산하게끔 처리했는데도 처음에는 괜찮다가 후반쯤부터 틀어지는 것을 식별했다.

우선 조치된 코드를 올리겠다. 물론 이것도 틀린 답일수도 있을 것 같긴 한데,

서버 측 점수 검증


export const moveStageHandler = (userId, payload) => {
  // 최근 스테이지 로드
  const currentTime = payload.time;
  let currentStage = getStage(userId);
  const { stage, item } = getGameAssets();

  if (!currentStage.length) {
    return { status: "fail", message: "No stages found for user" };
  }

  // 스테이지 오름차순 정렬
  currentStage.sort((a, b) => a.id - b.id);
  const currentStageId = currentStage[currentStage.length - 1].id;

  // 현재 스테이지 ID 검증
  if (currentStageId !== payload.currentStage) {
    return {
      status: "fail",
      message: `Expected stage ${currentStageId}, but got ${payload.currentStage}`,
    };
  }

  // 타겟 스테이지 검증
  const targetStage = stage.data.find(
    (stage) => stage.id === payload.targetStage
  );

  if (!targetStage) {
    return { status: "fail", message: "Target stage not found" };
  }

  // 현재 시간
  const currentScore = currentStage[currentStage.length - 1].score; // 최근 스코어
  const elapsedScore = payload.score - currentScore; // 현재 스코어 - 최근 스코어
  const curStage = stage.data.find((stage) => stage.id === currentStageId);
  const currentStagePerSecond = curStage.perSecond; // 기존 초당 점수
  const elapsedTime =
    (currentTime - currentStage[currentStage.length - 1].timestamp) / 1000; // 현재 서버시간 - 전 서버시간

  // 현재 스테이지에서 먹은 아이템 점수 계산
  const eatItems = getItems(userId).filter(
    (item) => item.stage === currentStageId
  );
  const eatItemScore = eatItems.length
    ? eatItems.reduce((acc, cur) => {
        const itemInfo = item.data.find((i) => i.id === cur.id);
        const itemScore = itemInfo ? itemInfo.score : 0;
        return acc + itemScore;
      }, 0)
    : 0;

  // 예상 점수 계산
  // const expectedScore = elapsedTime * currentStagePerSecond + eatItemScore;
  const expectedScore =
    elapsedTime * currentStagePerSecond + currentScore + eatItemScore;
  console.log("예상 점수 : ", expectedScore);
  console.log("현재 점수 : ", payload.score);
  console.log(`elapsedTime : ${elapsedTime}`);
  console.log(`currentStagePerSecond : ${currentStagePerSecond}`);
  console.log(`eatItem:${eatItemScore}`);
  console.log(expectedScore);

  // 전 스코어 + 시간차 * per + 아이템
  // 점수 검증 - (현재 점수 - 최근 setStage 점수)  &  (현재 서버시간 - 전 서버시간) * perSecond + item 점수  두가지 비교
  if (
    payload.score <= expectedScore - 5 ||
    payload.score >= expectedScore + 5
  ) {
    console.log("[실패]예상 점수 : ", expectedScore);
    console.log("[실패]현재 점수 : ", payload.score);
    return { status: "fail", message: "Score verification failed" };
  }

  // 스테이지 업데이트
  setStage(userId, payload.targetStage, currentTime, payload.score);
  console.log(`스테이지 등록된 점수 ${payload.score}`);
  console.log(`전 스테이지와 점수차이 : `, elapsedScore);
  return { status: "success", message: "Move to target stage" };
};

수 많은 console.log의 흔적들... 뭐가 문제인지 찾기 위한 디버깅의 흔적들이다..

중간 중간 보면 주석처리 되어있는 코드가 있는데 처음에 생각했던 계산식이다. 코드가 처음 생각했던 것과 많이 바뀌었는데 기록이 저것밖에 남아있지 않다... 커밋을 안해서 ...
전에 등록된 스테이지와 변경요청 들어온 현재 스테이지 간 시간차이와 perSecond 배율, 먹은 아이템을 계산하는 방식, 처음부터 다 계산해서 검증하는 방식 등등..

최종 코드는 전 스테이지도 검증이 완료된 점수이니까 전 스테이지 점수 + 전 스테이지와 현재 스테이지 시간 차이 * perSecond + 먹은 아이템 점수로 검증하였다.

이제.. 문제가 되는 프론트 코드이다.

클라이언트 측 스테이지 변경 코드

async nextStage(score) {
    const result = await sendEvent(11, {
      currentStage: stageScore[this.stage].id,
      targetStage: stageScore[this.stage + 1].id,
      score,
      time: performance.now(),
    });
    if (result.status === "success") {
      this.stage++;
      return result;
    }
  }

여기까지는 문제가 없었다. 그냥 전달 받은 최근 스테이지와 다음 스테이지 정보, 점수, 시간을 넘겨주고 돌아오는 것을 기다렸다가 정상이면 스테이지를 증가시키는 방식이었으니까...

 async update(deltaTime, stage) {
    this.score += deltaTime * this.perSecond * 0.001;
    // 등록된 스테이지 데이터 테이블 점수 내에서만 구동
    if (Math.floor(this.score) <= stageScore[stageScore.length - 1].score) {
      // 다음 스테이지 조건 점수와 현재 점수가 같거나 높으면서 stageChance가 true일 때 스테이지 변경 시도
      if (
        Math.floor(this.score) >= stageScore[stage.getStage() + 1].score &&
        this.stageChange
      ) {
        // 프레임단위 호출이기 때문에 여러번 호출되는 것을 방지
        this.stageChange = false;
        const result = await stage.nextStage(this.score);
        // 스테이지 증가와 동시에 초당 점수 변경
        if (result.status === "success") {
          this.perSecond = stageScore[stage.getStage()].perSecond;
          console.log(
            `${stage.getStage()} 스테이지로 변동되었습니다. 지금부터 초당 ${this.perSecond}점씩 오릅니다.`
          );
          setTimeout(() => {
            this.stageChange = true;
          }, 1000);
        }
      }
    }
  }

아니, 여기 코드에서 스테이지 변동을 담당하는 nextStage를 비동기처리 하지 않아서 다음 스테이지 점수가 정상적으로 등록되지 않은 것이다.. 2스테이지면 2배씩 늘어야하는데 1배씩 늘때도 있고 2배씩 늘어날때도 있는 것 이었다.
뭔 이런 실수를 다 했는지... 어제 머리아픈 상태에서 고민하다가 그냥 자고 일어나서 다시 둘러보니까 바로 답을 찾았다.

역시 컨디션이 중요하다.

현재는 스테이지 당 예상점수와 현재점수가 0.1 내 오차범위로 검증이 잘 된다.
(이거 아니였으면 진작에 도전 풀었을텐데;;;)

도전기능 준비를 위한 Redis 설치

설치환경

Ubuntu 24.04.1 LTS
Docker 24.0.7

Redis 이미지 다운로드

$ sudo docker pull redis

먼저 위 명령어로 도커 이미지를 다운받습니다.

설치가 완료되면 다음 명령어로 확인할 수 있습니다.
$ sudo docker images

Docker 컨테이너

이미지를 다운받았으니 컨테이너를 생성하면 됩니다.
redis의 기본 포트는 6379이고, 원하는 포트로 지정해도 됩니다.

$ sudo docker run -p 6379:6379 --name redis redis

다운이 완료되면, 바로 실행되므로 CTRL + C 를 통해 종료해줍니다.

sudo docker ps -a 명령어로 컨테이너 상태를 확인할 수 있습니다.

Redis 접속

$ sudo docker exec -it 컨테이너이름 /bin/bash

위 명령어로 컨테이너 내부에 접속할 수 있고,

redis-cli

위 명령어로 redis cli 환경을 구동할 수 있습니다.

redis 기초 공부좀 하고 내일 하루 도전과제 구현으로 보내야겠다.😁

profile
하루 최소 1시간이라도 공부하자..

0개의 댓글