/trys-ketch-client

🎮보이스 채팅과 함께 즐기는 드로잉 게임🎨

Primary LanguageJavaScript

눈치 코치 캐치 : TRY'S KETCH !

눈코캐 배경


✨ 프로젝트 소개

🎤 보이스 채팅과 함께 즐기는 🎨 드로잉 게임입니다 !!

  1. 떠오르는 문장 또는 단어를 입력해주세요 !
    게임을 시작하면 유저들은 다른사람이 그릴 문장 또는 단어를 제출해 주세요.
    떠오르는게 없으셔도 괜찮습니다. 저희가 랜덤하게 드리니 그것을 사용하셔도 👌
  2. 제시어를 받으면 그림을 그려요!
    다른 유저가 제출한 제시어를 보고 나만의 그림을 통해 표현해주세요.
  3. 그림을 받으면 제시어를 입력해요!
    다른 유저가 그린 그림을 보고 여러분이 느끼신 그대로 적어주세요.
  4. 모든 라운드가 종료되면...?
    게임이 진행되면서 결과물들이 어떻게 표현되는지 함께 보면서 즐겨주세요 😆

🔍 주요 기능

  • 보이스 채팅 게임이 진행되는 동안 서로 대화를 주고받으면서 플레이 해주세요.
  • 스케치북 내가 상상하는 것을 그림을 통해 마음껏 표현해 주세요.
  • 결과 페이지 모두가 그린 그림을 함께 보면서 보이스 채팅을 통해 즐겨주세요.
  • 좋아요 회원은 맘에 들었던 그림들을 좋아요를 통해 보고싶으실 때 얼마든지 다시 보실 수 있습니다.
  • 이미지 저장 회원은 결과 페이지에서 맘에 드는 이미지가 있으면 저장해서 간직하실 수 있습니다.
  • 초대 코드 각 방마다 가지고 있는 랜덤한 코드를 이용해서 친구에게 초대를 보낼 수 있습니다.
  • 뱃지 특정한 상황마다 지급되는 뱃지를 통해 소소한 성취감을 느끼실 수 있습니다.

📆 프로젝트 기간

2022.12.30 ~ 2022.02.10 / 서비스 런칭 : 2022.01.30



🎮 실제 플레이 화면

ingame

result


📒 기술스택

FRONT-END


BACK-END



🧱 ERD

Trys-ketch-ERD


🏗️ 서비스 아키텍처

서비스 아키텍쳐


🤔️ 기술적 의사 결정

우리는 이렇게 생각하고 결정했습니다 !
요구 사항 선택지 핵심 기술을 선택한 이유 및 근거
WebRTC를 이용한
사용자 음성 연결
- Mesh(p2p),
- SFU
- SFU는 하나의 서버를 더 구축해야하기 때문에 프로젝트의 규모에 맞지 않음
- 영상 없이 음성통신만 구현하면 되기 때문에 클라이언트의 부하가 심하지 않다고 판단
- Mesh방식의 코드 샘플이 가장 많아 정보를 찾아보기 편했음
게임 로비에서 실시간
방 정보 조회
- Polling,
- Long Polling,
- SSE,
- WebSocket
- 리소스와 실시간성 두가지 측면 고려 필요
- 많은 데이터를 주고받아야하므로 서버의 부하가 적어야함
- polling은 구현이 간단하나 실시간으로 반영되지 않고 실시간으로 반영시키고 싶다면 서버에 짧은 간격으로 요청을 하면 되나 서버의 부하가 심해짐
- Long Polling은 실시간성을 보장받을 수 있으나 구현이 번거로움
- 클라이언트와 서버간 상호작용 불필요, 서버에서 클라이언트로 단방향 통신만
필요한 케이스이므로 리소스 측면 고려하여 SSE방식을 채택
게임 진행 과정에서
간단한 그림 그리기
- Canvas API,
- WebGL
- WebGL은 복잡한 3D 렌더링에 더 선호되고 Canvas API는 일반적으로 2D
렌더링에 더 선호됨
- 간단한 그림을 그리는 기능을 구현하기 위해 WebGL은 너무나 많은 러닝커브를 필요로 함
- 따라서 제로베이스에서 금방 구현할 수 있는 Canvas API를 채택하였음
그림 이미지
파일 관리
- Spring Scheduler,
- Spring Batch,
- Scheduler
- quartz
- 그림을 DB에 저장하여 게임 중 데이터 손실을 방지하며 필요없는 데이터를
주기적으로 없애기 위해 스케줄링 필요
- 이벤트 일정에 변동이 없으며 이벤트 시 동작하는 로직이 단순하고 프로젝트 규모에 맞게 간단하게 구현 가능한 Spring Scheduler 사용
비회원 정보 관리 - MySQL,
- Redis,
- Memcached
- Redis는 데이터 입력과 삭제가 MySQL에 비해서 10배정도 빠름
- 관계형 데이터베이스와 같이 쿼리 연산을 지원하지 않지만, 대신 데이터의
고속 읽기와 쓰기에 최적화 되어 있음
- Redis는 Memcached 와 달리 단순한 key/value 자료구조 외에도 다양한 자료구조 지원
- 하나의 비회원 정보에 “고유번호, 닉네임, 이미지URL” 여러개의 값을 저장이 가능
- Redis 자체적으로 만료 시간 설정 가능

