
아이폰 노치에 UI가 가려질 때 (SafeArea)
아이폰 X 이후, 직사각형 화면의 시대는 끝났습니다. 노치와 홈 인디케이터로부터 당신의 UI를 지키는 방법, 그리고 SafeArea를 쓰지 말아야 할 때를 정리해봤습니다.

아이폰 X 이후, 직사각형 화면의 시대는 끝났습니다. 노치와 홈 인디케이터로부터 당신의 UI를 지키는 방법, 그리고 SafeArea를 쓰지 말아야 할 때를 정리해봤습니다.
로그인 화면을 만들었는데 키보드가 올라오니 노란 줄무늬 에러가 뜹니다. resizeToAvoidBottomInset부터 스크롤 뷰, 그리고 채팅 앱을 위한 reverse 팁까지, 키보드 대응의 모든 것을 정리해봤습니다.

안드로이드는 오는데 iOS는 조용합니다. 혹은 앱이 켜져 있을 때만 옵니다. Background/Terminated 상태 처리, APNs 인증서, 그리고 Notification Channel 설정까지 완벽하게 해결합니다.

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

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

안드로이드 에뮬레이터(Pixel 4)에서 개발할 때는 모든 것이 완벽했습니다. 화면 꼭대기부터 바닥까지 네모반듯했으니까요.
그런데 멘토가 화면을 보더니 한마디 했습니다. "어, 이거 아이폰 15에서 보니까 상단 타이틀이 노치(Notch)에 가려져서 안 보이는데요? 하단 버튼도 홈 바(Home Indicator)랑 겹쳐서 안 눌리고요."
시뮬레이터를 켜보고 경악했습니다. 내 소중한 '뒤로가기' 버튼이 M자 탈모 같은 노치 뒤로 숨어버렸고, '저장' 버튼은 바닥에 있는 얇은 검은 줄(홈 인디케이터)과 겹쳐서 터치가 안 됐습니다.
2017년 아이폰 X의 등장과 함께, 모바일 개발자들에게 "화면은 직사각형이다"라는 명제는 깨졌습니다. 화면 상단에는 카메라와 센서가 들어가는 Notch(또는 Dynamic Island)가 생겼고, 하단에는 물리 버튼 대신 제스처를 위한 Home Indicator 영역이 생겼습니다.
이 영역들은 "하드웨어적으로는 화면이지만, 소프트웨어적으로는 터치하거나 그리면 안 되는 금지 구역"입니다. 이것을 시스템적으로 알려주는 값이 바로 Padding(ViewPadding)입니다.
MediaQuery.of(context).padding.top: 상태 표시줄 + 노치 높이 (보통 47px ~ 59px)MediaQuery.of(context).padding.bottom: 홈 인디케이터 높이 (보통 34px)Flutter는 이 문제를 해결하기 위해 SafeArea라는 축복받은 위젯을 제공합니다.
이 위젯으로 감싸기만 하면, 노치와 홈 인디케이터 영역만큼 자동으로 안쪽 패딩(Padding)을 줍니다.
Scaffold(
body: SafeArea( // 👈 이것만 있으면 됨
child: Column(
children: [
Header(), // 이제 노치 아래에 안전하게 그려짐
Content(),
Footer(), // 이제 홈 바 위에 안전하게 그려짐
],
),
),
)
"와! 해결됐다! 모든 화면을 SafeArea로 감싸야지!"
...라고 생각했다면, 당신은 곧 디자이너와 싸우게 될 것입니다.
SafeArea를 쓰면 UI가 안전 영역 안으로 들어옵니다. 그 말은 즉, 안전 영역 밖(노치 주변, 홈 바 주변)은 아무것도 없는 빈 공간(흰색/검은색)이 된다는 뜻입니다.
만약 우리 앱의 헤더가 파란색(Colors.blue) 배경을 가지고 있다면?
SafeArea를 쓰면 파란색 헤더는 노치 아래부터 시작되고, 노치 부분은 그냥 시스템 기본색(흰색)으로 남습니다.
디자이너가 의도한 "화면 끝까지 꽉 찬 파란색 헤더"가 아니라, "머리가 하얗게 센 헤더"가 되어버립니다. 퀄리티가 급격히 떨어져 보입니다.
"배경색은 화면 끝까지 채우되, 글자(콘텐츠)만 안전 영역에 넣고 싶다." 이게 진짜 프로들의 요구사항입니다.
방법은 두 가지입니다.
SafeArea의 top/bottom 속성 제어Scaffold(
backgroundColor: Colors.blue, // 1. 전체 배경을 파란색으로
body: SafeArea(
top: false, // 2. 상단은 SafeArea 무시 (배경이 끝까지 참)
bottom: true, // 하단은 보호
child: Column(
children: [
// 3. 대신 헤더 안에 수동으로 패딩을 줘야 함... 귀찮음
Container(
height: 50 + MediaQuery.of(context).padding.top,
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
child: Text("헤더"),
),
],
),
),
)
이건 좀 복잡합니다. 더 쉬운 방법이 있습니다.
SafeArea로 감싸기배경색을 가진 컨테이너는 SafeArea 밖에 두고, 그 안의 내용물만 SafeArea로 감싸는 겁니다.
Stack(
children: [
// 1. 배경 (화면 전체)
Container(color: Colors.blue),
// 2. 콘텐츠 (안전 영역)
SafeArea(
child: Column(
children: [
Text("헤더 제목"), // 노치 아래에 예쁘게 위치
...
],
),
),
],
)
하지만 Scaffold 구조에서는 appBar를 쓰는 게 제일 깔끔합니다. Flutter의 AppBar는 내부적으로 알아서 상태 표시줄 높이만큼 패딩을 잡고, 배경색은 상태 표시줄까지 확장해줍니다.
요즘 앱들은 하단에 "꽉 찬 버튼(Full Width Button)"을 많이 씁니다.
이때 SafeArea를 무작정 적용하면, 버튼이 홈 인디케이터 위로 붕 떠서 안 예쁩니다.
디자이너: "버튼 배경색은 바닥까지 꽉 채우고, '저장' 글자만 홈 바 피해 주세요."
Container(
color: Colors.blue, // 버튼 배경색
child: SafeArea(
top: false, // 상단 무시
child: Container(
height: 60,
alignment: Alignment.center,
child: Text("저장"),
),
),
)
이렇게 하면:
Container의 파란색 배경은 홈 인디케이터 영역까지 덮습니다(심미성).SafeArea 덕분에 "저장" 글자는 홈 인디케이터 위로 올라옵니다(기능성).앱이 가로 모드(Landscape)를 지원한다면 상황이 더 복잡해집니다. 노치(Notch)가 왼쪽에 있을 수도, 오른쪽에 있을 수도 있기 때문입니다.
SafeArea의 left와 right 속성의 기본값은 true입니다.
가로 모드에서 이 속성들이 켜져 있으면, 화면 양옆에 검은 레터박스(Letterbox)가 생기는 것처럼 보일 수 있습니다.
배경 이미지를 쓰는 게임이나 미디어 앱이라면 left: false, right: false를 주고, 중요 버튼에만 패딩을 수동으로 계산해서 넣어야 할 수도 있습니다.
// 가로 모드에서 왼쪽 노치만 피하고 싶을 때 (수동 계산)
Padding(
padding: EdgeInsets.only(left: MediaQuery.of(context).padding.left),
child: ...
)
노치 뒤에 파란색 배경을 깔았는데, 상태 표시줄 아이콘(배터리, 시간)이 검은색이라서 안 보인다면?
AnnotatedRegion이나 AppBar의 systemOverlayStyle로 아이콘 색상을 흰색으로 바꿔줘야 합니다.
AppBar(
systemOverlayStyle: SystemUiOverlayStyle.light, // 아이콘을 흰색으로
backgroundColor: Colors.blue,
)
이건 SafeArea와 직접적인 관련은 없지만, 노치 디자인을 완성하는 화룡점정입니다.
CustomScrollView를 사용할 때는 일반 SafeArea를 쓰면 안 됩니다.
중간에 Sliver가 아닌 일반 위젯(SafeArea)이 끼어들면 에러가 나거나 스크롤 로직이 꼬일 수 있습니다.
이때는 SliverSafeArea를 사용해야 합니다.
CustomScrollView(
slivers: [
SliverSafeArea(
sliver: SliverList(
delegate: SliverChildBuilderDelegate(...),
),
),
],
)
특히 SliverAppBar와 함께 쓸 때 유용합니다. 노치 영역만큼 리스트를 자동으로 내려주면서도, 스크롤될 때 자연스럽게 말려 올라가는 효과를 유지할 수 있습니다.
SafeArea: 일단 감싸라. 콘텐츠가 잘리는 것보단 낫다.SafeArea 밖에 두고, 내부 텍스트만 SafeArea 안에 둬라.AppBar를 믿어라: 기본 AppBar는 이 모든 처리가 이미 되어 있다. 커스텀 헤더를 만들 때만 고생하면 된다.bottom: false: 리스트뷰처럼 스크롤 되는 콘텐츠는 하단 SafeArea를 꺼도 된다. 콘텐츠가 홈 바 뒤로 비치는 게 더 자연스러울 때가 있다(iOS 스타일).직사각형의 시대는 갔습니다. 이제 우리는 불규칙한 화면 모양과 공존하는 법을 배워야 합니다.