Thursday, August 6, 2020

Flutter 텍스트필드 한글 입력, 천지인 키보드에서 막히셨나요? (완벽 해결 가이드)

Flutter로 멋진 앱을 개발하고 드디어 사용자들에게 선보였을 때, 가장 당혹스러운 피드백 중 하나는 "특정 글자가 입력되지 않아요"일 것입니다. 특히 한국 사용자들을 대상으로 하는 앱이라면, 이 문제는 더욱 치명적일 수 있습니다. 분명히 한글 입력을 허용하도록 코드를 작성했는데, 왜 '어', '여', '쓰'와 같은 특정 단어들이 입력되지 않는다는 불만이 접수될까요? 이 문제의 주범은 바로 우리가 흔히 사용하는 정규식과 한국의 독특한 키보드 입력 방식, 특히 '천지인(天地人)' 키보드와의 충돌에 있습니다.

이 글에서는 Flutter의 TextField 위젯에서 FilteringTextInputFormatter를 사용하여 한글 입력을 제한할 때 왜 특정 글자들이 누락되는지, 그 근본적인 원인을 한국어의 문자 조합 원리와 천지인 키보드의 동작 방식을 통해 심층적으로 파헤쳐 봅니다. 그리고 단순히 해결 코드 한 줄을 제시하는 것을 넘어, 왜 그 코드가 정답인지, 더 나아가 어떤 상황에 어떤 정규식을 사용해야 하는지에 대한 완벽한 가이드를 제공하고자 합니다. 이 글을 끝까지 읽으신다면, 다시는 텍스트 입력 문제로 사용자의 불만을 사는 일은 없을 것입니다.

Flutter TextField 한글 입력 문제

1. 모든 문제의 시작: TextField와 InputFormatters

사용자로부터 텍스트 입력을 받는 것은 모든 애플리케이션의 가장 기본적인 기능입니다. Flutter에서는 TextField 위젯을 통해 이 기능을 손쉽게 구현할 수 있습니다. 이름, 아이디, 비밀번호, 검색어 등 다양한 정보를 입력받기 위해 우리는 TextField를 사용합니다.


import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(
        child: Padding(
          padding: EdgeInsets.all(20.0),
          child: TextField(
            decoration: InputDecoration(
              labelText: '자유롭게 입력하세요',
              border: OutlineInputBorder(),
            ),
          ),
        ),
      ),
    );
  }
}

위 코드는 가장 기본적인 형태의 TextField입니다. 이 상태에서는 사용자가 키보드로 입력할 수 있는 모든 문자(한글, 영어, 숫자, 특수문자 등)가 아무런 제약 없이 입력됩니다. 하지만 대부분의 경우, 우리는 특정 형식의 입력만을 허용해야 합니다. 예를 들어, '나이'를 입력하는 필드에는 숫자만, '이름'을 입력하는 필드에는 한글이나 영어만 허용하고 싶을 수 있습니다.

이때 사용하는 것이 바로 TextFieldinputFormatters 속성입니다. 이 속성은 List<TextInputFormatter> 타입을 받으며, 사용자의 입력을 실시간으로 가공하거나 필터링하는 역할을 합니다. Flutter는 몇 가지 유용한 기본 TextInputFormatter를 제공합니다.

  • LengthLimitingTextInputFormatter: 입력 가능한 최대 글자 수를 제한합니다.
  • FilteringTextInputFormatter: 정규식(Regular Expression)을 기반으로 허용하거나 거부할 문자를 지정합니다.

우리가 겪는 한글 입력 문제의 핵심은 바로 이 FilteringTextInputFormatter의 사용법에 있습니다.

2. 흔히 사용하는 '잘못된' 한글 필터링 코드

인터넷이나 여러 튜토리얼에서 한글 입력만 허용하기 위한 코드를 찾아보면, 다음과 같은 형태의 코드를 쉽게 발견할 수 있습니다.


