Flutter 로 커스텀 키보드 구현하기 feat. 카카오뱅크

Written by 코드팩토리 JC

1월 15, 2024

Flutter로 Custom Keyboard 만들기

서론

오늘은 Flutter 로 커스텀 키보드를 만들어보려고 합니다. 특수한 상황이 아닌 경우 각 OS 에서 제공해주는 기본 키보드의 기능이 충분하지만 만약에 특수한 키보드를 사용해서 UI/UX 를 대폭 증진 시킬 수 있다면 직접 키보드를 제작해야하는 상황이 올 수도 있습니다. 예를들면 금융 앱에서 숫자를 입력할때라던가 캘린더 앱에서 날짜를 쉽게 지정할 수 있도록 해야할때가 해당되죠. 연습으로 카카오뱅크의 이체금액 입력 키보드를 카피 해보도록 하겠습니다.

Youtube 영상

목표 스크린샷

카카오 뱅크 키보드

완성된 UI

아래 화면을 제작 해보도록 하겠습니다!

커스텀 키보드

개발환경 및 요구사항

[✓] Flutter (Channel stable, 1.22.4, on Mac OS X 10.15.7 19H114 darwin-x64, locale en-KR)

[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
[✓] Xcode - develop for iOS and macOS (Xcode 12.3)
[✓] IntelliJ IDEA Ultimate Edition (version 2020.2.1)
ShellScript

키 만들기

어떤 개발이든 개발할 목표를 가장 작은 단위로 쪼개서 개발을 시작하는게 중요합니다. 이번에 제작할 키보드는 키보드의 각 키를 가장 작은 단위로 생각하고 KeyboardKey 클래스 먼저 생성해보도록 하겠습니다.

KeyboardKey.dart

class KeyboardKey extends StatefulWidget {
  final String label;
  final dynamic value;
  final ValueSetter<dynamic> onTap;

  KeyboardKey({
    @required this.label,
    @required this.onTap,
    @required this.value,
  })  : assert(label != null),
        assert(onTap != null),
        assert(value != null);

  @override
  _KeyboardKeyState createState() => _KeyboardKeyState();
}

class _KeyboardKeyState extends State<KeyboardKey> {
  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: (){
        widget.onTap(widget.value);
      },
      child: Container(
        child: Center(
          child: Text(
            widget.label,
            style: TextStyle(
              fontSize: 20.0,
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
      ),
    );
  }
}
Dart

일단 이정도로 키를 생성 해보도록 할게요.

키보드에 키 넣기

위에서 제작한 KeyboardKey 위젯을 사용해서 키보드를 구현 해보겠습니다.

Keyboard.dart

class CustomKeyboardScreen extends StatefulWidget {
  @override
  _CustomKeyboardScreenState createState() => _CustomKeyboardScreenState();
}

class _CustomKeyboardScreenState extends State<CustomKeyboardScreen> {
  final keys = [
    ['1', '2', '3'],
    ['4', '5', '6'],
    ['7', '8', '9'],
    ['00', '0', '<-'],
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.end,
          children: keys
              .map(
                (x) => Row(
                  children: x.map((y) {
                    return KeyboardKey(
                      label: y,
                      onTap: (val) {},
                      value: y,
                    );
                  }).toList(),
                ),
              )
              .toList(),
        ),
      ),
    );
  }
}
Dart

일단 키가 보이게 만들어 봤습니다. 아래같은 그림이 나오네요. UI/UX 개선이 매우 시급합니다.

커스텀 키보드 1

키보드 균등하게 배치하기

각 키가 한 Row 의 3분의 1을 차지하면 되기 때문에 간단하게 Expand 위젯을 사용해서 키 배치 문제를 해결 해보겠습니다.

Keyboard.dart Key mapping 하는 부분

...
Column(
  mainAxisAlignment: MainAxisAlignment.end,
  children: keys
      .map(
        (x) => Row(
          children: x.map((y) {
            return Expanded(
              child: KeyboardKey(
                label: y,
                onTap: (val) {},
                value: y,
              ),
            );
          }).toList(),
        ),
      )
      .toList(),
)
...
Dart
커스텀 키보드 2

