[Flutter 심화] 1. 키보드가 올라올 때 버튼이 가려지는 문제

김주희's avatar
Aug 04, 2025
[Flutter 심화] 1. 키보드가 올라올 때 버튼이 가려지는 문제

상황

화면에 고정된 크기의 노란색 컨테이너와 그 아래에 TextFormField가 두 개와 로그인 버튼이 존재한다.
 
문제는 텍스트 폼 필드에 값을 입력하기 위해 클릭하는 순간 키보드가 올라오면서 텍스트 폼 필드를 덮어버린다.
 
목표는 세 번째 사진처럼 키보드가 텍스트 폼 필드와 로그인 버튼 영역을 가리지 않고 밀려 올라가도록 하는 것이다.
notion image
notion image
notion image
 
 

resizeToAvoidBottomInset

  • Scaffold의 속성
  • true일 때
    • 키보드가 올라오면 body 영역이 키보드 높이 만큼 줄어듦
  • false일 때
    • 키보드가 올라와도 body 영역 유지
    • 키보드에 가려질 수 있음

Flexible

  • 자식 위젯이 가능한 만큼만 커지게 허용
  • cf) Expanded
    • 자식 위젯을 무조건 남은 공간만큼 꽉 채우도록 확장
    • 자식 위젯이 스스로 크기를 정할 수 없음
  • 현재 내 크기만큼 잡음
     
     
     

    ScrollController

    • 스크롤 동작 제어 및 관찰해주는 클래스
    • 스크롤 위치 제어
      • jumpTo() : 지정한 위치로 즉시 스크롤 이동
      • animateTo() : 부드럽게 지정 위치로 스크롤 애니메이션
    • 스크롤 위치 확인
      • position.pixels : 현재 스크롤 위치(픽셀 단위)
      • position.maxScrollExtent : 스크롤 가능한 최대 범위(=스크롤 가능한 가장 아래쪽 위치의 값)
    • 스크롤 이벤트 감지
      • addListener() : 스크롤이 변경될 때마다 콜백 실행
    • 스크롤 끝 감지 (무한 스크롤 구현 시 사용)
      • 현재 위치와 position.maxScrollExtent 비교해서 끝에 도달했는지 판단
     
     
    구조
    Scaffold 내부
    리스트뷰 내부
     
    Expanded로 잡으면 끝까지 확장되는 문제
    Flexible로 잡으면 끝까지 확장되는 문제
    shrinkWrap
     
     
    전체를 컬럼으로 잡은 이유
    → 리스트뷰로 하면 스크롤 가능하니까 버튼이 inset(사용불가능한) 영역이 아님 (resizeToAvoidBottomInset : true)
    → Expanded대신에 shrinkWrap: true로 하면 자기 크기만큼 잡음 → 스크롤이 안생김
     
    컬럼으로 잡아야 화면을 꽉 채움 - 리스트뷰로 잡으면 꽉 안채우고 버튼을 가림
    리스트뷰 + shrinkWrap으로 하면 스크롤이 안생김
    이때 flexible을 사용함
    flexible은 expanded와 똑같은 기능을 함
    flexible = 현재 내 크기만큼 잡음
    리스트뷰에는 크기가 없는데 shrinkWrap으로 크기 만들고 flexible로 만들면 안터진다.
    flexible은 근데 내 크기가 좁아지면 변한다.
     
    근데 위의 텍스트 폼필드를 잡으면 키보드가 아래쪽 텍스트 폼필드를 가리는 문제가 생김
    → 스크롤 이벤트를 주면 해결 가능하다.
     
    ScrollController는 리스트뷰의 스크롤 감지 가능함. →
    onTap의 타이밍은 키보드가 올라오기 전이기 때문에 키보드가 올라오는 걸 await를 통해 기다려야 됨 → 0.6초(디바이스 성능에 따라 달라진다.) 정도 기다렸다가 키보드 올라오고 나서 onTap이 동작하면 밀려 올라간다.
    ⇒ 제일 밑의 텍스트폼필드만 빼고 다 걸어주면 된다!
     
     

    핵심 포인트

    1. resizeToAvoidBottomInset: true
    • 키보드가 올라올 때 Scaffold가 바디를 위로 밀어 올려서 가림/오버플로우를 방지.
    1. 스크롤 가능한 영역 + 컨트롤러(ScrollController)
    • 입력 포커스가 하단일 때도 스크롤로 뷰를 따라가게 함.
    1. 레이아웃 분리: “스크롤 영역” vs “고정 버튼”
    • 입력 폼은 Flexible/Expanded + ListView(또는 SingleChildScrollView)
    • 제출 버튼은 스크롤 밖에 두고 SafeArea로 하단 안전영역 보장.
    1. 포커스 이동 시 스크롤 보정
    • Scrollable.ensureVisible(context, alignment: …) 또는 controller.animateTo(...)로 포커스 필드를 가리지 않게 이동.
    1. 키보드 높이만큼 패딩 보정(선택)
    • 스크롤 영역 또는 바텀 버튼에 AnimatedPadding(padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom)) 적용하면 부드럽게 피함.
     
     
    import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: HomePage(), ); } } class HomePage extends StatefulWidget { @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { final username = TextEditingController(); final password = TextEditingController(); final scroll = ScrollController(); @override Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: true, body: Column( children: [ Flexible( child: ListView( controller: scroll, shrinkWrap: true, children: [ Container( color: Colors.yellow, height: 500, ), TextFormField( onTap: () async { await Future.delayed(Duration(milliseconds: 600)); scroll.jumpTo(scroll.position.maxScrollExtent); }, controller: username, ), TextFormField( onTap: () async {}, controller: password, ), ], ), ), SizedBox( width: double.infinity, child: ElevatedButton(onPressed: () {}, child: Text("로그인")), ), ], ), ); } }
    Share article

    jay0628