프로젝트에서 보기 →

Flutter 모바일 개발이 이렇게 쉽다고? (완전판)

태그
기술
시작일
종료일
수정일

https://www.youtube.com/watch?v=iMG6AOOJCIk

1. 이건 꼭 알아야 한다[^1]

[? 질문] Flutter에서 복잡해 보이는 화면(UI) 레이아웃을 처음부터 어떻게 “분석 → 분해 → 구현”할 수 있는가[^2]
[= 답] 화면을 행(Row)과 열(Column) 관점으로 먼저 분해하고, 그리드/겹침(Stack)/탭 여부를 점검한 뒤, 가장 바깥 구조부터 안쪽으로 내려가는 하향식(top-down) 접근으로 구현한다.[^3]

[? 질문] 레이아웃 예제 코드를 작성할 때 코드가 너무 길어지고 복잡해지는 문제를 어떻게 줄이는가[^4]
[= 답] 중첩이 깊은 UI 코드를 변수/함수/별도 위젯 클래스로 분리해 시각적 혼란을 줄이되, 함수 분리는 빌드/리렌더링 과정에서 퍼포먼스 영향이 있을 수 있어 상황에 따라 StatelessWidget 등 별도 위젯으로 분리하는 방식을 권장한다.[^5]

[? 질문] 사용자 클릭(별 아이콘)처럼 “상태 변화”가 있는 UI를 Flutter에서 어떻게 구현하는가[^6]
[= 답] 런타임에 값(아이콘 모양/카운트)이 바뀌는 것은 **StatefulWidget + setState()**로 처리하고, 별 아이콘 토글 로직(선택/해제)에 따라 **아이콘(채움/테두리)과 카운트(증감)**를 함께 갱신한다.[^7]


2. 큰 그림[^8]

이 콘텐츠는 Flutter 공식 레이아웃 예제를 그대로 따라 하면서, 화면을 어떻게 분석하고 어떤 위젯들로 조립하는지를 단계적으로 구현하는 실습 강의다.[^9] 먼저 정적인 레이아웃(이미지/타이틀/버튼/설명 텍스트)을 완성한 뒤, 마지막에 별(좋아요) 버튼을 눌렀을 때 UI가 바뀌는 상호작용까지 확장한다.[^10]

  • 레이아웃 분석법: 화면을 Row/Column 단위로 나누고, 그리드 여부, 겹침 요소(Stack 필요성), 탭 필요 여부 등을 체크한 뒤 구현 전략을 세운다.[^11]
  • 구현 패턴(재사용/정리): 반복되는 UI(버튼 컬럼 3개 등)는 **도우미 메서드(buildButtonColumn)**로 묶어 중복을 줄이고, 전체 코드는 변수/함수/위젯 분리로 관리한다.[^12]
  • 상태 기반 UI: 별 아이콘 클릭 시 아이콘 모양과 숫자를 바꾸는 예제로, StatefulWidget, setState, onPressed, IconButton을 통해 “상태 → UI” 갱신 흐름을 보여준다.[^13]

3. 하나씩 살펴보기[^14]

3.1 예제 화면을 먼저 관찰하고 “9분할”로 구조 분해하기[^15]

📸 0:15

강의자는 Flutter 레이아웃 예제를 시작하며, 먼저 완성된 화면이 어떤 구성인지 “겉모습”을 관찰한다.[^16] 화면 상단에는 이미지가 있고, 그 아래에는 (1) 장소/설명 텍스트와 (2) 오른쪽 별 아이콘+숫자(좋아요 수처럼 보이는)가 있는 행이 있다.[^17] 더 아래에는 CALL / ROUTE / SHARE처럼 아이콘과 텍스트가 세트로 있는 버튼 영역이 있으며, 마지막으로 긴 description 텍스트 블록이 내려온다.[^18]

강의자는 이 화면을 크게 나누면 다음처럼 분해할 수 있다고 설명한다.[^19]

  • 위쪽: 이미지 영역[^20]
  • 중간 1: 타이틀 섹션(두 줄 텍스트 + 별 + 숫자)[^21]
  • 중간 2: 버튼 섹션(3개 버튼: 콜/라우트/쉐어)[^22]
  • 아래: 텍스트(설명) 섹션[^23]

이렇게 큰 덩어리로 먼저 나누고, 각 덩어리 내부도 Row/Column으로 다시 쪼개면 구현이 쉬워진다고 한다.[^24]


3.2 레이아웃 분석 체크리스트: Row/Column, Grid, Stack(겹침), Tab, Padding/Border[^25]

📸 1:41

강의자는 구현 전에 다이어그램(도표)로 “행과 열”을 먼저 식별하라고 말한다.[^26] 그리고 다음 항목을 순서대로 점검한다.[^27]

  1. 그리드가 있는가?
  • 이 예제는 균일한 격자(그리드) 기반으로 보이지 않아서 GridView 같은 구조가 필요 없어 보인다고 판단한다.[^28]
  1. 겹치는 요소가 있는가? (Stack 필요성)
  • 겹침 요소란 예를 들어 어떤 요소가 이미지 위에 떠 있는(오버레이) 경우인데, 이 예제는 그런 형태가 아니므로 Stack을 쓸 일이 없을 것이라고 말한다.[^29]
  1. 탭 UI가 필요한가?
  • 한 페이지 화면처럼 보이므로 탭(TabBar 등)은 필요 없다고 정리한다.[^30]
  1. Padding/정렬/테두리 필요성
  • 대략 중앙 정렬로 모여 있는 듯 보이지만, 정확히 어디에 패딩이 들어가야 하는지(양쪽 32 등)는 구현하면서 확인해야 한다고 언급한다.[^31]

또한 “큰 틀”은 하나의 Column 안에 위에서부터 이미지/타이틀/버튼/텍스트가 차례로 쌓이는 형태로 정리한다.[^32] 즉, 바깥은 Column, 내부는 필요한 곳에 Row/Column이 중첩되는 구조다.[^33]


3.3 하향식(top-down) 구현과 코드 정리 전략: 변수/함수/위젯 분리, 그리고 성능 주의[^34]

📸 4:49

