Skip to content

[BE] 프로젝트 코드 읽기

JIN edited this page Jan 14, 2025 · 2 revisions
⚙️ Web BE
김진
2025.01.06. 작성

Note

server 디렉터리 하위에 있는 코드 읽었습니다.

📖 방해꾼은 못말려

기술 스택

  • TypeScript

  • Node.js

  • NestJS

  • Redis

  • Socket.io

  • Prettier

  • ESLint

  • pnpm

env 파일

REDIS_HOST=''
REDIS_PORT=''
  • env 파일은 정말 단순히 Redis 이용을 위해서 사용
  • 밑에 파일들 훑고 와서 알게된 점. clova 관련 누락되어있음!

eslint

module.exports = {
  parser: '@typescript-eslint/parser',
  parserOptions: {
    project: 'tsconfig.json',
    tsconfigRootDir: __dirname,
    sourceType: 'module',
  },
  plugins: ['@typescript-eslint/eslint-plugin'],
  extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
  root: true,
  env: {
    node: true,
    jest: true,
  },
  ignorePatterns: ['.eslintrc.js'],
  rules: {
    '@typescript-eslint/interface-name-prefix': 'off',
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/explicit-module-boundary-types': 'off',
    '@typescript-eslint/no-explicit-any': 'off',
    'prettier/prettier': ['error', { endOfLine: 'auto' }],
  },
};
  • 인터페이스 이름 접두사 강제성 off
  • 함수 반환 타입 명시적으로 지정 off
  • 모듈 경계 타입 명시적으로 지정 off
  • any 타입 사용 금지 off
    • 얘는 왜 off 인지 아직은 잘 모르겠음. 따로 외부 API 등을 사용하지 않는 이상 any 타입을 쓸 일이 없을 거 같은데..?
    • 밑에서 확인했는데 Redis에서 일단 any 타입 많이 씀

chat 디렉터리

  • chat.gateway.ts
    • socket.io 사용
    • handleConnection
      • roomId, playerId 확인해서 경우에 따라 에러 처리
    • handleSendMessage
      • 클라이언트의 데이터가 중간에 변할 수도 있으므로 메시지 처리 전에 한번 더 연결 확인
      • 채팅 메시지 전송
    • chat.gateway.spec.ts
      • gateway가 정의되었는지만 확인하는 단순한 테스트코드
  • chat.repository.ts
    • getPlayer
      • 완전히 동일한 코드가 game.repository.ts 에 존재 → util 등으로 빼는 건?
    • existsRoom existsPlayer
      • redis에 방/플레이어의 정보를 저장하고 관리
        • redis에서 roomId, playerId가 존재하는지를 확인해서 boolean 타입으로 반환
        • redis에서 exists는 0 또는 1을 반환하기 때문에 boolean 타입으로 반환을 위해 return exists === 1 로 작성
        • 밑에 redis 디렉터리 부분에 작성하긴 했는데, 애초에 redis.service.ts 에서 exists 함수를 boolean 타입 반환으로 바꾸는게 더 깔끔해보이긴함(완전 주관적인 내 생각)
  • chat.service.ts
    • sendMessage
      • 플레이어 id, 닉네임, 메시지, 메시지 쓴 시간 전달
    • existsRoom existsPlayer
      • 방/플레이어 정보 확인용
    • chat.service.spec.ts
      • 테스트 코드가 다 이런 식으로만 작성이 되어있나요? gateway랑 똑같이 service 모듈이 잘 되어있는지만 확인하는 코드

common 디렉터리

  • enums 디렉터리
    • game.status.enum.ts
      • PlayerStatus 왜 필요한거지? 플레이어가 게임 중인지 아닌지는 친구 기능이 있지 않는 이상 방 상태로 확인하는 편이 더 낫지 않았을까 하는 생각이 드네요
      • RoomStatus 여기에 PLAYING이 있는게 훨씬 자연스러울 거 같은데..
  • services 디렉터리 / timer.service.ts
    • 게임할 때 쓰는 타이머!
  • types 디렉터리
    • player, room, roomsettings에 대한 interface
  • clova-client.ts
    • .env.exampleclova_api_keyclova_gateway_key 누락되어있네