🛠️ 트러블슈팅

FRONT-END

webRTC 연결 관련 이슈

진행 순서 내용
😱 문제 미디어 스트림을 접근 권한을 허가 혹은 거부하지 않으면 소켓이 연결되지 않아 게임을 제대로 진행이
불가능하다.
내 소리를 다른 사람은 들을 수 없으나 나는 다른 사람의 소리를 들을 수 있는 문제가 있음
🤔 원인 미디어 스트림 접근 권한을 요청할 때 코드의 흐름이 정지되기 때문에 이후 코드가 실행되지 않으므로
소켓 연결이 되지 않음
미디어 스트림 권한이 처리되지 않았을 때 다른 사용자가 rtc연결을 요청시 나의 미디어 스트림이
undefined 상태이므로 상대의 음성은 들리나 나의 음성이 전달되지 않음
😭 시도 • getUserMedia()를 별개의 useEffect로 분리하여 소켓과 rtc를 연결하는 로직에 병렬적으로 처리되게 구현
이 경우 미디어스트림을 허가하기 전에 rtc연결을 요청하므로 내 로컬스트림이 undefined 인 문제가 발생함
• getUserMedia()함수를 소켓 연결 이후에 호출하도록 변경함. 그러나 이 경우 다른 사용자가 rtc연결을
요청했을 때마이크 접근 권한을 허가하거나 거부하지 않은 상태이면 나의 미디어스트림이 undefined 인
경우가 발생하므로 나의 말을 다른 사람이 들을 수가 없음
😄 해결 • getUserMedia()함수를 별개의 useEffect를 이용해 병렬적으로 처리하는 로직은 그대로 둠
• 요청을 받아 rtc연결을 실행하거나, 내가 새로운 rtc연결을 시도하고자 하는 경우 사용자가 마이크 사용을
허가/거부하지 않은 상태면 반복문과 함께 Promise, setTimeOut으로 구현한 sleep함수를 이용해 코드의
흐름을 막음
• 사용자가 마이크 사용을 허가한 경우 그대로 연결을 진행함, 사용을 거부한 경우 본인의 마이크는 사용이
불가능하지만 사용자가 의도한 것이므로 특별히 예외처리하지 않음

특정 영역을 한 가지 색으로 색칠하는 floodfill 알고리즘

