꿀꺽
2인 팀 프로젝트, 2026.04 ~ 진행 중 · 개발 전체 (앱, 통화 파이프라인, 관리자 웹)
화면 조작을 최소화해도 고령 환자의 앱 적응에 오랜 시간 소요
배경
삼킴장애(연하장애) 환자가 집에서 혼자 재활을 이어가도록 돕는 음악 기반 재활 서비스입니다. 국립재활원 연하곤란 환자 대상 디지털 재활 연구에서 퇴원 뒤 재활이 끊기는 공백을 보고 시작했고, 환자는 걸려온 전화를 받기만 하면 별도 조작 없이 세션이 시작됩니다. 두 명이 함께 진행하고 있고, 개발은 환자 앱부터 통화 파이프라인, 치료사용 관리자 웹까지 모두 제가 맡았습니다. 앱 중심이던 구조를 전화 기반으로 바꾸는 설계도 제가 했습니다.
문제
첫 버전은 환자가 앱을 열고 세션을 골라 음원을 들으며 따라 하는 구조였습니다. 화면 조작을 최대한 줄였는데도 고령, 뇌졸중 환자가 적응하는 데 오래 걸렸고, 버튼이나 단계를 줄이는 것으로는 "환자가 스스로 앱을 열어야 한다"는 전제가 남았습니다. 구조를 바꾼 뒤에도 통화 품질 문제가 남았습니다. 무료 플랜에서는 통화가 없을 때 음원을 트는 Agent 서버가 꺼져 있다가 깨어나는 데 10~20초가 걸렸고, 그 사이 벨이 먼저 울려 환자가 받으면 무음이 흘렀습니다. Agent가 환자 마이크가 올라오기를 기다린 뒤에 음원을 재생해서 여기서도 최대 15초 무음이 생겼습니다. 환자가 통화 중간에 먼저 끊으면 녹음도 기록도 남지 않아 치료사 쪽에서는 그 통화가 없었던 것처럼 보였습니다.
판단
진입 방식을 화면 조작에서 전화 수신으로 바꿨습니다. pg_cron이 1분마다 Edge Function을 불러 예정 시각이 된 환자를 고르고, LiveKit 방을 만들어 통화 Agent를 호출한 뒤 Android는 FCM, iOS는 APNs VoIP 푸시로 벨을 울립니다. 앱이 꺼져 있어도 OS 통화 화면이 뜨도록 iOS는 PushKit을 받는 즉시 CallKit에 신고하게 만들었습니다. 같은 시간대에 전화가 두 번 걸리지 않게 부분 유니크 인덱스로 차단했고, 못 받으면 5분 간격으로 두 번 더 겁니다. 실제 전화(PSTN) 대신 앱 안 VoIP를 고른 것은 비용과 구현 속도, 그리고 전화번호를 따로 모으지 않아도 되기 때문입니다. 통화는 처음에 직접 구현한 P2P였는데, 서버의 Agent가 통화에 참가자로 들어갈 수 없고 TURN 중계와 재접속 처리도 없어 LiveKit으로 교체했습니다. 무음 문제는 Agent가 방에 들어온 것을 확인한 뒤에만 벨을 울리고, 마이크를 기다리지 않고 받는 즉시 음원을 재생하도록 수정했습니다. 기록 유실은 재생을 중간에 끊을 때 ffmpeg 프로세스가 영영 끝나지 않던 것이 원인이어서, 남은 파이프를 비우면서 종료하도록 수정하고 녹음 업로드에 제한 시간을 뒀습니다.
결과
환자는 전화를 받기만 하면 재활이 시작되고 끝나면 통화가 자동으로 끊깁니다. 받은 뒤 음원이 나오기까지 걸리던 무음은 최대 15초에서 0초로 줄었고, 끝까지 받은 통화, 중간에 끊은 통화, 부재중 세 경우 모두 기록이 남는 것을 실제 LiveKit과 Supabase로 확인했습니다. 2026년 9월 ICT 이노베이션 스퀘어 개별역량강화에서 대상을 받았고 임상 파일럿을 준비하고 있습니다.
개선 전
개선 후
iOS에서 재발신 전화가 받기 버튼 없이 끊김
문제
3일간 테스트 통화 기록을 보니 받은 통화는 전부 1회차였고, 재발신으로 넘어간 5건은 모두 부재중이었습니다. 재현해 보니 재발신 때 벨이 1초 울리고 받기 버튼 없이 끊겼습니다.
판단
푸시에 담는 통화 ID를 세션 ID로 고정해 쓴 것이 원인이었습니다. iOS는 이 값을 CallKit 전화 ID로 쓰는데, 이미 끝난 ID로 다시 신고하면 곧바로 정리합니다. 시도마다 새 UUID를 벨 ID로 쓰고 LiveKit 방 이름에도 시도 번호를 붙여 닫히는 중인 방을 다시 받지 않게 했습니다. Agent에는 세션 ID를 그대로 넘겨 결과가 맞는 기록에 남도록 했습니다.
결과
통화 ID만 새로 바꾸자 재발신 벨이 정상으로 울렸고, 같은 통화에 두 번 걸어 시도마다 방과 벨 ID가 달라지는 것을 로그로 확인했습니다.
치료사용 관리자 웹
- Flutter
- LiveKit
- Supabase