[Flutter] 3. Flutter 프로젝트 만들기 (Store)

편준민's avatar
May 22, 2025
[Flutter] 3. Flutter 프로젝트 만들기 (Store)
💡
사용한 위젯
  • MaterialApp
  • Scaffold
  • Column
  • Row
  • Text
  • SafeArea
  • Image
  • Spacer
  • Expanded
  • Padding
  • SizedBox

1️⃣ flutter 프로젝트 생성

notion image
notion image
  • 프로젝트 명은 무조건 소문자 + _(언더스코어 스코어 기법)
notion image
💡
프로젝트를 생성 후 프로젝트를 어떤 방식으로 띄울 것인지 물어보는 것에서 This Window 시에 폴더가 나오지 않는다면 새로 프로젝트 만들기 아래에 처럼 보이지 않구 빈 폴더가 보이는 경우가 있는데 버그이기 때문에 새로운 프로젝트를 만들어야 한다.
notion image

2️⃣ 기본 코드 작성

💡
처음에 프로젝트를 생성 후 lib/main.dart에 있는 모든 코드를 지운다. 이후 아래 코드 작성
import 'package:flutter/cupertino.dart'; void main() { runApp(MyApp()); }
지금은 MyApp이라는 함수가 없기 때문에 오류가 날 것이다. 그럼 MyApp을 추가.
  • Stateless 코드는 stl 이라고만 적으면 자동 완성 해준다
// stl 이라고 적으면 자동완성 활성화 class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return const Placeholder(); } }
해당 코드를 작성 후 const를 아직 제대로 배우기 전까지는 어떤 역할을 하는지 모르기 때문에 모두 지워준다. 지우지 않아도 상관없지만 모르는 오류가 발생 시 const를 지워보자.
 
App을 만들 때에는 가장 먼저 선택해야하는 것이 있다. 그것은 AndroidApp을 만들 것인지, IOSApp을 만들 것인지 선택해야한다.
AndroidApp을 만들 때에는 MaterialApp 디자인을 사용하고, IOSAppCupertinoApp 디자인을 사용한다.
두 디자인은 Class로 제공을 하고 있다. AndroidApp을 만들기로 선택했다면 그에 맞는 코드를 짜보자.
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp(home: StorePage()); // } } // class StorePage extends StatelessWidget { StorePage({super.key}); @override Widget build(BuildContext context) { return Placeholder(); } }
모두 만들었으면 이제 App을 실행 시킬 수 있다. 현재 StorePage에서 Placeholderreturn 중인데, 이 때 Placeholder위젯(widget)이다.

위젯(Widget)?

위젯이란 UI를 구성하는 기본 단위로, App의 모든 것이 위젯으로 이루어져 있다고 볼 수 있다. Placeholder라는 위젯은 빈 박스를 하나 만드는 것이다. 레이아웃을 미리 구성해두고 나중에 채워 넣고 싶을 때 사용한다.
아래의 사진은 위에 App을 실행 시켰을 때 나오는 화면이다.
notion image

Scaffold

Flutter에서 Scaffold는 앱의 기본 뼈대(레이아웃 구조)를 제공하는 UI 프레임입니다. 쉽게 말하면 앱 화면의 기본 틀을 만들어주는 위젯입니다. 보통 하나의 화면(Screen)마다 하나의 Scaffold를 사용한다.
class StorePage extends StatelessWidget { StorePage({super.key}); @override Widget build(BuildContext context) { return Scaffold(); } }
Scaffold는 전체 화면의 구조를 정의하고, 그 안에 AppBar, body, FloatingActionButton 등을 포함할 수 있게 합니다.

3️⃣ Column 위젯, Row 위젯