강의자는 레이아웃 구현은 “하향식 접근(top-down)”이 가장 쉽다고 설명한다.[^35] 즉, 전체 화면의 바깥 구조부터 잡고(예: Column), 그 안에 섹션들을 하나씩 넣고, 각 섹션 내부를 구현한다.[^36]

그런데 Flutter UI 코드는 보통 build() 아래로 위젯 트리가 길게 중첩되며 시각적으로 복잡해진다.[^37] 그래서 일부 구현을 변수와 함수로 빼서 관리하는 방식을 소개한다.[^38]

다만 여기서 “함수로 빼는 것”은 주의가 필요하다고 말한다.[^39]

  • Flutter는 UI가 바뀌면 build가 다시 실행되어 리렌더링이 발생한다.[^40]
  • 이 과정에서 함수로 쪼갠 UI가 퍼포먼스에 영향을 줄 수 있으니, 상황에 따라 함수 대신 별도의 StatelessWidget 클래스로 빼는 경우가 많다고 한다.[^41]

[!IMPORTANT] 함수 분리 vs 위젯 분리의 관점
UI 조각을 함수로 만들면 코드 정리는 쉬워지지만, 리빌드 시 호출 비용 관점에서 주의가 필요할 수 있다.[^42] 강의자는 실무에서 종종 별도 위젯(클래스)로 분리하는 패턴을 언급한다.[^43]


3.4 새 Flutter 프로젝트 기본 구조 확인 → 카운터 앱 제거 → “Hello World” 기본 뼈대 만들기[^44]

📸 7:06

강의자는 Flutter 프로젝트를 새로 만들면 기본으로 생성되는 구조를 보여준다.[^45] 기본 템플릿에는 카운터 증가 앱이 포함되어 있고, 버튼을 누르면 incrementCounter로 숫자가 올라가는 형태다.[^46]

이번 강의에서는 그 기본 카운터 예제를 쓰지 않고, 아주 간단한 앱 뼈대로 바꾸어 레이아웃 예제를 구현한다.[^47]

  • main.dart에서 카운터 관련 코드를 제거한다.[^48]
  • 타이틀은 “Welcome to Flutter” 비슷한 형태로 두고, 본문에는 중앙에 “Hello World” 텍스트를 보이게 한다.[^49]
  • Android Studio에서 디바이스 매니저(에뮬레이터)를 실행하고 디버그 실행을 한다.[^50]
  • 화면 크기가 디바이스 매니저 창에 따라 달라져 보기 싫으면 패널을 접거나 크기를 조정한다고 설명한다.[^51]

이 시점까지는 “레이아웃 구현 전에 기본 실행 환경을 갖추는 단계”다.[^52]


3.5 타이틀 섹션 구현(첫 번째 본격 UI): Row(3 children) + Expanded(텍스트 2줄) + Icon + 숫자 텍스트[^53]

📸 9:38

다음 파트에서 강의자는 “지난 시간에 이어서” 타이틀 섹션을 구현한다고 말한다.[^54] 타이틀 섹션은 그림상 다음처럼 구성된다.[^55]

  • 하나의 Row에 3개의 자식(children)[^56]
    1. 왼쪽: 텍스트 2줄(제목/부제) 영역 → 남은 공간을 차지해야 하므로 Expanded로 감싸야 할 것[^57]
    2. 가운데/오른쪽: 별 아이콘[^58]
    3. 맨 오른쪽: 숫자 텍스트(예: 41)[^59]

3.5.1 titleSection 변수로 UI 조각 분리[^60]

코드가 길어지는 것을 막기 위해, build() 아래쪽에 **변수 titleSection**을 선언하고 그 안에 위젯 트리를 넣겠다고 한다.[^61] 즉 titleSection = Container(child: Row(children:[...])) 같은 형태로 시작한다.[^62]

처음에는 Row의 children 3개를 빠르게 “자리잡기”로 컨테이너 3개를 넣어놓고, 그 후에 각각을 실제 위젯으로 바꿔간다.[^63] (일종의 스캐폴딩 방식)[^64]

3.5.2 첫 번째 child: Expanded로 감싼 Column(텍스트 2개) 만들기[^65]

왼쪽 텍스트 영역은 Row에서 넓은 폭을 차지해야 하므로, 첫 번째 child 컨테이너를 Expanded로 감싼다.[^66] 그리고 Expanded 내부 child는 세로로 두 줄 텍스트가 들어가므로 Column으로 바꾼다.[^67]

  • Column의 children에 텍스트 위젯 2개를 배치한다.[^68]
  • 첫 번째 텍스트(제목)는 TextStyle(fontWeight: FontWeight.bold)로 굵게 한다.[^69]
  • 두 번째 텍스트(부제/주소)는 회색 톤(Colors.grey[500] 같은 느낌)으로 준다.[^70]

강의에서는 실제 문자열로 “Lake Oeschinen…(발음 어렵다고 언급)” 같은 제목을 넣고, 아래 줄에 “Kandersteg, Switzerland”류의 위치 정보를 넣는다.[^71]

또한 “초보자는 그림 보고 바로 Column/Row가 눈에 들어오지 않을 수 있으며, 다른 방식으로 구현해도 큰 상관은 없다”는 식으로 난이도를 언급한다.[^72]

3.5.3 body에 titleSection을 실제로 배치하기: Center → Column으로 교체[^73]

초기에는 body에 Center(child: Text("Hello")) 같은 형태였는데, 이를 **Column(children:[titleSection])**로 바꿔서 타이틀 섹션이 실제 화면에 그려지게 한다.[^74]

처음엔 컨테이너 크기/정렬 문제로 “아무것도 안 보이는 것처럼” 느껴질 수 있음을 언급하며, 계속 코딩을 진행한다.[^75]

3.5.4 두 번째 child: 별 아이콘 + 색상(빨강 계열) 지정, named parameter vs positional parameter 설명[^76]

Row의 두 번째 자리를 컨테이너에서 Icon으로 바꾸고 Icons.star를 넣는다.[^77] 색상은 Colors.red[500] 같은 빨강 계열로 지정한다.[^78]

