Flutter 입력 화면에서 키보드가 올라오면 마지막 입력칸이나 제출 버튼이 사라질 수 있습니다. 이때 resizeToAvoidBottomInset: false로 노란 overflow 표시만 숨기면, 입력 내용이 키보드 뒤에 남을 수 있습니다. 먼저 화면이 줄어든 것인지, 줄어든 화면 안의 내용이 넘친 것인지를 나눠 확인해야 합니다.
이 글은 일반적인 Scaffold.body 안의 입력 폼을 다룹니다. 입력값을 가져오는 방법은 기존 TextField 글에 두고, 여기서는 키보드가 열릴 때의 높이·스크롤·여백만 점검합니다. 모달, WebView, 화면 위에 겹쳐 놓은 별도 패널은 다른 제약을 가질 수 있습니다.
1. 키보드를 열기 전후에 세 가지를 비교하세요
같은 기기에서 첫 입력칸과 마지막 입력칸을 각각 눌러 보세요. 키보드가 열린 뒤 본문의 아래 경계가 위로 움직이는지, 스크롤로 마지막 항목을 볼 수 있는지, 오류 로그에 어떤 제약이 표시되는지를 기록합니다. 버튼이 항상 화면 안에 있어야 하는 설계인지, 스크롤해서 도달해도 되는 설계인지도 먼저 정합니다.
| 보이는 현상 | 먼저 구분할 원인 | 첫 확인 |
|---|---|---|
| 키보드 위에 노란 줄과 RenderFlex overflowed가 나타남 | 줄어든 본문에 스크롤 없는 Column 또는 고정 높이가 들어 있음 | 키보드를 닫았을 때만 내용이 맞는지, 부모의 고정 높이가 있는지 확인 |
| 오류는 없지만 마지막 입력칸이 키보드 뒤로 숨음 | 본문 크기 조정이 꺼져 있거나 입력칸 주변에 스크롤 경로가 없음 | Scaffold 설정과 입력칸을 감싼 Scrollable 확인 |
| 입력칸은 보이지만 버튼에 도달할 수 없음 | 입력칸만 스크롤되고 버튼은 별도 위치에 있음 | 입력칸과 버튼이 같은 스크롤 영역에 있는지 확인 |
| 키보드 위에 큰 빈 공간이 생김 | 본문 축소와 직접 추가한 키보드 여백의 중복 가능성 | 수동 bottom padding을 한 번씩 제거해 전후 비교 |
| 키보드를 열어도 높이 값이 0으로 관찰됨 | 관찰한 context에서 inset이 제거됐거나 소프트 키보드가 아닌 상황일 수 있음 | 측정 위치와 하드웨어·플로팅 키보드 여부를 함께 기록 |
이 표는 현상에서 다음 확인 지점을 고르는 순서입니다. 현상 하나만으로 운영체제나 Flutter 버그라고 단정하지 마세요.
2. 화면 크기 조정과 스크롤을 각각 해결하세요
Scaffold API에서 resizeToAvoidBottomInset의 기본값은 true이며, 키보드에 가려진 영역에 맞춰 본문 등의 크기를 조정합니다. 그러나 본문을 줄이는 일과 본문 안의 모든 내용을 스크롤할 수 있게 만드는 일은 별개입니다. 높이가 줄어든 Column에 입력칸을 그대로 쌓아두면 공간이 부족해질 수 있습니다.
입력 항목과 마지막 버튼이 함께 움직여도 되는 폼이라면 하나의 ListView에 넣는 구조부터 검토하세요. 길지 않은 단일 덩어리에 SingleChildScrollView를 사용할 수도 있지만, 무한 높이를 주는 스크롤 안에 Expanded를 그대로 넣으면 제약이 충돌할 수 있습니다. 공식 스크롤 문서의 제약 설명을 확인하고, 큰 목록 전체를 한꺼번에 만드는 구조와 구분하세요.
SafeArea는 시스템 UI를 피할 여백을 처리하는 위젯입니다. SafeArea를 추가하는 것만으로 긴 폼에 스크롤이 생기거나 키보드가 가린 모든 항목이 자동으로 배치되는 것은 아닙니다.
3. 입력칸과 버튼을 한 영역에 둔 최소 예제
새 Flutter 프로젝트의 lib/main.dart에서 비교할 수 있도록 작성한 설명용 예제입니다. 본문 축소는 Scaffold가 맡고, 메모와 버튼은 같은 ListView 안에 둡니다. 별도의 키보드 높이 padding은 넣지 않았습니다. ‘입력 마치기’는 포커스를 해제하는 버튼이며, 메모를 저장하거나 서버로 보내지 않습니다.
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: KeyboardFormPage()));
class KeyboardFormPage extends StatefulWidget {
const KeyboardFormPage({super.key});
@override
State<KeyboardFormPage> createState() => _KeyboardFormPageState();
}
class _KeyboardFormPageState extends State<KeyboardFormPage> {
final _memo = TextEditingController();
@override
void dispose() {
_memo.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(title: const Text('키보드와 입력 폼')),
body: SafeArea(
child: ListView(
padding: const EdgeInsets.all(16),
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
children: [
const Text('아래 메모 칸까지 이동한 뒤 키보드를 열어 보세요.'),
const SizedBox(height: 16),
const TextField(
textInputAction: TextInputAction.next,
decoration: InputDecoration(labelText: '이름'),
),
const SizedBox(height: 16),
const TextField(
textInputAction: TextInputAction.next,
decoration: InputDecoration(labelText: '제목'),
),
const SizedBox(height: 16),
TextField(
controller: _memo,
minLines: 3,
maxLines: 5,
scrollPadding: const EdgeInsets.all(24),
decoration: const InputDecoration(labelText: '메모'),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => FocusScope.of(context).unfocus(),
child: const Text('입력 마치기'),
),
],
),
),
);
}
}
scrollPadding은 포커스를 받은 TextField가 주변 스크롤 영역 안에서 보이도록 이동할 때의 여유를 조정합니다. TextField API 설명처럼 주변에 스크롤 가능한 영역이 있어야 하며, 이 값을 크게 설정하는 것만으로 별도 고정 버튼까지 이동시키는 것은 아닙니다.
4. 여백이 커졌다면 viewInsets를 더한 위치를 찾으세요
MediaQueryData.viewInsets는 키보드 같은 시스템 UI에 완전히 가려진 영역을 나타내며, padding·viewPadding과는 구분됩니다. 위 예제처럼 Scaffold가 이미 본문을 줄이는 경우에는 원래 화면의 키보드 높이를 다시 읽어 본문 아래에 더하지 않는 구조로 시작합니다.
그렇다고 MediaQuery.of(context).viewInsets.bottom을 사용하는 모든 코드가 잘못된 것은 아닙니다. 본문 축소를 사용하지 않는 별도 패널이나 다른 route에서 직접 회피가 필요할 수 있습니다. 중요한 것은 어느 부모가 높이를 줄였고, 어느 context의 값을 어떤 자식에 더했는가입니다. 위쪽에서 inset을 소비한 뒤의 context와 원래 화면 context를 섞어 해석하지 마세요.
- 원래 상태를 캡처하고 Scaffold 설정과 직접 추가한 bottom padding을 적습니다.
- 키보드를 닫은 상태와 연 상태의 본문 높이 및 관찰한 viewInsets.bottom을 같은 위치에서 비교합니다.
- 수동 padding만 제거한 뒤 입력칸과 마지막 버튼에 도달하는지 확인합니다.
- 여백이 줄었지만 버튼이 다시 숨는다면 무조건 값을 0으로 고정하지 말고 버튼의 배치 위치를 확인합니다.
5. 수정 후 확인할 항목과 완료 조건
아래는 직접 적용할 때 쓸 검증 계획입니다. 이 사이트에서 여러 기기에 실행해 얻은 측정 결과를 뜻하지 않습니다. 각 조건에서 성공 여부와 스크린샷을 따로 남기면 회귀 문제를 찾기 쉽습니다.
| 확인 조건 | 기록할 관찰 | 완료 조건 |
|---|---|---|
| 세로 화면에서 마지막 메모 칸 선택 | 커서, 입력 줄, 스크롤 이동 여부 | 입력 중인 줄을 볼 수 있고 내용을 계속 편집할 수 있음 |
| 키보드가 열린 상태에서 아래로 이동 | 마지막 버튼의 도달 가능 여부 | 스크롤로 버튼 전체에 도달하고 누를 수 있음 |
| 가로 화면 또는 작은 창 | 높이가 줄었을 때의 overflow 로그 | 내용이 잘리지 않고 스크롤로 접근 가능 |
| OS 글자 크기를 크게 설정 | 라벨과 버튼 줄바꿈, 가림 여부 | 입력칸과 버튼을 식별하고 조작할 수 있음 |
| 키보드 닫기와 재열기 | 빈 여백, 포커스, 기존 입력 내용 | 키보드 여백이 남지 않고 의도하지 않은 값 초기화가 없음 |
본문 밖에 고정한 하단 버튼까지 다루려면 이 예제에 코드를 계속 더하기 전에 그 버튼이 어떤 레이아웃의 자식인지부터 분리하세요. 폼 전체가 스크롤되는 방식과 버튼을 고정하는 방식은 서로 다른 완료 조건을 가집니다.
6. 해결되지 않을 때 공유할 최소 기록
Flutter / Dart 버전:
대상 OS / 기기 / 소프트 키보드 종류:
세로·가로 / 창 크기 / 글자 크기:
Scaffold.resizeToAvoidBottomInset 값:
본문의 부모와 스크롤 위젯:
별도 bottom padding을 추가한 위치:
키보드 열기 전·후 본문 높이와 viewInsets.bottom:
마지막 입력칸·버튼에 도달 가능한지:
정확한 overflow 또는 제약 오류:
한 번에 바꾼 설정과 관찰 결과:
재현 코드를 공유할 때는 이름·이메일·실제 메모를 지우고 가짜 입력으로 바꾸세요. 전체 화면에서 문제가 보이지 않으면 네트워크나 계정 처리를 잠시 빼고 입력 폼의 배치만 남겨 비교하면 원인 범위를 줄일 수 있습니다.
확인 범위와 공식 자료
2026년 10월 20일 Flutter 공식 API 문서에서 본문 축소, 시스템 inset, 스크롤 제약과 입력칸 이동의 동작 범위를 확인했습니다. 코드와 판단표는 이를 바탕으로 작성한 설명용 자료이며, 이 글의 예제를 Flutter SDK로 빌드하거나 Android·iPhone 실기기에서 검증한 결과는 아닙니다. 실제 프로젝트의 SDK·기기 조건에서 위 확인표를 적용하세요.
- Scaffold.resizeToAvoidBottomInset
- MediaQueryData.viewInsets
- SingleChildScrollView와 높이 제약
- SafeArea
- TextField.scrollPadding