💡
Column위젯수직 방향의 레이아웃 구조를 만들어주는 위젯이다. Column에는 하위 속성이 childchildren 속성이 있는데 child를 사용하게 되면, Column에는 하나의 위젯만 가질 수 있다.children을 사용하게되면, 여러 가지의 위젯을 Column위젯이 여러 위젯을 가지고 있을 수 있다.
class StorePage extends StatelessWidget { StorePage({super.key}); @override Widget build(BuildContext context) { return Scaffold(body: Column(children: [ Text("Column"), Text("Column"), Text("Column")] )); } }
  • 수직 방향으로 배치가 됨
notion image

Row위젯수평 방향으로 레이아웃 구조를 만들어주는 위젯이다. Column의 수평 버전인 것이다.
class StorePage extends StatelessWidget { StorePage({super.key}); @override Widget build(BuildContext context) { return Scaffold(body: Row(children: [ Text("Column"), Text("Column"), Text("Column")] )); } }
notion image

4️⃣ Text 위젯

💡
Text 위젯은 문자열을 담을 수 있는 위젯이다. 아래의 사진과 같이 Text 안에 적은 문자열이 출력된다.
class StorePage extends StatelessWidget { StorePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: Row( children: [ Text("Woman"), Text("Kids"), Text("Shoes"), Text("Bag"), ], ), ); } }
notion image

1. stlye 속성

💡
Text 위젯은 stlye속성을 사용하여 디자인을 할 수 있다. Text 위젯에 Ctrl + 마우스 왼쪽 클릭 을 하게 되면 아래와 같은 코드가 보인다
notion image
style, styrutStlye 등 과 같은 것을 속성이라고 부른다. 속성이란 어떤 대상을 구성하고 있는 요소라고 생각하면 된다. Text뿐만 아니라 많은 위젯이 속성을 가지고 있다.

style 속성을 이용하여 글자의 굵기를 굵게 만들어보겠다.
  • 해당 글자가 더 굵어진 모습이다.
notion image

5️⃣ SafeArea 위젯

💡
SafeArea는 휴대폰 기기별로 조금씩 다른 상태바StatusBar 영역에 여백padding을 넣어주는 역할은 한다. 상태바와 같이 개발자가 건드릴 수 없는 영역을 inset영역이라고 한다. 가장 위에 상태바와 같은 영역을 top inset, 가장 아래에 뒤로가기 버튼 또는 키보드가 나오는 영역은 bottom inset, 그리고 휴대폰 가장자리 전체의 영역은 edge inset 이라고 한다.

SafeArea 위젯이 생기기 전 까지는 휴대폰 기종마다 inset영역이 다르기 때문에 개발자들이 엄청나게 긴 코드를 작성해야 했는데 이제는 SafeArea 위젯을 설정해주면 어떠한 기종이든 알아서 inset 영역에 여백을 넣어준다.
  • SafeArea 전 App에는 상태바와 Row위젯이 겹쳐서 보인다.
notion image
  • SafeArea를 적용하여 상태바와 Row위젯의 여백이 생긴 모습
notion image
class StorePage extends StatelessWidget { StorePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Row( children: [ Text("Woman"), Text("Kids"), Text("Shoes"), Text("Bag"), ], ), ), ); } }

코드 삽입 시 유용한 팁

SafeArea를 입력 할 때에는 직접 작성하게 되면 오타 또는 괄호의 영역이 잘못 지정 될 수 있어서 위험하다. 그럴 때는 Row 위젯 위에서 Alt + Enter 를 하게 되면 아래의 사진이 나온다.
notion image
SafeArea가 있으면 가장 좋지만 없기 때문에, 가장 위에 Wrap With widget 을 선택해준다. 그러면 아래 같은 코드가 될 것이다.
@override Widget build(BuildContext context) { return Scaffold( body: widget( child: Row( children: [ Text("Woman"), Text("Kids"), Text("Shoes"), Text("Bag"), ], ), ), ); } }
이 상태에서 body: widget에서 widget 부분을 SafeArea로 바꿔주게 되면 실수 할 일이 사라지기 때문에 이 방식에 습관이 들면 좋다.

6️⃣ Spacer 위젯

