Plan Do! 출시 회고 — 혼자서 풀스택, 그리고 iOS까지
Plan Do!는 할 일·캘린더·팀 협업·메시지·AI·위치 알림을 통합한 모바일 생산성 앱입니다. 웹 PWA로 시작해서 iOS 네이티브까지 한 코드베이스로 출시했어요. 이 글은 그 과정의 회고입니다.
왜 만들었나
쓰던 도구가 너무 많았어요. 할 일은 한 앱, 일정은 다른 앱, 팀 메시지는 또 다른 앱. 서로 연결이 안 되니까 지금 뭘 해야 하는지 한눈에 보이지 않았습니다.
처음엔 내가 쓰려고 만들었습니다. 할 일을 적어두고, 그 할 일을 그대로 캘린더에서 보고, 팀원과 같은 워크스페이스에서 메시지로 이야기하고, AI가 자연어로 일정을 정리해주는 흐름. 하나로 묶이면 어떻게 느껴질지 궁금해서 작게 만들기 시작한 게 점점 커졌어요.
출발점 — 작은 PWA
처음엔 웹에서 동작하는 단순한 할 일 도구였습니다. React 19 + Vite로 셋업하고, Spring Boot에 PostgreSQL을 붙인 정도. 캘린더도 없었고 팀 기능도 없었어요.
쓰면서 불편한 게 보일 때마다 하나씩 붙였습니다.
- 캘린더가 필요해서 → 캘린더 뷰
- 팀이랑 같이 쓰고 싶어서 → 워크스페이스
- 메시지를 다른 앱에서 보기 싫어서 → DM
- 자연어로 일정 적고 싶어서 → AI 어시스턴트
- 폰에서도 잘 쓰고 싶어서 → 모바일 우선 UI, 결국 Capacitor로 iOS
지금 돌아보면 이 작게 시작해서 자라게 둔 점이 가장 잘한 결정 같아요. 처음부터 다 그려놓고 시작했다면 절반도 못 만들었을 거예요.
기술 선택
Frontend: React 19 + Vite + MUI 7
이전 프로젝트에서 React를 써봤고, Vite는 빌드가 빠르고 설정이 가벼워서 골랐습니다. MUI는 컴포넌트 처음부터 만드는 시간을 줄이기 위한 선택이었어요. 혼자 만들 때는 디자인 시스템을 직접 그리는 비용이 생각보다 큽니다.
Backend: Spring Boot 3.5 + JPA + PostgreSQL
가장 익숙한 스택이라서 골랐어요. 실시간 기능 때문에 WebSocket이 필요했고, Spring의 STOMP 지원이 자연스럽게 붙는 점도 컸습니다.
JWT 인증, OAuth2(Google·Kakao·Apple), 메일, 푸시 알림, AI 호출, 이미지 리사이징까지 다 Spring 생태계 안에서 해결할 수 있었어요.
Mobile: Capacitor over React Native
iOS 출시를 결정하면서 가장 큰 갈림길이었습니다.
| Capacitor | React Native | |
|---|---|---|
| 코드베이스 | 웹과 통합 | 분리 |
| 학습 비용 | 낮음 (웹 그대로) | 새로 익혀야 함 |
| 네이티브 성능 | WebView, 다소 느림 | 거의 네이티브 |
| 유지보수 | 한 곳만 | 두 곳 |
혼자 만드는 환경에서 유지보수 비용이 절대적이었습니다. 성능을 일부 양보하더라도 한 코드베이스를 유지하는 쪽으로 갔어요.
자세한 기록은 Capacitor로 PWA를 iOS 앱으로 출시하기에 따로 정리해뒀습니다.
AI: Spring AI + GPT-4o-mini
자연어 일정 입력, 채팅 안에서 todo 생성, 상태 변경 같은 기능에 AI를 붙였어요. GPT-4o-mini를 고른 이유는 비용과 응답 속도입니다. 정밀한 추론보다 짧고 일관된 응답이 필요한 작업이라 mini로 충분했어요.
Function calling을 활용해서 “이 메시지를 todo로 만들어줘” 같은 요청을 실제 DB 액션으로 연결하는 부분에 가장 공을 들였습니다.
Infra: AWS Lightsail + Nginx
Lightsail을 고른 이유는 단순합니다. 비용이 예측 가능하고 셋업이 단순해서. ECS나 EKS는 혼자 운영하기에 부담이 컸어요. SSL은 Let’s Encrypt로 자동 갱신, 정적 자산은 Nginx에서 그대로 서빙합니다.
만들면서 부딪힌 것들
1. WebSocket 두 개로 분리
처음엔 한 연결로 다 묶었는데, DM 방을 빠르게 옮겨다닐 때 구독이 꼬이는 문제가 있었어요. 결국 글로벌(presence) 연결과 방별(DM) 연결을 분리했습니다.
자세한 건 WebSocket 두 개를 한 앱에에 따로 적었어요.
2. iOS 키보드와 safe-area
데스크탑 Safari에선 잘 보이던 화면이 iOS에선 키보드가 올라올 때 입력창이 가려지거나
노치 아래로 콘텐츠가 잘리는 경우가 있었습니다.
resize: "native"와 env(safe-area-inset-*)를 일관되게 적용하면서 해결.
3. 푸시 알림 — APNs와 Web Push의 이중 구현
iOS는 APNs, 웹은 VAPID 기반 Web Push라 두 가지 다른 흐름을 양쪽에 두는 게 어려웠어요. 권한 요청 타이밍, 토큰 캐싱, 실패 시 사용자에게 보여줄 메시지까지 다 미묘하게 다릅니다.
4. iOS 위젯 동기화
홈 화면 위젯에 오늘의 todo가 뜨게 만들었어요. WidgetKit + NSUserDefaults App Group으로 데이터를 공유하고, 앱이 포그라운드로 돌아올 때 다시 한 번 동기화합니다. 이 작은 디테일이 앱을 안 켜도 사용자가 보는 표면을 늘려줘서 결과적으로 효과가 컸어요.
가장 어려웠던 결정 — 무엇을 안 만들지
혼자 만들면 결정 속도는 빨라지지만, 모든 결정에 책임이 따라옵니다. 가장 어려웠던 건 만드는 것보다 안 만드는 것이었어요.
- 다국어 지원은 미뤘어요. 한국어만 먼저.
- Android 빌드는 일단 두고 iOS만.
- 데스크탑 Electron 앱은 셋업만 해두고 본격 배포는 보류.
- Recurring task는 단순한 형태로만, 복잡한 패턴은 후순위.
잘라낸 것들이 다 언젠가는 필요한 것이었지만, 다 만들려고 했으면 지금까지도 출시 못 했을 거예요.
출시 직후
가까운 지인·동료들에게 먼저 공유해서 피드백을 받았습니다. 가장 자주 들은 말이 “앱이 빠르다”, “위젯이 좋다” 였어요.
지표를 일정 수준 이상 자세히 적기엔 아직 이른 단계라, 반응의 종류만 짧게 남겨둡니다.
- 위젯과 푸시 같은 작은 표면들이 의외로 가장 좋아하는 부분
- AI 기능은 “이런 게 되는구나” 정도의 반응, 본격 활용은 아직
- 팀 협업은 동시에 같은 워크스페이스에 있는 사람이 있을 때 가치가 보임
혼자 만든다는 것
좋은 점은 확실합니다. 결정이 빠르고, 전체가 보이고, 원하는 디테일에 시간을 쓸 수 있다.
하지만 솔직하게 어려운 점도 있었어요. 한 사람의 시야에 갇히는 건 진짜였습니다. “이게 정말 좋은 결정인가?”를 검증해줄 사람이 없으니 내가 내 결정을 의심하는 시간이 길어집니다.
그래서 의식적으로 기록을 늘렸어요. 결정의 이유를 짧게라도 남기면, 일주일 뒤의 내가 과거의 나를 동료처럼 점검할 수 있더라고요. 이 블로그도 그 연장선에 있어요.
배운 것
세 가지로 정리하면:
- 작게 시작하고 자라게 두기. 초기 PWA가 지금의 핵심이 되었어요.
- 잘라낼 줄 알기. 모든 기능을 다 만들려고 하면 아무것도 못 만든다.
- 유지보수 비용을 기준으로 결정하기. 만들 때보다 운영할 때가 훨씬 깁니다.
앞으로
iOS 출시는 끝이 아니라 시작에 가깝습니다. 다음으로 손볼 것들:
- AI 일정 관리 기능을 더 자연스럽게 (function calling 패턴 정리)
- Android 출시 — Capacitor라 코드는 그대로지만 권한·푸시는 다시 잡아야 함
- 협업 기능 강화 — 공동 편집, 실시간 알림
직접 써보고 싶으시면 plando.imjaewoo.dev에서 가입하실 수 있고, iOS는 App Store에서 받으실 수 있어요. 피드백은 이메일로 언제든 환영합니다.