[Flutter] 4. Flutter 프로젝트 만들기(Recipe)

편준민's avatar
May 27, 2025
[Flutter] 4. Flutter 프로젝트 만들기(Recipe)
💡
사용한 위젯
  • Appbar
  • Icon
  • ListView
  • Container
  • ClipRRect
  • AspectRatio

1️⃣ 라이브러리 설치 (pub.dev)

💡
flutter 모든 라이브러리 다운 받는 곳

1. Flutter 라이브러리 다운 브라우저

notion image
  • 필요한 라이브러리 검색
notion image
  • 원하는 라이브러리를 선택 후 installing 클릭
notion image

2. 라이브러리 다운로드 방식 선택

💡
pub.dev에서는 라이브러리를 다운로드 하는 방식이 2가지가 있다.
  • 터미널에 명령어를 입력하여 라이브러리를 다운 받는 방식.
  • dependencies에 직접 코드를 입력.
위와 같이 방식이 2가지가 있는데 하나씩 알아보겠다.
notion image

2-1 명령어로 라이브러리 다운로드

💡
해당 pub.dev에서 With Flutter에 적혀있는 명령어를 복사하여 android Studio에 터미널에 그대로 복사 붙여넣기 후 실행 시키면 된다.
붙여넣기를 할 경우 주의 해야 할 점은 Ctrl + V 를 사용하여 붙여넣기를 하기 보다는 Shift + insert 를 사용하는 것이 좋다. 이유는 터미널은 GUI 환경과 달라서 Ctrl + V가 동작하지 않을 수 있기 때문이다. 동작한다면 Ctrl + V 를 사용해도 무관하지만 습관을 터미널에서 Shift + insert로 습관을 들이는 것이 더 좋다.
  • 명령어 입력 후 다운로드 된 것을 확인
notion image
  • pubspec.yaml에 추가된 것을 확인
notion image

2-2 Dependencise에 직접 추가하기

💡
똑같이 필요한 라이브러리를 선택 후 Installing 탭에 들어가 아래에 있는 코드를 복사한다.
notion image
  • pubspec.yaml 파일에 추가하기
notion image
  • 추가 후에 오른쪽 위에 Pub get 클릭하여 라이브러리 다운 받기

2️⃣ 기본세팅

💡
이전 프로젝트와 같이 메인을 runApp을 제외하고 모두 지운다. 현재 아래 코드와 같다면 MyApp이 없기 때문에 오류가 날 것이다.
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); }
  • stl 자동완성기능을 이용하여 MyApp 만들기
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { MyApp({super.key}); @override Widget build(BuildContext context) { return Placeholder(); } }
  • 우선적으로 어떤 환경의 앱을 만들지 선택한다. 이번 프로젝트도 Android앱을 위하여 MaterialApp을 선택 MyApp클래스에서는 환경만을 세팅하기 때문에 직접적인 화면 구성은 home: 에서 할 것이다.
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: , ); } }
  • 위젯들을 그리기 위해서는 한 장의 도화지가 필요하다. 이 도화지의 역할을 하는 것이 Scaffold 이다. 앱이 몇 페이지가 나올지는 모르지만, 굉장히 많은 페이지가 나올 것이다. 한 장의 도화지에 모든 위젯들을 그릴 수는 없기 때문에 각각 페이지 마다 도화지Scaffold가 필요하다.
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold(), ); } }

3️⃣ Appbar

💡
Appbar란 앱 화면 상단에 표시되는 상단 바를 말하며, 일반적으로 아래와 같은 UI 요소들을 포함한다.
  • title : 앱 제목
  • leading : 뒤로가기 버튼 또는 메뉴 버튼
  • actions : 오른쪽 액션 버튼들

Appbar를 쓰는 이유는 모바일 앱의 기본이 되는 상단 바로서 사용자들에게 익숙한 UI/UX를 제공해준다. 그리고 또한 커스터마이징이 쉬워서 개발자의 마음대로 바꿀 수 있다.
직접 만들어도 괜찮지만, 만들기엔 시간도 들고 익숙한 UI/UX가 아니면 사용자가 사용하는데 불편함을 겪을 수 있기 때문에 Appbar를 쓰는 것이 좋다.
  • Flutter에서 appBar는 HTML header와 매우 유사하다
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar() ), ); } }
  • HTML에서 header다음에는 무엇이 오는가? 전체적인 내용을 담는 body가 필요하다. Flutter에서도 똑같이 body를 사용한다.

프로젝트 시 기본 세팅이 되는 코드

  • 아래와 같은 코드가 어떤 프로젝트를 만들 때에 기본 뼈대가 될 수 있는 코드이다.
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar(), body: Placeholder(), ), ); } }
  • 위 코드의 실제 화면
