Expanded와 Flexible로 공간 분배하기
웹사이트를 만들 때 “이 박스가 남은 공간을 모두 차지했으면 좋겠는데…” 하고 생각해본 적 있나요? Flitter의 Expanded
와 Flexible
위젯이 바로 그 문제를 해결해줍니다.
🎯 학습 목표
이 튜토리얼을 완료하면 다음을 할 수 있습니다:
- Expanded로 남은 공간을 균등하게 나누기
- flex 속성으로 공간 비율 조정하기
- Flexible과 Expanded의 차이점 이해하기
- 실용적인 레이아웃 패턴 만들기
- FlexFit 속성의 동작 원리 이해하기
🤔 공간 분배가 왜 중요할까요?
보통 Row나 Column에 위젯들을 넣으면, 각 위젯은 자신이 필요한 만큼의 공간만 차지합니다. 하지만 현대적인 UI에서는 화면 크기에 맞춰 유연하게 공간을 활용해야 합니다.
실제 사용 사례:
- 네비게이션 바에서 로고는 왼쪽에, 메뉴는 오른쪽에 배치
- 카드 레이아웃에서 여러 박스가 균등한 크기로 분배
- 진행률 표시줄에서 완료된 부분과 남은 부분 표시
- 모바일 앱의 탭 바에서 각 탭이 동일한 너비로 분배
- 대시보드에서 사이드바와 메인 컨텐츠 영역 비율 조정
🏗️ Expanded 위젯 완전 정복
Expanded란?
Expanded
는 Row, Column, Flex 위젯의 자식 위젯이 사용 가능한 공간을 모두 채우도록 강제하는 위젯입니다.
기본 속성
Expanded({
flex: 1, // 공간 비율 (기본값: 1)
child: Widget // 자식 위젯 (필수)
})
1. 기본적인 Expanded 사용
Row({
children: [
Container({
width: 100,
height: 50,
color: '#FF6B6B',
child: Text("고정 크기")
}),
Expanded({
child: Container({
height: 50,
color: '#4ECDC4',
child: Text("남은 공간 모두 차지")
})
})
]
})
결과: 첫 번째 박스는 100px 너비를 가지고, 두 번째 박스는 남은 공간을 모두 차지합니다.
2. 여러 개의 Expanded로 균등 분배
Row({
children: [
Expanded({
child: Container({
height: 100,
color: '#FF6B6B',
child: Text("1/3")
})
}),
Expanded({
child: Container({
height: 100,
color: '#4ECDC4',
child: Text("1/3")
})
}),
Expanded({
child: Container({
height: 100,
color: '#45B7D1',
child: Text("1/3")
})
})
]
})
결과: 3개의 박스가 화면 너비를 균등하게 1/3씩 나누어 차지합니다.
🎨 실습: 균등하게 나누어진 3개 박스 만들기
위의 시작 코드에서 3개의 박스가 고정 크기(width: 100
)로 되어있습니다. 이를 Expanded
를 사용해서 균등하게 공간을 나누도록 수정해보세요.
단계별 힌트:
Expanded
위젯을 import에 추가- 각 Container를 Expanded로 감싸기
- Container에서 width 속성 제거하기 (Expanded가 자동으로 처리)
- height는 그대로 유지
⚖️ flex 속성으로 비율 조정하기
flex
속성을 사용하면 각 Expanded 위젯이 차지할 공간의 비율을 정할 수 있습니다.
flex 비율 계산 원리
Row({
children: [
Expanded({
flex: 1, // 1 / (1 + 2 + 3) = 1/6
child: Container({
height: 100,
color: '#FF6B6B',
child: Text("1")
})
}),
Expanded({
flex: 2, // 2 / (1 + 2 + 3) = 2/6 = 1/3
child: Container({
height: 100,
color: '#4ECDC4',
child: Text("2")
})
}),
Expanded({
flex: 3, // 3 / (1 + 2 + 3) = 3/6 = 1/2
child: Container({
height: 100,
color: '#45B7D1',
child: Text("3")
})
})
]
})
계산 과정:
- 전체 flex 합계: 1 + 2 + 3 = 6
- 첫 번째 박스: 1/6 (약 16.7%)
- 두 번째 박스: 2/6 = 1/3 (약 33.3%)
- 세 번째 박스: 3/6 = 1/2 (50%)
결과: 세 번째 박스가 가장 크고, 첫 번째 박스가 가장 작습니다.
실용적인 비율 예제
// 사이드바(1/4)와 메인 컨텐츠(3/4) 레이아웃
Row({
children: [
Expanded({
flex: 1, // 25%
child: Container({
color: '#F7FAFC',
child: Text("사이드바")
})
}),
Expanded({
flex: 3, // 75%
child: Container({
color: '#EDF2F7',
child: Text("메인 컨텐츠")
})
})
]
})
🔄 Flexible 위젯 심화 이해
Flexible이란?
Flexible
은 Expanded
보다 유연한 공간 분배를 제공합니다. 자식 위젯이 실제로 필요한 공간만큼만 차지할 수 있도록 합니다.
Flexible 속성
Flexible({
flex: 1, // 공간 비율 (기본값: 1)
fit: FlexFit.loose, // 공간 사용 방식 (기본값: loose)
child: Widget // 자식 위젯 (필수)
})
FlexFit 옵션
FlexFit.loose
: 필요한 만큼만 공간 사용 (기본값)FlexFit.tight
: 할당된 공간을 모두 사용 (Expanded와 동일)
Expanded vs Flexible 비교
특성 | Expanded | Flexible |
---|---|---|
기본 동작 | 할당된 공간을 모두 차지 | 필요한 만큼만 차지 |
FlexFit | 항상 tight | loose (기본값) 또는 tight |
사용 목적 | 공간을 완전히 채우고 싶을 때 | 유연한 크기 조정이 필요할 때 |
관계 | Flexible의 특수한 경우 | 더 일반적인 위젯 |
Flexible 사용 예제
Row({
children: [
Flexible({
child: Container({
height: 50,
color: '#FF6B6B',
child: Text("짧은 텍스트") // 텍스트 크기만큼만 공간 차지
})
}),
Flexible({
child: Container({
height: 50,
color: '#4ECDC4',
child: Text("이것은 매우 긴 텍스트입니다") // 텍스트 크기만큼 공간 차지
})
}),
Container({
width: 100,
height: 50,
color: '#45B7D1',
child: Text("고정 크기")
})
]
})
결과: 텍스트가 짧은 첫 번째 박스는 작은 공간을 차지하고, 긴 텍스트를 가진 두 번째 박스는 더 많은 공간을 차지합니다.
🏆 실전 예제 모음
1. 모던 네비게이션 바
import { StatelessWidget, Container, Row, Text, Expanded, EdgeInsets, TextStyle } from "@meursyphus/flitter";
class ModernNavigationBar extends StatelessWidget {
build(context) {
return Container({
height: 64,
color: '#1A202C',
padding: EdgeInsets.symmetric({ horizontal: 24, vertical: 12 }),
child: Row({
children: [
// 로고 영역
Container({
padding: EdgeInsets.only({ right: 16 }),
child: Text("MyApp", {
style: new TextStyle({
color: 'white',
fontSize: 20,
fontWeight: 'bold'
})
})
}),
// 중간 빈 공간
Expanded({
child: Container()
}),
// 메뉴 영역
Row({
children: [
Text("홈", { style: { color: 'white', marginRight: 24 } }),
Text("제품", { style: { color: 'white', marginRight: 24 } }),
Text("회사소개", { style: { color: 'white', marginRight: 24 } }),
Text("연락처", { style: { color: 'white' } })
]
})
]
})
});
}
}
export default function ModernNavigationBar(props) {
return new _ModernNavigationBar(props);
}
2. 인터랙티브 진행률 표시기
import { StatefulWidget, State, Container, Column, Row, Text, Expanded, GestureDetector, EdgeInsets, BoxDecoration, BorderRadius, TextStyle, Center } from "@meursyphus/flitter";
class InteractiveProgressBar extends StatefulWidget {
constructor({ initialProgress = 0.3, ...props } = {}) {
super(props);
this.initialProgress = initialProgress;
}
createState() {
return new _InteractiveProgressBarState();
}
}
class _InteractiveProgressBarState extends State {
progress = 0.3;
initState() {
super.initState();
this.progress = this.widget.initialProgress;
}
increaseProgress() {
this.setState(() => {
this.progress = Math.min(1.0, this.progress + 0.1);
});
}
decreaseProgress() {
this.setState(() => {
this.progress = Math.max(0.0, this.progress - 0.1);
});
}
build(context) {
return Column({
children: [
// 진행률 바
Container({
height: 24,
margin: EdgeInsets.only({ bottom: 16 }),
decoration: new BoxDecoration({
color: '#E2E8F0',
borderRadius: BorderRadius.circular(12)
}),
child: Row({
children: [
Expanded({
flex: Math.round(this.progress * 100),
child: Container({
decoration: new BoxDecoration({
color: '#4299E1',
borderRadius: BorderRadius.circular(12)
})
})
}),
Expanded({
flex: Math.round((1 - this.progress) * 100),
child: Container()
})
]
})
}),
// 진행률 텍스트
Text(`진행률: ${Math.round(this.progress * 100)}%`, {
style: new TextStyle({ fontSize: 16, fontWeight: 'bold' })
}),
// 컨트롤 버튼
Row({
children: [
Expanded({
child: GestureDetector({
onClick: () => this.decreaseProgress(),
child: Container({
height: 40,
margin: EdgeInsets.only({ right: 8 }),
decoration: new BoxDecoration({
color: '#E53E3E',
borderRadius: BorderRadius.circular(4)
}),
child: Center({ child: Text("감소", { style: new TextStyle({ color: 'white' }) }) })
})
})
}),
Expanded({
child: GestureDetector({
onClick: () => this.increaseProgress(),
child: Container({
height: 40,
margin: EdgeInsets.only({ left: 8 }),
decoration: new BoxDecoration({
color: '#38A169',
borderRadius: BorderRadius.circular(4)
}),
child: Center({ child: Text("증가", { style: new TextStyle({ color: 'white' }) }) })
})
})
})
]
})
]
});
}
}
export default function InteractiveProgressBar(props) {
return new _InteractiveProgressBar(props);
}
3. 반응형 카드 그리드
import { StatelessWidget, Container, Row, Text, Column, Expanded, EdgeInsets, BoxDecoration, BorderRadius, BoxShadow, TextStyle, CrossAxisAlignment } from "@meursyphus/flitter";
class ResponsiveCardGrid extends StatelessWidget {
constructor({ cards = [], ...props } = {}) {
super(props);
this.cards = cards;
}
build(context) {
return Row({
children: this.cards.map((card, index) => (
Expanded({
child: Container({
height: 120,
margin: EdgeInsets.only({
left: index > 0 ? 8 : 0,
right: index < this.cards.length - 1 ? 8 : 0
}),
decoration: new BoxDecoration({
color: card.color,
borderRadius: BorderRadius.circular(8),
boxShadow: [new BoxShadow({
color: 'rgba(0, 0, 0, 0.1)',
offset: { dx: 0, dy: 2 },
blurRadius: 4
})]
}),
padding: EdgeInsets.all(16),
child: Column({
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(card.title, {
style: new TextStyle({
fontSize: 16,
fontWeight: 'bold',
color: 'white',
marginBottom: 8
}
}),
Text(card.description, {
style: new TextStyle({
fontSize: 14,
color: 'rgba(255, 255, 255, 0.8)'
})
})
]
})
})
})
))
});
}
}
export default function ResponsiveCardGrid(props) {
return new _ResponsiveCardGrid(props);
}
// 사용 예제
const cardData = [
{
title: "매출",
description: "이번 달 매출 현황",
color: '#4299E1'
},
{
title: "주문",
description: "신규 주문 처리",
color: '#48BB78'
},
{
title: "고객",
description: "고객 만족도 조사",
color: '#ED8936'
}
];
const widget = Container({
padding: EdgeInsets.all(16),
child: ResponsiveCardGrid({ cards: cardData })
});
🚀 고급 활용 패턴
1. 중첩된 Expanded 사용
Column({
children: [
Container({
height: 60,
color: '#2D3748',
child: Text("헤더")
}),
Expanded({
child: Row({
children: [
Container({
width: 200,
color: '#4A5568',
child: Text("사이드바")
}),
Expanded({
child: Container({
color: '#F7FAFC',
child: Text("메인 컨텐츠")
})
})
]
})
}),
Container({
height: 40,
color: '#718096',
child: Text("푸터")
})
]
})
2. Spacer 위젯 활용
Row({
children: [
Text("왼쪽 컨텐츠"),
Spacer(), // Expanded({ child: Container() })와 동일
Text("오른쪽 컨텐츠")
]
})
// 여러 개의 Spacer로 균등 분배
Row({
children: [
Text("A"),
Spacer(),
Text("B"),
Spacer(),
Text("C")
]
})
📝 연습 문제
연습 1: 탭 바 만들기
4개의 탭이 균등하게 배치된 탭 바를 만들어보세요.
// TODO: 4개의 탭을 균등하게 배치하는 탭 바를 만들어보세요
const tabNames = ["홈", "검색", "알림", "프로필"];
// 힌트: Row와 Expanded를 사용하세요
const tabBar = Row({
children: [
// 여기에 코드를 작성하세요
]
});
연습 2: 대시보드 레이아웃
왼쪽 사이드바(1/4), 메인 컨텐츠(1/2), 오른쪽 패널(1/4)로 구성된 3단 레이아웃을 만들어보세요.
// TODO: 1:2:1 비율의 3단 레이아웃을 만들어보세요
const dashboardLayout = Row({
children: [
// 여기에 코드를 작성하세요
]
});
연습 3: 동적 진행률 바
버튼을 클릭할 때마다 진행률이 20%씩 증가하는 진행률 바를 만들어보세요.
// TODO: StatefulWidget을 사용해서 동적 진행률 바를 만들어보세요
class DynamicProgressBar extends StatefulWidget {
createState() {
return new _DynamicProgressBarState();
}
}
class _DynamicProgressBarState extends State {
// 여기에 코드를 작성하세요
}
🐛 흔한 실수와 해결법
❌ 실수 1: Row/Column 밖에서 Expanded 사용
// 에러 발생!
Container({
child: Expanded({
child: Text("Hello")
})
})
✅ 올바른 방법:
// Expanded는 반드시 Flex 위젯(Row, Column, Flex) 안에서 사용
Row({
children: [
Expanded({
child: Text("Hello")
})
]
})
❌ 실수 2: Expanded 안에 무한 크기 설정
// 에러 발생!
Row({
children: [
Expanded({
child: Container({ width: double.infinity })
})
]
})
✅ 올바른 방법:
// Expanded가 이미 최대 크기를 제공하므로 width 지정 불필요
Row({
children: [
Expanded({
child: Container() // width는 자동으로 결정됨
})
]
})
❌ 실수 3: Column에서 height 무한대 시도
// 에러 발생!
Column({
children: [
Container({ height: double.infinity })
]
})
✅ 올바른 방법:
// Column에서 세로 공간을 모두 차지하려면 Expanded 사용
Column({
children: [
Expanded({
child: Container()
})
]
})
❌ 실수 4: flex 값을 0으로 설정
// 잘못된 사용
Expanded({
flex: 0, // 0은 의미가 없음
child: Container()
})
✅ 올바른 방법:
// flex는 1 이상의 양수 사용
Expanded({
flex: 1, // 기본값 사용 또는 적절한 비율 설정
child: Container()
})
🎊 완성된 결과
이 튜토리얼을 완료하면:
- 기본 구현: 3개의 박스가 화면 너비를 균등하게 나누어 차지
- 반응형 동작: 창 크기를 바꿔도 비율이 유지됨
- 시각적 구분: 각 박스는 서로 다른 색상으로 구분됨
- 비율 제어: flex 속성으로 원하는 비율로 조정 가능
🔥 추가 도전 과제
1. 반응형 그리드 시스템 만들기
화면 크기에 따라 열 개수가 달라지는 그리드 시스템을 만들어보세요.
2. 애니메이션 진행률 바
시간에 따라 자동으로 진행률이 증가하는 애니메이션 진행률 바를 만들어보세요.
3. 복잡한 대시보드 레이아웃
헤더, 사이드바, 메인 컨텐츠, 우측 패널, 푸터를 포함한 완전한 대시보드를 만들어보세요.
4. 터치 인터랙션 추가
드래그로 각 영역의 크기를 조정할 수 있는 인터랙티브 레이아웃을 만들어보세요.
🎯 다음 단계
다음 튜토리얼에서는 **Padding과 여백 관리**를 배워보겠습니다. 시각적으로 깔끔하고 전문적인 레이아웃을 만드는 핵심 기술을 익혀보세요!