[Socket.io] Chrome Dino 도전기능 - 레디스2

GDORI·2024년 10월 7일
0

SOCKET

목록 보기
6/8

역시 DOCS..

Redis Docs 바로가기
아니 velog나 구글링으로 찾아본 레디스 사용법으로 적용하니까 오류는 발생 안하지만 실행은 안되는... 그런 불상사가 자꾸 벌어져서
Redis Docs 기초부분으로 대체하니까 바로 됐다..

지난 게시글에 이어..

우선 사용자가 게임을 종료하는 sendEvent 종료 핸들러 번호인 3번을 날릴 때 서버에서 점수검증을 하고, 레디스에 저장하도록 구현하였다.

redis.handler.js

import { redisClient } from "../init/redis.js";

// 사용자의 점수를 가져오는 함수
export const getUserScore = async (uuid) => {
  const hashKey = `uuid:${uuid}:score`;
  try {
    let userScore = await redisClient.hGetAll(hashKey);
    if (!userScore.bestScore) {
      return 0;
    }
    return Number(userScore.bestScore);
  } catch (err) {
    return { status: "fail", message: "Error reading user score" };
  }
};

// 사용자의 점수를 설정하는 함수
export const setUserScore = async (uuid, score) => {
  const hashKey = `uuid:${uuid}:score`;

  try {
    // 이전 점수를 가져옵니다.
    const userScore = await getUserScore(uuid);

    if (userScore.status === "fail") throw new Error(userScore.message);

    // 기본값 설정
    const bestScore = userScore
      ? Math.max(Math.floor(userScore), score)
      : score;

    // Redis에 점수를 설정합니다.

    await redisClient.hSet(hashKey, "bestScore", bestScore);

    return { status: "success", message: "Score set successfully" };
  } catch (err) {
    console.error("Error retrieving user score:", err);
    return { status: "fail", message: "Error retrieving user score" };
  }
};

나중에 score뿐만 아니라 stage 등 여러가지 넣을 생각으로 해쉬로 작업했다. 그냥 키-값 구조로 구성해도 가능하다.
등록된 uuid의 점수가 없으면 0을 반환함으로써 클라이언트 측에서 처음 들어오는 유저의 최고점수를 쉽게 처리할 수 있다.

gameEnd