drawing 디렉터리

  • drawing.gateway.ts
    • handleConnection
      • 얘는 chat.gateway 에 있는 거랑 똑같음
    • handleDraw
      • 얘는 그림 그린 사람이랑 그림 데이터 보내주는 역할!
    • drawing.gateway.spec.ts
      • 얘도… 그냥 잘 정의됐는지만 확인… test 코드가 작성이 아예 안된 거로 봐도 되겠다
  • drawing.repository.ts
    • 여기에 들어있는 함수 두개 다 chat.repository 에서 똑같이 쓰는 건데 이런거 공통으로 분리해도 되려나?
  • drawing.service.ts
    • 여기도 chat.service 에서 쓰는거 똑같이 쓰는데 이런거 공통 레포지, 서비스로 나누는게 낫지 않을까 싶음
    • 둘이 공통된게 찾아보니까 똑같이 gateway에서만 단 한번 쓰는건데… 공통 레포지, 서비스 만들어서 게이트웨이에서 그 공통 서비스 불러와서 이용하는 편이 나을듯

exceptions 디렉터리

  • game.exception.ts
    • 특정 상황에서 발생하는 예외 정리한 커스텀 예외 클래스

filters 디렉터리

  • ws-exception.filter.ts
    • socket.io 에서 발생한 에러 캐치하는 filter