💡
Spacer 위젯은 위젯과 위젯 사이의 간격을 조정하는 사용한다. 사용법은 간단하다. 조정하고 싶은 위젯 사이에 Spacer() 를 입력하면된다.
class StorePage extends StatelessWidget { StorePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Row( children: [ Text("Woman", style: TextStyle(fontWeight: FontWeight.bold)), Spacer(), Text("Kids", style: TextStyle(fontWeight: FontWeight.bold)), Spacer(), Text("Shoes", style: TextStyle(fontWeight: FontWeight.bold)), Text("Bag", style: TextStyle(fontWeight: FontWeight.bold)), ], ), ), ); } }
Shoes와 Bag 사이에는 Spacer()가 들어가지 않아서 위젯이 서로 붙어있다.
notion image

7️⃣ Debug 배너 해제

💡
에뮬레이터로 나의 App을 실행 해보니 계속해서 오른쪽 위에 Debug 라는 배너가 있다. 배너가 테스트를 방해하기 때문에 없애보겠다.
방법은 MaterialApp 속성 중에 debugShowCheckedModeBanner 를 이용하면 된다.
class MyApp extends StatelessWidget { MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( home: StorePage(), debugShowCheckedModeBanner: false, ); // } }
배너가 사라짐
배너가 사라짐

8️⃣ Padding 위젯

💡
Padding 자식 위젯 주위에 빈 공간을 만들어 줍니다. 아래의 사진과 같이 하늘색의 부분이 여백Padding 이라고 합니다.
notion image
지금까지 만든 App에 Row 위젯에 Padding 위젯으로 감싸보겠다. SafeArea와 같이 Alt + Enter 를 사용하여 실수와 편의성을 늘리면 좋습니다.
notion image
@override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Padding( padding: EdgeInsets.all(8.0), child: Row( children: [ Text("Woman", style: TextStyle(fontWeight: FontWeight.bold)), Spacer(), Text("Kids", style: TextStyle(fontWeight: FontWeight.bold)), Spacer(), Text("Shoes", style: TextStyle(fontWeight: FontWeight.bold)), Spacer(), Text("Bag", style: TextStyle(fontWeight: FontWeight.bold)), ], ), ), ), ); } }
양 옆에 여백이 조금 생긴 모습
양 옆에 여백이 조금 생긴 모습
Padding을 더 주고 싶다면, (8.0)을 더 큰 값을 넣어주면 된다.
25를 넣은 모습
25를 넣은 모습
지금 상태는 상하좌우 모두 padding이 들어가 있는 모습이다. 하지만 상황에 따라서는 왼쪽만 주고, 또는 상하만 주고 그러고 싶을 때가 있을 것이다. 그러한 경우는 어떻게 해야하나? EdgeInsets.all을 바꾸어 주면된다.

  • EdgeInsets.all : 상, 하, 좌, 우 모든 방향에 여백을 줄 때 사용
  • EdgeInsets.only : 4방향 중 내가 원하는 곳만 여백을 줄 때 사용 ex ) EdgeInsets.only(top: 8.0)
  • EdgeInsets.symmetric : 수평이나 수직 중 선택하여 여백을 줄 때 사용 ex ) EdgeInsets.symmetric(horizontal: 25.0)

9️⃣ Image 위젯

💡
Image 위젯을 사용하기 전 사전에 세팅해야 할 것이 있다.

1. Image 파일 생성

💡
JAVA에서는 공통으로 Img 파일을 만들어 img파일들을 모았다면, Flutter에서는 assets 파일에 img들을 모은다. 해당 파일을 만드는 위치는 다음과 같이 프로젝트 바로 아래에 만들면 된다.
project/ ├── lib/ ├── assets
notion image
이후에는 해당 폴더를 인식할 수 있게 해줘야한다.

2. pubspec.yaml 수정

💡
pubspec.yaml 해당 파일에 assets를 인식 할 수 있게 경로를 적어준다. assets/ 로 적게 되면 인식되는 이유는 pubspec.yaml 은 항상 프로젝트 루트에 위치하고, 그 안에서 지정하는 경로들도 루트 기준 상대 경로로 해석하기 때문이다.

