Skip to content

Releases: boostcampwm-2024/refactor-web42-stop-troublepainter

3주차 릴리즈 노트 v1.1.0

23 Jan 08:57
4d31fe0
Compare
Choose a tag to compare

[FE]

1. Shared Worker를 이용해 브라우저 내의 여러 탭에서 소켓 연결 관리하기 #35

  • 목표
    • 기존 여러개의 탭에서 각각 소켓 연결을 하게 되는데 이부분을 SharedWorker를 통해 하나의 연결을 여러 탭에서 공유하게 만들기.
      • 기존 Socket 아키텍처에 존재하는 socket.storage 분리
      • 3개의 Socket(Chat, Game, Draw)에 대한 코드를 Shared Worker에서 진행
  • 문제
    • Game Socket 연결 지연으로 joinRoom 이벤트가 실행되지 않는 문제
  • 해결
    • gameSocketManager 내부에 연결 완료 전에 받은 요청을 Queue로 저장한 후, 연결이 완료된 이후에 처리하는 로직을 추가
  • 결과
    • 2개의 Socket(Chat, Game)을 Shared Worker로 관리하는데 성공
    • Draw Socket은 시간 관계상 완료하지 못함

2. Playwright를 이용한 성능 테스트 #30

  • 문제
    • 기존의 랜덤 드로잉 테스트는 실제 환경과 다르다는 문제
  • 해결
    • 실제 사용자의 마우스 이벤트로 드로잉 데이터 수집
    • 3개의 드로잉 시나리오 제작
  • 결과
    • 5명의 플레이어로 약 20초 동안 게임을 진행하며 성능 측정 (CDP 사용)

3. 드로잉 최적화 #30

  • 문제
    • 저성능 기기에서 렉이 발생하는 문제
  • 해결
    • 마우스 무브 이벤트에 16ms 쓰로틀링 적용

4. 리액트 리렌더링 횟수 줄이기 #30

  • 문제
    • 게임 캔버스의 수 많은 리렌더링에 따른 성능 저하 문제
  • 해결
    • 잉크 부족 토스트 메시지에 3초 쓰로틀링 적용
    • GameCanvas 컴포넌트에 memo 적용
    • 여러 함수의 useCallback 의존성 배열 수정
  • 결과
    • LayoutCount: 92.26% 향상(443.33 ➔ 34.33)
    • RecalcStyleCount: 87.79% 향상(497.33 ➔ 60.67)
    • LayoutDuration: 58.72% 향상(0.04107 ➔ 0.01695)
    • RecalcStyleDuration: 87.74% 향상(0.09295 ➔ 0.01140)
    • ScriptDuration: 53.77% 향상(1.65815 ➔ 0.76654)
    • TaskDuration: 20.68% 향상(7.96949 ➔ 6.31876)
    • ThreadTime: 25.19% 향상(5.26321 ➔ 3.93703)

[BE]

1. Prometheus와 Grafana 적용하기 #37

  • 서버 데이터를 수집하여 성능 테스트 시 병목 구간 확인
  • Docker로 구성해 다양한 서버의 데이터 수집 가능

2. Redis List 삽입 방식 변경 #27

  • 문제
    • List 삽입에 LPUSH 사용으로 서버에서 reverse() 가 필요해 연산 횟수가 늘어나는 문제
  • 해결
    • List 삽입에 RPUSH 사용
  • 결과
    • reverse() 제거로 연산 횟수 약 50% 감소

3. Artillery를 이용한 성능 테스트 #38

  • Artillery로 사용자가 사용하는 환경을 묘사한 성능 테스트 진행.
  • Artillery’s 모니터링 툴로 부하 발생 구간 시각화.

4. Redis Adaptor 적용하기 #32

  • 목표
    • 기존 Adaptor에서 Redis Adaptor로 전환해 다중 서버 확장 가능하도록 변경
  • 결과
    • Nginx의 IP Hash로 로드밸런싱 설정
      • 같은 방에 있는 유저 중 일부가 게임 시작 실패
      • Redis Adaptor 설정 오류로 추정
      • Nginx 로드밸런싱의 경우 롤백

What's Changed

New Contributors

Full Changelog: 1.0.1...1.1.0

2주차 릴리즈 노트 v1.0.1

17 Jan 07:05
57e2dcb
Compare
Choose a tag to compare

테스트 코드 작성을 통한 버그 수정

  1. 단위 테스트를 진행하면서 발견한 문제 #3
    1. 비동기 함수에 누락된 await 키워드
            const roomExists = this.drawingService.existsRoom(roomId);
            if (!roomExists) throw new RoomNotFoundException('Room not found');
            const playerExists = this.drawingService.existsPlayer(roomId, playerId);
            if (!playerExists) throw new PlayerNotFoundException('Player not found in room');

drawingService의 existsRoom 메서드와 existsPlayer 메서드는 비동기 함수이지만, await 키워드가 누락되어 있었습니다. 이 때문에 boolean 타입이 아닌 Promise<boolean> 타입으로 반환되어 바로 아래의 if 조건문에서 에러 체크가 되지 않는 문제가 있었습니다.