가로로는 이제 배치가 균등하게 돼서 봐줄만 하네요. 그런데 아직도 세로로 너무 납작한 경향이 있어요. 이걸 해결할 수 있는 방법은 여러가지가 있는데 저는 AspectRatio 위젯을 사용해서 가로 길이가 세로 길이의 2배가 되도록 설정 해볼게요.

AspectRatio 로 키의 넓이에 따른 높이 설정하기

KeyboardKey.dart InkWell 바로 밑에

  AspectRatio(
    aspectRatio: 2, // 넓이/높이 = 2
    child: Container(
      child: Center(
        child: Text(
          widget.label,
          style: TextStyle(
            fontSize: 20.0,
            fontWeight: FontWeight.bold,
          ),
        ),
      ),
    ),
  ),
Dart
커스텀 키보드

어떤가요? 이제 조금 더 키보드 다워졌죠? InkWell 을 사용했기 때문에 누를때마다 Ripple Effect 가 생기는게 아주 이쁘군요.

백스페이스 버튼 아이콘으로 변경하기

다른 버튼들은 상태가 좋은데 백스페이스 버튼에 아이콘을 사용하지 않아서 이쁘지가 않아요. 아이콘을 교체 해볼게요.

KeyboardKey.dart

class KeyboardKey extends StatefulWidget {
  final dynamic label; // 이거 dynamic 으로 변경
  final dynamic value;
  final ValueSetter<dynamic> onTap;

  KeyboardKey({
    @required this.label,
    @required this.onTap,
    @required this.value,
  })  : assert(label != null),
        assert(onTap != null),
        assert(value != null);

  @override
  _KeyboardKeyState createState() => _KeyboardKeyState();
}

class _KeyboardKeyState extends State<KeyboardKey> {
  
  // 조건부 렌더링!
  renderLabel(){
    if(widget.label is String){
      return Text(
        widget.label,
        style: TextStyle(
          fontSize: 20.0,
          fontWeight: FontWeight.bold,
        ),
      );
    }else{
      return widget.label;
    }
  }

  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: (){
        widget.onTap(widget.value);
      },
      child: AspectRatio(
        aspectRatio: 2,
        child: Container(
          child: Center(
            child: renderLabel(),
          ),
        ),
      ),
    );
  }
}
Dart

Keyboard.dart Key 리스트 선언 부분

final keys = [
    ['1', '2', '3'],
    ['4', '5', '6'],
    ['7', '8', '9'],
    ['00', '0', Icon(Icons.keyboard_backspace)],
];
JSX

키를 dynamic 타입을 받을 수 있도록 하고 String 타입이 들어올경우 기존의 Text 위젯을, 나머지는 직접 입력한 위젯을 렌더링 하도록 했습니다.

커스텀 키보드

이제 백스페이스 버튼도 카카오 UI 와 상당히 비슷해졌어요.

Refactoring 및 확인 버튼 만들기

코드를 조금 더 보기 쉽게 정리하고 확인 버튼을 만들어 보겠습니다!

Keyboard.dart

class CustomKeyboardScreen extends StatefulWidget {
  @override
  _CustomKeyboardScreenState createState() => _CustomKeyboardScreenState();
}

class _CustomKeyboardScreenState extends State<CustomKeyboardScreen> {
  final keys = [
    ['1', '2', '3'],
    ['4', '5', '6'],
    ['7', '8', '9'],
    ['00', '0', Icon(Icons.keyboard_backspace)],
  ];

  renderKeyboard() {
    return keys
        .map(
          (x) => Row(
            children: x.map((y) {
              return Expanded(
                child: KeyboardKey(
                  label: y,
                  onTap: (val) {},
                  value: y,
                ),
              );
            }).toList(),
          ),
        )
        .toList();
  }

