1. 🧩 위젯 추출: 단어쌍 표시 줄을 분리하기
현재 단어 쌍을 표시하는 코드는 Text(appState.current.asLowerCase) 입니다.
단순한 코드라도, UI가 복잡해질 가능성이 있다면 별도의 위젯으로 분리해 두는 것이 좋습니다.
Flutter에서는 각 UI의 논리적 단위별로 위젯을 나누는 것이 복잡성을 관리하는 핵심 전략 중 하나입니다.
✨ 위젯을 분리하는 일반적인 이유 3가지
- 📦 역할 분리
단어 표시만 전담하는 위젯으로 분리하면, 메인 화면 코드를 간결하게 유지할 수 있습니다. - 🎨 꾸미기 쉬움
별도 위젯 안에서 TextStyle, Card, Padding 등의 꾸밈 요소를 자유롭게 추가할 수 있습니다. - 🔁 재사용성 증가
동일한 스타일로 단어쌍을 표시하는 상황이 생겼을 때, 다른 화면에서도 손쉽게 재사용할 수 있습니다.
🧠 리팩토링 시 주의할 점
Flutter는 위젯 추출을 위한 자동 리팩터링 기능을 제공합니다.
하지만 추출 대상이 꼭 필요한 데이터만 참조하고 있는지 먼저 확인해야 합니다.
현재 코드는 appState.current에 접근하고 있지만, 실제로는 “현재 단어쌍”만 필요합니다.
따라서 WordPair 하나만 전달받는 구조로 위젯을 분리하는 것이 더 적절합니다.
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current; // ← 추가한 코드
return Scaffold(
body: Column(
children: [
Text('A random idea:'),
//Text(appState.current.asLowerCase),
Text(pair.asLowerCase), // ← 단어쌍 출력부분 수정
Text('추가된 텍스트'),
ElevatedButton(
onPressed: () {
appState.getNext(); //print('button pressed!');
},
child: Text('Next'),
),
],
),
);
}
}
Text 위젯이 더 이상 전체 appState를 참조하지 않습니다.
이제 Refactor 메뉴를 불러옵니다. VS Code에서는 두 가지 방법 중 하나로 이를 실행합니다.
- 리팩터링하려는 코드 부분(여기서는 Text)을 마우스 오른쪽 버튼으로 클릭하고 드롭다운 메뉴에서 Refactor...를 선택합니다.
- 리팩터링하려는 코드 부분(여기서는 Text)으로 커서를 이동하고 Ctrl+.(Win/Linux) 또는 Cmd+.(Mac)를 누릅니다.
Refactor 메뉴에서 Extract Widget을 선택합니다. 이름(예: BigCard)을 할당하고 Enter를 클릭합니다.
그러면 자동으로 Text(pair.asLowerCase)가 BigCard(pair: pair),로 바뀌고 현재 파일의 끝부분에 새 클래스 BigCard가 생성됩니다.
class BigCard extends StatelessWidget {
const BigCard({
super.key,
required this.pair,
});
final WordPair pair;
@override
Widget build(BuildContext context) {
return Text(pair.asLowerCase);
}
}
2. 카드 추가하기 – UI를 더 보기 좋게 만들기
앞서 분리한 BigCard 위젯을 활용해, 이제는 단어 쌍을 굵고 크게, 카드 형태로 예쁘게 표시해 보겠습니다.
이제 이 새 위젯을 이 섹션을 시작할 때 목표로 한 굵게 표시된 UI로 만들어 보겠습니다.
BigCard 클래스와 클래스 내에 있는 build() 메서드를 찾습니다. 이전처럼 Text 위젯에서 Refactor 메뉴를 불러옵니다. 하지만 이번에는 위젯을 추출하지 않습니다.
대신 Wrap with Padding을 선택합니다. 이렇게 하면 Text 위젯 주위에 Padding이라는 새 상위 위젯이 만들어집니다. 저장하면 임의의 단어 주위에 이미 공간이 많이 생긴 것을 확인할 수 있습니다.
return 부분의 코드가 바뀌었습니다.
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Text(pair.asLowerCase),
);
}
패딩을 기본값 8.0에서 늘립니다. 예를 들어 20 정도 값을 사용하여 패딩을 널찍하게 만듭니다.
참고: Flutter는 가능한 경우 상속 대신 컴포지션을 사용합니다. 여기서 패딩은 Text의 속성이 아니라 위젯입니다.
이렇게 하면 위젯은 위젯의 단일 책임에 집중할 수 있고 개발자는 UI를 완전히 자유롭게 구성할 수 있습니다. 예를 들어 Padding 위젯을 사용하여 텍스트나 이미지, 버튼, 자체 맞춤 위젯, 전체 앱에 패딩을 적용할 수 있습니다. 위젯은 래핑하는 항목에 관심이 없습니다.
이제 한 단계 올라갑니다. Padding 위젯에 커서를 놓고 Refactor 메뉴를 불러온 다음 Wrap with widget...을 선택합니다.
Padding이 widget으로 바뀌었습니다.
이렇게 하면 상위 위젯을 지정할 수 있습니다. widget 위치에 'Card'를 입력하고 Enter를 누릅니다.
이렇게 하면 Padding 위젯과 Text 위젯이 Card 위젯으로 래핑됩니다.
3. 테마와 스타일
카드를 좀 더 눈에 띄게 만들려면 좀 더 강렬한 색상으로 칠하세요. 항상 색 구성표는 일관되게 유지하는 것이 좋으므로 앱의 Theme을 사용하여 색상을 선택합니다.
BigCard의 build() 메서드를 다음과 같이 변경합니다.
@override
Widget build(BuildContext context) {
final theme = Theme.of(context); // ← 코드 추가
return Card(
color: theme.colorScheme.primary, // ← 코드 추가
child: Padding(
padding: const EdgeInsets.all(20),
child: Text(pair.asLowerCase),
),
);
}
- 먼저 코드는 Theme.of(context)로 앱의 현재 테마를 요청합니다.
- 그런 다음, 코드는 테마의 colorScheme 속성과 동일하도록 카드의 색상을 정의합니다. 색 구성표에는 여러 색상이 포함되어 있으며 primary가 앱을 정의하는 가장 두드러진 색상입니다.
카드가 이제 앱의 기본 색상으로 칠해졌습니다.
색 수정하려면 MyApp 클래스를 수정하세요
MyApp으로 스크롤하고 거기에서 ColorScheme의 시드 색상을 변경하여 이 색상과 전체 앱의 색 구성표를 변경할 수 있습니다.
도움말: Flutter의 Colors 클래스를 사용하면 Colors.deepOrange 또는 Colors.red 등 직접 선택한 색상의 팔레트에 쉽게 액세스할 수 있습니다.
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MyAppState(),
child: MaterialApp(
title: 'Namer App',
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
// 앱 기본색상
seedColor: const Color.fromARGB(160, 119, 214, 238)),
),
home: MyHomePage(),
),
);
}
}
4.🎨 테마 스타일 적용 – TextStyle
이제 BigCard 위젯 안의 텍스트를 더 보기 좋게 폰트 스타일을 적용해 보겠습니다.
Flutter에서는 전체 앱의 테마를 기반으로 일관된 스타일을 쉽게 적용할 수 있습니다.
텍스트 테마를 수정하기 위해 BigCard의 build() 메서드를 수정합니다.
class BigCard extends StatelessWidget {
const BigCard({
super.key,
required this.pair,
});
final WordPair pair;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
// ↓ 코드 추가
final style = theme.textTheme.displayMedium!.copyWith(
color: theme.colorScheme.onPrimary,
);
return Card(
color: theme.colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(20),
// child: Text(pair.asLowerCase),
// ↓ 코드 수정
child: Text(pair.asLowerCase, style: style),
),
);
}
}
테마 기반 폰트 스타일 적용
final theme = Theme.of(context);
final style = theme.textTheme.displayMedium!.copyWith(
color: theme.colorScheme.onPrimary,
);
코드 | 설명 |
Theme.of(context) | 앱 전체의 테마 정보에 접근 |
textTheme.displayMedium | 크고 강조된 텍스트 스타일 |
! (느낌표) | null 아님을 확신하는 연산자 |
copyWith(color: ...) | 기존 스타일에서 색상만 수정 |
colorScheme.onPrimary | 배경색(primary) 위에 잘 보이는 글자 색상 |
✅ 즉, displayMedium을 사용해 굵고 큼직한 폰트를 적용하고, 배경색에 어울리는 색으로 텍스트 컬러를 설정한 것입니다.
BigCard는 다음과 같은 역할을 수행합니다:
- 단어 쌍을 크고 읽기 쉽게 보여줍니다.
- 배경과 텍스트 색상이 조화를 이뤄 시각적으로 뛰어납니다.
- 카드형 레이아웃으로 더욱 깔끔한 UI를 제공합니다.
5. 🛠 카드 더 꾸며보기 & 👁🗨 접근성 향상하기
현재 BigCard 위젯은 심플하면서도 세련된 카드 UI를 보여줍니다.
이번에는 디자인을 더 개선하고, 동시에 접근성까지 챙기는 팁을 알려드릴게요.
1) 스타일 꾸미기
✅ 텍스트 스타일 더 바꾸기
현재는 글자 색상만 바꿨지만, copyWith()를 활용하면 글꼴 굵기, 자간, 높이, 정렬 등 다양한 속성도 커스터마이징할 수 있습니다.
final style = theme.textTheme.displayMedium!.copyWith(
color: theme.colorScheme.onPrimary,
fontWeight: FontWeight.bold,
letterSpacing: 2,
);
💡 Tip: copyWith() 괄호 안에서 Ctrl+Shift+Space 또는 Cmd+Shift+Space를 누르면 가능한 옵션 목록이 뜹니다!
✅ 카드에 그림자 효과 추가
return Card(
color: theme.colorScheme.primary,
elevation: 8, // ← 그림자 깊이
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), // ← 둥근 모서리
),
child: Padding(
padding: const EdgeInsets.all(20),
child: ...
),
);
🎨 카드 UI에 그림자와 곡선을 주면 더 입체적이고 자연스러운 느낌을 줄 수 있어요.
✅ 다양한 색상 실험해보기
theme.colorScheme에는 다음과 같은 색상이 있습니다:
속성 | 설명 |
primary | 메인 색상 |
secondary | 포인트 색상 |
surface | 카드, 시트, 배경용 |
background | 전체 배경 |
error | 오류 메시지 등 경고용 색상 |
각 색상에는 onXXX 색상도 제공됩니다. 예: onPrimary, onSurface 등 → 해당 배경에서 읽기 좋은 텍스트 색상입니다.
2) 접근성 개선 – 스크린 리더를 위한 배려
Flutter는 기본적으로 TalkBack(Android), VoiceOver(iOS)와 같은 스크린 리더를 지원합니다. 하지만 앱이 단어 쌍을 자동 생성할 경우, 발음 문제가 생길 수 있습니다.
📍 예시
- "cheaphead" → 중간의 ph를 f로 잘못 읽을 수 있음
- "cheap head"처럼 단어를 띄어쓰기하면 더 정확히 읽음
✅ 해결 방법: semanticsLabel 사용하기
속성 | 설명 |
asLowerCase | UI에 표시되는 문자열 (예: "cheaphead") |
semanticsLabel | 스크린 리더용 설명 텍스트 ("cheap head") |
📢 이렇게 하면 UI는 그대로 유지되면서도 스크린 리더가 올바른 발음을 제공하게 됩니다.
return Card(
color: theme.colorScheme.primary,
child: Padding(
padding: const EdgeInsets.all(20),
//child: Text(pair.asLowerCase, style: style),
// ↓ 코드 수정
child: Text(
pair.asLowerCase,
style: style,
semanticsLabel: "${pair.first} ${pair.second}",
),
),
6. UI 중앙 배치
세로 중앙 정렬
먼저 BigCard는 Column의 일부라는 점을 고려합니다. 기본적으로 열은 하위 요소를 상단으로 일괄 처리하지만 쉽게 재정의할 수 있습니다. MyHomePage의 build() 메서드로 가서 다음과 같이 변경합니다.
기본(세로) 축을 따라 Column 내 하위 요소가 중앙에 배치됩니다.
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var appState = context.watch<MyAppState>();
var pair = appState.current;
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center, // ← 세로 가운데 정렬
children: [
Text('A random idea:'),
BigCard(pair: pair),
Text('추가된 텍스트'),
ElevatedButton(
onPressed: () {
appState.getNext(); //print('button pressed!');
},
child: Text('Next'),
),
],
),
);
}
}
가로 중앙 정렬
하위 요소는 이미 열의 교차 축을 따라 중앙에 배치되어 있습니다(즉, 이미 가로로 중앙에 배치되어 있음). 그러나 Column 자체는 Scaffold 내에서 중앙에 배치되어 있지 않습니다. Widget Inspector를 사용하여 이를 확인할 수 있습니다.
select 손모양 클릭
앱 화면 클릭하면 범위가 보입니다.
Widget Inspector 자체는 이 Codelab의 범위에 포함되지 않지만 Column이 강조 표시될 때 앱의 전체 너비를 차지하지 않는 것을 확인할 수 있습니다. 하위 요소에 필요한 가로 공간만큼만 차지합니다.
열 자체만 중앙에 배치하면 됩니다. Column 위에 커서를 두고 Refactor 메뉴를 불러와(Ctrl+. 또는 Cmd+. 사용) Wrap with Center를 선택합니다.
body: Column에서 body: Ceneter로 바뀌었습니다.
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center, // ← 세로 가운데 정렬
children: [
Text('A random idea:'),
BigCard(pair: pair), // ← 단어쌍 출력부분 수정
Text('추가된 텍스트'),
ElevatedButton(
onPressed: () {
appState.getNext(); //print('button pressed!');
},
child: Text('Next'),
),
],
),
),
);
가로도 중앙으로 이동했습니다.
+
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BigCard(pair: pair),
SizedBox(height: 10), // ← 두 위젯 사이에 공간 띄우기기
ElevatedButton(
onPressed: () {
appState.getNext(); //print('button pressed!');
},
child: Text('Next'),
),
],
),
),
);
'Flutter + Dart > Flutter + Dart 공부' 카테고리의 다른 글
UI 수정 - 즐겨찾기 기능, 아이콘, 레이블, 화면 전환 (0) | 2025.05.07 |
---|---|
day1 Flutter 실습: 버튼으로 리스트뷰 정렬 및 새로고침 구현하기 (0) | 2025.05.07 |
프로젝트 만들기 (1) | 2025.05.06 |
Flutter 위젯 기본 이해 (1) | 2025.05.05 |
Windows에서 Flutter 개발환경 구축하기 (0) | 2025.05.03 |