notion image

4️⃣ Appbar 위젯

💡
Appbar에 위젯을 배치하기 위해서는 우선적으로 Appbar 위젯의 구조를 알아야한다. 아래 사진을 참고하기 바란다.
notion image
지금 우리 프로젝트에는 Appbar에는 actions위치에만 Icon을 놓을 예정이다.

5️⃣ Icon 위젯

💡
Icon 위젯은 Icon을 표시해주는 위젯이다. MaterialIcon 또는 CupertionIcon 중 원하는 Icon을 사용할 수 있다. 플러터에서 제공하는 기본 Icon이 아닌 다른 Icon을 사용하고 싶다면 pub.dev에서 원하는 Icon 라이브러리를 다운받아서 사용하면 된다.
  • Appbar구조에서 actions의 위치에 놓기 위해서 Icon을 actions의 안에 추가하였다.
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold( appBar: AppBar( actions: [ Icon(Icons.search), Icon(CupertinoIcons.heart, color: Colors.redAccent), ], ), body: Placeholder(), ), ); } }
  • 가장 오른쪽 돋보기 모양과 하트
notion image

폰트 적용

💡
Flutter에 기본 글자 폰트가 맘에 들지 않는다면 라이브러리를 사용하여 바꿀 수 있다. 가장 처음에 다운 받은 라이브러리가 font의 라이브러리이기 때문에 다운 받는 방법은 위에서 참고 하기

처음 사용해보는 라이브러리를 사용 할 때에는 다른 사람이 만든 것을 사용하는 것이기 때문에 제대로된 사용법을 알 수 없다. 이럴 때에는 문서를 읽어봐야한다.
pub.dev에서 라이브러리의 사용법에 대한 문서도 모두 있다.
notion image
해당 Readme가 사용법에 대한 문서이다.
폰트를 적용하는 법은 간단하게 아래 코드와 같다고 한다.
Text( 'This is Google Fonts', style: GoogleFonts.lato(), ),
하지만 해당 폰트가 내가 원하는 폰트가 아니라면, 저 폰트 밖에 없냐? 그것은 아니다. 문서를 찾다보면 해당 폰트가 모여있는 문서가 따로 있을 것이다. 이 링크는 Google Font에 대한 문서이다. 원하는 폰트의 이름을 찾아서 사용하면 된다.

class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold( appBar: _appbar(), body: Text( "Recipes", style: TextStyle(fontSize: 30), ), ), ); }
생각해보니 우리는 폰트 사이즈를 주는 TextStyle을 사용하고 있엇다. 폰트를 적용하기 위해서는 아래와 같이 해야한다.
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold( appBar: _appbar(), body: Text( "Recipes", style: GoogleFonts.patuaOne(), ), ), ); }
notion image
폰트는 바뀌었지만 크기는 다시 줄어든 상태이다. 폰트를 적용하면서 크기도 키우고 싶을 때에도 라이브러리를 우리가 직접 만든 것이 아니기 때문에 문서를 읽어봐야한다. Readme를 확인해보자.
Text( 'This is Google Fonts', style: GoogleFonts.lato( textStyle: TextStyle(color: Colors.blue, letterSpacing: .5), ), ),
해당 코드를 보니까 폰트도 적용하고 textStyle도 적용하는 방법이 나와있다. 해당 문서에는 색을 주는 코드이지만 우리는 색 말고 크기를 적용해보겠다.
  • 폰트도 바뀌고 사이즈도 커진 화면
notion image

Container 위젯

💡
Container 위젯은 빈 박스 위젯이다. SizedBox와 차이점이 있다면 Container 내부에는 decoration 속성이 있어서 박스에 생상을 입히거나 박스의 모양을 바꾼다거나 테두리 선을 줄 수 있다. SizedBox는 보통 마진을 줘야할 때 사용한다.

Container 위젯 안에는 Icon과 Text 위젯이 들어있다. 이것을 하나의 Container로 묶어서 재활용 가능한 위젯으로 만들 수 있다.
  • Icon과 Text가 한번에 출력 되는 위젯을 나만의 아이콘인 MenuItem() 으로 만들었다.
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold( appBar: _appbar(), body: Column( children: [ Text("Recipes", style: GoogleFonts.patuaOne(textStyle: TextStyle(fontSize: 30))), Row( children: [ MenuItem(), MenuItem(), MenuItem(), MenuItem(), ], ), ], ), ), ); } class MenuItem extends StatelessWidget { @override Widget build(BuildContext context) { return Column( children: [ Icon(Icons.food_bank), Text("ALL"), ], ); } }
notion image