// 흔히 사용되지만, 문제가 있는 코드
TextField(
  decoration: const InputDecoration(
    labelText: '이름 (한글만)',
    border: OutlineInputBorder(),
  ),
  inputFormatters: [
    FilteringTextInputFormatter.allow(RegExp(r'[ㄱ-ㅎ|가-힣]')),
  ],
)

이 코드를 분석해 보겠습니다. FilteringTextInputFormatter.allow()는 정규식(RegExp)에 매칭되는 문자만 입력을 '허용'하는 포매터입니다. 위 코드에서 사용된 정규식 [ㄱ-ㅎ|가-힣]의 의미는 다음과 같습니다.

  • [ㄱ-ㅎ]: 한글 자음 'ㄱ'부터 'ㅎ'까지의 모든 문자를 의미합니다. (예: ㄱ, ㄴ, ㄷ, ...)
  • |: '또는(OR)'을 의미하는 논리 연산자입니다.
  • [가-힣]: 완성형 한글 '가'부터 '힣'까지의 모든 문자를 의미합니다. 이는 유니코드상에 존재하는 모든 현대 한글 음절을 포함합니다. (예: 가, 나, 다, ..., 강, 밥, 힣)

얼핏 보기에는 완벽해 보입니다. 한글 자음과 완성형 한글을 모두 허용했으니, 한글 입력에는 아무런 문제가 없을 것이라고 생각하기 쉽습니다. 실제로 대부분의 쿼티(QWERTY) 기반 한글 키보드(두벌식)에서는 이 코드가 큰 문제 없이 동작하는 것처럼 보입니다. 그러나 사용자가 '천지인' 키보드를 사용하는 순간, 문제는 수면 위로 드러납니다.

3. 문제의 근원: 한글 조합 원리와 천지인 키보드

[ㄱ-ㅎ|가-힣] 정규식이 천지인 키보드에서 문제를 일으키는지 이해하려면, 먼저 한글이라는 문자가 어떻게 구성되는지, 그리고 천지인 키보드가 어떤 방식으로 글자를 만들어내는지를 알아야 합니다.

한글의 제자(製字) 원리: 초성, 중성, 종성

한글은 자음과 모음을 결합하여 하나의 음절(글자)을 만드는 '모아쓰기' 방식을 사용합니다. 하나의 음절은 초성(첫 자음), 중성(가운데 모음), 그리고 선택적으로 종성(끝 자음)으로 이루어집니다.

예를 들어, '한'이라는 글자는 다음과 같이 조합됩니다.

  • 초성: ㅎ (자음)
  • 중성: ㅏ (모음)
  • 종성: ㄴ (자음)

우리가 키보드로 '한'을 입력할 때, 운영체제의 입력기(IME, Input Method Editor)는 'ㅎ', 'ㅏ', 'ㄴ' 키 입력을 순차적으로 받아 이를 조합하여 '한'이라는 완성된 글자로 화면에 보여줍니다. 이 과정에서 우리는 중간 단계를 거의 인지하지 못합니다.

천지인 키보드의 동작 방식

천지인 키보드는 피처폰 시절부터 널리 사용되어 온 모바일용 한글 입력 방식입니다. 적은 수의 버튼으로 모든 한글을 입력할 수 있다는 장점이 있어 스마트폰 시대에도 여전히 많은 사용자들이 선호하는 방식입니다.

천지인의 핵심 원리는 '천(ㆍ)', '지(ㅡ)', '인(ㅣ)' 세 가지 기본 모음을 조합하여 모든 모음을 만들어내는 것입니다.

  • = ㅣ + ㆍ
  • = ㆍ + ㅣ
  • = ㆍ + ㅡ
  • = ㅡ + ㆍ
  • = ㅣ + ㆍ + ㆍ
  • = ㆍ + ㆍ + ㅣ
  • ... 등등

