0. 페이지를 구현하기에 앞서
- 페이지 내의 요소들을 모조리 한 곳에 몰아서 적으면 유지보수와 가독성이 떨어짐
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: RecipePage(), // 메소드안에 페이지를 구현해 놓고 갈아끼우는 방식
);
}
}
class RecipePage extends StatelessWidget {
const RecipePage({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold();
}
}
1. 뼈대 생성하기
1. 앱 제작시 뼈대를 먼저 만들기
- 컴포넌트 제작 후 조정 시 무너질 가능성 증가
- 또한 위젯마다 정렬방식이 달라 무너질 경우도 있음. 아래의 예시의 차이>> Column과 ListView
// 페이지
class RecipePage extends StatelessWidget {
const RecipePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar:_appbar(),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20), // 여백을 주는 메소드
child: Column( // Column의 기본 정렬은 센터 >> 밑의 Placeholder가 3개가 되면서 칸이 모자라서 오류 >> ListView 칸이 모자라면 스크롤 + 기본 정렬 start, 기본 스크롤 세로
children: [
Text("Recipes"),
Row(
children: [
// Container >> html의 div와 비슷한 역할
Container(
width: 60,
height: 80,
color: Colors.red,
),
],
),
Placeholder(),
Placeholder(),
Placeholder(),
],
),
),
);
}
// 길어지니까 메소드 추출을 이용해 빼내기
AppBar _appbar() {
return AppBar(
leading: Text("리딩 영역"),
title: Text("타이틀 영역"),
// appbar의 액션 영역에 아이콘 넣기
actions: [
Icon(Icons.search), // 돋보기 아이콘
SizedBox(width: 16), // 여백용
Icon(CupertinoIcons.heart, color: Colors.redAccent), // 하트 아이콘 (CupertinoIcons >> 자동으로 들어가 있는 아이콘 외부 라이브러리)
SizedBox(width: 16),
],
);
}

// 페이지
class RecipePage extends StatelessWidget {
const RecipePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar:_appbar(),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20), // 여백을 주는 메소드
child: ListView( // Column의 기본 정렬은 센터 >> 밑의 Placeholder가 3개가 되면서 칸이 모자라서 오류 >> ListView 칸이 모자라면 스크롤 + 기본 정렬 start, 기본 스크롤 세로
children: [
Text("Recipes"),
Row(
children: [
// Container >> html의 div와 비슷한 역할
Container(
width: 60,
height: 80,
color: Colors.red,
),
],
),
Placeholder(),
Placeholder(),
Placeholder(),
],
),
),
);
}
// 길어지니까 메소드 추출을 이용해 빼내기
AppBar _appbar() {
return AppBar(
leading: Text("리딩 영역"),
title: Text("타이틀 영역"),
// appbar의 액션 영역에 아이콘 넣기
actions: [
Icon(Icons.search), // 돋보기 아이콘
SizedBox(width: 16), // 여백용
Icon(CupertinoIcons.heart, color: Colors.redAccent), // 하트 아이콘 (CupertinoIcons >> 자동으로 들어가 있는 아이콘 외부 라이브러리)
SizedBox(width: 16),
],
);
}
}

2. 앱 바 생성
1. 앱바의 기본 구조