루트란? 프로젝트 구조에서 가장 상위에 있는 시작점을 의미한다.

루트를 기준으로 상대 경로로 해석 하기 때문에 assets/ 앞에 나의 프로젝트 명인 flutter_recipe2 가 생략 되어있는 것이다. 실제로 인식 하는 경로는 flutter_recipe2/assets/ 인것이다.
assets: - assets/
경로를 추가 한 뒤에 모든 App을 종료하고 Pub get 을 클릭하여 오류가 없으면 인식 완료! 꼭 모든 App을 종료해야한다. 이유는 asset을 처음 추가하거나 경로를 바꿨을 때는 앱을 재시작해야 반영됩기 때문이다.
notion image

3. 이미지 추가

💡
assets 폴더에 내가 사용하고 싶은 폴더를 넣기.
Img.asset(”파일 경로”) 를 추가해주면 이미지가 나온다
notion image
// class StorePage extends StatelessWidget { StorePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Column( children: [ Padding( padding: EdgeInsets.all(25.0), child: Row( children: [ Text("Woman", style: TextStyle(fontWeight: FontWeight.bold)), Spacer(), Text("Kids", style: TextStyle(fontWeight: FontWeight.bold)), Spacer(), Text("Shoes", style: TextStyle(fontWeight: FontWeight.bold)), Spacer(), Text("Bag", style: TextStyle(fontWeight: FontWeight.bold)), ], ), ), Image.asset("assets/bag.jpeg"), Image.asset("assets/bag.jpeg") ], ), ), ); } }
notion image

🔟 Expanded 위젯

💡
Expanded 위젯은 남은 공간을 위젯의 크기를 확장하여 공간을 채울 수 있도록 하는 위젯이다. 현재 만들어진 App은 아래의 공간이 많이 남는다. 이 공간을 위젯의 크기를 확장하여 공간을 채워보겠다.
class StorePage extends StatelessWidget { StorePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Column( children: [ Padding( padding: EdgeInsets.all(25.0), child: Row( children: [ Text("Woman", style: TextStyle(fontWeight: FontWeight.bold)), Spacer(), Text("Kids", style: TextStyle(fontWeight: FontWeight.bold)), Spacer(), Text("Shoes", style: TextStyle(fontWeight: FontWeight.bold)), Spacer(), Text("Bag", style: TextStyle(fontWeight: FontWeight.bold)), ], ), ), Expanded(child: Image.asset("assets/bag.jpeg")), Expanded(child: Image.asset("assets/cloth.jpeg")), ], ), ), ); } }
notion image
확장된 모습이다. 하지만 먼가 이상하지 않은가? 아까까지는 이미지가 서로 붙어 있엇는데, 서로 떨어졌다. 그리고 남은 공간을 전부 채우고 싶은데 이렇게 되면 중간에 공간이 또 비어있게 된다. Expanded의 공간이 어떻게 잡혀 있는지 확인해보자.

1️⃣1️⃣ Flutter Devtools

💡
App을 개발하고 디버깅 할 때 사용하는 시각적 도구 모음이다. 이것을 사용하게 되면 시각적으로 해당 위젯들의 크기도 알 수 있고, 디거빙, 성능분석 등등 많은 것을 할 수 있다.

Flutter Devtools를 키는 법은 간단하다.
notion image
콘솔 창에 빨간 네모 버튼만 눌러주면 된다. 그러면 웹 브라우저가 하나 열리게 된다.
notion image
해당 창이 보이게 된다면 위에서 2번째 탭인 Flutter Inspector를 클릭하게 될 경우 아래의 사진 처럼 보인다.
notion image
여기서 내가 작성한 위젯들을 한 번에 볼 수 있다. 그리고 해당 위젯들의 영역을 보고 싶다면, 보고 싶은 위젯을 클릭 후 Select Widget Mode를 클릭하면 된다.
notion image
누르게 되면 내가 실행한 App에서 보라색 영역으로 표시 된다. 여기서 확인을 해보면 Expanded 위젯의 영역은 넓은데 이미지가 꽉 차지 않은 모습이다. 이것은 Img 위젯의 속성을 추가해주면 해결 가능하다.