여기서 강의자는 Flutter/Dart 문법 관점의 설명을 덧붙인다.[^79]

  • Icon(Icons.star, color: ...)에서 Icons.starpositional argument로 “첫 번째 자리”에 들어간다.[^80]
  • color, size 등은 중괄호로 정의된 named parameter이므로 color: ...처럼 이름을 명시해야 한다.[^81]
  • Android Studio에서 Ctrl + Shift + I로 해당 위젯 시그니처/설명을 확인할 수 있다고 한다.[^82]

3.5.5 세 번째 child: 숫자 텍스트 추가[^83]

Row의 세 번째 자리 컨테이너를 Text("41") 같은 형태로 바꿔 숫자를 표시한다.[^84] 리스타트를 하면 별 옆에 숫자가 나타난다.[^85]

3.5.6 정렬과 패딩: 바깥 padding(32) + Column crossAxisAlignment.start + 텍스트 간 간격(bottom padding 8)[^86]

이제 배치는 되었지만 “정렬이 안 된 상태”이므로 정렬을 잡는다.[^87]

  1. 타이틀 섹션 전체 padding
  • Row를 감싸는 컨테이너에 padding을 주어 상하좌우 여백을 확보한다.[^88]
  • 강의에서는 전반적으로 32 픽셀을 주면 가운데로 보기 좋게 들어온다고 설명한다.[^89]
  1. 왼쪽 텍스트 Column 정렬
  • Column의 기본 정렬 때문에 텍스트가 가운데/왼쪽이 어색할 수 있어 crossAxisAlignment: CrossAxisAlignment.start로 왼쪽 정렬한다.[^90]
  1. 제목-부제 텍스트 간 간격
  • 텍스트 두 줄이 너무 붙어 보여, Column 내부에서 첫 번째 텍스트를 감싸는 컨테이너에 padding: EdgeInsets.only(bottom: 8)을 주어 간격을 만든다.[^91]

이로써 타이틀 섹션이 예제 화면과 유사하게 정리된다.[^92]


3.6 버튼 섹션 구현: Row(3 columns) + 공통 빌더 메서드 buildButtonColumn(color, icon, label)[^93]

📸 27:51

다음 파트에서 강의자는 버튼 섹션을 구현한다.[^94] 버튼 섹션은 “한 행(Row) 안에 3개의 동일한 형태의 열(Column)”로 구성되며, 각각이 아이콘+텍스트를 가진다.[^95]

  • 3개 버튼: CALL, ROUTE, SHARE[^96]
  • 간격은 균일하게 배치되어 있고, 색상은 앱의 **테마 기본 색(primary color)**를 사용한다.[^97]

3.6.1 중복 제거: buildButtonColumn 도우미 메서드 선언[^98]

3개 컬럼의 코드가 거의 동일하므로, 강의자는 buildButtonColumn(Color color, IconData icon, String label) 형태의 메서드를 만들어 재사용하겠다고 한다.[^99] 이 메서드는 다음을 반환한다.[^100]

  • Column(children:[ Icon(icon, color: color), Container(margin: EdgeInsets.only(top: 8), child: Text(label, style: TextStyle(..., color: color))) ])[^101]

여기서 텍스트는 폰트 크기(예: 12), 두께(예: w400) 등을 지정하고, 아이콘-텍스트 사이 간격을 위해 텍스트를 Container로 감싸 margin top을 준다.[^102]

[!TIP] 아이콘과 텍스트 사이 간격 만들기
아이콘 바로 아래 텍스트를 붙이면 답답해 보이므로, 텍스트를 Container로 감싸 margin: EdgeInsets.only(top: 8)처럼 “위 여백”을 두는 패턴을 사용한다.[^103]

3.6.2 테마 색상 가져오기: Theme.of(context).primaryColor[^104]

버튼 섹션은 기본 테마 색을 쓰므로, Theme.of(context).primaryColor로 color를 가져온다.[^105] 이때 context가 필요하다는 점을 짚는다.[^106]

3.6.3 buttonSection 위젯 만들기: Row + children 3개 + mainAxisAlignment.spaceEvenly[^107]

버튼 섹션은 Row로 만들고 children에 buildButtonColumn(color, Icons.call, 'CALL') 등 3개를 넣는다.[^108] 아이콘은 머티리얼 아이콘을 사용하며, 구글에서 Material Icons 검색하면 어떤 아이콘이 있는지 확인할 수 있다고 한다.[^109]

배치는 Row에서 가로 간격을 균등 분배해야 하므로, mainAxisAlignment: MainAxisAlignment.spaceEvenly를 적용해 CALL/ROUTE/SHARE가 예제처럼 가운데 균일 정렬되게 한다.[^110]

마지막으로 body Column에서 titleSection 아래에 buttonSection을 추가한다.[^111]

또한 코드에 노란 줄(린트)이 뜨는 경우 const를 붙일 수 있는 곳에 붙이라는 안내이며, 가능한 부분은 const를 추가해 경고를 줄인다고 언급한다.[^112]


3.7 텍스트(설명) 섹션 구현: Padding 위젯으로 감싸 여백 확보 + 긴 본문 텍스트 삽입[^113]

📸 38:23

다음 파트에서 강의자는 “마지막으로 텍스트 박스(설명 블록) 넣고, 이미지도 추가”하겠다고 말한다.[^114] 먼저 텍스트 섹션부터 만든다.[^115]

3.7.1 textSection 생성 위치와 삽입 위치[^116]

  • main.dart에서 buttonSection 아래에 textSection을 만들고[^117]
  • body Column의 children에서 buttonSection 아래에 textSection을 추가한다.[^118]

3.7.2 Container 대신 Padding 위젯 사용 이유[^119]

설명 텍스트는 좌우/상하 여백이 필요하므로, 컨테이너로 할 수도 있지만 강의자는 Padding 위젯을 직접 사용한다.[^120] 예제 화면처럼 양쪽에도 여백이 있고 위아래도 약간 띄워져 있기 때문이다.[^121]

  • Padding(padding: EdgeInsets.all(32), child: Text(...)) 형태로 구현한다.[^122]
  • 패딩 값을 32 정도로 맞추면 위쪽 섹션들과 라인이 맞는다고 말한다.[^123]

강의 중간에 Padding을 감싸는 작업(Alt+Enter 랩핑)이 IDE에서 가끔 원하는 형태로 자동 변환이 안 될 때가 있어 직접 감싸고, required 파라미터인 padding:을 넣어야 한다는 점을 설명한다.[^124] 또한 EdgeInsets.all은 전 방향 동일, only는 특정 방향 지정이라는 차이도 언급한다.[^125]