현재는 Icon과 Text를 상수로 지정하여서 위젯으로 만들었기 때문에 다 똑같은 모습이다. 매개변수를 받아서 아이콘의 모양과 Text도 필요한 값으로 바꿔보겠다.
  • 생성자를 만들어서 매개변수를 받고 거기에 해당 하는 값을 집어 넣었다.
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold( appBar: _appbar(), body: Column( children: [ Text("Recipes", style: GoogleFonts.patuaOne(textStyle: TextStyle(fontSize: 30))), Row( children: [ MenuItem(Icons.food_bank, "ALL"), MenuItem(Icons.emoji_food_beverage, "Coffee"), MenuItem(Icons.fastfood, "Burger"), MenuItem(Icons.local_pizza, "Pizza"), ], ), ], ), ), ); } class MenuItem extends StatelessWidget { IconData mIcon; var mText; MenuItem(this.mIcon, this.mText); @override Widget build(BuildContext context) { return Column( children: [Icon(mIcon), Text(mText)], ); } }
notion image

7️⃣ 메서드 또는 위젯으로 만들기

💡
한 페이지에서 모든 코드를 작성하게되면 코드가 엄청나게 길어지게 되어 가독성이 떨어지고, 재사용도 불가능하다. 이럴 때 우리는 필요한 위젯을 메서드 또는 위젯 여러개를 묶어 나만의 위젯(componet)로 만들 것이다.
※ 메서드 또는 위젯으로 만들 때 주의 점은 마우스 커서의 위치텍스트 커서가 만들고자 하는 위젯의 위에 있어야 한다는 것이다.

메서드

메서드로 만들 때에 컨벤션은 맨 앞에 _ 가 들어가야 한다는 것이다. _ 의 뜻은 JAVA에서는 private으로 다른 클래스에서 접근을 못하게 끔 만드는 것이다.
단축키 : Ctrl + Alt + M 컨벤션 : 메서드 이름 앞에 _ , 모두 소문자
  • 메서드로 만들어 MyApp 밖에 메서드로 만들어진 모습
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold( appBar: _appbar(), body: ListView( children: [ Text("Recipes"), ], ), ), ); } AppBar _appbar() { return AppBar( actions: [ Icon(Icons.search), Icon(CupertinoIcons.heart, color: Colors.redAccent), ], ); } }
notion image

위젯 (component)

💡
위젯으로 만드는 이유는 해당 위젯의 재활용과 코드의 가독성을 위해서 이다. 똑같이 생긴 위젯이 여러 개가 필요하면 코드를 복사, 붙여넣기를 하게 되면 코드가 길어지면서 가독성도 떨어진다. 이럴 때 위젯으로 만들어서 4개가 필요하다면 우리가 만든 위젯 4줄만 사용하면 된다.
아래의 코드는 위젯으로 만들지 않은 것이다.
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold( appBar: _appbar(), body: ListView( children: [ Text("Recipes"), Row( children: [ Column(children: [Icon(Icons.food_bank), Text("ALL")]), Column(children: [Icon(Icons.emoji_food_beverage), Text("Coffee")]), Column(children: [Icon(Icons.fastfood), Text("Burger")]), Column(children: [Icon(Icons.local_pizza), Text("Pizza")]), ], ), ], ), ), ); }
notion image
이제 해당 아이콘과 Text를 묶어서 위젯으로 만들어 보겠다.

6️⃣ ListView

💡
이전 프로젝트에서는 화면을 구성할 때에 Column 위젯을 사용하였다. 하지만 만약 해당 App에 내용이 너무 많은 내용이 담겨서 스크롤이 필요한 경우에는 Column 위젯을 사용 할 수가 없다. Column 위젯은 화면에 딱 맞게 정해져있다. 내용이 너무 많아 스크롤이 필요한 경우에는 ListView를 사용하면 된다.

위에서 Icon과 Text를 하나의 위젯으로 만든 것 처럼 리스트에 imag와 Text 2개를 하나로 묶어서 위젯으로 만든 다음 재활용 하였다.
문자열에 변수를 넣을 때는 $[변수] 이렇게 써도 되고, ${ [변수] } 이렇게 써도 된다. 하지만 만약 연산이 필요한 경우에는 무조건 {}가 필요하다. 예시로는 ${ [변수]+1} 가 있다.
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold( appBar: _appbar(), body: ListView( children: [ Text("Recipes", style: GoogleFonts.patuaOne(textStyle: TextStyle(fontSize: 30))), Row( children: [ MenuItem(Icons.food_bank, "ALL"), MenuItem(Icons.emoji_food_beverage, "Coffee"), MenuItem(Icons.fastfood, "Burger"), MenuItem(Icons.local_pizza, "Pizza"), ], ), ListItem("coffee"), ListItem("burger"), ListItem("pizza"), ], ), ), ); } class ListItem extends StatelessWidget { var title; ListItem(this.title); @override Widget build(BuildContext context) { return Column( children: [ Image.asset("assets/$title.jpeg"), Text("$title"), Text("Have you ever made your own $title? Once you've tried a homemade $title, you'll never go back."), ], ); } }
  • 완성된 App화면에서의 스크롤 바가 있는 화면