요구 사항 핵심 기술을 선택한 이유 및 근거
😱 문제 floodfill 알고리즘의 성능이 좋지 않아 색을 칠하는데 지나치게 오랜 시간이 소요됨
선을 그었을 때 테두리의 rgba값이 선의 rgba값과 아주 작은 차이가 있어 floodfill 알고리즘 이 제대로
적용되지 않음
🤔 원인 선을 그었을 때 테두리가 픽셀의 중간에 겹치는 경우 Canvas API에서 자동으로 보정하여 픽셀의 색상이
바뀌게 되면서 floodfill 알고리즘이 적용되지 않음
floodfill 알고리즘은 기본적으로 수만~수십만개의 픽셀을 대상으로 적용되기 때문에 함수의 오버헤드나
시간복잡도에 큰 영향을 받게 됨 따라서 오버헤드를 줄여야 하며 메모리 공간 역시 최적화 되어야함
😭 시도 • 재귀 형태의 floodfill 알고리즘 을 stack의 pop과 push로 구현해보았으나 pop과 push메소드의 오버헤드로
인해 만족할만한 성능을 내지 못했음
• rgba값에 tolerance를 주어 rgba값의 차이가 크지 않다면 같은 색으로 인식하고 floodfill 알고리즘의 적용을
받게 구현함
• rgb값을 string을 이용해 구하고자 했으나 이는 너무 많은 오버헤드를 발생시켰음. 또한 해당 픽셀의
rgb값이 tolerance 범위 내에 존재하는지를 판단하기 위해 Math.abs() 혹은 rgb값의 표준편차를
이용해 tolerance와 비교하였음 그러나 이 역시 많은 오버헤드를 발생시킴
😄 해결 • pop과 push를 사용하지 않고 배열에 x와 y의 픽셀 위치 정보를 담아 현재의 픽셀을 포인터를 이용해 배열의
인덱스를 가리킴으로서 특정하고 해당 픽셀에 대한 floodfill 알고리즘을 적용하여 성능 향상을 이끌어냄
• rgba값을 추출해내기 위해 unsigned int로 표현된 rgba값의 해당하는 비트에 비트연산자 &로 마스킹하여
rgba값을 추출하고 오른쪽으로 shift하여 rgba값을 사용하도록 함
• tolerance와 현재 rgba값을 비교하는 부분에서 다른 방식을 차용하지 않고 그냥 tolerance값과 일일이
비교함 다른 방식보다 이 방식이 가장 빨랐음

다른 시간에 생성된 토스트가 동일한 타임아웃을 공유하는 문제

요구 사항 핵심 기술을 선택한 이유 및 근거
😱 문제 토스트에서 생성시 3초뒤에 삭제되는 타임아웃을 적용해놓음
각각의 토스트가 서로 다른 종료 시점을 가지지 않고 모두 같은 종료시점을 가짐
🤔 원인 rerendering 되면서 타임아웃도 재설정됨
😭 시도 • react devtools render highlight
디버깅을 위해 렌더링되는 컴포넌트를 하이라이트해주는 react devtools 기능 사용 ⇒ 디버깅 결과 토스트가
생성될 때 이미 존재하는 토스트들도 함께 렌더링되는 것을 발견
😄 해결 • 토스트 컴포넌트에 memo 적용
• 토스트 컴포넌트에 전달되는 함수 props에 useCallback 적용
위 두가지를 적용하면서 리렌더링해야할 컴포넌트로 인식하지 않아서 각각의 타임아웃이 업데이트되지 않음
BACK-END

Redis @Indexed 의 참조값 삭제 문제

요구 사항 핵심 기술을 선택한 이유 및 근거
😱 문제 레디스에서 키값으로 조회하기 위해서 사용했던 @Indexed 어노테이션을 사용
이 어노테이션을 붙여 줘야지만 key 값으로 검색이 가능하며, 비회원 정보의 검증을 하기 위해서 추가
예)새로운 비회원이 생기면 guest:10001 이라는 하나의 파일이 생기고 동시에 guest:10001:idx 라는 새로운
파일이 생기는데 이 파일이 만료시 삭제가 되지 않는 문제 발생
🤔 원인 @Indexed 로 인해 같이 생성된 참조값들은 만료시 자동으로 삭제가 되지 않는것이 문제였다.
😭 시도 • 강제 지정 (redistemplate.expire)
참조값이 생성될 때 생기는 이름은 동일한 패턴이기 때문에 RedisTemplate 에서 만료시간을 해당 파일이
생성되면 바로 같이 지정하는 방식을 사용하면 가능하지만 이는 근본적인 해결법이 아니여서 다른 방법을
더 찾아보기로 결정
• @Id 만을 사용
레디스를 통해 비회원정보 검증하는 부분이 존재하기 때문에 @Id 어노테이션 만으로는 찾는 비회원정보를
찾을수가 없어서 @Indexed 를 사용하는 것은 유지
😄 해결 @EnableRedisRepositories(enableKeyspaceEvents = EnableKeyspaceEvents.ON_STARTUP)
위처럼 RedisRepo 에 속성을 추가해서 사용해본 결과 Server 가 내려간 사이 Redis 에서 삭제가 되는 경우가
아닌 이상 삭제시의 이벤트를 수신해서 참조값도 함께 잘 삭제가 되는것을 확인

