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