notion image

메인 코드

💡
현재까지 작성한 모든 코드를 보면 아래와 같다. 아래와 같은 코드는 너무 길고, 가독성도 낮아진다. 그렇기 때문에 메소드와 위젯에 같은 경우는 따로 파일을 만들어서 분리하는 것이 좋다.
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: Scaffold( appBar: _appbar(), body: ListView( children: [ Text("Recipes", style: GoogleFonts.patuaOne(textStyle: TextStyle(fontSize: 30))), Row( children: [ MenuItem(Icons.food_bank, "ALL"), MenuItem(Icons.emoji_food_beverage, "Coffee"), MenuItem(Icons.fastfood, "Burger"), MenuItem(Icons.local_pizza, "Pizza"), ], ), ListItem("coffee"), ListItem("burger"), ListItem("pizza"), ], ), ), ); } AppBar _appbar() { return AppBar( actions: [ Icon(Icons.search), Icon(CupertinoIcons.heart, color: Colors.redAccent), ], ); } } class ListItem extends StatelessWidget { var title; ListItem(this.title); @override Widget build(BuildContext context) { return Column( children: [ Image.asset("assets/$title.jpeg"), Text("$title"), Text("Have you ever made your own $title? Once you've tried a homemade $title, you'll never go back."), ], ); } } class MenuItem extends StatelessWidget { IconData mIcon; var mText; MenuItem(this.mIcon, this.mText); @override Widget build(BuildContext context) { return Column( children: [Icon(mIcon), Text(mText)], ); } }

7️⃣ 파일 분리

💡
해당 사진 처럼 재활용 가능한 위젯은 component 파일에 넣고 메소드는 해당 페이지에만 필요하기 때문에 분리한다.
notion image

component/list_item

import 'package:flutter/cupertino.dart'; class ListItem extends StatelessWidget { String title; ListItem(this.title); @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ AspectRatio( aspectRatio: 2 / 1, // 가로 / 세로 child: ClipRRect( // 사진 모따기 borderRadius: BorderRadius.circular(20), child: Image.asset("assets/$title.jpeg", fit: BoxFit.cover), ), ), Text("$title"), Text("Have you ever made your own $title? Once you've tried a homemade $title, you'll never go back."), SizedBox(height: 20), ], ); } }

component/m_title

import 'package:flutter/cupertino.dart'; import 'package:google_fonts/google_fonts.dart'; class MTitle extends StatelessWidget { @override Widget build(BuildContext context) { return Text( "Recipes", style: GoogleFonts.patuaOne( textStyle: TextStyle(fontSize: 30), ), ); } }

component/menu_item

import 'package:flutter/material.dart'; class MenuItem extends StatelessWidget { IconData mIcon; var mText; MenuItem(this.mIcon, 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: 5), Text(mText), ], ), ); } }

component/menus

import 'package:flutter/material.dart'; import 'package:flutter_cook/component/menu_item.dart'; class Menus extends StatelessWidget { @override Widget build(BuildContext context) { return Row( children: [ MenuItem(Icons.food_bank, "ALL"), SizedBox(width: 25), MenuItem(Icons.emoji_food_beverage, "Coffee"), SizedBox(width: 25), MenuItem(Icons.fastfood, "Burger"), SizedBox(width: 25), MenuItem(Icons.local_pizza, "Pizza"), ], ); } }

page/main_page

import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_cook/component/list_item.dart'; import 'package:flutter_cook/component/m_title.dart'; import 'package:flutter_cook/component/menus.dart'; class MainPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: _appbar(), body: Padding( padding: EdgeInsets.symmetric(horizontal: 16), child: ListView( children: [ MTitle(), SizedBox(height: 20), Menus(), SizedBox(height: 20), ListItem("coffee"), ListItem("burger"), ListItem("pizza"), ], ), ), ); } } AppBar _appbar() { return AppBar( actions: [ Icon(Icons.search), SizedBox(width: 16), Icon(CupertinoIcons.heart, color: Colors.redAccent), SizedBox(width: 16), ], ); }
Share article

YunSeolAn