  renderConfirmButton() {
    return Row(
      children: [
        Expanded(
          child: FlatButton(
            onPressed: () {},
            color: Colors.orange,
            child: Padding(
              padding: EdgeInsets.symmetric(vertical: 16.0),
              child: Text(
                '확인',
                style: TextStyle(
                  color: Colors.white,
                ),
              ),
            ),
          ),
        ),
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Padding(
          padding: EdgeInsets.symmetric(horizontal: 16.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.end,
            children: [
              ...renderKeyboard(),
              Container(height: 16.0),
              renderConfirmButton(),
            ],
          ),
        ),
      ),
    );
  }
}
JSX
커스텀 키보드

이제 제법 그럴싸한 UI 가 나왔죠?

키보드 입력 받기

이제는 키보드를 누를때마다 입력을 받아서 화면에 보여줘야 합니다. 이건 TextEditingController 를 사용해도 되고 String 값을 하나 지정해서 작업을 해도 됩니다. 만약에 TextField 와 연동을 하고싶다면 TextEditingController 를 사용하는게 맞지만 저희는 카카오뱅크처럼 그냥 텍스트로 보여줄 계획이기 때문에 간단하게 String 값 하나를 운영하도록 할게요!

Keyboard.dart

class CustomKeyboardScreen extends StatefulWidget {
  @override
  _CustomKeyboardScreenState createState() => _CustomKeyboardScreenState();
}

class _CustomKeyboardScreenState extends State<CustomKeyboardScreen> {
  String amount;

  @override
  void initState() {
    super.initState();

    amount = '';
  }

  final keys = [
    ['1', '2', '3'],
    ['4', '5', '6'],
    ['7', '8', '9'],
    ['00', '0', Icon(Icons.keyboard_backspace)],
  ];

  onNumberPress(val) {
    setState(() {
      amount = amount + val;
    });
  }

  onBackspacePress(val) {
    setState(() {
      amount = amount.substring(0, amount.length - 1);
    });
  }

  renderKeyboard() {
    return keys
        .map(
          (x) => Row(
            children: x.map((y) {
              return Expanded(
                child: KeyboardKey(
                  label: y,
                  onTap: y is Widget ? onBackspacePress : onNumberPress,
                  value: y,
                ),
              );
            }).toList(),
          ),
        )
        .toList();
  }

  renderConfirmButton() {
    return Row(
      children: [
        Expanded(
          child: FlatButton(
            onPressed: () {},
            color: Colors.orange,
            child: Padding(
              padding: EdgeInsets.symmetric(vertical: 16.0),
              child: Text(
                '확인',
                style: TextStyle(
                  color: Colors.white,
                ),
              ),
            ),
          ),
        ),
      ],
    );
  }

  renderText() {
    String display = '보낼금액';
    TextStyle style = TextStyle(
      color: Colors.grey,
      fontWeight: FontWeight.bold,
      fontSize: 30.0,
    );

    if (amount.length != 0) {
      display = amount + '원';
      style = style.copyWith(
        color: Colors.black,
      );
    }

    return Expanded(
      child: Center(
        child: Text(
          display,
          style:style,
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: Padding(
          padding: EdgeInsets.symmetric(horizontal: 16.0),
          child: Column(
            children: [
              renderText(),
              ...renderKeyboard(),
              Container(height: 16.0),
              renderConfirmButton(),
            ],
          ),
        ),
      ),
    );
  }
}
Dart
커스텀 키보드

이제 키보드의 키를 누르면 결과가 화면에 보여요. 키를 누르면 String 에 글자를 append 해주는 형태로 진행을 했고 backspace 를 누르면 String 에서 마지막으로 작성한 글자를 제거하는 형태로 구현 했어요.

숫자에 컴마 찍고 값이 입력 안됐을때 버튼 disable 하기

Keyboard.dart renderConfirmButton 함수