game 디렉터리

  • game.controller.ts
    • 방 생성하는 것만 있다!
    • game.controller.spec.ts
      • 얘도 똑같다.. 그냥 toBeDefined()다..
  • game.gateway.ts
    • handleJoinRoom
      • 단순.. 방 참가
    • handleReconnect
      • 재연결 로직
    • handleSettings
      • 세팅 바뀌면 바뀌었다고 다 알려줘! 다같이 바꿔!
    • handle
      • 점수 같은거 바뀔 때마다 업데이트 해줘야 하니까
      • 근데 얜 왜 이름이 그냥 handle일까 handlePlayer 이런 것도 아니고..
    • handleGameStart
      • 게임 시작~
    • startNewRound
      • 새로운 라운드를 시작한다는게 첫 라운드를 시작한다는게 아니라 라운드 + 1해서 쭉쭉 올라가는 그런 새로운 라운드
        • 여기 시점에서 역할 분배는 끝
      • 그래서 라운드 만약에 끝났으면 게임 종료니까 게임 성공적으로 종료할 수 있게 하고 return
      • 그런거 아니면 그 라운드 시작하면서 자기 역할 같은거 알려주기
      • 타이머도 동작 시작하고
      • 그림 그리는 시간 끝나면 끝난대로 상태 업데이트하고, 맞추는 시간 진행. 얘도 끝나면 끝난대로 점수 체크하고… 새로운 라운드 시작!
    • notifyPlayerRoundStart
      • 여기서 현재 라운드랑, 배정 받은 역할이랑, 그림 그리는 시간이랑 이런거 관리
      • 그림꾼/방해꾼이면 단어 이제 보여주고
      • 추측하는 사람들은 멍때리면서 바라보기
    • runTimer
      • 타이머
    • handleCheckAnswer
      • 제한시간 내 지속적으로 답안체크. 답 맞으면 이제 라운드 종료하고 새로운 라운드 시작할 수 있게!
      • 근데 여기저기서 다 startNewRound를 해도 되는 것인가?? 다음 라운드 시작하는 로직을 하나로 합쳐도 될 거 같은데…
        • 정답 맞췄을 경우 startNewRound 호출
        • 정답 못맞췄을 때 타이머 다 끝나고 난 이후에 startNewRound 호출 이런 식으로
    • handleDisconnect
      • 일정 시간 동안 재접속 안하면 완전히 연결을 끊는 로직
    • game.gateway.spec.ts
      • 얘도 잘 주입하고 있는지..
  • game.repository.ts
    • createRoom
      • 트랜잭션 이용해서 해시 데이터 순차적으로 실행
      • 근데 지금 객체를 레디스에 저장하려고 하는거니까 JSON.stringify 써서 직렬화해도 괜찮게 저장하는 건 어떨까?
      • 실패한 경우가 처리가 안되어있음
        • 지금 코드 그대로 활용하려면, 명령 실행하고 실패한 경우에는 생성된거 다 지워주고 에러 던지기 (화면에서 실패했습니다! 다시 생성해주세요! 이런거 띄워줘야하니까)
        • redis에서는 해당 키가 없을 때 del 한다고 에러가 뜨진 않으니까 그냥 hset 한거 둘 다 del 해서 지우면 될듯
    • getRoom
      • 게임 방에 대한 정보를 싹 가져오기
      • as 문법 안쓰는게 좋다고 어디서 들은거 같은데 아시는 거 있나요?
    • updateRoom
      • hostId나 room status같은거 변경 일어나면 업데이트
    • deleteRoom
      • 방 삭제 시 룸에 대한 정보 다 지우기
      • 얘도 실패했을 경우 처리해줘야 함
      • 그러면 실패했을 땐 다시 hset해서 지정하고 실패했다고 에러 던져줘야겠지?
    • getRoomStatus
      • 방 status 대해서만 꺼내올 때 쓰는거
      • 근데 안쓰는 건 왜 만들어두신거지.. 지우고 싶게 ㅠㅠ 그냥 밑에거랑 세트 메뉴라 그런거겠죠..
    • getRoomSettings
      • room setting 가져오는 거
    • getRoomPlayers
      • 방에 참가한 플레이어들 정보 가져오는거
      • 마지막에 filter(boolean) 해서 null이나 undefined 없이 유효한 사람들 정보만 남김
    • addPlayerToRoom
      • 게임 방에 유저 추가
      • 얘도 실패햇을 때 처리
    • getPlayer
      • 얘가 위에 chat.repository 완전 똑같은거 있는 그거
      • 근데 이제보니 이 코드가 존재하는데 왜 위에 getRoomPlayers에서 얘 재활용안하고 그냥 썼지?
    • updatePlayer
      • 플레이어 상태 등 업데이트할 때 쓰는거
    • removePlayerFromRoom
      • 게임방에서 플레이어가 나갔을 때 처리
      • 얘도 실패했을 경우 처리
  • game.service.ts
    • createRoom
      • 방 생성
    • joinRoom
      • 방 참가
      • 게임 진행 중일 땐 참가 불가
        • 나중에 관전 기능 같은 거 넣어도 재밌겟당
      • 처음 들어온 사람이면 이 사람이 자동으로 host
        • host로 업데이트 해주기
      • 호스트 + 다른 사람들 모두 방에 입장한 사람들이니까 addPlayerToRoom 도 해주고
      • 이거 근데 updatedPlayers를 reverse해서 쓸 거면 lpush말고 rpush하면 reverse 안해도 되지 않나?
        • 찾아보니까 lpush, rpush 둘 다 리스트의 시작/끝 부분에 새로운 요소를 추가하는 작업이니까 시간 복잡도는 O(1)
        • 근데 reverse하는 과정의 시간 복잡도는 O(n)이니까 lpush하고 reverse하는게 성능면에서 불리
        • 이런 걸 CS적(시간 복잡도 최적화)으로 개선했다!! 하면서 내세워도 되지 않을까 하는 생각 ㅎㅎ
    • generateNickname
      • 뭐 그냥 닉네임 만드는거
      • 얘도 클로바 시켜서 형용사/부사랑 명사 랜덤으로 뽑게 하는건 안되나??
    • reconnect
      • 연결 끊겼을 때 재연결하는 건가봥
    • leaveRoom
      • 방 퇴장할 때
      • 호스트면 다른 사람한테 호스트 넘기고 퇴장
        • 여기 코드도 왜 length - 1인 사람한테 호스트 넘기나 했는데 lpush 해서 두번째로 들어온 사람이 맨 뒤에 있어서 그런거였ㄷㅏ… 그냥 rpush해서 관리하면 되지 않을까?
    • intializeGame
      • 이름을 intializeGame이라고 해서 게임 시작 전 준비 이런거라 생각했는데 아니었네용.. 그냥 게임 룸을 초기화하는건가봄 초기 상태로. 게임이 완전히 끝났을 때 되돌리는? 그런 거인듯
    • updateSettings
      • 게임 세팅 업데이트
      • 호스트 아니면 못하게 하고
    • updatePlayer
      • 얘도 위에랑 동일!
      • 점수 같은거 업데이트할 때 사용하나봄
    • startGame
      • 룸 세팅 기반해서 게임 시작
    • fetchWords
      • 10번 시도해서 클로바로부터 단어 받아오기
      • 10번했는데도 개수 안맞거나 그러면 default 단어 라운드 수만큼 잘라서 가져오기
    • setupRound
      • 라운드 + 1하면서 방 상태 drawig 으로 바꾸고 진행하고.. 만약에 최종라운드까지 끝났으면 게임 종료되게
      • 라운드 새로 시작되면 그림꾼, 방해꾼 이런 역할들 새로 뽑고..
    • distributeRoles
      • 플레이어들 섞어서 detremineRole 함수로 역할 분배
    • detremineRole
      • 섞인거 기반으로 그림꾼, 방해꾼, 맞추는 사람 결정할 수 있게 하는 코드
    • categorizePlayerRoles
      • 역할 나뉘었으면 그 역할 분류하기(방 설정 업데이트 할 때 사용)
    • handleDrawingTimeout
      • 그림 그리는 시간 끝나면 키워드 맞추는 시간으로 넘겨줘야 하니까
    • checkAnswer
      • 그림꾼, 방해꾼 제외 키워드 맞추기
      • 정답 아닌거 들어오면 바로바로 오답이라고 리턴 받을 수 있게 early return
      • 정답 맞춘 사람/그림꾼은 각각 결과에 따른 점수 얻을 수 있게
    • calculateScores
      • 그림꾼, 일반 플레이어들 점수 계산하는 로직
    • handleGuessingTimeout
      • 추측 시간 끝나면 방해꾼이 승리한거니까 방해꾼 점수 계산 로직
      • 여기까지 보니까 sort하는 로직 자체는 겹치니까 하나로 합쳐도 되지 않을까 하는 생각…
    • game.service.spec.ts
      • 얘도 toBeDefined()