1️⃣2️⃣ Image 위젯 속성

💡
Image 위젯과 속성을 같이 다루고 싶었지만 상황을 보게 되면 더 이해하기 쉬울 것 같아서 뒤 쪽에 정리하였다.

Image 기본 특성으로 비율이라는 것이 있다. 기본적으로는 이 비율을 부술 수 없다. 하지만 속성을 이용하게 되면 비율을 부수어 화면에 꽉차게 보이게도 할 수 있다.
Image 속성에 대해 가장 많이 사용하는 fit 속성 3가지를 알아보겠다.
  • BoxFit.contain : 원본사진의 가로 세로 비율 변화 없이 이미지를 추가한다. 비율 변화 없이 이미지를 추가하기 때문에 박스 크기와 맞지 않으면 빈 공간이 생긴다.
  • BoxFit.fill : 원본사진의 비율을 무시하고 지정한 영역에 사진을 맞춘다. 비율을 무시하기 때문에 사진이 화질또는 이런 것이 망가진다.
  • BoxFit.cover : 원본사진의 가로 세로 비율을 유지한 채로 지정한 영역에 사진을 맞춘다. 비율을 유지 할 수 있는 장점이 있지만, 사진이 지정한 크기를 벗어나면 잘릴 수 있다.
아래의 속성 예시를 붙이겠다.

  • BoxFit.contain
notion image

  • BoxFit.fill
notion image
  • BoxFit.cover
notion image

1️⃣3️⃣ SizedBox

💡
Flutter에서 width 혹은 height 크기를 가지는 빈 상자이다.
이번 프로젝트에서는 이미지 사이에 빈 공간을 주기 위해 사용했다.
class StorePage extends StatelessWidget { StorePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Column( children: [ Padding( padding: EdgeInsets.all(25.0), child: Row( children: [ Text("Woman", style: TextStyle(fontWeight: FontWeight.bold)), Spacer(), Text("Kids", style: TextStyle(fontWeight: FontWeight.bold)), Spacer(), Text("Shoes", style: TextStyle(fontWeight: FontWeight.bold)), Spacer(), Text("Bag", style: TextStyle(fontWeight: FontWeight.bold)), ], ), ), Expanded(child: Image.asset("assets/bag.jpeg", fit: BoxFit.cover)), SizedBox(height: 2), Expanded(child: Image.asset("assets/cloth.jpeg", fit: BoxFit.cover)), ], ), ), ); } }
  • 약간의 빈 공간이 생긴 모습
notion image
 

1️⃣4️⃣ 최종코드

import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( home: StorePage(), debugShowCheckedModeBanner: false, ); // } } // class StorePage extends StatelessWidget { StorePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Column( children: [ Padding( padding: EdgeInsets.all(25.0), child: Row( children: [ Text("Woman", style: TextStyle(fontWeight: FontWeight.bold)), Spacer(), Text("Kids", style: TextStyle(fontWeight: FontWeight.bold)), Spacer(), Text("Shoes", style: TextStyle(fontWeight: FontWeight.bold)), Spacer(), Text("Bag", style: TextStyle(fontWeight: FontWeight.bold)), ], ), ), Expanded(child: Image.asset("assets/bag.jpeg", fit: BoxFit.cover)), SizedBox(height: 2), Expanded(child: Image.asset("assets/cloth.jpeg", fit: BoxFit.cover)), ], ), ), ); } }

출처

최주호 , 김근호 , 이지원 저자(글) / (2023) / 만들면서 배우는 플러터 앱 프로그래밍 (7가지 모바일 앱 UI 제작 & RiverPod) / 앤써북
Share article

YunSeolAn