테스트 코드 작성을 통해 문제가 되는 부분을 정확히 파악할 수 있었고, 해당 부분에 await 키워드를 추가함으로써 정상적으로 동작하게 되었습니다.

  1. 통합 테스트, e2e 테스트를 진행하면서 발견한 문제 #9
    1. handleConnection 메서드 내 에러 처리 로직이 의도대로 동작하지 않는 문제
        @WebSocketGateway({
          cors: '*',
          namespace: '/socket.io/drawing',
        })
        @UseFilters(WsExceptionFilter)
        export class DrawingGateway implements OnGatewayConnection {
          @WebSocketServer()
          server: Server;
        
          constructor(private readonly drawingService: DrawingService) {}
        
          handleConnection(client: Socket) {
            const roomId = client.handshake.auth.roomId;
            const playerId = client.handshake.auth.playerId;
        
            if (!roomId || !playerId) throw new BadRequestException('Room ID and Player ID are required');
        
            const roomExists = await this.drawingService.existsRoom(roomId);
            if (!roomExists) throw new RoomNotFoundException('Room not found');
            const playerExists = await this.drawingService.existsPlayer(roomId, playerId);
            if (!playerExists) throw new PlayerNotFoundException('Player not found in room');
        
            client.data.roomId = roomId;
            client.data.playerId = playerId;
        
            client.join(roomId);
          }
          
          // ...
        }

기존 handleConnection 메서드는 @UseFilters(WsExceptionFilter) 필터를 사용해 에러 처리를 하려는 의도로 작성된 것으로 보입니다.

그러나 테스트 결과, handleConnection 메서드에는 filters가 적용되지 않는다는 점을 확인할 수 있었습니다. 해당 내용은, NestJS repository의 Issue #336에서도 확인이 가능합니다. 해당 이슈에는 Filters가 @SubscribeMessage() 데코레이터가 적용된 메서드에만 동작한다는 내용이 명시되어 있습니다.

따라서, 기존 코드는 개발자의 의도와 다르게 동작하고 있었으며, 이를 해결하기 위해 Filters 동작을 직접 수행할 수 있도록 수정했습니다.

        @WebSocketGateway({
          cors: '*',
          namespace: '/socket.io/drawing',
        })
        @UseFilters(WsExceptionFilter)
        export class DrawingGateway implements OnGatewayConnection {
          @WebSocketServer()
          server: Server;
        
          constructor(private readonly drawingService: DrawingService) {}
        
          async handleConnection(client: Socket) {
            const roomId = client.handshake.auth.roomId;
            const playerId = client.handshake.auth.playerId;
        
            if (!roomId || !playerId) {
              client.emit('error', {
                code: 4000,
                message: 'Room ID and Player ID are required',
              });
              client.disconnect();
              return;
            }
        
            // ...
        
            client.data.roomId = roomId;
            client.data.playerId = playerId;
        
            client.join(roomId);
          }
        
          // ...
        }

Filters에 담겨있는 것과 동일하게 error 이름의 이벤트를 발행하고, 에러 메시지를 수신받을 수 있게 변경하였습니다.

  1. 그림이 그려지지 않는 문제 #18
    1. Repository에 포함된 오타 수정

      2-a 문제를 해결하게 되고 난 이후 실제 서버 배포를 해본 결과, 한 가지 오류가 추가로 발생했습니다.

image

위 이미지와 같이 게임이 시작되면 플레이어를 찾을 수 없습니다. 오류로 인해 그림이 그려지지 않는 문제가 발생했습니다.

이 오류는 기존 Gateway 코드에 await 누락 및 에러 처리 로직 문제(1번 및 2번 상황)로 인해 에러 처리가 제대로 되지 않으면서 발견되지 않아, 이전에는 발견되지 않았던 문제가 정확히 드러나게 된 경우입니다.

문제를 해결하기 위해 Redis에 실제로 저장된 값과 코드를 비교하며 오류의 원인을 찾기 시작했습니다. 문제는 drawingRepository 코드에서 발생한 것으로 확인되었습니다.

Redis에는 다음 이미지와 같이 room:{roomId}:players:{playerId} 형태로 저장되고 있는데, 실제 코드에서는 키 이름에 s 가 빠진 room:{roomId}:player:{playerId} 형태로 존재 여부를 검사하고 있었습니다.

image

import { Injectable } from '@nestjs/common';
import { RedisService } from 'src/redis/redis.service';

@Injectable()
export class DrawingRepository {
  constructor(private readonly redisService: RedisService) {}

  // ...

  async existsPlayer(roomId: string, playerId: string) {
    const exists = await this.redisService.exists(`room:${roomId}:player:${playerId}`);
    return exists === 1;
  }
}

위 문제 때문에 Redis 내 해당 플레이어가 존재함에도 불구하고 무조건 false를 반환하게 되어 에러가 발생했습니다.

이 문제는 간단하게 s 를 추가하는 것으로 해결할 수 있었습니다.

chat.gateway.ts chat.repository.ts 파일에도 위와 같은 문제가 발견되어 모두 수정했습니다.