Releases: boostcampwm-2024/refactor-web42-stop-troublepainter
릴리즈 노트 v1.4.0
TroublePainter 릴리즈 노트 - 버전 1.4.0
주요 기능 및 개선 사항:
- OCR 비용 절감:
- 4개의 이미지를 하나의 캔버스에 이미지 스프라이트로 만듭니다.
- 한 라운드에서 4번의 OCR 요청을 보내던 것을 1번으로 줄였습니다.
- OCR 인식률 향상:
- 이미지 상하좌우에 여백을 주어 큰 글자 인식률을 향상 시켰습니다.
- 게임플레이 개선:
- 플레이어가 연관 단어 작성으로 패널티를 받을 때 토스트 메시지가 나오는 기능을 추가했습니다.
- OCR 인식 시간을 10초에서 5초로 줄였습니다.
- 연관 단어를 작성한 수만큼 패널티를 받도록 수정했습니다.
- Winston logger 적용:
- 기존
console.log가 아닌 winston logger를 적용해 각 단계별로 (warn,debug...) 적절한 logging을 할 수 있었습니다.
- 기존
- HTTP3.0 적용:
- 기존
http3.0을 지원하지 않는nginx도커파일 대신http_3_module와 같이 빌드된 도커파일을 사용했습니다.
- 기존
- 선 굵기 수정:
- OCR 성능 향상
- 선 굵기를 1로 고정시켜서 OCR 인식률을 향상시켰습니다.
- 기타 개선 사항:
- Clova Studio로 부정확한 OCR 성능을 개선했습니다.
- Clova OCR API 요청 방식을 multipart/form-data 형식으로 변경했습니다.
버그 수정:
- 캔버스에 색 채우기 시 서버가 다운되는 현상을 수정했습니다.
- 구경꾼이 정답을 맞췄을 때 패널티 관련 토스트 메시지가 보이지 않는 현상을 수정했습니다.
- 캔버스 여백이 추가됨에 따라 좌표 값이 잘못 계산되던 현상을 수정했습니다.
- 제시어와의 연관 정도가 문자열을 포함해 NaN을 응답하던 현상을 수정했습니다.
전체 변경 사항: [1.3.0 … 1.4.0]
What's Changed
- Bug/#85 이미지 채우기 오류 수정, nginx http3.0 드래프트버전 추가, winston logger 추가 by @swkim12345 in #88
- Feature/#86 패널티 받으면 토스트 메시지 띄우기 by @ijun17 in #87
- refactor: 이미지 스프라이트로 플레이어들의 그림 생성 by @ijun17 in #94
- refactor/#89 Clova Studio로 부정확한 OCR 성능 개선하기 by @uuuo3o in #95
- Refactor/#92 Clova ocr API 요청 방식을 multipart/form-data로 변경하기 by @uuuo3o in #96
- refactor: 캔버스 서비스에 워커 스레드 적용 by @ijun17 in #99
- Refactor/#97 nginx 설정 변경, 배포시 사용하는 이미지 변경 by @swkim12345 in #100
- Refactor/#97 server 에러 수정, nginx 설정 분리 by @swkim12345 in #101
- Refactor/#102#103 클로버 스튜디오 단어 인식률 NaN 문제 및 캔버스 여백 추가 by @ijun17 in #104
- fix: 정답을 맞춰도 패널티 메시지가 뜨도록 수정 by @ijun17 in #106
- fix: 캔버스 여백 좌표 계산 수정 by @ijun17 in #107
- Refactor/#108 docker file copy 변경, ocr 시간 변경 by @swkim12345 in #109
- chore/#110 불필요한 console.log 제거 by @uuuo3o in #111
- fix: panelty에서 penalty로 수정 by @ijun17 in #113
Full Changelog: 1.3.0...1.4.0
릴리즈 노트 1.3.0
TroublePainter 릴리즈 노트 - 버전 1.3.0
주요 기능 및 개선 사항:
- OCR 성능 향상:
- 캔버스 크기 조정을 통해 OCR 이미지 처리 시간을 단축했습니다.
pureimage라이브러리를node-canvas로 교체하여 렌더링 속도를 향상시켰습니다.- 이미지 분석 시간을 10초로 단축했습니다.
- 게임플레이 개선:
- Clova Studio를 통합하여 인식된 단어가 제시어와 관련 있는지 판단하고, 관련 없을 경우 패널티를 적용하는 기능을 추가했습니다.
- Redis pub/sub 기능을 사용하여 서로 다른 소켓 네임스페이스 간에 데이터를 공유하는 메커니즘을 구현하여 클라이언트 간 캔버스 표시 및 게임 로직의 일관성을 유지했습니다.
- OCR 처리 시간 동안 모달 창을 표시하도록 했습니다.
- Docker 이미지 최적화:
- 멀티 스테이지 빌드를 구현하고, 프로덕션 단계에서 불필요한 의존성을 제거하여 Docker 이미지 크기를 47.2% (2.58GB에서 1.22GB로) 감소시켰습니다.
- Docker 레이어 캐싱을 구현하여 빌드 시간을 단축했습니다.
- Alpine Linux에서 의존성이 올바르게 설치되도록 수정했습니다.
- 기타 개선 사항:
- 더 빠른 통신을 위해 HTTP/2 및 HTTP/3 지원을 활성화했습니다.
- 배경 음악 및 효과음을 기본적으로 끄도록 설정했습니다.
- 클라이언트 측 타입 처리를 개선했습니다.
버그 수정:
- OCR 동작 중 채팅이 전송되는 버그를 수정했습니다.
- 클라이언트 측 타입 에러를 해결했습니다.
전체 변경 사항: [1.2.0...1.3.0]
풀 리퀘스트:
- Feature/#54: Clova OCR로 단어 인식 구현 (#62)
- Refactor/#63: 캔버스 내 글자 지우는 부분 비동기로 변경 (#65)
- Feature/#64: OCR 탐지 시간 동안 캔버스 이미지 숨기는 기능 추가 (#66)
- fix: 클라이언트 타입 에러 수정 (#68)
Feature/#69: 배경음 및 효과음 기본 끄기로 설정 (#70) - bug/#72: OCR 동작 시간 동안 채팅 전송 오류 수정 (#73)
- Refactor/#75: pureimage에서 node-canvas로 변경 (#76)
- Feature/#71: Clova Studio 적용하여 제시어와 연관된 단어 발견 시 패널티 적용 (#77)
- Deploy/#74: 서버 도커 경량화, core minify, docker server 의존성 추가, 로컬 빌드 명령어 추가 (#78)
- Deploy/#74: docker image 캐싱 적용, tsconfig 의존성 dockerfile 추가 (#79)
- build(docker-build/prod/nginx.conf): http 2, 3 버전 활성화 (#80)
- build(github/*-ci-cd.yml): server, client paths 업데이트 (#81)
- Refactor/#82: 그림 분석 시간 10초로 단축 (#83)
Deploy/#74: alpine linux 상에서 의존성 설치 (#84)
릴리즈 노트 v1.2.0
📖 릴리즈 노트 v1.2.0
[FE]
1. 점이 경계선 안에 있는지 확인하는 메소드 구현 #58
- 목표
- 바운더리 안에 존재하는 선 제거 CRDT 메시지 생성
- 문제
- 공용 캔버스의 바운더리 기준으로 모든 선 제거 시 불필요한 삭제 발생 가능
- 개인 캔버스의 바운더리와 겹치는 공용 캔버스 바운더리 처리 기준 필요
- 해결
isPointInBoundary메서드 구현하여 점이 바운더리 안에 포함되는지 판별- 개인 캔버스의 바운더리와 겹치는 공용 캔버스 바운더리 제거
- 필터링된 바운더리를 기준으로 선 필터링 후 CRDT
SYNC메시지 생성
- 결과
- 공용 캔버스의 바운더리를 필터링하여 안전한 삭제 가능
- 필요한 선만 제거하는 CRDT 메시지 생성
2. 테스트 데이터 생성 및 플레이라이트 테스트 코드 작성 #60
- 목표
- E2E 테스트를 위한 테스트 데이터 생성
- 플레이라이트 테스트 코드 작성
- 문제
- E2E 테스트를 통해 보다 안정적인 소프트웨어 제작 가능
- 해결
- 테스트 데이터 생성하여 일관된 환경 제공
- 플레이라이트 테스트 코드 작성하여 자동화 진행
- 테스트 실행 방법 정리하여 재현 가능하도록 구성
- 결과
drawing-text.spec.ts실행을 통한 UI 테스트 가능- OCR 설정 후 로컬에서 E2E 테스트 실행 가능
3. 다른 사람의 그림이 안보이는 오류 해결 #61
- 목표
- 다른 사용자의 그림이 보이지 않는 오류 수정
- 문제
- 다른 사용자의 그림이 실시간으로 표시되지 않는 문제 발생
- 해결
gameSocket이sharedWorker로 넘어가 조건을 제대로 체크하지 못함- 이를 반영해
useDrawingSocket에서gameSocket과 관련된 코드를 삭제
- 결과
- 다른 사용자의 그림이 실시간으로 정상적으로 표시됨
[BE]
1. 서버에서 가상 캔버스에 그림 그리기 #52
- 목표
- 서버에서 가상 캔버스에 그림을 그리고 이를 base64 형식의 JPEG 이미지로 변환
- 문제
- 클라이언트가 보내는 이미지에서 글자가 발견될 때 이를 지워야 함
- Node.js 환경에서 브라우저의 Canvas API를 사용할 수 없음
- 해결
pureimage라이브러리를 이용해 이미지로 변경- 게임 방마다 LWWMap 관리
- 각 플레이어마다 캔버스에 그림을 그리고 이를 base64 형식의 JPEG 이미지로 변환
- 결과
- 서버에서 신뢰할 수 있는 방식으로 가상 캔버스에 그림을 그리고 이를 이미지로 변환하여 활용 가능
- 이미지를 바탕으로
OCR를 할 수 있음
2. 버그가 의심되는 멀티서버, Redis 어댑터 관련 설정 롤백 #55
- 목표
- 멀티서버 및 Redis 어댑터 설정으로 인한 버그 의심으로 기존 설정으로 롤백
- 문제
- 멀티서버 및 Redis 어댑터 설정 이후 발생한 예상치 못한 버그
- 해결
- Redis 어댑터 적용을 이전 방식으로 롤백
- Nginx 설정 및 Docker 설정을 이전 방식으로 롤백
- 결과
- 멀티서버 및 Redis 어댑터 설정 이전의 안정적인 상태로 복원
3주차 릴리즈 노트 v1.1.0
[FE]
1. Shared Worker를 이용해 브라우저 내의 여러 탭에서 소켓 연결 관리하기 #35
- 목표
- 기존 여러개의 탭에서 각각 소켓 연결을 하게 되는데 이부분을 SharedWorker를 통해 하나의 연결을 여러 탭에서 공유하게 만들기.
- 기존 Socket 아키텍처에 존재하는
socket.storage분리 - 3개의 Socket(Chat, Game, Draw)에 대한 코드를 Shared Worker에서 진행
- 기존 Socket 아키텍처에 존재하는
- 기존 여러개의 탭에서 각각 소켓 연결을 하게 되는데 이부분을 SharedWorker를 통해 하나의 연결을 여러 탭에서 공유하게 만들기.
- 문제
- Game Socket 연결 지연으로
joinRoom이벤트가 실행되지 않는 문제
- Game Socket 연결 지연으로
- 해결
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 삽입에
- 해결
- List 삽입에
RPUSH사용
- List 삽입에
- 결과
reverse()제거로 연산 횟수 약 50% 감소
3. Artillery를 이용한 성능 테스트 #38
- Artillery로 사용자가 사용하는 환경을 묘사한 성능 테스트 진행.
- Artillery’s 모니터링 툴로 부하 발생 구간 시각화.
4. Redis Adaptor 적용하기 #32
- 목표
- 기존 Adaptor에서 Redis Adaptor로 전환해 다중 서버 확장 가능하도록 변경
- 결과
- Nginx의 IP Hash로 로드밸런싱 설정
- 같은 방에 있는 유저 중 일부가 게임 시작 실패
- Redis Adaptor 설정 오류로 추정
- Nginx 로드밸런싱의 경우 롤백
- Nginx의 IP Hash로 로드밸런싱 설정
What's Changed
- Test/#20 플레이 라이트 성능 테스트 자동화 by @ijun17 in #21
- Feature/#23 구글 애널리틱스 스크립트 삽입 by @ijun17 in #24
- develop 브랜치로 병합 by @ijun17 in #25
- Refactor/#22 성능 테스트용 코드 추가 by @uuuo3o in #26
- Refactor/#22 Redis List 삽입 방식 변경 by @uuuo3o in #27
- Dev be to develop by @swkim12345 in #28
- Refactor/#29 프론트엔드 최적화 by @ijun17 in #30
- Dev fe to develop by @ijun17 in #33
- Refactor/#15 redis adaptor socket io에 적용하기 by @swkim12345 in #32
- Dev be to develop by @swkim12345 in #34
- Refactor/shared worker/#14 by @dannysir in #35
- develop 브랜치 머지 by @dannysir in #36
New Contributors
Full Changelog: 1.0.1...1.1.0
2주차 릴리즈 노트 v1.0.1
테스트 코드 작성을 통한 버그 수정
- 단위 테스트를 진행하면서 발견한 문제 #3
- 비동기 함수에 누락된
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 키워드를 추가함으로써 정상적으로 동작하게 되었습니다.
- 통합 테스트, e2e 테스트를 진행하면서 발견한 문제 #9
- 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 이름의 이벤트를 발행하고, 에러 메시지를 수신받을 수 있게 변경하였습니다.
- 그림이 그려지지 않는 문제 #18
-
Repository에 포함된 오타 수정
2-a 문제를 해결하게 되고 난 이후 실제 서버 배포를 해본 결과, 한 가지 오류가 추가로 발생했습니다.
-
위 이미지와 같이 게임이 시작되면 플레이어를 찾을 수 없습니다. 오류로 인해 그림이 그려지지 않는 문제가 발생했습니다.
이 오류는 기존 Gateway 코드에 await 누락 및 에러 처리 로직 문제(1번 및 2번 상황)로 인해 에러 처리가 제대로 되지 않으면서 발견되지 않아, 이전에는 발견되지 않았던 문제가 정확히 드러나게 된 경우입니다.
문제를 해결하기 위해 Redis에 실제로 저장된 값과 코드를 비교하며 오류의 원인을 찾기 시작했습니다. 문제는 drawingRepository 코드에서 발생한 것으로 확인되었습니다.
Redis에는 다음 이미지와 같이 room:{roomId}:players:{playerId} 형태로 저장되고 있는데, 실제 코드에서는 키 이름에 s 가 빠진 room:{roomId}:player:{playerId} 형태로 존재 여부를 검사하고 있었습니다.
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 파일에도 위와 같은 문제가 발견되어 모두 수정했습니다.

