플러터(Flutter) 애플리케이션에서 사용자 인터페이스(UI)를 구축할 때, PopupMenuButton
은 간결하고 효과적인 상호작용을 제공하는 핵심 위젯 중 하나입니다. 주로 앱 바(AppBar)의 액션 영역이나 특정 위젯 옆에 배치되어, 사용자에게 추가적인 옵션 목록을 제시하는 역할을 합니다. 하지만 플러터의 기본 Material Design 테마가 제공하는 PopupMenuButton
은 직각 모서리를 가지고 있어, 현대적이고 부드러운 디자인 트렌드와는 다소 거리가 있을 수 있습니다. 앱의 전반적인 디자인 언어가 둥근 모서리, 즉 '라운딩'을 강조한다면, 이 작은 메뉴 창 하나가 전체적인 통일성을 해치는 요소가 될 수 있습니다.
다행히도 플러터는 위젯의 시각적 요소를 매우 유연하게 제어할 수 있는 강력한 기능을 제공합니다. PopupMenuButton
역시 예외는 아닙니다. 개발자는 shape
속성을 활용하여 메뉴의 형태를 자유롭게 변경할 수 있으며, 이를 통해 앱의 디자인 정체성에 완벽하게 부합하는 맞춤형 팝업 메뉴를 구현할 수 있습니다. 이 글에서는 PopupMenuButton
의 모서리를 둥글게 만드는 기본적인 방법부터 시작하여, 테두리 추가, 특정 모서리만 둥글게 처리하는 고급 기법, 그리고 실제 앱에 적용할 수 있는 종합적인 예제까지 심도 있게 다룰 것입니다. 이를 통해 단순히 기능을 구현하는 것을 넘어, 사용자의 시선을 사로잡는 세련되고 일관된 UI를 완성하는 방법을 배우게 될 것입니다.
핵심 속성 이해하기: `shape`와 `ShapeBorder`
PopupMenuButton
의 외형을 결정하는 가장 중요한 속성은 바로 shape
입니다. 이 속성은 위젯의 윤곽과 경계를 정의하는 역할을 하며, ShapeBorder
라는 추상 클래스 타입을 값으로 받습니다. ShapeBorder
는 플러터에서 위젯의 형태를 정의하기 위한 청사진과 같습니다. PopupMenuButton
뿐만 아니라 Card
, Dialog
, FloatingActionButton
등 다양한 Material 위젯들이 이 shape
속성을 공유하므로, 한번 개념을 익혀두면 여러 곳에서 유용하게 활용할 수 있습니다.
ShapeBorder
를 상속받는 여러 구체적인 클래스 중, 둥근 사각형을 만드는 데 가장 널리 사용되는 것이 바로 RoundedRectangleBorder
입니다. 이 클래스는 팝업 메뉴의 모서리 반경(radius)과 테두리(side)를 세밀하게 제어할 수 있는 속성들을 제공합니다.
RoundedRectangleBorder
의 주요 속성
-
borderRadius
: 모서리의 둥근 정도를 결정합니다.BorderRadius
객체를 값으로 받으며, 모든 모서리를 동일하게 둥글게 하거나 특정 모서리만 선택적으로 둥글게 만들 수 있습니다.BorderRadius.all(Radius.circular(value))
: 모든 네 모서리를 동일한 반경 값으로 설정합니다. 가장 일반적으로 사용되는 방식입니다.BorderRadius.circular(value)
:BorderRadius.all
의 단축형입니다.BorderRadius.only(topLeft: Radius.circular(value), ... )
:topLeft
,topRight
,bottomLeft
,bottomRight
중 원하는 모서리의 반경을 개별적으로 지정할 수 있습니다.BorderRadius.vertical(top: Radius.circular(value), ... )
: 위쪽 두 모서리(top
)나 아래쪽 두 모_서리(bottom
)의 반경을 한 번에 지정합니다.
-
side
: 메뉴의 테두리를 정의합니다.BorderSide
객체를 값으로 받으며, 테두리의 색상(color
), 두께(width
), 스타일(style
) 등을 설정할 수 있습니다. 기본값은BorderSide.none
으로, 테두리가 없는 상태입니다.
이 두 가지 속성을 조합하면 단순한 둥근 모서리를 넘어, 테두리가 있는 둥근 메뉴, 특정 부분만 둥글게 처리된 독특한 형태의 메뉴 등 다채로운 디자인을 구현할 수 있습니다.
기본적인 둥근 모서리 `PopupMenuButton` 구현하기
먼저 가장 기본적인 형태, 즉 모든 모서리가 동일한 곡률을 가지는 PopupMenuButton
을 만들어 보겠습니다. 이는 앱 디자인의 일관성을 유지하기 위한 가장 첫걸음입니다.
구현을 위해 PopupMenuButton
위젯 내부에 shape
속성을 추가하고, 값으로 RoundedRectangleBorder
를 지정합니다. 그리고 RoundedRectangleBorder
의 borderRadius
속성에 BorderRadius.circular()
를 사용하여 원하는 반경 값을 설정하면 됩니다.
예제 코드: 모서리 반경 15.0 설정
아래 코드는 모서리 반경을 15.0으로 설정한 PopupMenuButton
의 기본적인 예시입니다. itemBuilder
는 팝업 메뉴가 열릴 때 표시될 항목들을 동적으로 생성하는 역할을 합니다. 각 메뉴 항목은 PopupMenuItem
위젯으로 정의됩니다.
import 'package:flutter/material.dart';
class RoundedPopupMenuExample extends StatelessWidget {
const RoundedPopupMenuExample({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Rounded PopupMenuButton'),
actions: [
PopupMenuButton<String>(
// 이 부분이 팝업 메뉴의 모양을 결정합니다.
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(15.0),
),
),
onSelected: (String value) {
// 메뉴 항목 선택 시 실행될 로직
print('Selected: $value');
},
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
const PopupMenuItem<String>(
value: 'Profile',
child: Text('Profile'),
),
const PopupMenuItem<String>(
value: 'Settings',
child: Text('Settings'),
),
const PopupMenuDivider(), // 구분선
const PopupMenuItem<String>(
value: 'Logout',
child: Text('Logout'),
),
],
// 아이콘을 버튼으로 사용
icon: Icon(Icons.more_vert),
),
],
),
body: Center(
child: Text('Click the three-dot icon in the AppBar.'),
),
);
}
}
위 코드를 실행하면 앱 바 오른쪽에 세로 점 3개 아이콘이 나타납니다. 이 아이콘을 클릭하면, 날카로운 직각 모서리 대신 부드러운 곡선의 모서리를 가진 팝업 메뉴가 나타나는 것을 확인할 수 있습니다. Radius.circular(15.0)
의 숫자 값을 조절하여 둥근 정도를 쉽게 변경할 수 있습니다.
고급 커스터마이징 기법
기본적인 둥근 모서리 구현에 익숙해졌다면, 이제 더 나아가 다양한 디자인 요구사항을 만족시키는 고급 커스터마이징 기법들을 살펴보겠습니다.
1. 특정 모서리만 둥글게 만들기
때로는 팝업 메뉴가 화면의 특정 가장자리에 붙어 있거나, 다른 UI 요소와 연결되는 듯한 느낌을 주어야 할 때가 있습니다. 이런 경우, BorderRadius.only()
를 사용하면 특정 모서리의 반경만 개별적으로 제어할 수 있습니다.
예를 들어, 화면 하단에 위치한 버튼에서 팝업 메뉴가 위로 열리는 경우, 메뉴의 아래쪽 두 모서리(bottomLeft
, bottomRight
)만 둥글게 처리하여 버튼과 자연스럽게 연결되는 디자인을 만들 수 있습니다.
PopupMenuButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20.0),
topRight: Radius.circular(20.0),
bottomLeft: Radius.circular(5.0),
bottomRight: Radius.circular(5.0),
),
),
// ... itemBuilder, onSelected 등 나머지 속성
)
2. 테두리(Border) 추가하기
메뉴의 경계를 명확히 하거나 디자인에 포인트를 주고 싶을 때, RoundedRectangleBorder
의 side
속성을 사용하여 테두리를 추가할 수 있습니다. BorderSide
객체를 통해 테두리의 색상, 두께 등을 자유롭게 설정할 수 있습니다.
아래 예제는 회색의 1픽셀 두께 테두리를 가진 둥근 팝업 메뉴를 만듭니다.
PopupMenuButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(12.0)),
side: BorderSide(color: Colors.grey.shade300, width: 1),
),
// ... itemBuilder, onSelected 등 나머지 속성
)
이를 통해 팝업 메뉴가 배경과 시각적으로 더 잘 분리되는 효과를 얻을 수 있으며, 앱의 브랜드 컬러를 테두리에 적용하여 일관성을 높일 수도 있습니다.
3. 색상, 그림자, 위치 조정하기
PopupMenuButton
은 모양 외에도 배경색, 그림자(elevation), 화면상 위치 등을 커스터마이징할 수 있는 여러 속성을 제공합니다. 이들을 shape
속성과 함께 사용하면 더욱 완성도 높은 결과물을 만들 수 있습니다.
color
: 팝업 메뉴의 배경색을 지정합니다. 기본적으로는 테마의 팝업 메뉴 색상을 따르지만, 이 속성을 통해 원하는 색상으로 변경할 수 있습니다.elevation
: 팝업 메뉴가 떠 있는 듯한 효과를 주는 그림자의 깊이를 조절합니다. 값이 클수록 그림자가 더 짙고 넓게 퍼집니다. 값을 0으로 설정하면 그림자가 사라집니다.offset
: 팝업 메뉴가 나타나는 위치를 미세하게 조정합니다.Offset(dx, dy)
객체를 값으로 받으며,dx
는 수평 이동,dy
는 수직 이동 거리를 나타냅니다. 예를 들어Offset(0, 50)
은 메뉴를 버튼 아래로 50픽셀만큼 이동시킵니다.
PopupMenuButton(
color: Colors.lightBlue.shade50, // 연한 파란색 배경
elevation: 8.0, // 그림자 강조
offset: Offset(0, 40), // 버튼 아래 40픽셀 지점에서 메뉴 시작
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
),
// ... itemBuilder, onSelected 등 나머지 속성
)
4. 메뉴 아이템(`PopupMenuItem`) 커스터마이징
팝업 메뉴의 컨테이너뿐만 아니라, 그 안에 들어가는 각 항목(`PopupMenuItem`) 또한 커스터마이징이 가능합니다. PopupMenuItem
의 child
속성에는 Text
위젯뿐만 아니라 Row
, ListTile
등 어떤 위젯이든 들어갈 수 있습니다. 이를 활용하여 아이콘과 텍스트가 함께 있는 메뉴 항목을 만들 수 있습니다.
itemBuilder: (context) => [
PopupMenuItem(
value: 'share',
child: Row(
children: [
Icon(Icons.share, color: Colors.blue),
SizedBox(width: 10),
Text('Share'),
],
),
),
PopupMenuItem(
value: 'edit',
child: ListTile(
leading: Icon(Icons.edit, color: Colors.green),
title: Text('Edit'),
contentPadding: EdgeInsets.zero, // ListTile의 기본 패딩 제거
),
),
PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete, color: Colors.red),
SizedBox(width: 10),
Text('Delete', style: TextStyle(color: Colors.red)),
],
),
),
],
이렇게 메뉴 아이템을 커스터마이징하면 사용자에게 각 옵션의 기능을 시각적으로 더 명확하게 전달할 수 있으며, 전체적인 UI의 미적 수준을 한 단계 높일 수 있습니다.
종합 실전 예제: 모든 기법을 통합한 커스텀 팝업 메뉴
지금까지 배운 모든 기법을 하나로 모아, 실제 애플리케이션에 바로 적용할 수 있는 종합적인 예제를 만들어 보겠습니다. 이 예제는 다음과 같은 특징을 가집니다.
- 둥근 모서리와 미세한 테두리 적용
- 커스텀 배경색과 그림자 효과
- 메뉴 위치 미세 조정
- 아이콘과 텍스트로 구성된 커스텀 메뉴 아이템
- 메뉴 아이템 그룹화를 위한 구분선 사용
- 선택된 항목에 따라
SnackBar
를 표시하는 로직 포함
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Advanced PopupMenu Demo',
theme: ThemeData(
primarySwatch: Colors.deepPurple,
scaffoldBackgroundColor: Colors.grey.shade100,
),
home: const AdvancedPopupMenuScreen(),
);
}
}
class AdvancedPopupMenuScreen extends StatelessWidget {
const AdvancedPopupMenuScreen({Key? key}) : super(key: key);
void _onMenuItemSelected(BuildContext context, String value) {
// SnackBar를 표시하여 사용자에게 피드백을 줍니다.
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('You selected: $value'),
duration: const Duration(seconds: 1),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Advanced PopupMenu'),
actions: [
PopupMenuButton<String>(
// 1. 모양 커스터마이징 (둥근 모서리 + 테두리)
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
side: BorderSide(color: Colors.deepPurple.shade100, width: 1.5),
),
// 2. 색상 및 그림자
color: Colors.white,
elevation: 10,
// 3. 위치 조정
offset: const Offset(0, 55),
// 4. 아이콘 커스터마이징
icon: const Icon(Icons.palette_outlined, size: 28),
// 5. 선택 핸들러
onSelected: (value) => _onMenuItemSelected(context, value),
// 6. 커스텀 메뉴 아이템 빌더
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
_buildPopupMenuItem(
value: 'Copy',
icon: Icons.copy_outlined,
text: 'Copy',
color: Colors.blueAccent,
),
_buildPopupMenuItem(
value: 'Paste',
icon: Icons.paste_outlined,
text: 'Paste',
color: Colors.green,
),
const PopupMenuDivider(height: 1.0), // 구분선
_buildPopupMenuItem(
value: 'Archive',
icon: Icons.archive_outlined,
text: 'Archive',
color: Colors.orange,
),
PopupMenuItem<String>(
value: 'Delete',
child: Row(
children: const [
Icon(Icons.delete_forever_outlined, color: Colors.redAccent),
SizedBox(width: 12),
Text('Delete', style: TextStyle(color: Colors.redAccent, fontWeight: FontWeight.bold)),
],
),
),
],
),
const SizedBox(width: 10), // 오른쪽 여백
],
),
body: const Center(
child: Padding(
padding: EdgeInsets.all(20.0),
child: Text(
'Click the palette icon in the AppBar to see the customized popup menu.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18, color: Colors.black54),
),
),
),
);
}
// 반복되는 PopupMenuItem 코드를 위한 헬퍼 메서드
PopupMenuItem<String> _buildPopupMenuItem({
required String value,
required IconData icon,
required String text,
Color? color,
}) {
return PopupMenuItem<String>(
value: value,
child: Row(
children: [
Icon(icon, color: color ?? Colors.black54),
const SizedBox(width: 12),
Text(text),
],
),
);
}
}
위 예제 코드는 단순한 둥근 모서리 적용을 넘어, shape
, color
, elevation
, offset
, 커스텀 child
등 다양한 속성을 종합적으로 활용하여 매우 세련되고 기능적인 팝업 메뉴를 구현했습니다. 또한, 반복되는 UI 코드를 _buildPopupMenuItem
헬퍼 메서드로 추출하여 코드의 가독성과 재사용성을 높였습니다. 이러한 접근 방식은 복잡한 UI를 구축할 때 매우 유용합니다.
마치며
플러터에서 PopupMenuButton
의 모서리를 둥글게 만드는 것은 shape
속성과 RoundedRectangleBorder
를 사용하는 간단한 작업으로 시작됩니다. 하지만 여기서 멈추지 않고 테두리, 색상, 그림자, 위치, 그리고 내부 아이템 구성까지 세밀하게 제어함으로써, 우리는 앱의 전체적인 디자인 품질을 크게 향상시키고 사용자에게 일관되고 만족스러운 경험을 제공할 수 있습니다.
UI 디자인에서 '디테일이 전부'라는 말이 있습니다. PopupMenuButton
과 같은 작은 컴포넌트 하나하나에 주의를 기울이는 것이 결국 사용자가 앱에 대해 느끼는 전반적인 인상을 결정합니다. 오늘 살펴본 다양한 커스터마이징 기법들을 적극적으로 활용하여, 여러분의 플러터 애플리케이션을 기능적으로 뛰어날 뿐만 아니라 시각적으로도 매력적인 작품으로 만들어 보시길 바랍니다.
0 개의 댓글:
Post a Comment