이제 결정적인 단서가 나왔습니다. 천지인 키보드로 '어'라는 글자를 입력하는 과정을 생각해 봅시다.

  1. 사용자는 'ㅇ'을 누릅니다. (현재 입력창: 'ㅇ')
  2. 사용자는 모음 'ㅓ'를 만들기 위해 먼저 'ㆍ' 키를 누릅니다. 이때 입력기는 'ㅇ'과 조합되기 전의 중간 글자인 'ㆍ'를 잠시 내부적으로 처리하거나 표시합니다.
  3. 사용자는 이어서 'ㅣ' 키를 누릅니다. 입력기는 'ㆍ'와 'ㅣ'가 조합되어 'ㅓ'가 되는 것을 인지하고, 앞서 입력된 'ㅇ'과 합쳐 최종적으로 '어'를 만듭니다.

문제의 재구성: 필터가 중간 과정을 막아버린다!

다시 우리의 '잘못된' 필터링 코드로 돌아가 봅시다.

FilteringTextInputFormatter.allow(RegExp(r'[ㄱ-ㅎ|가-힣]'))

이 필터는 [ㄱ-ㅎ] 범위의 자음과 [가-힣] 범위의 완성형 한글만 허용합니다. 그런데 천지인 키보드로 '어'를 입력하는 2단계에서 어떤 일이 벌어질까요? 사용자가 'ㆍ' 키를 누르는 순간, FilteringTextInputFormatter는 이 'ㆍ'라는 문자를 검사합니다. 하지만 'ㆍ'는 [ㄱ-ㅎ]에도 속하지 않고, [가-힣]에도 속하지 않는 별개의 문자입니다. 따라서 필터는 이 문자의 입력을 그 즉시 '거부'해 버립니다.

결과적으로 입력기(IME)는 'ㅓ'를 조합하기 위한 핵심 재료인 'ㆍ'를 전달받지 못하므로, '어'라는 글자를 절대로 만들어낼 수 없게 됩니다. 이는 'ㆍ'가 들어가는 모든 모음, 즉 'ㅓ, ㅕ, ㅐ, ㅔ, ㅚ, ...' 등의 입력이 모두 막히는 심각한 문제로 이어집니다.

4. 완벽한 해결책: '중간 문자'를 허용하라

문제의 원인을 명확히 알았으니, 해결책은 간단합니다. 한글 조합에 사용되는 중간 문자들을 필터의 허용 목록에 추가해주면 됩니다. 천지인 키보드에서 사용하는 핵심 중간 문자는 다음과 같습니다.

  • (아래아): 'ㅓ', 'ㅗ' 등을 만드는 데 사용됩니다.
  • (쌍아래아): 'ㅕ', 'ㅛ' 등을 만드는 데 사용됩니다.

이 두 문자를 기존의 정규식에 추가하면 됩니다.


// 완벽하게 동작하는 해결 코드
TextField(
  decoration: const InputDecoration(
    labelText: '이름 (천지인 완벽 지원)',
    border: OutlineInputBorder(),
  ),
  inputFormatters: [
    // 'ㆍ'와 'ᆢ'를 정규식에 추가
    FilteringTextInputFormatter.allow(RegExp(r'[ㄱ-ㅎ|가-힣|ㆍ|ᆢ]')),
  ],
)

새로운 정규식 [ㄱ-ㅎ|가-힣|ㆍ|ᆢ]는 이제 자음, 완성형 한글뿐만 아니라 천지인 키보드의 조합용 문자인 'ㆍ'와 'ᆢ'까지 허용합니다. 이 코드를 적용하면, 천지인 키보드 사용자가 '어'를 입력할 때 'ㆍ' 문자가 필터에 의해 차단되지 않고 정상적으로 입력기에 전달되어 '어'라는 글자가 성공적으로 조합될 수 있습니다.

전체 예제 코드: 문제점과 해결책 비교

직접 문제를 확인하고 해결책을 적용해볼 수 있도록 전체 비교 코드를 작성했습니다. 아래 코드를 직접 실행하여 두 개의 텍스트 필드에서 천지인 키보드로 '여름' 또는 '어머니'를 입력해 보세요. 첫 번째 필드에서는 입력이 불가능하고, 두 번째 필드에서는 원활하게 입력되는 것을 확인할 수 있습니다.


