“Tune-a-Keyboard” 키보드를 조율하세요!
사용자 입력패턴을 학습하여 맞춤형 레이아웃을 제공하는 안드로이드 전용 커스텀 키보드 어플리케이션 입니다.
*
스마트폰 기종 변경 시 특정 영역에서 오타 발생 빈도가 증가하는 현상을 경험했습니다. 스마트폰 화면의 다양화 및 대형화로 인해 이러한 문제가 더욱 두드러졌습니다. 최신 기종에 내장된 보완 기능이 존재하나, 이는 완전한 해결책이 되지 못했습니다.
이러한 일상적인 불편을 해소하고자 직접 개발하기로 결정했습니다.
- 편집기: Android Studio
- 개발 언어: Kotlin
- 안드로이드 환경: Gradle(build), Jetpack Compose(view)
- 오픈 소스 프로젝트: FlorisBoard
- 편집기: Google Colab
- 개발 언어: Python
- 머신러닝 플랫폼: TensorFlow
- 딥러닝 모델: TensorFlow Lite
좌측에서부터 홈 화면, 테스트 안내, 테스트 화면, 개별 키 커스터마이징 화면 입니다.
이 어플리케이션은 사용자가 키보드를 개인화할 수 있는 설정 화면을 제공합니다. 설정은 두 가지 주요 기능으로 나뉩니다:
-
테스트: 사용자의 타이핑 패턴을 분석하여 맞춤형 키보드 레이아웃을 제공하는 초기 설정 단계입니다. 사용자의 입력패턴의 기본 정보를 수집합니다. 이 테스터를 통해 개인화된 키보드를 사용할 수 있습니다.
-
커스터마이징: 테스트 후 사용자가 직접 키 크기, 간격, 위치를 세부적으로 조절할 수 있는 기능입니다. 이를 통해 사용자는 빠르게 커스텀 가능한 키보드를 제공받을 수 있습니다.
이 프로젝트를 만들기로 했을 때, 실제로 무리 없이 쓸 수 있는 키보드를 만들자는 목표를 세웠습니다.
‘키보드’ 는 가장 기본적인 ‘입력 방식’ 입니다. 형태가 다양할 수 있지만 어떤 기기든 반드시 지원하는 사용자 인터페이스 입니다. 이를 통제하는 것은 운영체제와 연관이 깊기 때문에 네이티브 언어(java / kotlin)를 사용한 코드 작성이 필수적 입니다.
일부분 네이티브 언어를 사용해야 한다는 점을 인지한 이후, 키보드와 크기가 변경되는 로직을 어디서 구현할지 고민했습니다. 자바스크립트를 공부 했기 때문에 React Native CLI(이하 리액트 네이티브) 를 이용해 키보드를 구현하고 기능을 안드로이드의 InputMethodService
를 이용해 연결하자는 계획을 세웠습니다.
여기에는 3가지 문제점이 있었습니다.
-
React Native AutoLinking 버그가 실시간으로 발생하다.
POC 를 진행하던 당시 리액트 네이티브는 0.75.2 버전으로 리액트 네이티브에서 안드로이드 프로젝트를 찾지 못하는 이슈가 있었습니다. 리액트 네이티브와 안드로이드가 연결이 되지 못하는 치명적인 사태가 발생했습니다. 0.75.3 버전에 급하게 패치 되었지만 제가 POC 를 진행하던 기간에는 해결이 되지 못했습니다. ( 이슈로그1, 이슈로그2 )
-
키보드는 다양한 언어와 형태의 모드들을 지원해야한다.
키보드는 다양한 모드(각종 언어별, 기본형, 이메일 입력형, ‘이모지’ 와 전화용 번호 키패드 등등…)가 있습니다. 커스텀 키보드는 기존 키보드를 대체하여 사용하게 되기 때문에 사용자가 사용하는 모든 경우에 전부 대응할 수 있어야 합니다. 이 가짓수는 상당했고 목표했던 개발 기간을 고려할때 적절하지 않았습니다.
-
InputMethodService
가 Deprecated 되다.이것은 모든 키를 사용하여 자신만의 보기를 만들어야한다는 것을 의미하며, 이는 키보드 입력, 삭제 및 전환과 같은 모든 클릭 이벤트를 직접 처리하는 것을 의미합니다.
종합해보면 ‘운 좋게 빌드가 성공하길 기대하고, 기능은 연결되어 있지만 일부만 호환 가능한, 키보드 같이 생긴 것’ 이 나올 확률이 높았습니다. 제 목표와 상반되는 결과물이 예상 되었고, 익숙한 언어를 사용하기 위해 애쓰기 보다, 모든 것을 네이티브 언어를 사용하여 개발하는 것이 적합하다 결론 내렸습니다.
공식 문서에서는 InputMethodService
대신 AOSP(안드로이드 오픈소스 프로젝트)를 사용할 것을 권장했습니다. 키보드 자체를 만드는 것이 목표가 아니었기 때문에 오픈소스 키보드 모듈을 활용하는 것이 개발 기간을 함께 고려했을때 적절했습니다. 후보로는 FlorisBoard 와 AnySoftKeyboard 가 있었습니다.
< GitHub 레포지토리 정보 비교 >
FlorisBoard | AnySoftKeyboard | |
---|---|---|
개발 언어 | Kotlin | Java |
최신 버전 | v0.4.0 | v1.11-r1 |
마지막 업데이트 | 2024.09.03 | 2022.01.06 |
(프로젝트 시작 기준) | 1주 전 | 약 1년 8개월 전 |
Stars | 6K | 2.9K |
한글 지원 | ✅ | ❌ |
사용자 개인정보 보호 | ✅ | ✅ |
< 커뮤니티 비교 > (출처: Redit, F-Droid)
FlorisBoard | AnySoftKeyboard | |
---|---|---|
커뮤니티 평판 | 신생 프로젝트, 빠르게 성장 중 | 오래된 프로젝트, 널리 사용됨 |
퍼포먼스 | 베타 버전이지만 안정적 성능 제공 | 메이저 버전으로 신뢰할 수 있는 성능 |
비교를 통해 FlorisBoard는 최신 기술을 기반으로 개발되며 지속적인 업데이트가 이루어지고 있다는 점이 긍정적으로 작용했습니다. 특히 Kotlin 기반으로 최신 안드로이드 성능을 극대화할 수 있는 이점이 있었고, 한글 지원도 중요한 선택 요소였습니다.
결과적으로 FlorisBoard와 함께 코틀린을 선택한 이유는 최신 안드로이드 기술과 성능을 활용할 수 있다는 점과, 코틀린의 장점을 살려 개발 속도와 안정성을 높일 수 있다는 점이었습니다.
키보드를 사용하면서 사용자는 개인 정보(전화번호, 비밀번호, 주민등록번호 등)를 입력하는 경우가 발생하기도 합니다. 이를 고려하여 최대한 안전하게 처리할 수 있는 방법을 고민했습니다.
이 프로젝트에서 온디바이스로 처리가 어려울 수 있는 유일한 동작은 딥러닝 모델과의 통신이었습니다.
키보드를 통해 비밀번호 등 민감한 정보를 입력했을 때는 어떻게 될까요? 딥러닝 차곡차곡 쌓은 데이터를 순차적으로 전달하기 때문에 누군가 데이터를 탈취한다면 그대로 노출될 위험이 있습니다. 만약 기기 내에서 이 모든 정보를 처리할 수 있다면 서버를 이용하는 것 보다 안전할 것입니다.
또한 사용자 별 특화된 입력패턴 학습이 필요하기 때문에 기기별로 모델을 개별적으로 할당할 필요가 있었습니다. 그래서 기본 모델을 탑재하고 스스로 학습할 수 있는 모델을 챙기고 싶었습니다.
만약 적합한 모델이 없었거나 휴대용 기기에 탑재되는 부담이 컸다면 서버를 설치하는 것이 적절 했을 것 입니다. 다행히 이런 부분을 충족하는 신뢰할 만한 모델로 Tensor Flow Lite 가 있었습니다.
주요 로직
- 키보드 레이아웃 로직에 간섭하기
- 터치 좌표 수집하기
- 터치 좌표로부터 최적화된 레이아웃 계산하기
키보드 레이아웃이 커스텀 된다는 것은 어떤 의미일까요? 키 하나하나의 크기가 달라질 수 있어야 한다는 것 입니다. 이를 위해 키 레이아웃을 그리는 로직에 직접적인 간섭을 하기 위해 모듈화가 아닌 오픈소스 키보드 위에서 직접 작업을 하게 되었습니다.
키 레이아웃 로직은 먼저 '비율 값'으로 계산을 수행합니다. 비율을 통한 조정이 완료된 후, 이 비율값을 실제 수치로 변환하는 과정이 있습니다. 각 키는 이 단계에서 실제 사이즈(top, bottom, left, right)값을 갖게 됩니다. 이 사이즈를 이용해 모든 레이아웃이 계산된 후, 내가 조정한 사이즈로 덮어씌우는 방식을 시도해보았습니다.
<문제상황 발생>
⓵ index 기반의 데이터 저장문제: 키보드 모드가 변경되면 레이아웃이 무너진다.
사용한 데이터는 2차 배열 형태로, 가장 많이 쓰는 문자 키보드의 34개 키 위치를 담고 있었습니다. 그러나 키보드 모드를 전환하면 키 개수가 변동되어 잘못되거나 없는 값을 참조하게 되었고, 이로 인해 모드 변화에 전혀 동적으로 대응하지 못하는 문제가 발생했습니다.
또한 사이즈 값을 가져오기 위해선 현재 키의 가로 인덱스, 세로 인덱스 2종의 데이터가 필요했습니다. 내부적으로 키는 자신의 인덱스를 기억하지 않기 때문에 인덱스를 찾기위한 반복 탐색을 진행해야 한다는 점이 코드상으로도 비효율적이었습니다.
⓶ 실사값 고정에 대한 부작용: 이후 가공 동작 및 주변 키의 버튼 영역은 고려되지 않는다.
사이즈를 고정시키니, 이후 적용되는 세부 조절 로직이 무시되었습니다. 대표적인 예로, footer에서 버튼을 떨어뜨리는 로직이 적용되지 않아 버튼이 잘리는 현상이 발생했습니다.
또한 키 간 겹침 영역이 생겼습니다. 이를 방지하려면 실사값이 변할 때마다 주변 키 사이즈를 리사이징 하는 로직을 내부적으로 실행해야 하는 비효율이 발생했습니다. 리사이징 로직은 기본으로 사용하는 레이아웃 로직과 흡사한 내용이었습니다. 사실상 같은 로직을 2번씩 도는 거나 다름없는 흐름이었습니다.
두 번째로 사용할 수 있는 방법은 비율(flayWidthFactor
)을 활용하는 것입니다. 얼마나 늘거나 줄어야 하는 지를 '비율'로 표현한 값입니다.사용하는 비율 값 종류는 총 3가지 입니다.
flayWidthFactor
: 각 키의 실제 너비를 결정하는 요소로, 키보드 모드와 코드에 따라 값이 달라집니다.flayShrink
: 키보드 모드와 특정 키 코드에 따라 키가 차지하는 공간을 줄일 수 있는 정도를 결정합니다.flayGrow
: 키보드 모드와 특정 키 코드에 따라 키가 차지하는 공간을 확장할 수 있는 정도를 결정합니다.
flayWidthFactor
요소를 이용해 넓이를 조절한 후 flayShrink
와 flayGrow
값으로 넘치거나 부족한 넓이를 커버할 수 있습니다.
이를 이용해 레이아웃 로직이 시작될 때, 새로운 비율값을 계산하여 적용했습니다. 이를 바탕으로 이후 사이즈 결정 연산을 수행하게 됩니다. 결과 자유자재로 키보드 크기를 변형할 수 있게 되었습니다.
B 키와 J 키의 너비값이 변경하고 유동적으로 같은 줄에 해당하는 키값 변동을 확인할 수 있습니다.
추가로 데이터 저장 방식도 key.code를 키로 갖는 맵 형태로 변경했습니다. map 을 통해 원하는 키코드의 값을 O(1) 로 찾을 수 있도록 변경했기 때문에 관련 추가 로직을 작성할때도 보다 탐색 효율이 올라갔으며, 비율값으로 조절을 하기 때문에 기존 오픈소스의 로직과도 무리없이 작동하게 됩니다.
사용자가 누른 터치 좌표와 사용자가 예측한 키를 실제로 연관 짓기 위해서 과정이 필요했습니다. 단순 연산으로는 부족했기 떄문에 추론형 딥러닝의 도움을 받아 예측값을 받고 후처리를 하는 방향으로 결정했습니다.
좌표로부터 키를 예측하는 흐름은 대략 이와 같습니다.
발생한 좌표가 어느 키를 누르려 한 것인지 파악할 수 있다면, 각 키의 최적 위치를 계산할 수 있었습니다. 이를 위해 좌표를 분류하는 데 필요한 요소들은 다음과 같았습니다.
- 기본형 딥러닝 모델을 학습된 모델로 만들어줄 학습 데이터.
- 발생한 좌표로부터 유저가 원했던 키를 추론해줄 딥러닝 모델.
- 비정확한 정보들은 걸러줄 필터링 함수.
딥러닝 모델을 학습시키기 위해서는 Input(터치 좌표 값)과 Output(결과가 되는 키의 좌표값)이 필요합니다. 딥러닝으로 이를 추론하려는데 값이 필요하다니, 모순처럼 보입니다. 하지만 이는 향후 발전을 위해 꼭 필요한 과정이므로, 사용자에게 통제된 환경을 제공하여 데이터를 수집해야 합니다.
데이터는 충분히 확보하는 것이 이상적이지만, 사용자에게 과도한 테스트는 스트레스와 불편함을 줄 수 있습니다. 따라서 자주 사용하는 "자모음"으로 범위를 좁혀 데이터를 수집하기로 했습니다.
"The quick brown fox jumps over the lazy dog"는 영문 A부터 Z까지 모든 알파벳을 포함하는 문장입니다. 자연스러운 문장이면서도 모든 문자에 대한 데이터를 수집할 수 있도록 구성되어 있습니다. 이를 통해 미리 정해진 목표 좌표에 대한 실제 터치 좌표를 수집합니다. 데이터 수집 과정에서 목적 키 좌표와 지나치게 동떨어진 좌표는 제외합니다.
TensorFlow Lite에는 다양한 사전 학습 모델이 있습니다. 그러나 키보드와 같은 특수한 좌표에 대한 반환값을 제공하는 형태로 활용할 수 있는 모델은 없었습니다. 따라서 이를 위해 추론형 모델을 직접 만들어야 했습니다.
모델은 주피터 노트북과 유사한 환경을 제공하는 Google Colab에서 파이썬으로 제작되었습니다. 이 모델을 만들 수 있었던 이유는 입력값과 출력값이 일반적인 딥러닝 모델에 비해 매우 단순했기 때문입니다. 목표는 '시간에 따라 누적되는 데이터'와 '다양한 좌표값에 대한 여러 출력'을 '예측'하는 것이었습니다.
이를 처리하기 위해 장단기 메모리(Long Short-Term Memory, LSTM)를 사용했습니다. LSTM은 과거에 입력한 데이터를 기억하면서 현재의 입력값에 기억을 반영하기 때문에, 터치 패턴과 같이 시간에 따라 변화하는 데이터를 처리하는데 적합합니다.
기본 Tensor Flow 모델에 LSTM 레이어와 알아서 추론해줄 신경망 레이어를 얹어 모델을 구축했습니다. 그리고 이를 Tensor Flow Lite 모델로 변환해 리소스로 파일을 넣어주었습니다.
테스터 화면에서 좌표값을 받거나 아직 충분히 학습되지 않은 딥러닝 모델의 출력값은 함부로 신뢰해서는 안됩니다. 이를 걸러내기 위해 딥러닝 모델을 공부하다 발견한 최근접이웃(KNN) 알고리즘을 사용해서 필터 함수를 만들었습니다. 일정 거리 내로 가깝다면 이웃이고, 그렇지 않다면 이웃이 아니라는 접근법입니다.
기준이 되는 거리값은 주변 키 1개 만큼의 거리로 상정했습니다. 일반적으로 오타가 나도 의도했던 키의 1칸 이상 떨어진 버튼을 누르진 않기 때문입니다. 예측좌표와 목적 좌표의 거리를 재고, 여기서 너무 동떨어져 있다면 해당 값은 사용하지 않습니다. 피타고라스 정리를 사용해 직선거리를 구했습니다.
필터함수는 2가지 경우에 사용됩니다.
- 딥러닝을 학습시키는 경우: 딥러닝 모델 통과 ‘전’ 사용. 학습할 데이터의 값의 정확도를 올리기 위해 사용합니다.
- 딥러닝 결과 값을 사용하는 경우: 딥러닝 모델의 통과 ‘후’ 사용. 결과값을 수집용 저장소로 옮기기 전 검증을 하기 위해 사용합니다. (딥러닝 모델의 학습도가 충분하지 않을 때 효과적입니다.)
사용자가 진짜 사용하는 좌표를 알게 되었습다. 이제 키가 좌표를 품도록 조절하면 됩니다.
딥러닝 모델의 출력값은 필터함수를 거친 후 입력값과 함께 저장됩니다. 데이터는 (key: key.code, value: 자주 누르는 좌표값)
으로 구성된 map 객체입니다. 자주 누르는 좌표값은 입력되는 좌표값들의 평균으로 계산됩니다. 가장 자주 누르는 위치값을 저장하게 되는 것입니다.
다음으로, 현재 키 버튼의 중앙값과 자주 누르는 좌표값을 포함하는 최소 넓이를 구해봅니다. 비례식을 활용하여 수식을 완성했습니다.
X에 해당하는 값이 새로운 넓이 배율(flayWidthFactor
)이 됩니다. 이 값은 키보드 레이아웃 로직에 적용되어 실제 사이즈를 계산하는데 사용됩니다.
A와 S사이에 찍히던 좌표가 안전하게 A영역 안으로 들어오는 것을 확인할 수 있습니다.
딥러닝을 통해 터치 좌표를 분석하고, 이를 기반으로 각 키의 크기를 조정했습니다.
이러한 과정의 결과로, 이전에는 주변 키버튼 영역과 혼동될 수 있었던 터치 좌표들이 이제는 사용자가 의도한 키의 정확한 히트박스 영역 내로 안정적으로 인식됩니다. 이는 사용자의 입력 정확도를 크게 향상시키며, 오타 발생 빈도를 현저히 줄이는 효과를 가져왔습니다.
또한, 이 최적화 과정은 사용자의 개인적인 터치 패턴을 지속적으로 학습하여 더욱 정교한 키보드 레이아웃을 제공합니다. 결과적으로, 사용자는 더욱 자연스럽고 편안한 타이핑 경험을 즐길 수 있게 되었습니다.