redis 디렉터리

  • redis.service.ts
    • hset hget hgetall
      • Redis hash 자료구조
      • hset 으로 hash 자료구조 생성
        • field 로는 statushostId 를 갖고 있음
        • 게임 방 상태에 따라 status 변경
        • 게임 방 생성 및 호스트가 게임방을 나갔을 경우에만 hostId 업데이트
      • hget 으로 field 에 해당하는 value 값을 꺼내옴
        • 이제보니 여기서 any가 잔뜩 쓰였네….
      • hgetall 으로 hash 자료구조의 key가 가지고 있는 모든 field-value 값을 배열 형태로 반환
    • del
      • key에 해당하는 값 삭제
    • lpush lrange lrangeAll lrem
      • Redis List 자료구조
      • lpush
        • key 에 해당하는 List 처음에 value 삽입
        • rpush 라는 친구도 있는데 얘는 List 마지막에 삽입
      • lrange
        • key 에 해당하는 List의 start ~ end 범위까지의 데이트 반환
        • 일반적으로 생각하는 거랑 똑같음! 왼쪽부터 할 거면 0, 1, 2, … 이 순서
        • 오른쪽부터는 음수인 -1, -2, … 이 순서!
      • lrangeAll
        • 얘는 전체 조회를 위해 따로 만든 함수로 보임 good
      • lrem
        • 삭제만을 위해 사용하면 return 값 없어도 될듯
          • return 한다면 삭제된 항목의 개수를 반환해줌
        • count 양수
          • key 에 해당하는 List에서 입력받은 value 를 리스트의 왼쪽부터 삭제
        • count 음수
          • key 에 해당하는 List에서 입력받은 value 를 리스트의 오른쪽부터 삭제
        • count 0
          • key 에 해당하는 List에서 입력받은 value 전부 삭제
    • exists
      • 존재 여부를 0 또는 1(number 타입)로 반환
      • 여기를 boolean 반환으로 바꾸어서 위에 existsRoom 이나 existsPlayer 부분 코드를 줄이는 것은 어떨지
      • 불필요한 await 가 사용된 것은 아닐까?
    • multi
      • redis 트랜잭션을 시작하는 커맨드
      • multi 커맨드로 트랜잭션을 시작하면 Redis는 이후에 입력되는 커맨드를 바로 실행하지 않고 Queue에 쌓음

🔗 참고 사이트

기존 프로젝트

기존 프로젝트 깃허브 위키

HGETALL

Redis : HASH 자료구조 :: HSET , HGET , HGETALL , HEXISTS , HDEL , HINCRBY , HINCRBYFLOAT , HSTRLEN , HKEYS , HVALS

Redis 2 - Redis의 자료구조 및 명령어 [ List편 ]

LREM Redis

Clone this wiki locally