export const gameEnd = (uuid, payload) => {
  const { timestamp: gameEndTime, score } = payload;
  let currentStage = getStage(uuid);
  const { 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;

  const currentScore = currentStage[currentStage.length - 1].score; // 최근 스코어
  const elapsedTime =
    (gameEndTime - currentStage[currentStage.length - 1].timestamp) / 1000; // 게임종료시간 - 최근 저장된 스테이지 시간

  // 현재 스테이지에서 먹은 아이템 점수 계산
  const eatItems = getItems(uuid).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;

  // 예상 점수
  // 마지막 저장된 스테이지 -> s
  // (게임종료시간 - s의 시간) * 스테이지 perSecond + s 점수 + 해당 스테이지 먹은 아이템 점수
  const expectedScore =
    currentScore + elapsedTime * score.perSecond + eatItemScore;

  if (Math.abs(score.score - expectedScore) > 5) {
    return { status: "fail", message: "score verification failed" };
  }
  setUserScore(uuid, Math.floor(score.score)).then((result) => {
    if (result.status === "fail") {
      console.log(result.message);
      return result;
    }
    console.log("Game ended successfully"); // 게임 종료 확인 로그
    console.log(result.message);
  });

  return { status: "success", message: "Game Ended", score };
};

돌아와서, 최고점수를 담당하던 메서드는?

원래는 게임이 종료되면서 로컬스토리지에 점수를 저장하는 setHighScore() 메서드와 HIGH_SCORE_KEY 변수가 있었는데,
setHighScore가 아니라, getHighScore로 개명해서 받아온 점수를 HIGH_SCORE 변수에 담아 최신화하게끔 하면 될 것 같다.

getHighScore() {
    sendEvent(30, {})
      .then((score) => {
        console.log("score: ", score);

        // score 값이 없거나 실패한 경우 처리
        if (!score || score.status) {
          console.log("Fail loading user score");
          this.HIGH_SCORE = 0; // 기본값 0 설정
        } else {
          this.HIGH_SCORE = score || 0; // 점수 설정 (유효하지 않으면 0)
        }

        console.log("Updated HIGH_SCORE: ", this.HIGH_SCORE);
      })
      .catch((error) => {
        // 에러 처리
        console.error("Error fetching score: ", error);
        this.HIGH_SCORE = 0; // 에러 시 기본값 0 설정
      });
  }

아유 sendEvent 핸들러들이 다 동기인데..

redis 처리하는 핸들러는 비동기 처리해야 하는 부분인데, sendEvent 내 handler를 await 시켜주지 않으면 빈 객체만 자꾸 보낸다..
전부터 계속 async await가 괴롭힌다.
어차피 handlerEvent가 직접적으로 return하는게 아니라 emit으로 처리하기 때문에 비동기 처리한 것만 별도 처리하고 내보내면 될 것 같다.

export const handlerEvent = async (io, socket, data) => {
  if (!CLIENT_VERSION.includes(data.clientVersion)) {
    socket.emit("response", {
      status: "fail",
      message: "Client Version mismatch",
    });
    return;
  }

  const handler = handlerMappings[data.handlerId];
  if (!handler) {
    socket.emit("response", { status: "fail", message: "Handler not found" });
  }

  const response = await handler(data.userId, data.payload);

  if (response instanceof Promise) {
    response.then((responseValue) => {
      if (responseValue.broadcast) {
        io.emit("response", "broadcast");
        return;
      }
      socket.emit(`${data.handlerId}_response`, responseValue);
    });
  }

  if (response.broadcast) {
    io.emit("response", "broadcast");
    return;
  }
  socket.emit(`${data.handlerId}_response`, response);
  socket.emit(`response`, response);
};

내보낼 때 Promise 객체이면 벗겨주고 보내면 된다.

현재 등록된 유저들 중 최고 점수

gameEnd 시 setUserScore를 통해 자신의 최고 점수를 저장한다. 이를 이용해서 redis의 zadd기능을 이용하여 bestScore를 같이 저장한다. 그리고 getHighScore 메서드를 하나 더 만들어서 데이터를 가져올 수 있게 하면 될 것 같다.

수정된 setUserScore

export const setUserScore = async (uuid, score) => {
  const hashKey = `uuid:${uuid}:score`;

  try {
    // 이전 점수 가져오기
    const userScore = await getUserScore(uuid);

    if (userScore.status === "fail") throw new Error(userScore.message);

    // 기본값 설정
    const bestScore = userScore
      ? Math.max(Math.floor(userScore), score)
      : score;

    // Redis에 점수를 설정

    await redisClient.hset(hashKey, "bestScore", bestScore);
    // 랭크를 위해 zAdd로 set도 추가
    await redisClient.zadd("rank", bestScore, uuid);

    const HighScore = await getHighScore();
    console.log(HighScore);
    if (HighScore.status === "success") {
      if (Number(HighScore.score) === score) {
        console.log("최고기록 갱신");
        return { broadcast: true, types: "rank", score: bestScore };
      }
    }

    return { status: "success", message: "Score set successfully" };
  } catch (err) {
    console.error("Error retrieving user score:", err);
    return { status: "fail", message: "Error retrieving user score" };
  }
};

getHighScore(서버)

위의 메서드에서 저장된 Set을 역순으로 조회하여 맨위의 값을 불러오면 1등 점수이다.

export const getHighScore = async () => {
  try {
    // zrevrange를 await로 사용하여 결과를 불러옴
    const result = await redisClient.zrevrange("rank", 0, 0, "WITHSCORES");

    if (!result || result.length === 0) {
      return { status: "fail", message: "No scores found" };
    }

    return {
      status: "success",
      uuid: result[0], // 최고 점수를 가진 UUID
      score: result[1], // 해당 점수
    };
  } catch (err) {
    console.error("Error getting high score:", err); // 에러 로그
    return { status: "fail", message: "Server error" };
  }
};

변경된 getHighScore(클라이언트)

본인 최고 점수만 불러오는 것이 아니라 랭크 1위 점수도 같이 불러온다.

 // 처리할 때 자신의 최고점수와 유저랭크 최고점수를 가져옴
  async getHighScore() {
    try {
      const score = await sendEvent(30, {});
      const totalHighScore = await sendEvent(31, {});

      // score 존재 여부 검증
      if (!score || score.status) {
        console.log("Fail loading user score");
        this.HIGH_SCORE = 0; // 기본값 0 설정
      } else {
        this.HIGH_SCORE = score || 0; // 점수 설정 (유효하지 않으면 0)
      }
      // totalHighScore 존재 여부 검증
      if (!totalHighScore || totalHighScore.status === "fail") {
        console.log("Fail loading total user high score");
        this.TOTAL_HIGH_SCORE = 0; // 기본값 0 설정
      } else {
        this.TOTAL_HIGH_SCORE = totalHighScore.score || 0; // 점수 설정 (유효하지 않으면 0)
      }
    } catch (error) {
      // 에러 처리
      console.error("Error fetching score: ", error);
      this.HIGH_SCORE = 0; // 에러 시 기본값 0 설정
      this.TOTAL_HIGH_SCORE = 0; // 에러 시 기본값 0 설정
    }
  }
profile
하루 최소 1시간이라도 공부하자..

0개의 댓글