3.8 이미지 섹션 추가: assets 폴더 구성 + pubspec.yaml 등록 + Image.asset로 최상단 삽입[^126]

📸 44:12

이제 상단 이미지를 추가한다.[^127] 강의자는 예제 이미지(lake.jpg)를 다운로드 받아 사용하라고 안내한다.[^128] 블로그에 링크를 제공할 예정이며, 필요하면 구글에서 다른 큰 이미지를 받아도 된다고 말한다.[^129]

3.8.1 Android Studio “프로젝트 뷰” 전환과 Flutter 프로젝트 구조 혼란 해소[^130]

처음엔 Android 관련 폴더만 보일 수 있어 헷갈릴 수 있는데, Android Studio가 Flutter 프로젝트를 Android 프로젝트처럼 솔루션을 구성하는 경우가 있어서 그렇다고 설명한다.[^131] 왼쪽 상단 드롭다운에서 “Project”를 선택하면 전체 프로젝트 구조(예: lib/main.dart, pubspec.yaml)를 볼 수 있다고 안내한다.[^132]

3.8.2 assets 디렉토리 만들기: assets/images/ + 이미지 파일 복사[^133]

프로젝트 루트에 assets 디렉토리를 만들고, 그 아래 images 폴더를 만든다.[^134] 그리고 다운로드한 lake.jpgassets/images/에 복사해 넣는다.[^135]

3.8.3 pubspec.yaml 등록: 들여쓰기(2칸)와 상위/하위 경로 둘 다 명시해야 한다는 강조[^136]

이미지를 넣는 것만으로 끝이 아니라, pubspec.yaml(강의에서는 “퍼스펙…yaml”로 언급)에서 assets를 등록해야 한다.[^137] 이때 들여쓰기가 매우 중요하며, 칸이 맞지 않으면 에러가 난다고 강조한다.[^138]

강의자가 특히 강조한 포인트는 “상위 폴더만 등록하면 되는 게 아니라, 하위까지 인식되게 하려면 상/하 경로를 모두”처럼 보이는 설명이다.[^139] 즉 assets 섹션에 예를 들면 다음처럼 넣어야 한다는 뉘앙스다.[^140]

  • assets/
  • assets/images/

(강의자는 “둘 다 꼭 해줘야 된다”, “하나만 있으면 다른 걸 인식 못할 수 있어 에러가 난다”는 식으로 반복해 말한다.)[^141]

또한 yaml 수정 후에는 저장/반영을 해야 하며, 그렇지 않으면 이미지를 가져오지 못한다고 말한다.[^142]

[!WARNING] pubspec.yaml에서 가장 흔한 실수
들여쓰기(공백 수) 불일치로 에러가 나며, assets 경로를 올바르게 등록하지 않으면 Image.asset가 파일을 찾지 못한다.[^143]

3.8.4 Image.asset로 body 최상단(타이틀 섹션 위)에 이미지 추가 + width/height/fit 설정[^144]

body Column의 children 배열에서 titleSection 위에 이미지를 삽입한다.[^145]

  • Image.asset('assets/images/lake.jpg', width: 600, height: 240, fit: BoxFit.cover) 같은 형태로 작성한다.[^146]
  • 저장(핫 리로드)하면 이미지가 즉시 반영된다고 한다.[^147]

이로써 정적 레이아웃(이미지 + 타이틀 + 버튼 + 텍스트)이 완성되며, 다음 시간에는 사용자 상호작용을 하겠다고 예고한다.[^148]


3.9 사용자 상호작용 구현: 별 아이콘 토글(선택/해제) + 카운트 변경 + StatefulWidget + setState[^149]

📸 52:30

마지막 파트에서 강의자는 별 아이콘을 클릭하면 UI가 바뀌는 기능을 만든다.[^150] 목표 동작은 다음과 같다.[^151]

  • 별(빨간색 채움)을 클릭하면 **테두리 별(비어 있는 별)**로 바뀐다.[^152]
  • 숫자(예: 41)가 40으로 감소한다.[^153]
  • 다시 누르면 반대로 채워진 별로 돌아오고 숫자가 다시 증가한다.[^154]

이런 런타임 상태 변화는 StatelessWidget이 아니라 StatefulWidget이 필요하다고 명확히 말한다.[^155]

3.9.1 FavoriteWidget(StatefulWidget) 생성 + 자동 템플릿 생성 팁[^156]

클래스를 하나 만들고 StatefulWidget을 상속받는 FavoriteWidget을 만든다.[^157] IDE에서 탭(또는 자동완성)으로 기본 형태가 자동 생성된다고 설명한다.[^158]

State 클래스는 State<FavoriteWidget> 형태로 연결된다.[^159]

3.9.2 상태 변수 2개: _isFavorited(bool)와 _favoriteCount(int)[^160]

상태 관리를 위해 다음 변수를 둔다.[^161]

  • _isFavorited : 현재 별이 눌린(선택된) 상태인지 여부, 초기값 true(처음엔 선택된 상태로 시작)[^162]
  • _favoriteCount : 표시할 숫자 카운트, 초기값 41(강의에서는 40/41 전환을 보여주기 위해 설정)[^163]

3.9.3 토글 함수 _toggleFavorite() 로직 + setState로 리빌드 트리거[^164]

별을 눌렀을 때 호출될 함수 _toggleFavorite()를 만들고, 내부에서 상태를 반전/카운트 증감을 처리한다.[^165]

  • 만약 현재 _isFavorited == true라면, 카운트를 1 감소시키고 _isFavorited를 false로 바꾼다.[^166]
  • 그렇지 않으면(이미 false라면), 카운트를 1 증가시키고 _isFavorited를 true로 바꾼다.[^167]

그리고 중요한 점: 상태만 바꾸면 화면이 자동 갱신되지 않으므로, 변경 로직을 setState(() { ... })로 감싸 Flutter에게 “상태가 바뀌었으니 다시 빌드하라”고 알려야 한다고 설명한다.[^168] 강의자는 setState 내부에 익명 함수(클로저/람다식)가 들어간다고도 언급한다.[^169]