2. 앱 바 구성
AppBar _appbar() {
return AppBar(
leading: Text("리딩 영역"),
title: Text("타이틀 영역"),
// appbar의 액션 영역에 아이콘 넣기
actions: [
Icon(Icons.search),
// 돋보기 아이콘
SizedBox(width: 16),
// 여백용
Icon(CupertinoIcons.heart, color: Colors.redAccent),
// 하트 아이콘 (CupertinoIcons >> 자동으로 들어가 있는 아이콘 외부 라이브러리)
SizedBox(width: 16),
],
);
}
}
3. 내부 카테고리 위젯 생성(appbar 생략)
1. 내부에서 하나 생성
// 페이지
class RecipePage extends StatelessWidget {
const RecipePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _appBar(), // AppBar 메서드 호출
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20), // 좌우 여백 20
child: ListView(
// ListView: 스크롤 가능 + 기본 정렬은 위에서 아래로
children: [
Text("Recipes"), // 제목 텍스트
Row(
children: [
// 컨테이너를 통해 하나 생성
Container(
width: 60,
height: 80,
decoration: BoxDecoration(
border: Border.all(color: Colors.black12), // 테두리 색상
borderRadius: BorderRadius.circular(30), // 모서리 둥글게
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center, // 세로 가운데 정렬
children: [
Icon(
mIcon, // 아이콘 변수
color: Colors.redAccent, // 아이콘 색상
size: 30, // 아이콘 크기
),
SizedBox(height: 3), // 간격
Text(
"${mText}", // 텍스트
style: TextStyle(color: Colors.black87), // 텍스트 스타일
),
],
),
),
],
),
Placeholder(), // 플레이스홀더 1
Placeholder(), // 플레이스홀더 2
Placeholder(), // 플레이스홀더 3
],
),
),
);
}
}
2. 반복되는 부분 위젯화하여 반복 줄이기
// 페이지
class RecipePage extends StatelessWidget {
const RecipePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar:_appbar(),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20), // 여백을 주는 메소드
child: ListView( // Column의 기본 정렬은 센터 >> 밑의 Placeholder가 3개가 되면서 칸이 모자라서 오류 >> ListView 칸이 모자라면 스크롤 + 기본 정렬 start, 기본 스크롤 세로
children: [
Text("Recipes"),
Row(
children: [
MenuItem(mIcon: Icons.food_bank, mText: "ALL"),
SizedBox(width: 25), // 여백
MenuItem(mIcon: Icons.emoji_food_beverage, mText: "Coffee"),
SizedBox(width: 25),
MenuItem(mIcon: Icons.fastfood, mText: "Burger"),
SizedBox(width: 25),
MenuItem(mIcon: Icons.local_pizza, mText: "Pizza"),
],
),
Placeholder(),
Placeholder(),
Placeholder(),
],
),
),
);
}
class MenuItem extends StatelessWidget {
// 필드 생성
final mIcon;
final mText;
// 생성자 >> 헷갈리지 않게 필드명이 뜨도록 {}안에 넣기 + 반드시 받도록 required 붙이기
MenuItem({required this.mIcon, required this.mText});
@override
Widget build(BuildContext context) {
// Container >> html의 div와 비슷한 역할
return Container(
width: 60,
height: 80,
decoration: BoxDecoration(
border: Border.all(color: Colors.black12), // 테두리만 주기
borderRadius: BorderRadius.circular(30),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center, // 세로 방향 가운데 정렬
children: [
Icon(mIcon, color: Colors.redAccent, size: 30),
SizedBox(height: 3),
// "${}" 이렇게 하면 null이 나오거나 문자열이 너무 길면 빈칸 처리
Text("${mText}", style: TextStyle(color: Colors.black87))
],
),
);
}
}
3. 카테고리들 하나로 뭉치기
// 페이지
class RecipePage extends StatelessWidget {
const RecipePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _appbar(),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20), // 여백을 주는 메소드
child: ListView(
// Column의 기본 정렬은 센터 >> 밑의 Placeholder가 3개가 되면서 칸이 모자라서 오류 >> ListView 칸이 모자라면 스크롤 + 기본 정렬 start, 기본 스크롤 세로
children: [
Title(),
Menu(),
],
),
),
);
}
}
class Menu extends StatelessWidget {
const Menu({
super.key,
});
@override
Widget build(BuildContext context) {
return Row(
children: [
MenuItem(mIcon: Icons.food_bank, mText: "ALL"),
SizedBox(width: 25),
MenuItem(mIcon: Icons.emoji_food_beverage, mText: "Coffee"),
SizedBox(width: 25),
MenuItem(mIcon: Icons.fastfood, mText: "Burger"),
SizedBox(width: 25),
MenuItem(mIcon: Icons.local_pizza, mText: "Pizza"),
],
);
}
}
class MenuItem extends StatelessWidget {
final mIcon;
final mText;
MenuItem({required this.mIcon, required this.mText});
@override
Widget build(BuildContext context) {
return Container(
width: 60,
height: 80,
decoration: BoxDecoration(
border: Border.all(color: Colors.black12),
borderRadius: BorderRadius.circular(30),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(mIcon, color: Colors.redAccent, size: 30),
SizedBox(height: 3),
Text("${mText}", style: TextStyle(color: Colors.black87))
],
),
);
}
}
4. 내부 리스트 위젯 생성
1. 코드 (위와 중복되는 위젯 구성 부분은 생략)
// 페이지
class RecipePage extends StatelessWidget {
const RecipePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _appbar(),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
children: [
Title(),
Menu(),
// 동일하게 내부에서 하나만 생성후 위젯화 한 다음 생성자 파라미터만 넣어 반복 생성
RecipeItem(myText: "Burger"),
RecipeItem(myText: "Coffee"),
RecipeItem(myText: "Pizza"),
],
),
),
);
}
}
class RecipeItem extends StatelessWidget {
// 클래스 속성
final myText; // 텍스트를 받아오는 변수
// 생성자
RecipeItem({required this.myText}); // 생성자에서 myText 값을 필수로 전달받음
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start, // 왼쪽 정렬
children: [
SizedBox(height: 30), // 위쪽 간격 추가
AspectRatio(
aspectRatio: 2 / 1, // 가로:세로 비율 설정 (2:1)
child: ClipRRect(
borderRadius: BorderRadius.circular(20), // 둥근 모서리 설정
child: Image.asset(
"assets/${myText}.jpeg", // assets 폴더에서 이미지 파일을 불러옴
fit: BoxFit.cover, // 이미지가 영역을 꽉 채우도록 설정
),
),
),
SizedBox(height: 10), // 이미지와 텍스트 사이의 간격
Text(
"Made ${myText}", // 제목 텍스트
style: TextStyle(fontSize: 20), // 텍스트 크기 설정
),
Text(
// 설명 텍스트
"Have you ever made your own ${myText}? Once you've tried a homemade ${myText}, you'll never go back.",
style: TextStyle(color: Colors.grey, fontSize: 12), // 회색 텍스트, 작은 글자 크기
),
],
);
}
}
5. 최종 코드와 화면
1. 최종 코드
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
void main() {
runApp(const MyApp());
}
// 전체 앱 관리
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false, // 디버그 태그 사라짐
home: RecipePage(), // 메소드안에 페이지를 구현해 놓고 갈아끼우는 방식
theme:
ThemeData(textTheme: GoogleFonts.patuaOneTextTheme()), // 앱 전체 글꼴 설정
);
}
}
// 페이지
class RecipePage extends StatelessWidget {
const RecipePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _appbar(),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20), // 여백을 주는 메소드
child: ListView(
// Column의 기본 정렬은 센터 >> 밑의 Placeholder가 3개가 되면서 칸이 모자라서 오류 >> ListView 칸이 모자라면 스크롤 + 기본 정렬 start, 기본 스크롤 세로
children: [
Title(),
Menu(),
RecipeItem(myText: "Burger"),
RecipeItem(myText: "Coffee"),
RecipeItem(myText: "Pizza"),
],
),
),
);
}
// 길어지니까 메소드 추출을 이용해 빼내기
AppBar _appbar() {
return AppBar(
leading: Text("리딩 영역"),
title: Text("타이틀 영역"),
// appbar의 액션 영역에 아이콘 넣기
actions: [
Icon(Icons.search),
// 돋보기 아이콘
SizedBox(width: 16),
// 여백용
Icon(CupertinoIcons.heart, color: Colors.redAccent),
// 하트 아이콘 (CupertinoIcons >> 자동으로 들어가 있는 아이콘 외부 라이브러리)
SizedBox(width: 16),
],
);
}
}
class RecipeItem extends StatelessWidget {
final myText;
RecipeItem({required this.myText});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 30),
AspectRatio(
aspectRatio: 2 / 1,
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Image.asset(
"assets/${myText}.jpeg",
fit: BoxFit.cover,
),
),
),
SizedBox(height: 10),
Text("Made ${myText}", style: TextStyle(fontSize: 20)),
Text(
"Have you ever made your own ${myText}? Once you've tried a homemade ${myText}, you'll never go back.",
style: TextStyle(color: Colors.grey, fontSize: 12),
),
],
);
}
}
class Menu extends StatelessWidget {
const Menu({
super.key,
});
@override
Widget build(BuildContext context) {
return Row(
children: [
// 컴포넌트 생성 반복
MenuItem(mIcon: Icons.food_bank, mText: "ALL"),
SizedBox(width: 25), // 여백
MenuItem(mIcon: Icons.emoji_food_beverage, mText: "Coffee"),
SizedBox(width: 25),
MenuItem(mIcon: Icons.fastfood, mText: "Burger"),
SizedBox(width: 25),
MenuItem(mIcon: Icons.local_pizza, mText: "Pizza"),
],
);
}
}
class Title extends StatelessWidget {
const Title({
super.key,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 20),
child: Text(
"Recipes",
style: TextStyle(fontSize: 30),
),
);
}
}
class MenuItem extends StatelessWidget {
// 필드 생성
final mIcon;
final mText;
// 생성자 >> 헷갈리지 않게 필드명이 뜨도록 {}안에 넣기 + 반드시 받도록 required 붙이기
MenuItem({required this.mIcon, required this.mText});
@override
Widget build(BuildContext context) {
// Container >> html의 div와 비슷한 역할
return Container(
width: 60,
height: 80,
decoration: BoxDecoration(
border: Border.all(color: Colors.black12), // 테두리만 주기
borderRadius: BorderRadius.circular(30),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center, // 세로 방향 가운데 정렬
children: [
Icon(mIcon, color: Colors.redAccent, size: 30),
SizedBox(height: 3),
// "${}" 이렇게 하면 null이 나오거나 문자열이 너무 길면 빈칸 처리
Text("${mText}", style: TextStyle(color: Colors.black87))
],
),
);
}
}
2. 화면

