
키보드가 올라오면 UI가 다 깨집니다 (Bottom Overflow)
로그인 화면을 만들었는데 키보드가 올라오니 노란 줄무늬 에러가 뜹니다. resizeToAvoidBottomInset부터 스크롤 뷰, 그리고 채팅 앱을 위한 reverse 팁까지, 키보드 대응의 모든 것을 정리해봤습니다.

로그인 화면을 만들었는데 키보드가 올라오니 노란 줄무늬 에러가 뜹니다. resizeToAvoidBottomInset부터 스크롤 뷰, 그리고 채팅 앱을 위한 reverse 팁까지, 키보드 대응의 모든 것을 정리해봤습니다.
안드로이드는 오는데 iOS는 조용합니다. 혹은 앱이 켜져 있을 때만 옵니다. Background/Terminated 상태 처리, APNs 인증서, 그리고 Notification Channel 설정까지 완벽하게 해결합니다.

안드로이드는 Xcode보다 낫다고요? Gradle 지옥에 빠져보면 그 말이 쏙 들어갈 겁니다. minSdkVersion 충돌, Multidex 에러, Namespace 변경(Gradle 8.0), JDK 버전 문제, 그리고 의존성 트리 분석까지 완벽하게 해결해 봅니다.

Debug에선 잘 되는데 Release에서만 죽나요? 범인은 '난독화'입니다. R8의 원리, Mapping 파일 분석, 그리고 Reflection을 사용하는 라이브러리를 지켜내는 방법(@Keep)을 정리해봤습니다.

서버에서 잘 오던 데이터가 갑자기 앱을 죽입니다. 'type Null is not a subtype of type String' 에러의 원인과, 안전한 JSON 파싱을 위한 Null Safety 전략을 정리해봤습니다.