[!IMPORTANT] setState의 의미
상태 값 변경 자체보다, Flutter에 “다시 그려야 한다”를 알리는 트리거가 setState이며, setState로 감싸지 않으면 UI가 갱신되지 않는다.[^170]

3.9.4 UI 구성 변경: IconButton + 조건부 아이콘(채움/테두리) + count 텍스트를 Row로 묶기[^171]

이제 타이틀 섹션의 오른쪽(별+숫자)을 “고정 아이콘/고정 숫자”에서 “FavoriteWidget”으로 바꿔야 한다.[^172]

강의자는 IconButton을 사용해 클릭 가능한 별을 만든다.[^173]

  • onPressed: _toggleFavorite로 클릭 시 토글 함수 호출[^174]
  • 아이콘은 조건문으로 결정:
    • _isFavorited가 true면 Icons.star
    • false면 Icons.star_border (또는 유사한 테두리 아이콘)[^175]
  • 아이콘 색상은 빨강 계열로 지정한다.[^176]

또한 별 아이콘과 숫자를 옆에 붙여야 하므로, 두 요소를 Row(mainAxisSize: MainAxisSize.min, ...)로 감싸 “필요한 만큼만 공간을 차지하면서” 붙어 있도록 만든다.[^177] 숫자와 별 사이 간격이나 최소 폭 등을 위해 SizedBox 같은 위젯도 언급/사용한다.[^178]

결과적으로 별을 누를 때마다 아이콘 모양과 숫자가 바뀌는 것을 실제로 확인한다.[^179] 만약 바로 동작하지 않으면 디버그 재시작(Flutter hot restart 등)을 하라고 안내한다.[^180]

3.9.5 타이틀 섹션에 FavoriteWidget 삽입(기존 Icon/Text 제거)[^181]

기존 타이틀 섹션 Row의 두 번째/세 번째 child로 들어 있던 “별 아이콘 + 숫자 텍스트”를 주석 처리하거나 지우고, 그 위치에 FavoriteWidget()을 넣는다.[^182] 이렇게 해서 타이틀 섹션이 상태 기반 위젯을 포함하도록 변경된다.[^183]

3.9.6 상태 관리 확장 언급: BLoC / Provider / Riverpod[^184]

마무리로 강의자는 상태를 관리하는 방법이 StatefulWidget만 있는 것이 아니라, BLoC, Provider, Riverpod 같은 방법도 있으며 이런 것들을 쓰면 StatelessWidget 기반으로도 상태 관리를 구성할 수 있다고 언급한다.[^185] 다만 이번 강의에서는 기본 개념을 위해 StatefulWidget으로 구현했다고 정리한다.[^186]


4. 핵심 통찰[^187]

  1. 레이아웃 구현의 출발점은 “코드”가 아니라 **화면 분해(분석)**이며, Row/Column으로 큰 덩어리를 먼저 나누는 것이 가장 빠른 길로 제시된다.[^188]

    • 실행: 먼저 화면을 이미지/타이틀/버튼/텍스트처럼 섹션으로 나누고, 각 섹션 내부를 다시 Row/Column으로 도식화한다.[^189]
  2. Stack은 ‘겹침’이 있을 때만 고려한다는 판단 기준을 제공한다.[^190]

    • 실행: “요소가 다른 요소 위에 올라가 있는가?”를 먼저 체크하고, 아니라면 Column/Row로 끝낸다.[^191]
  3. 코드가 길어지는 문제는 “위젯 트리의 중첩”에서 오며, 이를 줄이기 위해 변수/함수/위젯 분리가 필요하다고 말한다.[^192]

    • 실행: 섹션 단위(titleSection, buttonSection, textSection)를 변수로 분리하거나, 반복 패턴은 buildButtonColumn 같은 도우미로 추출한다.[^193]
  4. Flutter에서 “상태 변화가 있는 UI”는 결국 StatefulWidget + setState로 귀결된다는 기본 원칙을 예제로 체감시키는 흐름이다.[^194]

    • 실행: 클릭 이벤트는 IconButton(onPressed: ...), 값 변경은 setState((){...}), 아이콘/텍스트는 상태 변수에 따라 조건부로 렌더링한다.[^195]
  5. assets 등록에서 pubspec.yaml 들여쓰기와 경로 지정이 실무적으로 자주 터지는 포인트라는 것을 반복 강조한다.[^196]

    • 실행: 이미지 파일을 폴더에 넣는 것과 별개로, pubspec.yaml의 assets 섹션을 정확히 맞추고 저장/반영 후 Image.asset를 사용한다.[^197]

5. 헷갈리는 용어 정리 (해당 시에만)[^198]

  • Row: 자식 위젯을 가로 방향으로 배치하는 위젯.[^199]
  • Column: 자식 위젯을 세로 방향으로 배치하는 위젯.[^200]
  • Expanded: Row/Column 내에서 남는 공간을 확장해 차지하도록 하는 위젯(예: 왼쪽 텍스트 영역이 폭을 대부분 차지).[^^201]
  • crossAxisAlignment: Row/Column에서 주축 반대 방향 정렬(예: Column에서 가로 방향 왼쪽 정렬을 start로 설정).[^202]
  • mainAxisAlignment: 주축 방향 정렬/간격 분배(예: Row에서 spaceEvenly로 균등 간격).[^203]
  • Padding / EdgeInsets: 위젯 외곽 여백을 주는 방식(전체 all, 특정 only 등).[^204]
  • StatefulWidget: 내부 상태가 바뀌며 UI가 달라지는 위젯을 만들 때 사용하는 형태.[^205]
  • setState(): 상태 변경을 Flutter에 알리고 리빌드를 트리거하는 함수.[^206]
  • IconButton: 아이콘을 버튼처럼 클릭 가능하게 만드는 위젯(onPressed 제공).[^207]
  • Theme.of(context).primaryColor: 현재 앱 테마의 기본(primary) 색상 값을 가져오는 방식.[^208]
  • pubspec.yaml: Flutter 프로젝트 설정 파일. assets(이미지 등) 리소스 등록을 여기서 한다.[^209]
  • BoxFit.cover: 이미지가 주어진 박스를 “덮도록” 크롭/스케일하는 방식.[^210]