6. 추가 정보 정리
1. 메소드 빼내기
컨트롤 + 알트 + M

2. 라이브러리 추가하기 (폰트 라이브러리 추가)
- 검색 후 인스톨에 명령어 찾기

- 터미널에 입력하고 실행하면

- pubspec.yaml 파일의 의존성 파트에 추가됨

- 라이브러리 추가전에 새로운 프로젝트를 파서 예제코드를 긁어서 테스트 하는 샘플링 과정을 거치는 것이 좋음

3. 반복되는 컴포넌트 위젯화 하기


class MenuItem extends StatelessWidget {
const MenuItem({
super.key,
});
@override
Widget build(BuildContext context) {
return Container(
width: 60,
height: 80,
decoration: BoxDecoration(
border: Border.all(color: Colors.black12), // 테두리만 주기
borderRadius: BorderRadius.circular(30),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center, // 세로 방향 가운데 정렬
children: [
Icon(Icons.food_bank, color: Colors.redAccent, size: 30),
SizedBox(height: 3),
Text("ALL", style: TextStyle(color: Colors.black87))
],
),
);
}
}
4. 이미지를 넣을때
이미지 넣을때 BoxFit과 aspect는 쌍으로 와야 이쁘게 들어감
AspectRatio(
aspectRatio: 2 / 1, // 가로:세로 비율을 2:1로 설정 (가로가 세로의 두 배 크기)
child: ClipRRect(
borderRadius: BorderRadius.circular(20), // 모서리를 둥글게 (반지름 20)
child: Image.asset(
"assets/${myText}.jpeg", // assets 폴더에서 ${myText} 값에 해당하는 이미지 로드
fit: BoxFit.cover, // 이미지를 영역에 꽉 채우되, 비율을 유지하며 넘치는 부분은 잘림
),
),
),
Share article