  renderConfirmButton() {
    return Row(
      children: [
        Expanded(
          child: FlatButton(
            onPressed: amount.length == 0 ? null : () {}, // 값이 없으면 disable
            color: Colors.orange,
            disabledColor: Colors.grey[200],
            child: Padding(
              padding: EdgeInsets.symmetric(vertical: 16.0),
              child: Text(
                '확인',
                style: TextStyle(
                  color: amount.length == 0 ? Colors.grey : Colors.white,
                ),
              ),
            ),
          ),
        ),
      ],
    );
  }
Dart

이제 금액에 입력이 안되어서 ‘보낼금액’이 표시가 되어있을 때는 확인 버튼이 회색으로 변하며 disabled 상태가 됩니다.

숫자를 쉽게 포메팅 하기 위해 Flutter Intl 패키지를 설치 해볼게요

pubspec.yaml

dependencies:
    flutter:
      sdk: flutter
    intl: ^0.16.1
Dart

Keyboard.dart renderText 함수

  renderText() {
    String display = '보낼금액';
    TextStyle style = TextStyle(
      color: Colors.grey,
      fontWeight: FontWeight.bold,
      fontSize: 30.0,
    );

    if (amount.length != 0) {
      NumberFormat f = NumberFormat('#,###');

      display = f.format(int.parse(amount)) + '원';
      style = style.copyWith(
        color: Colors.black,
      );
    }

    return Expanded(
      child: Center(
        child: Text(
          display,
          style:style,
        ),
      ),
    );
  }
JSX
커스텀 키보드

예외처리

눈썰미가 좋으신 분들은 바로 알아차리셨겠지만 현재는 버그가 한가지 있습니다. 현재 int 를 업데이트 하는게 아니라 String 을 업데이트 하다보니 처음부터 0을 눌렀을때 0원이라는 표시가 나오게 되는데요. 아무런 값이 없을때는 0을 눌러도 ‘보낼금액’이라는 글자가 나오도록 버그를 고쳐보겠습니다.

Keyboard.dart onNumberPress 함수

  onNumberPress(val) {
    if(val == '0' && amount.length == 0){
      return;
    }

    setState(() {
      amount = amount + val;
    });
  }
Dart

결과물

커스텀 키보드

관련 포스트

플러터에서의 Immutable Programming: copyWith 함수 마스터하기!

플러터에서의 Immutable Programming: copyWith 함수 마스터하기!

서론 불변 프로그래밍: 현대 개발의 핵심 현대 소프트웨어 개발에서 불변 프로그래밍(Immutable Programming)의 중요성은 간과할 수 없는 요소입니다. 플러터(Flutter)에서도 마찬가지로 불변 프로그래밍 개념이 매우 중요하며, copyWith 함수는 이러한 불변성을 유지하는 데 핵심적인 역할을 합니다. 이 글에서는 플러터를 배우기 시작하는 개발자들에게 불변 프로그래밍의 중요성을 강조하고, copyWith 함수의 역할과 사용 방법에 대해 설명 해보겠습니다!...

ChatGPT가 이야기하는 2024년 개발자 로드맵

ChatGPT가 이야기하는 2024년 개발자 로드맵

서론 개발자의 여정을 시작하며 안녕하세요, 미래의 개발자 여러분! 오늘부터 시작하는 여러분의 개발 여정에 함께할 수 있어서 기쁩니다. 2023년은 기술이 매우 빠르게 변화하는 해였으며, 이러한 변화 속에서 개발자가 되기 위한 길은 더욱 다채롭고 흥미로워졌습니다. 이 로드맵은 초보자인 여러분이 개발의 세계에 첫발을 내딛는 데 필요한 기초부터 시작해, 점차 심화 단계로 나아가는 길을 안내해 드릴 것입니다. 백엔드 개발 이 글은 단순히 기술을 배우는 것 이상의 의미를 가집니다....

Flutter Freezed 플러그인! Entity Code Generation은 이거 하나로 끝!

Flutter Freezed 플러그인! Entity Code Generation은 이거 하나로 끝!

https://youtu.be/i5p6wXLAX7I 서론 Flutter 는 Code Generation 기능이 상당히 많이 활성화되어 있어요. 흔히들 많이 사용하는 json_serializable 라이브러리도 있고 retrofit 및 chopper 라이브러리도 있습니다. 오늘 알려드릴 freezed 또한 데이터 클래스에 편의 기능들을 제공해주는 code generation 라이브러리입니다. Freezed vs Json Serializable Code Generation 이라는...