참고(콘텐츠 정보)[^211]

  • 제목: Flutter 모바일 개발이 이렇게 쉽다고? (완전판)[^211]
  • 채널: yogingang soft[^211]
  • 길이: 67분 38초[^211]
  • 링크: https://www.youtube.com/watch?v=iMG6AOOJCIk[^211]

[^1]: @[00:04] “오늘은 플루 타이어에서 레이어스(레이아웃) 에 대해서 좀 알아 볼 건데요” [^2]: @[01:26] “초기에 이제 기본 코드를 한번 만들어보고 … 이 레이아웃 예제에 대해서 분석” [^3]: @[01:41] “행과 열을 식별… 그리드가 있는지… 겹치는 요소… 스택…”, @[03:19] “이미지… 텍스트 블록… 열로 하나의 컬럼으로 정렬”, @[04:49] “하향식 접근 방식을 취하는 것이 가장 쉽다” [^4]: @[05:26] “코드의 제(중첩)이 깊게… 시각적 혼란… 변수와 함수에 배치” [^5]: @[06:14] “함수… 퍼포먼스… 빌드를 다시… 리렌더… 함수… 퍼포먼스가 떨어지는”, @[06:36] “함수로 빼지 않고… 스테이트리스… 위젯… 클래스를…” [^6]: @[52:30] “사용자의 어떤 행동에 대해 반응… 별… 클릭…” [^7]: @[53:22] “스테이트풀 위젯이 필요”, @[56:59] “빌드를 다시… setState”, @[01:02:24] “토글… 넣어주시면” [^8]: @[00:09] “플루터 홈페이지… 레이아웃 관련된 예제… 따라해”, @[52:09] “다음 시간에는 … 상호작용” [^9]: @[00:09] “레이아웃 관련된 예제… 따라해 볼 거에요” [^10]: @[51:59] “기본적인 레이아웃이 … 됐어요”, @[52:09] “다음 시간에는 … 상호작용” [^11]: @[01:41] “행과 열… 그리드… 겹치는 요소… 스택… 탭…”, @[02:59] “패딩… 테두리… 확인” [^12]: @[05:26] “변수와 함수에 배치”, @[29:08] “도우미 메소드를 만들어… 콜 하는 형태” [^13]: @[52:35] “클릭… 빨갛게… 하얀색… 테두리… 40…”, @[53:22] “스테이트 풀”, @[57:08] “setState” [^14]: @[01:32] “분석을 좀 해볼게요” [^15]: @[01:05] “이미지 영역… 로우 2개… 텍스트 블록…” [^16]: @[00:15] “이미지가 1 위쪽”, @[00:27] “문자열… 설명…” [^17]: @[00:36] “오른쪽… 별표 아이콘… 숫자” [^18]: @[00:51] “아이콘 전화… 라우트… 쉐어…”, @[00:58] “d 스크립션… 텍스트 블럭” [^19]: @[01:05] “간단하게… 분리… 이미지 영역… 로우… 텍스트 블록…” [^20]: @[00:15] “이미지가… 위쪽” [^21]: @[03:49] “타이틀 섹션” [^22]: @[04:24] “버튼 섹션” [^23]: @[38:41] “텍스트 섹션…” [^24]: @[04:49] “도표… 구현… 하향식… 가장 쉽다” [^25]: @[01:41]~@[02:59] 레이아웃 분석 체크 항목 전개 [^26]: @[01:41] “다이어그램… 행과 열을 식별” [^27]: @[01:44] “그리드…”, @[02:18] “겹치는 요소…”, @[02:48] “탭…”, @[02:55] “패딩… 테두리…” [^28]: @[01:48] “그리드는 없는 것 같아요” [^29]: @[02:18] “겹치는 요소… 스택…”, @[02:41] “겹침 요소가 없으니까 스택…” [^30]: @[02:48] “탭이 필요한지… 필요 없죠” [^31]: @[02:55]~@[03:09] “패딩… 정렬… 센터 영역…” [^32]: @[02:04] “컬럼 하나…”, @[02:18] “1 컬럼에… 1 2 3 4” [^33]: @[03:19] “이미지… 텍스트 블록… 열로… 하나의 컬럼” [^34]: @[04:49]~@[06:52] 하향식/코드정리/성능 주의 [^35]: @[04:49] “하향식 접근… 가장 쉽다” [^36]: @[03:19] “위에서부터…” [^37]: @[05:37] “빌드… 중첩… 너무 길어지” [^38]: @[05:26] “변수와 함수… 배치” [^39]: @[06:07] “함수 배치… 주의” [^40]: @[06:20] “빌드를 다시… 리렌더” [^41]: @[06:36] “함수로 빼지 않고… 스테이트리스… 위젯… 클래스로…” [^42]: @[06:14]~@[06:36] 함수와 퍼포먼스 관련 설명 [^43]: @[06:36] “스테이트리스… 위젯을… 클래스를…” [^44]: @[06:58]~@[09:23] 프로젝트 생성/기본앱/실행 [^45]: @[07:06] “초기 프로젝트를 만들어 보면…” [^46]: @[07:44] “카운트… 버튼 클릭… 증가” [^47]: @[08:02] “아주 간단하게… 하나의 앱” [^48]: @[08:16] “카운터… 빼보니” [^49]: @[09:18] “중앙에 헬로 월드”, @[09:18] “타이틀… 웰컴투…” [^50]: @[08:29] “디바이스 매니저… 에뮬레이터…” [^51]: @[08:46]~@[09:07] 디바이스 매니저 패널 크기 조정 [^52]: @[09:27] “기본적인 예제를 한번 만들어봤습니다” [^53]: @[09:45]~@[27:40] 타이틀 섹션 구현 전체 흐름 [^54]: @[09:38] “지난 시간에 이어서…” [^55]: @[09:49] “제목… 3개의 칠드런…” [^56]: @[10:55] “로우… 3개의 칠드런” [^57]: @[04:00] “첫 번째… 스페인디드(Expanded)로 래핑”, @[12:47] “텍스트 영역… 익스펜디드…” [^58]: @[09:55] “아이콘 영역” [^59]: @[10:01] “다시 텍스트… 숫자” [^60]: @[10:26] “변수를 선언… 타이틀 섹션” [^61]: @[10:30]~@[10:38] “titleSection…” [^62]: @[10:49]~@[10:55] “컨테이너… 차일드… 로우” [^63]: @[11:24] “컨테이너… 세번째…” [^64]: @[11:39] “3개의 칠드런… 구현” [^65]: @[13:12]~@[15:04] Expanded/Column 구성 [^66]: @[13:12]~@[13:24] “wrap with widget… Expanded” [^67]: @[13:36]~@[14:05] “컬럼으로…” [^68]: @[14:20] “children…” [^69]: @[15:54]~@[16:04] “텍스트 스타일… 폰트 웨이트… 볼드” [^70]: @[18:35]~@[18:52] “컬러… 그레이… 500” [^71]: @[15:43]~@[17:43] 제목/주소 텍스트 입력 언급 [^72]: @[14:08] “초기에… 피그 보고 바로… 눈에 들어오지 않습니다” [^73]: @[16:25]~@[16:43] Center→Column 교체 후 titleSection 삽입 [^74]: @[16:29] “여기를 바꿔서… 컬럼… children… titleSection” [^75]: @[16:52]~@[17:16] “포지션/크기… 아무것도 안 들어 있는 상태…” [^76]: @[20:27]~@[22:25] 아이콘/파라미터 설명 [^77]: @[20:34]~@[20:53] “아이콘… star” [^78]: @[20:53]~@[21:05] “컬러… Colors.red…” [^79]: @[21:22]~@[22:25] Ctrl+Shift+I, named/positional 설명 [^80]: @[22:18] “중괄호 바깥쪽… 포지션…” [^81]: @[21:43]~@[22:11] “이름을 지정… size color…” [^82]: @[21:22]~@[21:36] “컨트롤 쉬프트 i… 설명” [^83]: @[22:48]~@[23:24] 숫자 텍스트 추가 [^84]: @[22:55]~@[23:04] “텍스트… 41” [^85]: @[23:16]~@[23:24] “40p(41) … 나올” [^86]: @[24:21]~@[27:40] 패딩/정렬/간격 [^87]: @[23:34] “정렬…” [^88]: @[24:21]~@[24:47] “패딩…” [^89]: @[25:05] “모든 곳에… 32… 중앙으로” [^90]: @[25:33]~@[26:02] “crossAxisAlignment… start…” [^91]: @[26:48]~@[27:40] “only… bottom… 8” [^92]: @[27:40] “변경된 걸 알 수” [^93]: @[27:51]~@[37:35] 버튼 섹션 구현 [^94]: @[27:51] “버튼 섹션… 구현” [^95]: @[28:05] “로우… 3개의 칠드런… 아이콘/텍스트” [^96]: @[28:00] “CALL… ROUTE… SHARE” [^97]: @[28:52] “기본 색상… 프라이머리 컬러” [^98]: @[29:08] “도우미 메소드…” [^99]: @[30:00]~@[30:20] 메서드 시그니처 설명 [^100]: @[30:47]~@[30:55] “컬럼을 리턴” [^101]: @[31:20]~@[33:44] 아이콘/컨테이너/텍스트 구성 [^102]: @[33:14]~@[33:30] fontSize/weight, @[32:05]~@[32:28] 간격 필요, margin [^103]: @[31:55]~@[32:13] “아이콘… 텍스트 사이… 간격…” [^104]: @[34:08]~@[34:26] “Theme.of(context)… primaryColor” [^105]: @[34:12]~@[34:36] 테마 색 가져오기 [^106]: @[34:22] “context가 필요” [^107]: @[34:40]~@[37:28] buttonSection Row 구성 및 spaceEvenly [^108]: @[34:51]~@[35:37] CALL/ROUTE/SHARE 구성 [^109]: @[35:54] “매트리얼 아이콘… 구글에서 검색” [^110]: @[37:12]~@[37:28] “mainAxisAlignment… spaceEvenly” [^111]: @[36:43]~@[36:54] “titleSection 밑에… buttonSection” [^112]: @[37:39]~@[38:03] “노란색 줄… const…” [^113]: @[38:23]~@[44:02] 텍스트 섹션 구현 [^114]: @[38:23] “마지막으로 텍스트 박스… 이미지…” [^115]: @[38:41] “텍스트 섹션…” [^116]: @[39:11]~@[39:28] 위치/추가 설명 [^117]: @[39:14]~@[39:28] “버튼 섹션 밑에다가… textSection” [^118]: @[40:51]~@[41:07] “textSection 추가” [^119]: @[39:36]~@[39:44] 패딩 필요성 [^120]: @[39:44] “컨테이너로 해도… 그냥 패딩…” [^121]: @[39:36]~@[39:44] “위 아래… 양쪽… 패딩” [^122]: @[40:07]~@[40:24] “EdgeInsets… (32로)” [^123]: @[43:45]~@[43:55] “32 정도… 맞는 것 같” [^124]: @[42:47]~@[43:13] “required… padding…” [^125]: @[43:18]~@[43:27] “all… only…” [^126]: @[44:12]~@[51:44] 이미지/에셋 추가 전 과정 [^127]: @[44:12] “이미지를 하나 추가” [^128]: @[44:26]~@[44:56] lake.jpg 다운로드 안내 [^129]: @[44:52]~@[44:56] “다른 큰 이미지… 구글…” [^130]: @[45:17]~@[46:07] 프로젝트 뷰/구조 [^131]: @[45:34]~@[45:49] “안드로이드만… 솔루션 구성” [^132]: @[45:49]~@[46:07] “Project… 전체… lib… main.dart…” [^133]: @[46:28]~@[47:49] 폴더 생성/복사 [^134]: @[46:28]~@[46:47] “assets… images…” [^135]: @[47:39]~@[47:49] “lake.jpg… paste” [^136]: @[48:24]~@[49:29] pubspec.yaml 들여쓰기/경로 강조 [^137]: @[48:15] “이렇게 끝나는 게 아니고… yaml… 지정” [^138]: @[48:29]~@[48:46] “2칸… 못해서… 에러” [^139]: @[48:52]~@[49:13] “하위… 인식… 이것만… 아니고…” [^140]: @[48:46]~@[49:01] assets 경로 추가 설명 [^141]: @[48:55]~@[49:29] “꼭… 두개 다… 하나만… 에러” [^142]: @[50:12]~@[50:26] “깜빡… 안그러면 이미지를…” [^143]: @[48:29]~@[49:29] yaml 에러/인식 실패 경고 맥락 [^144]: @[50:26]~@[51:20] Image.asset 삽입/속성 [^145]: @[50:26]~@[50:36] “titleSection 바로 위쪽에” [^146]: @[50:36]~@[51:20] width/height/fit cover [^147]: @[51:38]~@[51:44] “핫 리로드 통해서 이미지 적용” [^148]: @[52:05]~@[52:09] “레이아웃 작업 끝… 다음… 상호작용” [^149]: @[52:15]~@[01:06:37] 상호작용 구현 전체 [^150]: @[52:30]~@[52:44] 별 클릭 시 변화 설명 [^151]: @[52:35]~@[52:44] 색/테두리/숫자 변화 시나리오 [^152]: @[52:35] “빨갛게… 하얀색… 테두리” [^153]: @[52:39]~@[52:44] “40일이… 40으로…” [^154]: @[52:44]~@[52:52] “다시 누르면 반대로…” [^155]: @[53:22]~@[53:28] “상태가 변하는 것은 스테이트풀…” [^156]: @[53:33]~@[54:15] 클래스 생성/탭 자동 생성 [^157]: @[53:41]~@[53:54] “FavoriteWidget… StatefulWidget…” [^158]: @[54:04]~@[54:10] “탭을 누르면 자동…” [^159]: @[54:24]~@[54:31] State 연결 언급 [^160]: @[54:38]~@[55:18] bool/int 변수 초기값 [^161]: @[54:38] “눌렀는지… 분리형 값” [^162]: @[54:47] “기본값… true” [^163]: @[55:06]~@[55:18] “count… 40/41… 기본값” [^164]: @[55:26]~@[57:24] toggle 함수와 setState [^165]: @[55:26] “호출할 함수를… 토글…” [^166]: @[55:36]~@[55:49] “이미… 카운터 -1…” [^167]: @[56:16]~@[56:36] else 분기 및 리팩토링 언급 [^168]: @[56:59]~@[57:24] “빌드를 다시… setState…” [^169]: @[57:48]~@[57:55] “클로저… 람다 식…” [^170]: @[56:59]~@[57:24] setState 필요성 설명 [^171]: @[58:22]~@[01:03:43] Row/IconButton/조건 아이콘/텍스트 배치 [^172]: @[01:04:05]~@[01:04:23] “아이콘과 텍스트… 고정… 고정시키지 않을…” [^173]: @[01:00:32]~@[01:02:15] IconButton 구성 흐름 [^174]: @[01:02:15]~@[01:02:37] “onPressed… 토글…” [^175]: @[01:01:06]~@[01:01:20] “스타… 아니면… 스타 보더…” [^176]: @[01:02:02]~@[01:02:06] “컬러… red…” [^177]: @[58:30]~@[59:09] Row로 감싸고 mainAxisSize min 등 언급 [^178]: @[01:02:51]~@[01:03:12] “SizedBox… 공간… 지정” [^179]: @[01:05:23]~@[01:05:33] “누르면 변경…” [^180]: @[01:05:10]~@[01:05:23] 디버그/재시작 관련 언급 [^181]: @[01:04:28]~@[01:05:03] titleSection에서 FavoriteWidget로 교체 [^182]: @[01:04:40]~@[01:04:55] “주석… 지우… FavoriteWidget 넣” [^183]: @[01:05:33]~@[01:05:54] “고정… 변경… 아이콘… 스타/스타보더” [^184]: @[01:06:51]~@[01:07:10] 상태관리 대안 언급 [^185]: @[01:06:51] “블럭… 프로바이더… 리버팟…” [^186]: @[01:06:42]~@[01:06:51] “스테이트풀 위젯…” [^187]: @[01:41]~@[06:52], @[52:15]~@[01:07:10] 전반 결론 취합 [^188]: @[01:41] “행과 열을 식별…”, @[03:19] “큰 틀… 컬럼…” [^189]: @[01:05]~@[01:26] “9분할…”, @[03:12] “큰 6(큰 것) 식별” [^190]: @[02:18]~@[02:44] 스택 필요 조건 [^191]: @[02:24] “겹치는 요소… 스택…”, @[02:41] “없으니까…” [^192]: @[05:26]~@[05:48] 중첩/시각적 혼란 [^193]: @[10:26] “변수 선언…”, @[29:08] “도우미 메소드…” [^194]: @[53:22]~@[53:28] stateful 필요, @[56:59]~@[57:24] setState 필요 [^195]: @[01:02:15] onPressed, @[01:01:06] 조건 아이콘, @[01:03:37] 카운트 텍스트 [^196]: @[48:29]~@[49:29] 들여쓰기/경로 강조 반복 [^197]: @[50:26]~@[51:20] Image.asset 적용 [^198]: @[01:41]~@[01:57] Row/Column, @[53:22] stateful, @[34:12] Theme 등 용어가 직접 등장/사용됨 [^199]: @[10:55] “로우…” [^200]: @[13:36] “컬럼…” [^201]: @[13:12]~@[13:24] Expanded 래핑 [^202]: @[25:33]~@[25:55] crossAxisAlignment start [^203]: @[37:16]~@[37:28] mainAxisAlignment spaceEvenly [^204]: @[43:18]~@[43:27] EdgeInsets all/only [^205]: @[53:22] “스테이트풀…” [^206]: @[56:59]~@[57:24] “setState” [^207]: @[01:00:32]~@[01:02:15] IconButton 구성 [^208]: @[34:12]~@[34:36] Theme.of(context).primaryColor [^209]: @[46:07] pubspec.yaml 언급, @[48:15] “yaml에서 지정” [^210]: @[51:11]~@[51:20] “BoxFit.cover” [^211]: 사용자 제공 메타데이터(제목/채널/길이/링크): “Flutter 모바일 개발이 이렇게 쉽다고? (완전판) / yogingang soft / 67분 38초 / https://www.youtube.com/watch?v=iMG6AOOJCIk”

← 프로젝트에서 보기