iOS 앱을 개발하고 있다면, 당장 핫픽스를 해야 할지도...

Ximya(심야)·2024년 10월 15일
2
post-thumbnail

새롭게 출시된 iOS 18 버전에 '이중 언어 키보드' 기능이 추가되었습니다.

덕분에 한국어와 다른 언어(영어)를 더욱 편리하게 입력할 수 있게 되었지만, 숫자 패드 키보드에서 숫자를 입력하고 이를 일정한 형식으로 포맷팅하는 과정에서 오류가 발생하는 이슈가 있었습니다.

해당 이슈로 급하게 핫픽스를 했고, 관련 사례를 간단히 소개드리려고 합니다.

Flutter(플러터) 환경에서 발생한 버그를 기반으로 공유드리지만, iOS(네이티브), React Native 등 다른 환경에서 iOS 앱을 개발하고 계신다면, 여러분의 프로젝트에서도 동일한 문제가 발생할 가능성이 높습니다.

핸드폰번호 하이픈(-) 삽입 로직

저희 앱의 로그인 화면에서 휴대전화 번호를 입력받는 TextField가 있으며, 번호가 입력될 때 형식에 맞게 하이픈을 자동으로 삽입해주는 InputFormatter 로직이 존재합니다.

class SampleTextField extends StatelessWidget {
  const SampleTextField({super.key});

  
  Widget build(BuildContext context) {
    return TextField(
      inputFormatters: [
        PhoneNumForamtter(), 
      ],
      keyboardType: TextInputType.number, // OR TextInputType.phone
    );
  }
}

많은 앱들이 그러하듯, 일반적으로 11자리 번호를 입력할 때 아래와 같이 포맷이 되어야 하죠.

010
010-1
010-12
...
010-1234-5678

그런데 문제는 iOS 18 환경에서 이중 언어(한국어+영어) 키보드로 설정하고 가상 keyboardType을 숫자 패드(number, phone)로 설정했을 때, 아래와 같이 비정상적인 포맷팅 값이 떨어지게 됩니다.

010
010-1
001-012
000-101-23
000-0010-1234

포맥 로직을 관리하는 TextInputFormatterformatEditUpdate 메소드에 로그를 찍어보면 문제가 조금 더 확연히 보이는데요.

class SampleFormatter extends TextInputFormatter {
  SampleFormatter();

  
  TextEditingValue formatEditUpdate(
    TextEditingValue oldValue,
    TextEditingValue newValue,
  ) {
    log('oldValue: $oldValue');
    log('newValue: $newValue');
    return newValue;
  }
}

Expected Result

[log] oldValue : TextEditingValue(text: ┤123├, selection: TextSelection.collapsed(offset: 3, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
[log] newValue : TextEditingValue(text: ┤1234├, selection: TextSelection.collapsed(offset: 4, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))

이렇게 포맷팅 로직에서 현재 텍스트가 입력되기 이전 값인 oldValue와 새롭게 입력된 값인 newValue를 로그로 출력해보면, 위와 같은 기대값이 나와야 하지만,

Actual Result

[log] oldValue : TextEditingValue(text: ┤123├, selection: TextSelection(baseOffset: 1, extentOffset: 3, isDirectional: false), composing: TextRange(start: -1, end: -1))
[log] newValue : TextEditingValue(text: ┤1├, selection: TextSelection.collapsed(offset: 1, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
[log] oldValue : TextEditingValue(text: ┤1├, selection: TextSelection.collapsed(offset: 1, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
[log] newValue : TextEditingValue(text: ┤12├, selection: TextSelection.collapsed(offset: 2, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
[log] oldValue : TextEditingValue(text: ┤12├, selection: TextSelection(baseOffset: 0, extentOffset: 2, isDirectional: false), composing: TextRange(start: -1, end: -1))
[log] newValue : TextEditingValue(text: ┤├, selection: TextSelection.collapsed(offset: 0, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
[log] oldValue : TextEditingValue(text: ┤├, selection: TextSelection.collapsed(offset: 0, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
[log] newValue : TextEditingValue(text: ┤1├, selection: TextSelection.collapsed(offset: 1, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
[log] oldValue : TextEditingValue(text: ┤1├, selection: TextSelection(baseOffset: 0, extentOffset: 1, isDirectional: false), composing: TextRange(start: -1, end: -1))
[log] newValue : TextEditingValue(text: ┤├, selection: TextSelection.collapsed(offset: 0, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
[log] oldValue : TextEditingValue(text: ┤├, selection: TextSelection.collapsed(offset: 0, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))
[log] newValue : TextEditingValue(text: ┤1234├, selection: TextSelection.collapsed(offset: 4, affinity: TextAffinity.downstream, isDirectional: false), composing: TextRange(start: -1, end: -1))

문제가 발생하는 환경에서는 위와 같이 TextInputFormatterformatEditUpdate 이벤트가 여러 번 트리거되는 것을 확인할 수 있었습니다.

이 과정에서 하이픈을 삽입하는 로직이 예상치 못한 방식으로 작동하는것이였고, 입력된 숫자를 포맷팅하는 로직이 없더라도 입력 이벤트가 여러 번 실행되기 때문에 숫자를 입력하는 순간에 TextField의 숫자가 깜빡거리는 이슈가 발생하게 됩니다.

이 이슈를 해결하기 위해 텍스트 입력 이벤트가 여러번 트리거되는 특정 패턴을 찾아보려 했으나, 패턴이 일정하지 않아 리스크가 크다고 판단되어 채택하지 않았습니다.

그래서 이중 언어 키보드를 사용하는 환경에서만 하이픈 삽입 로직을 배제하려 했으나, iOS에서 사용자의 키보드 유형을 알 수 있는 방법이 없어서 결국 iOS 18 환경에서는 하이픈 삽입 로직을 제거하는 방향으로 코드를 수정했습니다.

이거 또또또.. 😮‍💨 Flutter 이슈인가?

또 수많은 플러터 이슈중에 하나라고 생각되어, Flutter 깃헙에 이슈를 등록했지만,

👉 깃헙 이슈

조금 더 확인해보니, 당근마켓, 뱅크샐러드 등 네이티브로 구현된 메이저 앱에서도 현재 동일한 문제가 발생하고 있더라고요.

당근 마켓 로그인 화면

번호 자동완성이나 복사 붙여넣기 기능이 있다면 어느 정도 문제가 커버되지만, 그렇지 않다면 몇몇 서비스에서는 로그인 자체가 불가능한 상황이 발생할 수 있어 꽤 크리티컬한 이슈라고 판단됩니다.

아쉽게 완벽하게 문제를 해결하지는 못했지만, 최근 iOS 18 버전 출시 이후 여러 이슈들을 팔로우업 하시면 조금이라도 도움이 되실 수 있을 것 같아서 공유드립니다:)

profile
https://medium.com/@ximya

0개의 댓글