๋ฐ์ํ ๋ ์ด์์ - MediaQuery, LayoutBuilder
์ง๋ ๊ธ์์ ํ
๋ง๋ฅผ ๋ค๋ค์ผ๋, ์์ ฏ๊ณผ ๋ ์ด์์ ์นดํ
๊ณ ๋ฆฌ์ ๋ง์ง๋ง ์ฃผ์ ๋ ํ๋ฉด ํฌ๊ธฐ๋ค. ํฐ๋ง๋ค ๊ฐ๋ก ํญ์ด ๋ค๋ฅด๊ณ ํ๋ธ๋ฆฟ๋ ์์ผ๋, ๋ด ํฐ์์ ์์๊ฒ ๋์จ ํ๋ฉด์ด ๋ค๋ฅธ ๊ธฐ๊ธฐ์์ ๊นจ์ง๋ ์ผ์ด ์๊ธด๋ค. ์นด๋ ๋ ์ฅ์ ๊ฐ๋ก๋ก ๋๋ํ ๋๋๋ค๊ณ ํด๋ณด์. ๋ด ํฐ์์ ๋ณด๊ธฐ ์ข๊ฒ ํญ์ 300์ผ๋ก ๋ง์ท๋ค.
Row(
children: [
Container(width: 300, height: 100, color: Colors.blue),
Container(width: 300, height: 100, color: Colors.green),
],
)๊ฐ๋ก ํญ์ด 360์ธ ์์ ํฐ์์ ์คํํ๋ฉด ๋ ์ด์์ ์์ ฏ ๊ธ์์ ๋ณธ ์๋ฌ๊ฐ ๊ทธ๋๋ก ๋์จ๋ค.
A RenderFlex overflowed by 240 pixels on the right.๋ฐ๋๋ก ํ๋ธ๋ฆฟ์์ ๋ณด๋ฉด ํ๋ฉด ํญ์ 1000์ด ๋๋๋ฐ ์นด๋ ๋ ์ฅ์ด ์ผ์ชฝ์ ์๊ฒ ๋ชฐ๋ ค ์๋ค. ํญ์ ์ซ์๋ก ๋ฐ์๋๋ฉด ์ด๋ค ํ๋ฉด์์๋ ๊ฐ์ ํฌ๊ธฐ๋ผ์, ์์ ํ๋ฉด์์๋ ๋์น๊ณ ํฐ ํ๋ฉด์์๋ ํํด์ง๋ค.
์์ธ: ํ๋ฉด ํฌ๊ธฐ๋ฅผ ๋ชจ๋ฅด๋ ์ฑ๋ก ํฌ๊ธฐ๋ฅผ ์ ํ๋ค
Flutter ๋ ์ด์์์ "์ ์ฝ์ ์๋๋ก, ํฌ๊ธฐ๋ ์๋ก" ํ๋ฅธ๋ค๊ณ ํ๋ค. ์์์ ๋ด๋ ค์ค๋ ์ ์ฝ(์ธ ์ ์๋ ํญ)์ ๊ธฐ๊ธฐ๋ง๋ค ๋ค๋ฅธ๋ฐ, ๋ด๊ฐ 300์ด๋ผ๋ ๊ณ ์ ๊ฐ์ผ๋ก ๋๋ตํด๋ฒ๋ฆฌ๋ฉด ๊ทธ ์ ์ฝ์ ๋ฌด์ํ๋ ์
์ด๋ค. ํด๊ฒฐ ๋ฐฉ๋ฒ์ ๋๋ก ๋๋๋ค. ๊ณ ์ ํฌ๊ธฐ ๋์ ๋จ๋ ๊ณต๊ฐ์ ๋ง์ถ๊ฑฐ๋, ํ๋ฉด ํฌ๊ธฐ๋ฅผ ์ฝ์ด์ ๋ ์ด์์ ์์ฒด๋ฅผ ๋ฐ๊พธ๊ฑฐ๋. ์์ ๊ฒ์ ์ด๋ฏธ Expanded๋ก ํด๋ดค๊ณ , ์ด๋ฒ ๊ธ์ ๋ค์ ๊ฒ์ด๋ค.
ํด๊ฒฐ 1: MediaQuery๋ก ํ๋ฉด ํฌ๊ธฐ ์ฝ๊ธฐ
class CardsPage extends StatelessWidget {
const CardsPage({super.key});
@override
Widget build(BuildContext context) {
final width = MediaQuery.sizeOf(context).width; // ํ๋ฉด ๊ฐ๋ก ํญ
final isWide = width >= 600;
final cards = [
const InfoCard(color: Colors.blue),
const InfoCard(color: Colors.green),
];
if (isWide) {
// ๋์ผ๋ฉด ๊ฐ๋ก๋ก ๋๋ํ, ๋จ๋ ํญ์ ๋ฐ๋ฐ์ฉ
return Row(
children: cards.map((c) => Expanded(child: c)).toList(),
);
}
// ์ข์ผ๋ฉด ์ธ๋ก๋ก ์๊ธฐ
return Column(children: cards);
}
}
class InfoCard extends StatelessWidget {
final Color color;
const InfoCard({super.key, required this.color});
@override
Widget build(BuildContext context) {
return Container(height: 100, margin: const EdgeInsets.all(8), color: color);
}
}MediaQuery.sizeOf(context)๋ ์ง๊ธ ํ๋ฉด์ ํฌ๊ธฐ๋ฅผ ๋๋ ค์ค๋ค. ํฐ์ ํญ์ด 600๋ณด๋ค ์์ผ๋ Column์ผ๋ก ์ธ๋ก๋ก ์์ด๊ณ , ํ๋ธ๋ฆฟ์ Row๋ก ๋๋ํ ๋์ธ๋ค. ๋์น๋ ์๋ฌ๋ ์ฌ๋ผ์ง๊ณ , ๊ณ ์ ํญ 300 ๋์ Expanded๊ฐ ๋จ๋ ๊ณต๊ฐ์ ๋๋ ๊ฐ์ง๋ค.
cards.map(...).toList()๋ ์ปฌ๋ ์
๊ธ์์ ๋ณธ ๊ทธ ํจํด์ด๋ค. map์ Iterable์ ๋๋ ค์ฃผ๊ธฐ ๋๋ฌธ์ children์ ๋ฃ์ผ๋ ค๋ฉด .toList()๊ฐ ํ์ํ๋ค.
MediaQuery์์ ์ฝ์ ์ ์๋ ๊ฑด ํฌ๊ธฐ๋ง์ด ์๋๋ค.
final size = MediaQuery.sizeOf(context); // ํ๋ฉด ํฌ๊ธฐ
final orientation = MediaQuery.orientationOf(context); // ์ธ๋ก/๊ฐ๋ก ๋ฐฉํฅ
final padding = MediaQuery.paddingOf(context); // ๋
ธ์น, ์ํ๋ฐ ๋ฑ ๊ฐ๋ ค์ง๋ ์์ญ
final bottom = MediaQuery.viewInsetsOf(context).bottom; // ํค๋ณด๋ ๋์ด์์ ์ฝ๋์๋ MediaQuery.of(context).size๊ฐ ๋ง์ด ๋ณด์ด๋๋ฐ, ๊ฐ์ ๊ฐ์ ๋๋ ค์ค๋ค. ๋ค๋ง of๋ ํค๋ณด๋๊ฐ ์ฌ๋ผ์ค๋ ๊ฒ์ฒ๋ผ MediaQuery์ ์ด๋ค ๊ฐ์ด ๋ฐ๋์ด๋ ์์ ฏ์ ๋ค์ ๊ทธ๋ฆฌ๊ณ , sizeOf๋ ํฌ๊ธฐ๊ฐ ๋ฐ๋ ๋๋ง ๋ค์ ๊ทธ๋ฆฐ๋ค. ์ ์ฝ๋์์๋ sizeOf, orientationOf ๊ฐ์ ์ ์ฉ ๋ฉ์๋๊ฐ ๋ซ๋ค.
MediaQuery์ ํ๊ณ: "ํ๋ฉด" ํฌ๊ธฐ์ด์ง "๋ด๊ฐ ์ธ ์ ์๋ ๊ณต๊ฐ"์ด ์๋๋ค
ํ๋ฉด ์ผ์ชฝ์ ํญ 300์ง๋ฆฌ ์ฌ์ด๋ ํจ๋์ ๋๊ณ , ๊ทธ ์์์ MediaQuery๋ก ํญ์ ์ฝ์ด๋ณด์.
Row(
children: [
SizedBox(
width: 300,
child: SidePanel(), // ์ด ์์์ MediaQuery.sizeOf(context).width๋ฅผ ์ฝ์ผ๋ฉด?
),
const Expanded(child: MainContent()),
],
)SidePanel ์์์ MediaQuery๋ก ํญ์ ์ฝ์ผ๋ฉด 300์ด ์๋๋ผ ๊ธฐ๊ธฐ ์ ์ฒด ํ๋ฉด ํญ(์: 1000)์ด ๋์จ๋ค. MediaQuery๋ ์์ ฏ์ด ์ด๋์ ๋์๋์ง์ ์๊ด์์ด ํญ์ ํ๋ฉด ์ ์ฒด๋ฅผ ๊ธฐ์ค์ผ๋ก ํ๊ธฐ ๋๋ฌธ์ด๋ค. ๊ทธ๋์ ํจ๋ ์์์ "ํญ์ด 600 ์ด์์ด๋ฉด ๊ฐ๋ก๋ก ๋ฐฐ์น"๋ผ๊ณ ํ๋จํ๋ฉด, ์ค์ ๋ก๋ 300๋ฐ์ ์ ๋๋ ๊ณต๊ฐ์ ๊ฐ๋ก ๋ฐฐ์น๋ฅผ ํด์ ๋ ๋์น๋ค.
ํด๊ฒฐ 2: LayoutBuilder๋ก ๋ถ๋ชจ๊ฐ ์ค ๊ณต๊ฐ ์ฝ๊ธฐ
LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth >= 600) {
return Row(children: [...]); // ๋์ ๋ฐฐ์น
}
return Column(children: [...]); // ์ข์ ๋ฐฐ์น
},
)LayoutBuilder์ constraints๋ ๋ถ๋ชจ๊ฐ ์ด ์์ ฏ์๊ฒ ๋ด๋ ค์ค ์ ์ฝ์ด๋ค. "์ ์ฝ์ ์๋๋ก ๋ด๋ ค๊ฐ๋ค"๊ฐ ๋์ ๋ณด์ด๋ ๊ฐ์ผ๋ก ๋์ค๋ ๊ฒ์ด๋ค. ์ฌ์ด๋ ํจ๋ ์์์ ์ฐ๋ฉด constraints.maxWidth๋ 300์ด๊ณ , ํ๋ฉด์ด ์๋ฌด๋ฆฌ ์ปค๋ ํจ๋ ์์ ํ๋จ์ ํจ๋์ ์ค์ ํญ์ ๊ธฐ์ค์ผ๋ก ํ๋ค. ๊ฐ์ ์์ ฏ์ ํฐ ์ ์ฒด ํ๋ฉด์๋, ํ๋ธ๋ฆฟ์ ์์ ํจ๋์๋ ์ฌ์ฌ์ฉํ ์ ์๋ค.
builder๋ ๋ฆฌ์คํธ ๊ธ์ itemBuilder์ฒ๋ผ "์ด๋ ๊ฒ ๊ทธ๋ ค๋ผ"๋ฅผ ์๋ ค์ฃผ๋ ์ฝ๋ฐฑ์ด๋ค. ์ ์ฝ์ ๋ ์ด์์ ๋จ๊ณ์ ๊ฐ์์ผ ์ ์ ์์ด์, ์์ ฏ์ ๋ฏธ๋ฆฌ ๋ง๋ค์ด ๋์ง ์๊ณ ์ ์ฝ์ด ์ ํด์ง ๋ค์ ํธ์ถ๋๋ ํจ์๋ก ๋๊ธฐ๋ ๊ตฌ์กฐ๋ค.
MediaQuery์ LayoutBuilder, ์ธ์ ๋ญ ์ฐ๋
MediaQuery: ๊ธฐ์ค์ ๊ธฐ๊ธฐ ํ๋ฉด ์ ์ฒด๋ค. ํ๋ฉด ํฌ๊ธฐ, ๋ฐฉํฅ, ๋ ธ์น, ํค๋ณด๋ ๋์ด๋ฅผ ์ ์ ์๊ณ , ์ฑ ์ ์ฒด ๋ ์ด์์ ๋ถ๊ธฐ๋ ์์ ์์ญ, ํค๋ณด๋ ์ฒ๋ฆฌ์ ์๋ง๋ค.LayoutBuilder: ๊ธฐ์ค์ ๋ถ๋ชจ๊ฐ ๋ด๊ฒ ์ค ๊ณต๊ฐ์ด๋ค.constraints์ ์ต์ยท์ต๋ ํญ๊ณผ ๋์ด๋ฅผ ์ ์ ์๊ณ , ์ฌ์ฌ์ฉ ์์ ฏ ์์์์ ๋ถ๊ธฐ์ ์๋ง๋ค.
ํ๋ฉด ์ ์ฒด ์ ๋ณด(๋ฐฉํฅ, ํค๋ณด๋, ๋
ธ์น)๋ MediaQuery, "๋ํํ
์ฃผ์ด์ง ๊ณต๊ฐ"์ ๋ฐ๋ฅธ ๋ถ๊ธฐ๋ LayoutBuilder๋ผ๊ณ ๊ธฐ์ตํ๋ฉด ๋๋ค.
๋ถ๊ธฐ ๊ธฐ์ค: ํญ ๊ตฌ๊ฐ์ผ๋ก ๋๋๊ธฐ
์ซ์๋ฅผ ์๋ฌด ๊ฐ์ด๋ ์ฐ๊ธฐ๋ณด๋ค Material ๋์์ธ์ด ๊ถํ๋ ๊ตฌ๊ฐ์ ๋ฐ๋ผ๊ฐ๋ฉด ํธํ๋ค.
- compact: ํญ 600 ๋ฏธ๋ง. ํฐ(์ธ๋ก)
- medium: ํญ 600 ์ด์ 840 ๋ฏธ๋ง. ์์ ํ๋ธ๋ฆฟ, ํฐ(๊ฐ๋ก)
- expanded: ํญ 840 ์ด์. ํฐ ํ๋ธ๋ฆฟ, ๋ฐ์คํฌํฑ
LayoutBuilder(
builder: (context, constraints) {
final w = constraints.maxWidth;
if (w >= 840) return const ThreeColumnLayout();
if (w >= 600) return const TwoColumnLayout();
return const OneColumnLayout();
},
)์ฑ ์ ์ฒด์์ ๊ฐ์ ๊ธฐ์ค์ ์ฐ๋๋ก 600, 840์ ์์๋ก ํ ๊ณณ์ ๋นผ๋๋ ํธ์ด ๋ซ๋ค.
๋ถ๊ธฐ ์์ด ํด๊ฒฐ๋๋ ๊ฒฝ์ฐ๋ ๋ง๋ค
ํ๋ฉด ํฌ๊ธฐ๋ณ๋ก ๋ ์ด์์์ ๋ฐ๋ก ๋ง๋๋ ๊ฑด ํ์ด ๋ง์ด ๋ ๋ค. ๋ค์ ๋๊ตฌ๋ก ํด๊ฒฐ๋๋ฉด ๋ถ๊ธฐ๋ฅผ ์ฐ์ง ์๋๋ค.
Expanded/Flexible: ๋จ๋ ๊ณต๊ฐ์ ๋น์จ๋ก ๋๋๋ค (๋ ์ด์์ ์์ ฏ ๊ธ)GridView์maxCrossAxisExtent: ์นธ ๋๋น ๊ธฐ์ค์ผ๋ก ์นธ ์๊ฐ ์์์ ๋ฐ๋๋ค (์คํฌ๋กค๊ณผ ๋ฆฌ์คํธ ๊ธ)SafeArea: ๋ ธ์น์ ์ํ๋ฐ์ ๊ฐ๋ ค์ง๋ ์์ญ์ ํผํ๋คConstrainedBox: ๋์ ํ๋ฉด์์ ๋ด์ฉ์ด ๋์์ด ํผ์ง์ง ์๊ฒ ์ต๋ ํญ์ ์ ํ๋ค
Scaffold(
body: SafeArea( // ๋
ธ์น, ์ํ๋ฐ, ํ๋จ ์ ์ค์ฒ ๋ฐ๋ฅผ ํผํด์ ๊ทธ๋ฆผ
child: content,
),
)Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600), // ์๋ฌด๋ฆฌ ๋์ด๋ 600๊น์ง๋ง
child: const SignupForm(),
),
)ํ๋ธ๋ฆฟ์์ ์ ๋ ฅ ํผ์ด ํ๋ฉด ์ ์ฒด๋ก ๋์ด๋๋ฉด ๋ณด๊ธฐ ์ด์ํ๋ฐ, ์ด๋ ๊ฒ ์ต๋ ํญ๋ง ์ ํด์ค๋ ๊ฐ์ด๋ฐ์ ์ ๋นํ ํฌ๊ธฐ๋ก ๋์ธ๋ค. ์ ๋ฌธ ๋จ๊ณ์์๋ ์ด ์ ๋๋ง์ผ๋ก๋ ๋๋ถ๋ถ์ ํ๋ฉด์ด ๋ฌด๋ํ๊ฒ ํด๊ฒฐ๋๋ค.
Compose, SwiftUI์ ๋น๊ตํ๋ฉด
// Compose
BoxWithConstraints {
if (maxWidth >= 600.dp) {
Row { ... }
} else {
Column { ... }
}
}// SwiftUI
GeometryReader { geo in
if geo.size.width >= 600 {
HStack { ... }
} else {
VStack { ... }
}
}ํญ๋ชฉ๋ณ๋ก ๋๋ํ ๋์ผ๋ฉด ์ด๋ ๋ค.
- ๋ถ๋ชจ๊ฐ ์ค ๊ณต๊ฐ์ผ๋ก ๋ถ๊ธฐ: Flutter
LayoutBuilder/ ComposeBoxWithConstraints/ SwiftUIGeometryReader - ํ๋ฉด ์ ์ฒด ์ ๋ณด: Flutter
MediaQuery/ ComposeLocalConfiguration/ SwiftUIUIScreen๋ฑ - ํญ ๊ตฌ๊ฐ ๊ฐ๋
: Flutter๋ ์ง์
600,840์ผ๋ก ๋ถ๊ธฐ / ComposeWindowSizeClass/ SwiftUIhorizontalSizeClass - ์์ ์์ญ: Flutter
SafeArea/ ComposeWindowInsets/ SwiftUIsafeAreaInset(๊ธฐ๋ณธ ์ ์ฉ)
Compose์ BoxWithConstraints์ Flutter์ LayoutBuilder๊ฐ ๊ฐ์ฅ ๋น์ทํ๋ค. ๋ ๋ค "๋ถ๋ชจ๊ฐ ์ค ๊ณต๊ฐ"์ ์๋ ค์ฃผ๋ ๋๊ตฌ๋ค. Compose๋ WindowSizeClass, SwiftUI๋ horizontalSizeClass๋ก ๊ตฌ๊ฐ์ ์์คํ
์ด ๋๋ ์ฃผ์ง๋ง, Flutter๋ 600, 840์ ๋ด๊ฐ ์ง์ ๋น๊ตํ๋ค๋ ์ ์ด ๋ค๋ฅด๋ค.
์ ๋ฆฌ
- ์์ ฏ ํฌ๊ธฐ๋ฅผ ์ซ์๋ก ๋ฐ์๋๋ฉด ์์ ํ๋ฉด์์๋ ๋์น๊ณ ํฐ ํ๋ฉด์์๋ ํํด์ง๋ฏ๋ก, ๋จ๋ ๊ณต๊ฐ์ ๋ง์ถ๊ฑฐ๋ ํ๋ฉด ํฌ๊ธฐ์ ๋ฐ๋ผ ๋ ์ด์์์ ๋ฐ๊พผ๋ค
MediaQuery.sizeOf(context)๋ ๊ธฐ๊ธฐ ํ๋ฉด ์ ์ฒด ํฌ๊ธฐ๋ฅผ ๋๋ ค์ฃผ๊ณ , ๋ฐฉํฅ, ๋ ธ์น, ํค๋ณด๋ ๋์ด๋ ์ฝ์ ์ ์๋คMediaQuery๋ ์์ ฏ์ด ์ด๋์ ๋์๋์ง์ ์๊ด์์ด ํ๋ฉด ์ ์ฒด ๊ธฐ์ค์ด๋ผ์, ํจ๋์ฒ๋ผ ์ผ๋ถ ๊ณต๊ฐ ์์์๋LayoutBuilder์constraints.maxWidth๋ก ํ๋จํด์ผ ํ๋ค- ๋ถ๊ธฐ ๊ธฐ์ค์ 600, 840 ๊ฐ์ ํญ ๊ตฌ๊ฐ์ผ๋ก ์ ํ๊ณ , ๋ถ๊ธฐ ์ ์
Expanded,maxCrossAxisExtent,SafeArea,ConstrainedBox๋ก ํด๊ฒฐ๋๋์ง ๋จผ์ ๋ณธ๋ค
๋ค์ ๊ธ๋ถํฐ๋ ์์ ฏ ์นดํ
๊ณ ๋ฆฌ๋ฅผ ๋์ด ์ํ ๊ด๋ฆฌ๋ฅผ ๋ค๋ฃฌ๋ค. setState๋ก๋ ๊ฐ๋นํ๊ธฐ ์ด๋ ค์์ง๋ ์ง์ ๊ณผ, ๊ทธ ํด๊ฒฐ์ฑ
์ธ InheritedWidget๊ณผ Provider๋ถํฐ ๋ณด๊ฒ ๋ค.
