2부. Flutter로 실시간 드로잉 게임 만들기
목차
Trouble Painter 개발 과정을 공유합니다.
👉 English Version
1부. Trouble Painter 드로잉 마피아 게임
✔︎ 2부. Flutter로 실시간 드로잉 게임 만들기
Team Xorker
Team Xorker는 YAPP 24기에서 모인 PM 1명, 디자이너 2명, 개발자 4명(Flutter 2 / Spring 2)으로 총 7인으로 구성된 팀입니다.
아이디어 선정
처음에는 동료들의 피드백을 받아 자기 성장 및 PR로 사용할 수 있는 서비스를 아이디어로 선정하여 진행하였습니다. 하지만 시간이 지나도 “사람들이 진솔한 피드백을 남길 수 있을까?”라는 문제를 해결하지 못했고, 팀빌딩 이후 4주만에 아이디어를 변경하기로 결정하였습니다. 🥹
이후 1주일 기한을 잡고 다시 새로운 아이디어를 나누고 논의하였습니다.
🐶 반려동물 기반 소개팅 앱 : 초창기 유저 모으는 문제 & 소개팅이라는 도메인에 대한 공감 부족으로 기각!
📝 AI 반려동물 일기 서비스 : 반려동물을 키우는 팀원이 한 명뿐인데, 그 팀원도 안 쓸 것 같다하여 기각!
🧩 보드 게임을 모바일화 : 게임에 대한 기획 부재 & 그래픽 위주의 작업에 대한 부담으로 기각!
서로 다른 기준으로 아이템을 기각하는 과정이 반복되었기에, 모두가 공감하는 목표를 논의하여 디프만과 연합으로 진행하는 데모데이에서 가장 많은 관심을 받을 수 있는 아이템이라는 정성적인 기준을 정하였습니다.
해당 기준으로 아이템을 논의하던 중 실시간으로 그림을 그려 마피아를 찾는 게임 아이디어가 나왔는데, Figma에서 모의로 플레이해 보니 재미도 있고, 기획상 그래픽 작업도 많지 않아 보였습니다. 결정적으로 해당 아이템을 잘 구현하면 데모데이에서 가장 많은 관심을 받을 수 있을 것 같아 Trouble Painter가 탄생하게 되었습니다 🎉
디프만 동아리와 연합으로 진행하는 데모데이는 상대 동아리 부스를 돌아다니며 가장 마음에드는 팀에 모의 머니를 투자하는 방식으로 진행되었는데, 결과적으로 Trouble Painter가 가장 많은 모의 투자금을 유치하면서 가장 많은 관심 받기라는 목표를 달성할 수 있었습니다. ✌️
프로젝트 관리
프로젝트 관리 도구로 Jira를 사용하였고, 1주 단위로 스프린트를 진행하였습니다. 초반에는 모든 팀이 동일한 기능 단위로 스프린트를 진행했는데, Flutter는 디자인과 백엔드 팀의 작업에 의존성이 있어 한 주 뒤로 미루어 스프린트를 진행하였습니다.
2주 단위로 3L(Liked, Learned, Lacked) 방식의 회고를 진행하였는데, 글을 작성하는 시점에서 되돌아 보면 회고 덕분에 팀 내 소통도 개선되었고 아이디어도 바꿀 수 있었던 것 같습니다. 👍👍
일정 관리
오늘 또 일을 미루고 말았다 - 나카지마 사토시
“마감을 잘 지키려면 라스트 스퍼트를 버리고 스타트 대시를 하라”
초기에는 YAPP 24기 기간 내에 유저를 받을 계획으로 데모데이 5주 전 스토어 배포를 목표로 일정을 계획하였습니다. 중간에 5주가 지난 시점에서 아이디어가 변경되는 이슈가 있었지만, 스타트 대시를 위해 무리한 일정임을 알고도 기존 계획을 그대로 진행하였습니다.
Flutter 작업을 기준으로 아이디어를 변경한 시점 부터 스토어 배포까지 실제 진행된 일정은 다음과 같습니다.
일정 | Flutter 작업 |
---|---|
6월 4주차 | 아이디어 변경 |
6월 5주차 | Firebase 기반 유저 테스트용 MVP 개발 |
7월 1주차 | 아키텍처 설계 & 스토어 배포 자동화 구축 |
7월 2주차 | 디자인 시스템 구축 |
7월 3주차 | 비즈니스 로직 구현 |
7월 4주차 | 디자인 반영 |
8월 1주차 | 웹소켓 설계 |
8월 2주차 | 서버 연동 |
8월 3주차 | QA & 스토어 배포 |
기존 계획보다 2주 늦어졌지만 웹소켓 기반의 실시간 게임을 단 7주 만에 만들어 스토어 배포까지 진행할 수 있었습니다.
유저 테스트
아이디어가 변경된 시점으로부터 1주일 뒤에 동아리에서 진행하는 유저 테스트(User Test)가 예정되어 있었습니다. 그래서 Cloud Firestore의 실시간 통신 기능을 이용하여 1주일 동안 유저 테스트용 MVP 앱 서비스를 만들었습니다.
초반에 Firebase App Distribution과 Fastlane으로 내부 테스트 환경을 구축해 두었기 때문에 MVP 앱을 팀 내 배포하여 유저 테스트를 무사히 진행할 수 있었습니다.
이번 유저 테스트를 통해서 배운점은 다음과 같습니다.
- 일반 앱의 UI는 유저들이 이미 학습되어 있어 익숙지만, 게임 UI는 익숙하지 않고 학습이 필요하다.
- 일반 앱은 화면 전환을 유저가 직접 제어할 수 있지만, 화면이 자동 전환되며 시간 제한까지 있는 게임은 무엇을 보고 어떤 액션을 취해야하는지 더욱 직관적이게 설계해야 한다.
- 대부분의 유저는 튜토리얼을 읽지 않으며, 읽어도 게임 룰을 쉽게 이해하지 못한다.
디자인
디자인 팀은 파이널 디자인이 나오기까지 페이지 6개를 가득 채울 정도로 열정이 불타오르는 팀입니다 🔥
UI & UX
유저 테스트때 배운 내용들을 반영하여 대기화면의 BGM과 플레이 화면의 BGM을 다르게 재생하여 보다 게임 시작 분위기를 환기하였습니다.
초기 게임 준비 페이지에서 카테고리, 역할과 그리고 순서까지 한 화면에서 너무 많은 정보를 보여주어 전달이 잘 안되었는데, 역할과 카테고리만 보여주도록 변경하였고, 캐릭터를 넣어 역할을 보다 직관적이게 인식하도록 개선하였습니다.
자신의 순서는 드로잉 화면 라운드 애니메이션과 함께 보여주었고, 막상 자기 턴이 되었어도 인지하지 못하는 문제는 본인 차례에 백그라운드 색상을 바꾸고 한 획 가이드 애니메이션도 재생하여 개선하였습니다. 이로써 한 턴에 한 획만 그리는 게임 룰도 게임 중간에 학습하도록 유도할 수 있었습니다.
캐릭터
마피아 게임의 마피아와 시민의 역할을 Trouble Painter에선 악동 미술가와 착한 미술가로 표현하였으며, 두 캐릭터의 덩치와 성격 차이가 보이도록 햄스터와 곰을 선택하여 디자인하였습니다.
또한 영화 라따뚜이를 오마주하여 착한 미술가의 모자 속에 악동 미술가가 숨어서 정체를 감추고 그림을 그리는 컨셉을 UI에 녹였습니다.
디자인 시스템
작업 효율은 높히고 커뮤니케이션 비용을 낮추기 위해 디자인 시스템을 구축하였으며, 게임 컨셉상 라이트 테마가 어울리지 않아 다크 테마만 지원하였습니다.
백엔드
백엔드 서버는 현재 라즈베리파이에서 운영되고 있습니다. 언젠간 동접자가 많아져 라즈베리파이가 불타고 클라우드로 이전하는 날이오면 좋겠습니다. 🔥
아키텍처
백엔드는 Kotlin & Spring Boot 기반으로 구현되어 있으며 상세 기술 스택과 아키텍처는 다음과 같습니다.
Category | Stack |
---|---|
IDE | IntelliJ |
Language | Kotlin |
Framework | Spring Boot 3.2.5, Gradle |
Authentication | Spring Security, JSON Web Tokens, Opaque Token |
ORM | Spring Data JPA |
Database | MySQL |
External | Nginx, Docker, Redis, Kubernetes, ELK |
CI/CD | ArgoCD, Github Actions |
API Docs | Notion, Swagger |
모니터링
Prometheus, Grafana, Sentry를 이용하여 로그 및 실시간 서버 상태를 모니터링 하고 있습니다.
Flutter
Trouble Painter는 Flutter로 만들었으며 다음과 같은 기술 스택을 사용하고 있습니다.
Firebase | |
---|---|
Analytics | 앱 사용 및 사용자 참여에 대한 통계 수집 |
App Distribution | Android & iOS 팀 내 배포 |
Cloud Firestore | 유저 테스트용 MVP 버전 서버 역할 |
Crashlytics | 앱 충돌 추적 및 에러 로깅 |
Remote Config | 강제 업데이트 & 공지사항 등의 원격 설정 |
Gemini의 경우, 플레이어들이 그린 그림 기반으로 악동 미술가에게 힌트를 제공하는 기능을 구현하기 위해 사용하였으며, 적용 결과물은 이전 글을 참고해 주세요. 덕분에 Gemini API 대회에 출품할 수 있었는데, 서비스를 응원해 주시고 싶다면 링크에서 투표 부탁드립니다 🫶
Flavor & Env
flutter_flavorizr 패키지를 이용하여 개발 환경(DEV)과 배포 환경(PROD)을 나누어 운영하였으며, Firebase의 경우 한 프로젝트에서 Remote Config 환경 분리가 불가능하므로 프로젝트를 두 개로 분리하여 진행했습니다.
또한 환경에 따라 다르게 적용되어야 하는 파일들은 별도로 분리하여 관리하였습니다.
1 | .env |
아키텍처
이번 프로젝트는 Riverpod 기반의 MVVM과 Clean Architecture를 사용한 형태로 설계하였으며, 폴더 구조는 Feature First 구조에서 Presentation
폴더를 app
폴더로 분리하여 구현하였습니다.
팀에서 Flutter 파트는 완성된 디자인과 백엔드 기능을 조합하는 역할을 맡습니다. 즉, 디자인과 백엔드 작업에 의존성이 있어 혼자 열심히 달릴 수 없는 포지션입니다.
안그래도 중간에 아이디어를 변경하면서 시간이 부족했기에, 의존성 문제로 작업을 못하는 상황이 오더라도 어떻게든 할 수 있는 일을 진행하여 여유 시간을 확보할 필요가 있었습니다.
Rober C.Martin의 Clean Architecture에는 다음과 같은 내용이 나옵니다.
Clean Architecture - Robert C.Martin
“아키텍트는 정책을 핵심적인 요소로 식별하고 세부사항을 정책과 무관하게 만들어 세부사항의 결정을 미루거나 연기하도록 한다.”
이 내용을 조금 다르게 바라보면 “세부사항이 결정되지 않아도 시스템의 핵심적인 요소를 만들 수 있다.”로 이해할 수 있으며 이번 프로젝트에선 개발 단계에서 부터 아키텍처의 의존성 제어 이점을 활용해 초반에는 Wireframe을 기반으로 앱의 기능을 구현하였고, 디자인이 나온 뒤에는 UI 테스트 모드용 ViewModel을 전달하여 원하는 게임 화면을 볼 수 있는 환경을 구성한 뒤 수월하게 작업을 진행하였습니다.
백엔드 연동 코드는 Data 레이어에 격리하였고, 상호작용하는데 필요한 데이터는 Model 클래스로 구현한 뒤, 상위 레이어로 전달시 Entity 클래스로 변환하여 기존 로직을 그대로 사용할 수 있었습니다.
이와 같이 서버 의존성을 데이터 레이어에 격리하였기에, 향후 서버를 새롭게 만들어도 기존 도메인 레이어의 로직들은 그대로 재활용 할 수 있습니다.
Riverpod
Riverpod을 상태 관리, MVVM 그리고 의존성 주입 용도로 사용하였는데, Code Generator를 사용하지 않고 직접 변형하여 사용했습니다.
Presentation 레이어에선 BaseView
와 BaseViewModel
을 만들어 내부적으로 Provider를 생성하고, Generic 타입을 자동으로 추론하도록 구현하여 상용구 코드를 많이 줄일 수 있었습니다.
1 | /// 전역 변수를 만들 필요가 없습니다. |
Domain & Data 레이어에선 주로 의존성 주입(Dependency Injection) 도구로 활용하였는데, Provider를 전역 변수에 할당하지 않고 클래스 내부에 static
으로 캡슐화하여, 전역 변수를 줄이고 사용 편의성을 높혔습니다.
1 | class TodosNotifier extends Notifier<List<Todo>> { |
디자인 커뮤니케이션
Trouble Painter는 최소 3인 이상이 모여야 게임을 실행할 수 있고, 역할에 따라 볼 수 있는 화면이 다릅니다. 따라서 디자이너 분들이 언제든지 원하는 화면을 보고 피드백을 주실 수 있도록 UI 테스트 모드를 만들어 제공하였습니다.
또한 모바일 앱 서비스는 웹과 다르게 컴포넌트의 크기를 확인하기가 쉽지 않기 때문에, 앱의 개발자 메뉴에 컴포넌트 페이지를 만들어 컴포넌트의 크기나 애니메이션 동작을 볼 수 있도록 제공하였습니다.
웹소켓 설계
실시간 통신은 웹소켓 기반으로 별도 프로토콜을 만들어 구현하였으며, HTTP 기반 API 보다 설계시 백엔드 팀과 긴밀한 커뮤니케이션이 필요했습니다.
구현하면서 배운 웹소켓과 HTTP 기반 API와 차이점은 다음과 같습니다.
- 상태가 존재하므로 도중에 재접속시 동기화 과정이 필요
- 라우팅이 서버측 브로드캐스트에 의존하므로 화면 추가시 서버측과 논의가 필요
- 요청과 응답이 1대1이 아니라 문서화 방식이 다름
네트워크가 불안정한 모바일에서 실시간으로 진행되는 게임이므로 재접속 기능은 반드시 구현해야하는 필수 기능이었으며, 재접속하는 시점의 서버측 상태 스냅샷을 전달받아 클라이언트에서 싱크를 맞춘 뒤 개별 브로드캐스트를 수신하는 방식으로 구현하였습니다.
원활한 커뮤니케이션을 위해 각 화면에서 필요한 상태와 라우팅 화면 전환 시점등을 Figma로 정리하여 소통하였습니다.
HTTP 기반의 API는 Swagger로 문서화를 할 수 있었지만, 웹소켓은 요청과 응답이 1대1이 아니므로 요청과 응답을 별도로 구분하여 Notion에 정리하여 커뮤니케이션하였습니다.
애니메이션 컨트롤
게임 시작시 자신의 차례와 라운드를 알려주는 다이얼로그 애니메이션을 구현해야 했습니다. 애니메이션 스팩은 Protopie로 전달 받았으며, 정리하면 다음과 같습니다.
- 다이얼로그 Fade In
- 3초 카운트 다운
- 카운터 Fade Out
- 라운드 Translate
- 다이얼로그 Fade Out
문제는 애니메이션이 어느 정도 재생된 시점에서 유저가 난입하는 경우, 다른 유저와 동일한 시점부터 애니메이션이 실행되어 함께 종료되어야 합니다. 그렇지 않은 경우, 드로잉 타이머가 줄어드는데 팝업이 화면을 가려서 그림을 그릴 수 없는 불상사가 발생하게 됩니다.
이 문제를 해결하기 위해 각각의 애니메이션들을 하나의 AnimationController
로 제어할 수 있도록 구현한 뒤, 난입 시점에 맞추어 서버측 애니메이션 진행 사항을 계산하여 실행하도록 구현하였습니다. 자세한 방법은 공식문서와 코드를 참고해 주세요.
성능 최적화
드로잉 화면에선 특정 유저가 그리는 그림이 실시간으로 모든 플레이어들에게 브로드캐스트됩니다. 이 과정에서 유저들이 그린 그림이 많아지는 경우 점점 더 많은 드로잉 데이터를 실시간으로 브로드캐스트해야 하므로 성능 문제가 우려되어 다음과 세 가지 방법을 도입하여 최적화를 진행했습니다.
- 현재 턴의 드로잉 데이터만 실시간 브로드캐스트하고, 턴이 끝날 때 이전 턴 드로잉 데이터와 합치도록 구현
- 스로틀링(Throttle)을 구현하여 드로잉 데이터 및 브로드캐스트 빈도 줄이기
- Ramer–Douglas–Peucker 알고리즘을 적용하여 더 적은 점의 개수를 가진 선으로 최적화
스로틀링과 Ramer-Douglas-Peucker 알고리즘의 epsilon factor는 Remote Config에서 컨트롤할 수 있도록 구현하였습니다.
플레이 영상
마무리
이번 프로젝트에선 언젠간 시도해 보고 싶었던 게임 만들기와 드로잉 기능 활용하기를 이번 프로젝트에서 모두 경험해 볼 수 있었고, 또한 아키텍처에 대한 실증적 경험도 할 수 있어서 즐거웠습니다. 웹소켓은 쉽지 않았지만 더 다양하게 활용해 보고 싶은 호기심을 자극하는 매력적인 기술인 것 같습니다.
좋은 팀원들과 YAPP 24기를 잘 마무리할 수 있어서 좋았고, 다음번엔 더 재미있는 프로젝트로 찾아뵙겠습니다 :)
1부. Trouble Painter 드로잉 마피아 게임
✔︎ 2부. Flutter로 실시간 드로잉 게임 만들기
이번 프로젝트는 오픈 소스로 진행하였으며 아래 Github 링크에서 확인하실 수 있습니다.
👉 Flutter : 링크
👉 Backend : 링크
Trouble Painter를 플레이해 보고 싶으신 분은 아래 링크에서 다운받아 주세요 🫶
👉 Android & iOS : 다운로드 링크
서비스가 마음에 드신다면 아래 링크에 투표 부탁드립니다 🫶
👉 Gemini API 대회 응원하러 가기 : 투표 링크