import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Korean Input Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        useMaterial3: true,
      ),
      home: const KoreanInputTestScreen(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('한글 입력 포매터 테스트'),
      ),
      body: GestureDetector(
        onTap: () => FocusScope.of(context).unfocus(),
        child: SingleChildScrollView(
          child: Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Text(
                  '아래 두 입력 필드에서 천지인(또는 유사한 조합형) 키보드로 \'여름\', \'어머니\', \'했다\' 등을 입력하여 차이를 확인해보세요.',
                  style: TextStyle(fontSize: 16),
                ),
                const SizedBox(height: 30),

                // 1. 문제가 발생하는 TextField
                const Text(
                  '1. 잘못된 포매터 (천지인 입력 불가)',
                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.red),
                ),
                const SizedBox(height: 8),
                TextField(
                  decoration: const InputDecoration(
                    labelText: '이름 입력',
                    hintText: '\'어\', \'여\' 등 입력 시도',
                    border: OutlineInputBorder(),
                  ),
                  inputFormatters: [
                    // 문제의 정규식: 'ㆍ', 'ᆢ'가 없음
                    FilteringTextInputFormatter.allow(RegExp(r'[ㄱ-ㅎ|가-힣]')),
                  ],
                ),

                const SizedBox(height: 40),

                // 2. 문제가 해결된 TextField
                const Text(
                  '2. 올바른 포매터 (천지인 입력 가능)',
                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.green),
                ),
                const SizedBox(height: 8),
                TextField(
                  decoration: const InputDecoration(
                    labelText: '이름 입력',
                    hintText: '정상적으로 입력됩니다.',
                    border: OutlineInputBorder(),
                  ),
                  inputFormatters: [
                    // 해결된 정규식: 'ㆍ', 'ᆢ' 추가
                    FilteringTextInputFormatter.allow(RegExp(r'[ㄱ-ㅎ|가-힣|ㆍ|ᆢ]')),
                  ],
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

5. 활용도를 높이는 추가 팁과 고려사항

단순히 한글만 허용하는 것을 넘어, 실제 앱 개발에서는 더 복잡한 요구사항을 마주하게 됩니다. 예를 들어, 닉네임에는 한글, 영어, 숫자를 모두 허용하지만 특수문자는 막고 싶을 수 있습니다. 또는, 이름 입력란에는 띄어쓰기를 허용해야 할 수도 있습니다.

상황별 추천 정규식 패턴

다양한 시나리오에 맞춰 사용할 수 있는 정규식 패턴들을 소개합니다. 자신의 요구사항에 맞게 조합하여 사용해 보세요.

  • 한글만 허용 (천지인 완벽 지원)
    
    RegExp(r'[ㄱ-ㅎ|가-힣|ㆍ|ᆢ]')
            
  • 한글 + 띄어쓰기(공백) 허용

    공백 문자를 의미하는 \s를 추가합니다.

    
    RegExp(r'[ㄱ-ㅎ|가-힣|ㆍ|ᆢ|\s]')
            
  • 한글 + 영어 + 숫자 허용

    영문(a-z, A-Z)과 숫자(0-9) 범위를 추가합니다.

    
    RegExp(r'[a-zA-Z0-9ㄱ-ㅎ|가-힣|ㆍ|ᆢ]')
            
  • 한글 + 영어 + 숫자 + 띄어쓰기 허용 (가장 일반적인 닉네임 형태)

    위 패턴에 공백(\s)을 추가합니다.

    
    RegExp(r'[a-zA-Z0-9ㄱ-ㅎ|가-힣|ㆍ|ᆢ|\s]')
            
  • 특정 특수문자만 추가로 허용 (예: 밑줄 `_`, 하이픈 `-`)

    허용할 특수문자를 직접 나열합니다.

    
    RegExp(r'[a-zA-Z0-9ㄱ-ㅎ|가-힣|ㆍ|ᆢ|\s|_|-]')
            

고급 기법: 나만의 커스텀 TextInputFormatter 만들기

정규식만으로 해결하기 어려운 매우 복잡한 입력 규칙이 필요할 때가 있습니다. 예를 들어, '첫 글자는 반드시 한글이어야 하고, 중간에는 숫자 입력이 가능하지만 연속된 두 개의 특수문자는 허용하지 않는다'와 같은 규칙은 정규식 하나로 표현하기 매우 까다롭습니다. 이런 경우에는 TextInputFormatter 클래스를 직접 상속받아 우리만의 포매터를 만들 수 있습니다.

TextInputFormatter를 상속받는 클래스는 formatEditUpdate라는 메서드를 오버라이드해야 합니다. 이 메서드는 사용자가 키를 누를 때마다 호출되며, 이전 텍스트 값(oldValue)과 새로운 텍스트 값(newValue)을 인자로 받습니다. 우리는 이 메서드 안에서 원하는 로직을 구현하여 최종적으로 반환할 TextEditingValue를 결정할 수 있습니다.


// 복잡한 규칙을 위한 커스텀 포매터 예시 (개념)
class MyComplexFormatter extends TextInputFormatter {
  @override
  TextEditingValue formatEditUpdate(
    TextEditingValue oldValue,
    TextEditingValue newValue,
  ) {
    // 여기에 복잡한 로직을 구현합니다.
    // 예: newValue.text가 특정 규칙을 만족하는지 확인

    // 규칙을 만족하면 newValue를 그대로 반환
    if (/* 규칙 만족 조건 */) {
      return newValue;
    }

    // 규칙을 만족하지 않으면 이전 값(oldValue)을 반환하여 입력을 무시
    return oldValue;
  }
}

이 방법은 유연성이 매우 높지만, 코드가 복잡해지고 모든 예외 케이스를 직접 처리해야 하는 부담이 있습니다. 따라서 대부분의 경우에는 FilteringTextInputFormatter와 적절한 정규식을 조합하는 것만으로도 충분하며, 이 방법은 최후의 수단으로 고려하는 것이 좋습니다.

결론: 사용자를 배려하는 디테일의 중요성

Flutter에서 TextField의 한글 입력 문제는 단순한 코드 한 줄의 실수가 아니라, 다양한 사용자 환경과 문자 인코딩의 복잡성을 간과했을 때 발생하는 대표적인 사례입니다. 특히 한국 시장을 목표로 하는 앱이라면, 두벌식 키보드뿐만 아니라 천지인, 나랏글, 베가 등 다양한 한글 입력 방식을 사용하는 사용자가 존재한다는 사실을 항상 인지해야 합니다.

이번에 살펴본 RegExp(r'[ㄱ-ㅎ|가-힣|ㆍ|ᆢ]') 패턴은 이러한 다양성을 고려한, 작지만 매우 중요한 디테일입니다. 이 해결책을 통해 우리는 기술적으로 올바른 코드를 작성하는 것을 넘어, 모든 사용자가 불편함 없이 우리 앱을 사용할 수 있도록 배려하는 개발자로서 한 걸음 더 나아갈 수 있습니다.

앞으로 텍스트 입력을 처리할 때는 다음의 체크리스트를 꼭 기억하세요.

  1. 요구사항 명확화: 정확히 어떤 문자(한글, 영어, 숫자, 공백, 특수문자)를 허용하고, 어떤 문자를 막아야 하는가?
  2. 입력 방식 고려: 타겟 사용자들이 사용할 가능성이 있는 모든 키보드 입력 방식(특히 조합형 문자)을 고려했는가?
  3. 정규식 검증: 작성한 정규식이 모든 엣지 케이스를 포함하는가? (예: 천지인의 'ㆍ', 'ᆢ')
  4. 실기기 테스트: 에뮬레이터뿐만 아니라 실제 Android 및 iOS 기기에서 다양한 키보드 설정으로 충분히 테스트했는가?

이러한 세심한 접근 방식은 앱의 완성도를 높이고 사용자의 만족도를 극대화하는 가장 확실한 방법이 될 것입니다.


0 개의 댓글:

Post a Comment