모바일 앱 개발을 시작하고 처음으로 "로그인 화면"을 만들 때의 일입니다. 디자인은 완벽했습니다.
그런데 시뮬레이터에서 이메일 입력창을 누르는 순간, 화면 아래에서 키보드가 치고 올라오면서 모든 UI를 위로 밀어버렸습니다. 그리고 화면 상단에 로고는 찌그러지고, 하단에는 그 공포의 노란색/검은색 줄무늬(Overflow Error)가 떴습니다.
Bottom overflowed by 150 pixels
"아니, 키보드가 올라온다고 화면이 왜 깨져? 그냥 키보드 위로 겹쳐지면 안 되나?" 이것은 모바일 OS(Android/iOS)와 Flutter의 ViewInsets 개념을 모르고 덤비던 시절의 제 절규였습니다.
모바일에서 소프트 키보드(가상 키보드)는 단순한 오버레이(Overlay)가 아닙니다. 키보드가 올라오는 순간, 시스템은 앱이 사용할 수 있는 화면 영역(Window) 자체를 줄여버립니다.
이때 Flutter의 Scaffold는 기본적으로 이 변화에 맞춰 자신의 크기를 줄이려 노력합니다.
그래서 높이 800px에 딱 맞춰서 디자인해 놓은 UI가 갑자기 500px 안에 구겨지게 되고, 공간이 부족해 터져버리는(Overflow) 것입니다.
이것이 MediaQuery.of(context).viewInsets.bottom의 정체입니다.
"나는 키보드가 올라오든지 말든지, 내 UI 크기를 줄이기 싫어! 그냥 키보드 뒤에 가려지게 놔둬!"
라고 외치고 싶다면, Scaffold에게 명령하면 됩니다.
Scaffold(
resizeToAvoidBottomInset: false, // 👈 핵심!
body: Column(
children: [
Expanded(child: Logo()),
TextField(),
LoginButton(),
],
),
)
장점:
단점:
TextField)이나 로그인 버튼이 화면 하단에 있었다면? 키보드가 그 위를 덮어버립니다. 사용자는 자기가 뭘 입력하는지 볼 수도 없고, 로그인 버튼을 누를 수도 없습니다. 키보드를 내려야만 버튼이 보입니다. UX 점수 0점입니다.가장 정석적인 방법은 "화면이 좁아지면 스크롤 할 수 있게 만드는 것"입니다. 이러면 줄어든 500px 영역 안에서도 사용자가 스크롤을 해서 가려진 위쪽 로고나 아래쪽 버튼을 다 볼 수 있습니다.
Scaffold(
body: SingleChildScrollView( // 👈 전체를 감싸기
child: SizedBox(
height: MediaQuery.of(context).size.height, // 전체 높이 확보
child: Column(
children: [
Spacer(), // 빈 공간
Logo(),
Spacer(),
TextField(),
TextField(),
SizedBox(height: 20),
LoginButton(),
Spacer(),
],
),
),
),
)
여기서 팁은 child에 SizedBox(height: screenHeight)를 주는 것입니다. 그래야 내용물이 적을 때도 화면 꽉 차게 디자인을 잡을 수 있습니다. (물론 키보드가 올라오면 높이가 부족해지니 스크롤이 활성화됩니다.)
이 방법의 유일한 단점은, 사용자가 입력창을 누르면 키보드가 올라오면서 포커스된 입력창을 가릴 때가 있다는 점입니다. Flutter가 자동으로 스크롤을 맞춰주긴 하지만, 가끔 빗나갑니다.
채팅 앱처럼 입력창이 항상 키보드 바로 위에 붙어 있어야 하는 경우는 어떨까요?
Scaffold의 자동 조정에 맡기지 않고, 우리가 직접 패딩(Padding)을 줘서 움직일 수 있습니다.
// 1. resizeToAvoidBottomInset를 끄고
Scaffold(
resizeToAvoidBottomInset: false,
body: Column(
children: [
Expanded(child: ChatList()), // 채팅 목록
// 2. 입력창 영역
Container(
padding: EdgeInsets.only(
// 3. 키보드 높이만큼 하단 패딩 줌
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: Row(
children: [
TextField(),
SendButton(),
],
),
),
],
),
)
이렇게 하면 부드러운 애니메이션을 우리가 직접 제어할 수 있습니다. 키보드 높이(viewInsets.bottom)는 키보드가 올라오면 300, 내려가면 0으로 변합니다. 이 값이 변할 때 자연스럽게 Container의 패딩이 늘어나면서 입력창이 키보드 위로 쑥 올라옵니다.
가끔 Flutter 코드만으로는 해결이 안 될 때가 있습니다.
안드로이드 네이티브 설정(AndroidManifest.xml)이 우선하기 때문입니다.
activity 태그 안에 android:windowSoftInputMode 속성을 확인하세요.
adjustResize (권장): 뷰의 크기를 줄여서 키보드 자리를 만듭니다. Flutter의 viewInsets가 정상 작동합니다.adjustPan: 뷰의 크기는 그대로 두고, 전체 화면을 위로 밀어 올립니다(Pan). 키보드가 포커스된 입력창을 가리지 않게 해주지만, 상단 타이틀바(AppBar)가 화면 밖으로 잘려 나갑니다.앱바가 잘리는 게 싫다면 무조건 adjustResize를 쓰고, Flutter 안에서 스크롤로 해결해야 합니다.
아이폰에서 keyboardType: TextInputType.number를 쓰면 숫자 패드가 뜹니다.
그런데 안드로이드와 달리 '완료(Done/Return)' 버튼이 없습니다.
사용자는 키보드를 내릴 방법이 없어서 갇혀버립니다(Trapped).
해결책: 커스텀 툴바 (Keyboard Actions)
keyboard_actions 같은 패키지를 써서 숫자 패드 위에 '완료' 버튼이 있는 바(Bar)를 붙여줘야 합니다.
KeyboardActions(
config: KeyboardActionsConfig(
actions: [
KeyboardActionsItem(
focusNode: _focusNode,
toolbarButtons: [
(node) {
return GestureDetector(
onTap: () => node.unfocus(), // 키보드 내리기
child: Padding(
padding: EdgeInsets.all(8.0),
child: Text("DONE"),
),
);
}
],
),
],
),
child: TextField(...),
)
이 디테일 하나가 "아이폰도 신경 쓴 앱"과 "대충 포팅한 앱"을 가릅니다.
채팅 앱을 만들 때 초보자들이 가장 많이 하는 실수는, 키보드가 올라올 때 채팅 목록이 가려지는 것입니다. 카카오톡을 보세요. 키보드가 올라오면 스크롤이 위로 밀리면서 가장 최근 메시지가 키보드 바로 위에 딱 붙습니다.
이걸 구현하려면 ListView의 reverse 속성을 써야 합니다.
ListView.builder(
reverse: true, // 👈 역순 스크롤 (바닥이 시작점)
itemCount: messages.length,
itemBuilder: (context, index) {
// 주의: index 0이 가장 최신 메시지임!
return MessageBubble(messages[index]);
},
)
reverse: true를 쓰면 리스트의 기준점(Anchor)이 화면 바닥이 됩니다.
그래서 키보드가 올라와서 화면 높이가 줄어들면, 기준점(바닥)도 같이 올라오기 때문에, 자연스럽게 메시지들도 딸려 올라옵니다. 별도의 스크롤 제어 코드가 필요 없습니다. 이것이 채팅 UX의 마법입니다.
문제: 회원가입 화면에서 '이메일' 입력 후 엔터를 치면 키보드가 내려갑니다. 사용자는 다시 '비밀번호' 창을 터치해야 합니다. (불편함)
목표: '이메일'에서 엔터(Next)를 누르면 자동으로 '비밀번호'로 포커스가 이동하게 만드세요.
힌트:
FocusNode 두 개를 만들고, TextField의 onSubmitted 콜백에서 FocusScope.of(context).requestFocus(nextNode)를 호출하세요.
// 의사 코드(Pseudo Code)
final emailNode = FocusNode();
final passwordNode = FocusNode();
TextField(
focusNode: emailNode,
textInputAction: TextInputAction.next,
onSubmitted: (_) => FocusScope.of(context).requestFocus(passwordNode), // 다음으로 토스!
);
사용자는 입력하다가 빈 화면을 터치하면 키보드가 내려가길 기대합니다. 하지만 플러터는 기본적으로 안 내려갑니다.
앱 전체를 GestureDetector로 감싸주세요.
GestureDetector(
onTap: () => FocusManager.instance.primaryFocus?.unfocus(), // 마법의 코드
child: Scaffold(...),
)
이 코드는 main.dart 레벨이나 Scaffold 상위에 두면 앱 전체에 적용됩니다. UX 만족도가 200% 상승합니다.
키보드 레이아웃 문제는 모바일 개발의 통과의례입니다. "왜 내 디자인을 망치는 거야!"라고 화내지 마세요. 키보드는 사용자와 앱이 대화하는 가장 중요한 수단입니다.
SingleChildScrollView로 감싸라. 사용자는 스크롤 할 수 있어야 한다.resizeToAvoidBottomInset: false를 고려하되, 입력창이 가려지지 않는지 체크하라.reverse: true를 활용하고 viewInsets로 입력창 위치를 정밀 제어하라.이 3가지 패턴만 익히면, 어떤 상황에서도 부드럽고 전문적인 입력 경험을 줄 수 있습니다.