SSE 연결 관련 이슈

요구 사항 핵심 기술을 선택한 이유 및 근거
😱 문제 emitter의 객체 시간을 길게 설정할 때, 데이터를 제대로 전송하지 못할 때 발생하는
IOException : Broken PIpe 에러가 발생
🤔 원인 JPA 사용시 open in view 설정이 기본으로 true로 설정됨. true로 설정되면 HTTP Connection이 열려있는 동안
DB Connection도 같이 열려있게 됨.
보통은 HTTP 호출이 끝나고 DB 커넥션도 종료되나, SSE 사용시에는 객체가 만료되기 전까지 계속해서 DB
커넥션이 열려 고갈되는 것이 문제였음
😭 시도 객체의 시간을 짧게 설정 해봄 → 객체의 만료시간이 지날 때마다 재연결되고 이는 결국 리소스의 낭비로
이어져 SSE를 사용하는 목적에 맞지않음
😄 해결 DB 커넥션을 계속 물고 있지 않도록 OSIV 설정을 끔. → 이러한 경우 프록시 객체를 초기화 시키지 못한다는
다른 에러가 발생 → @OneToMany의 fetch join 타입을 Eager로 설정하여 해결

게임 중 발생하는 동시성 제어(synchronized, DB Lock)

요구 사항 핵심 기술을 선택한 이유 및 근거
😱 문제 제한 시간을 넘어 미처 제출하지 못한 유저의 키워드나 이미지가 일괄 자동 제출 되었을 때 DB에 제대로
데이터가 쌓이지 않거나 다음 라운드로 진행되지 않는 이슈가 발생함.
🤔 원인 현재 로직상 save -> find의 구조를 가지고 있기에 자동제출 기능 구현 시 동시에 동일한 자원에 접근하려
하는 것을 원인으로 판단.
😭 시도 • synchronized
제출로직의 Controller method 에 synchronized 적용하여 스레드 간 데이터 동기화함. 의도대로 동작 했으나,
성능 상 속도 저하 이슈 발생함.
• Thread Scheduler
제출 인원을 확인하는 로직을 독립시켜서 thread를 만들고, 일정 시간 동안 주기적으로 돌아가게끔 구현함.
그러나 DB에 읽기 되는 순서를 제어 하지 못해 동일한 문제 발생함.
• Optimistic Lock
제출 로직 특성 상 빈번한 충돌이 예측 가능했기 때문에 롤백 비용을 고려하여 Optimistic Lock 미적용.
• Pessimistic Lock
제출 인원을 하나의 column으로 갖는 table을 생성하고, row level lock 적용, update용 find method를 구현 및,
해당 method에 @Lock 어노테이션과 모드를 설정함.
제출 인원을 수정할 때 write lock이 걸리고 transaction이 끝나야 lock이 풀리는 것을 이용함.
😄 해결 게임의 최대 인원이 8명으로 테스트 했을 때, 비교적 성능 이슈가 없던 Pessimistic Lock을 사용하는 것이
적절하다고 판단되어 도입함.

‍🧑‍💻 프로젝트 멤버

🔰장신원 이민규 🔰안은솔 김재영 서혁수 황미경 조문정
장신원 이민규 안은솔 김재영 서혁수 황미경 조문정
FRONT-END FRONT-END BACK-END BACK-END BACK-END BACK-END DESIGN