๋ฐ˜์‘ํ˜•

๋ฐฐ๊ฒฝ ์ง€์‹


Flutter ๊ตฌ์กฐ

๐Ÿ’ก Dart๋Š” Just-In-Time(JIT) ์ปดํŒŒ์ผ๊ณผ Ahead-Of-Time(AOT) ์ปดํŒŒ์ผ์„ ๋ชจ๋‘ ์ง€์›ํ•œ๋‹ค. JIT ์ปดํŒŒ์ผ์€ ๊ฐœ๋ฐœ ๋ชจ๋“œ์— ์‚ฌ์šฉ๋ผ์„œ Hot Reload ๊ฐ™์€ ๋น ๋ฅธ ํ”ผ๋“œ๋ฐฑ ๋ฃจํ”„๋ฅผ ์ œ๊ณตํ•œ๋‹ค. AOT ์ปดํŒŒ์ผ์€ ๋ฐฐํฌ ๋ชจ๋“œ์— ์‚ฌ์šฉ๋ผ์„œ ์ปดํŒŒ์ผ๋œ ์ฝ”๋“œ๊ฐ€ ๊ธฐ๊ธฐ์— ๋” ์ตœ์ ํ™”๋œ ์ƒํƒœ๋กœ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“ ๋‹ค.

 

์ด๋ฏธ์ง€ ์ถœ์ฒ˜ - Flutter ๊ณต์‹ ๋ฌธ์„œ

  • ๋„ค์ดํ‹ฐ๋ธŒ ๊ฐœ๋ฐœ์—์„  ์šด์˜์ฒด์ œ(OS)์™€ ์ง์ ‘ ์ƒํ˜ธ์ž‘์šฉํ•˜์—ฌ ๋ฒ„ํŠผ, ํ…์ŠคํŠธ ๊ฐ™์€ UI ์š”์†Œ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
  • Flutter๋Š” ๋„ค์ดํ‹ฐ๋ธŒ ํ”Œ๋žซํผ์˜ ์œ„์ ฏ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ์ž์ฒด์ ์œผ๋กœ UI๋ฅผ ๋ Œ๋”๋ง ํ•œ๋‹ค. ์ด๋Š” ๋น„๋””์˜ค ๊ฒŒ์ž„ ์—”์ง„์˜ ์ž‘๋™ ๋ฐฉ์‹๊ณผ ์œ ์‚ฌํ•˜๋‹ค.
  • Dart๋กœ ์ž‘์„ฑ๋œ ์ฝ”๋“œ๋Š” Flutter ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ UI๋ฅผ ์ •์˜ํ•˜๊ณ , ์‹ค์ œ ํ™”๋ฉด์— ๋ Œ๋”๋ง ํ•˜๋Š” ์ž‘์—…์€ C++๋กœ ์ž‘์„ฑ๋œ Flutter ์—”์ง„(Skia)์ด ๋‹ด๋‹นํ•œ๋‹ค.
  • ์ด ์—”์ง„์ด ๋ชจ๋“  UI ์š”์†Œ๋ฅผ ๊ทธ๋ฆฌ๊ธฐ ๋•Œ๋ฌธ์— ๋„ค์ดํ‹ฐ๋ธŒ ์œ„์ ฏ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค.
  • Dart ์ฝ”๋“œ์™€ Flutter SDK๋Š” ๋„ค์ดํ‹ฐ๋ธŒ ARM ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ์ปดํŒŒ์ผ๋ผ์„œ ๊ฐ ํ”Œ๋žซํผ์˜ ์‹คํ–‰ ํŒŒ์ผ๋กœ ํŒจํ‚ค์ง• ๋œ๋‹ค.
  • ์ด๋ ‡๊ฒŒ ๋งŒ๋“ค์–ด์ง„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ๊ฐ ํ”Œ๋žซํผ์—์„œ ๋™์ผํ•œ Flutter ์—”์ง„์„ ์‹คํ–‰ํ•˜์—ฌ UI๋ฅผ ๋ Œ๋”๋ง ํ•œ๋‹ค.
  • ์ฆ‰, Flutter ์œ„์ ฏ์€ ์šด์˜์ฒด์ œ๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๋„ค์ดํ‹ฐ๋ธŒ ์œ„์ ฏ์ด ์•„๋‹ˆ๋ผ Flutter ์—”์ง„์ด ๊ทธ๋ฆฐ ๊ฒƒ์ด๋‹ค.
  • ์ด๋Ÿฌํ•œ ํŠน์ง•์œผ๋กœ Flutter๋Š” iOS, Android, Windows ๋“ฑ ๋‹ค์–‘ํ•œ ํ”Œ๋žซํผ์—์„œ ๋™์ผํ•˜๊ฒŒ ์ž‘๋™ํ•œ๋‹ค.
  • ํ•˜์ง€๋งŒ Flutter ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ UI๋Š” ๋„ค์ดํ‹ฐ๋ธŒ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๊ณผ ์™„์ „ํžˆ ๋™์ผํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ๋‹ค.
  • ์ด๋กœ ์ธํ•ด Flutter ์•ฑ์ด ์ž์—ฐ์Šค๋Ÿฝ์ง€ ์•Š์„ ์ˆ˜ ์žˆ๋Š” ๋‹จ์ ์ด ์žˆ์ง€๋งŒ, ๊ฐœ๋ฐœ์ž๊ฐ€ ๋ชจ๋“  ํ”ฝ์…€์„ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜์—ฌ ๋‹ค์–‘ํ•œ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•๊ณผ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ์žฅ์ ์ด ์žˆ๋‹ค.

 

Flutter vs React Native

๐Ÿ’ก Flutter = ์ผ๊ด€์„ฑ, React Native = ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์ƒํƒœ๊ณ„

 

Flutter

  • ์—”์ง„ ๋ฐ UI ๋ Œ๋”๋ง: Flutter๋Š” ๊ฒŒ์ž„ ์—”์ง„์ฒ˜๋Ÿผ ์ž‘๋™ํ•˜์—ฌ ๋ชจ๋“  UI๋ฅผ ์ž์ฒด ์—”์ง„์œผ๋กœ ๋ Œ๋”๋ง ํ•œ๋‹ค. ์ฆ‰, iOS๋‚˜ Android์˜ ๋„ค์ดํ‹ฐ๋ธŒ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ง์ ‘ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ , ๋ชจ๋“  ๊ทธ๋ž˜ํ”ฝ์„ ์ž์ฒด์ ์œผ๋กœ ๋ Œ๋”๋ง ํ•œ๋‹ค. ์ด ๋ฐฉ์‹์€ ๋ชจ๋“  ํ”Œ๋žซํผ์—์„œ ๋™์ผํ•œ UI๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ๋Š” ์žฅ์ ์ด ์žˆ์ง€๋งŒ, ๋„ค์ดํ‹ฐ๋ธŒ ์œ„์ ฏ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋Š” ๋‹จ์ ์ด ์žˆ๋‹ค.
  • ์„ฑ๋Šฅ: Flutter๋Š” Dart๋กœ ์ž‘์„ฑ๋œ ์ฝ”๋“œ๋ฅผ ๋„ค์ดํ‹ฐ๋ธŒ ARM ์ฝ”๋“œ๋กœ ์ง์ ‘ ์ปดํŒŒ์ผํ•˜์—ฌ ์ค‘๊ฐ„ ๊ณ„์ธต์ด ํ•„์š”ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์„ฑ๋Šฅ ๋ฉด์—์„œ ๋” ๋งŽ์€ ์ด์ ์„ ์ œ๊ณตํ•œ๋‹ค.
  • ๋””์ž์ธ ์œ ์—ฐ์„ฑ: Flutter๋Š” ๋ชจ๋“  ํ”ฝ์…€์„ ์„ธ๋ฐ€ํ•˜๊ฒŒ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์–ด ๋””์ž์ธ ์š”๊ตฌ์‚ฌํ•ญ์ด ์ •๊ตํ•œ ์ƒํ™ฉ์—์„œ ํ›จ์”ฌ ์œ ๋ฆฌํ•˜๋‹ค. ๋˜ํ•œ, Flutter์˜ ์œ„์ ฏ ๊ธฐ๋ฐ˜ ์ธํ„ฐํŽ˜์ด์Šค๋Š” ์ž์œ ๋กœ์šด ๋””์ž์ธ ๊ตฌํ˜„์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ด ์ค€๋‹ค.

 

React Native

  • ๋„ค์ดํ‹ฐ๋ธŒ ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ: React Native๋Š” JavaScript๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์šด์˜์ฒด์ œ์™€ ์ƒํ˜ธ์ž‘์šฉํ•˜๋ฉฐ, ์šด์˜์ฒด์ œ๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๋„ค์ดํ‹ฐ๋ธŒ ์ปดํฌ๋„ŒํŠธ์™€ ์œ„์ ฏ์„ ์ƒ์„ฑํ•œ๋‹ค. ์ด๋กœ ์ธํ•ด iOS์™€ Android์—์„œ ๊ฐ๊ฐ ๋‹ค๋ฅธ ๋ชจ์–‘์˜ UI ์š”์†Œ๊ฐ€ ์ƒ์„ฑ๋  ์ˆ˜ ์žˆ๋‹ค. ์ด๋Š” ์šด์˜์ฒด์ œ์˜ ์Šคํƒ€์ผ์„ ํ™œ์šฉํ•  ๋• ์œ ๋ฆฌํ•˜์ง€๋งŒ, ๋””์ž์ธ ์ผ๊ด€์„ฑ ์œ ์ง€์—” ๋ถˆ๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์„ฑ๋Šฅ: React Native๋Š” JavaScript๋กœ ์ž‘์„ฑ๋œ ์ฝ”๋“œ๋ฅผ ๋„ค์ดํ‹ฐ๋ธŒ ์ฝ”๋“œ์™€ ์ƒํ˜ธ์ž‘์šฉํ•˜๊ธฐ ์œ„ํ•ด ๋ธŒ๋ฆฌ์ง€(Bridge)๋ผ๋Š” ์ค‘๊ฐ„ ๊ณ„์ธต์„ ์‚ฌ์šฉํ•˜๋ฉฐ, ์ด ๊ณผ์ •์—์„œ ์„ฑ๋Šฅ ์ €ํ•˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์ปค๋ฎค๋‹ˆํ‹ฐ ๋ฐ ์ƒํƒœ๊ณ„: React Native๋Š” JavaScript๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ํ’๋ถ€ํ•œ ์ปค๋ฎค๋‹ˆํ‹ฐ์™€ ์ƒํƒœ๊ณ„๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค. ๋‹ค์–‘ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์žฅ์ ์ด ์žˆ์ง€๋งŒ, ์œ ์ง€๋ณด์ˆ˜์— ๋” ๋งŽ์€ ๋…ธ๋ ฅ์ด ๋“ค์–ด๊ฐ€๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•˜๊ธฐ๋„ ํ•œ๋‹ค.

 

 

DevTools


Flutter Inspector

ํฌ๋กฌ ๊ฐœ๋ฐœ์ž ๋„๊ตฌ์˜ ์š”์†Œ ํƒญ์ฒ˜๋Ÿผ Flutter๋„ UI๋ฅผ ์‹œ๊ฐ์ ์œผ๋กœ ๋ถ„์„ํ•˜๊ณ  ์ดํ•ดํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” Flutter Inspector๋ผ๋Š” ๋„๊ตฌ๋ฅผ ์ œ๊ณตํ•œ๋‹ค. ๊ฐ ์œ„์ ฏ์˜ ์†์„ฑ, ๋ ˆ์ด์•„์›ƒ, ๋ Œ๋”๋ง ๋“ฑ์„ ์‹œ๊ฐ์ ์œผ๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์™ผ์ชฝ ๋์— ์žˆ๋Š” Select Widget Mode ๋ฒ„ํŠผ์— ์ฒดํฌํ•˜๋ฉด ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์˜ ํŠน์ • ์œ„์ ฏ์„ ํด๋ฆญํ•ด์„œ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋‹ค. ํฌ๋กฌ ๊ฐœ๋ฐœ์ž ๋„๊ตฌ์˜ ์š”์†Œ ์„ ํƒ ๊ธฐ๋Šฅ๊ณผ ๋น„์Šทํ•˜๋‹ค.

 

์šฐ์ธก ์ƒ๋‹จ ๋ฉ”๋‰ด ๋ชจ์Œ 2๋ฒˆ์งธ ๋ฒ„ํŠผ์„ ์ฒดํฌํ•˜๋ฉด ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์— ์˜ค๋ฒ„๋ ˆ์ด ๊ฐ€์ด๋“œ๋ผ์ธ์ด ํ‘œ์‹œ๋œ๋‹ค. ์ด ๊ธฐ๋Šฅ์„ ํ†ตํ•ด UI ์š”์†Œ๋“ค์ด ๊ทธ๋ฆฌ๋“œ์— ๋งž์ถฐ์ ธ ์žˆ๋Š”์ง€, ์ •๋ ฌ์€ ์ œ๋Œ€๋กœ ๋๋Š”์ง€ ๋“ฑ์„ ์‹œ๊ฐ์ ์œผ๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์ž๋™ ํฌ๋งคํŒ…

์•ˆ๋“œ๋กœ์ด๋“œ ์ŠคํŠœ๋””์˜ค Settings - Languages & Frameworks - Flutter ๋ฉ”๋‰ด์—์„œ ์•„๋ž˜ ๋‘ ํ•ญ๋ชฉ์— ์ฒดํฌํ•˜๋ฉด ์ €์žฅํ•  ๋•Œ๋งˆ๋‹ค ์ž๋™์œผ๋กœ ํฌ๋งคํŒ…ํ•ด ์ฃผ๊ณ  import ๊ตฌ๋ฌธ์„ ์ •๋ฆฌํ•ด ์ค€๋‹ค.

 

์ฝ”๋“œ ์•ก์…˜

๋งˆ์šฐ์Šค ์ปค์„œ๋ฅผ ์œ„์ ฏ์— ๋†“๊ณ  โŒฅ Enter ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด ํ•ด๋‹น ์œ„์ ฏ์— ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ฝ”๋“œ ์•ก์…˜ ๋ชฉ๋ก์ด ํ‘œ์‹œ๋œ๋‹ค. ์„ ํƒํ•œ ์œ„์ ฏ์„ ๋‹ค๋ฅธ ์œ„์ ฏ์œผ๋กœ ๊ฐ์‹ธ๊ฑฐ๋‚˜, ์ด๋ฏธ ๊ฐ์‹ธ์ ธ ์žˆ๋Š” ์œ„์ ฏ์„ ์ œ๊ฑฐํ•˜๋Š” ๋“ฑ์˜ ๋‹ค์–‘ํ•œ ์•ก์…˜์„ ์ œ๊ณตํ•œ๋‹ค.

 

VSCode๋Š” ์ €์žฅํ•  ๋•Œ const ์ˆ˜์ •์ž๋ฅผ ์ž๋™์œผ๋กœ ์ถ”๊ฐ€ํ•ด ์ฃผ๋Š” ๊ธฐ๋Šฅ์ด ์žˆ์ง€๋งŒ ์•„์‰ฝ๊ฒŒ๋„ ์•ˆ๋“œ๋กœ์ด๋“œ ์ŠคํŠœ๋””์˜ค๋Š” ๊ทธ๋Ÿฐ ๊ธฐ๋Šฅ์ด ์—†๋‹ค. const ์ˆ˜์ •์ž ์—ญ์‹œ ์ฝ”๋“œ ์•ก์…˜์„ ํ†ตํ•ด์„œ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

๐Ÿ’ก ์ฝ”๋“œ ์•ก์…˜ ๋ชฉ๋ก์ด ํ‘œ์‹œ๋œ ์ƒํƒœ์—์„œ ํ‚ค์›Œ๋“œ๋ฅผ ์ž…๋ ฅํ•˜๋ฉด ์›ํ•˜๋Š” ์•ก์…˜์„ ์ง์ ‘ ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

์œ„์ ฏ / ๋ฉ”์„œ๋“œ ์ถ”์ถœ

โŒฅ โŒ˜ W ๋‹จ์ถ•ํ‚ค๋ฅผ ๋ˆ„๋ฅด๊ฑฐ๋‚˜, ์ถ”์ถœํ•˜๊ณ  ์‹ถ์€ ๊ณณ์—์„œ ๋งˆ์šฐ์Šค ์šฐํด๋ฆญ → Refactor → Extract Flutter Widget ๋ฉ”๋‰ด๋ฅผ ํด๋ฆญํ•˜๋ฉด ํ˜„์žฌ ์ปค์„œ๊ฐ€ ๊ฐ€๋ฆฌํ‚ค๋Š” ์œ„์ ฏ์„ ๋ณ„๋„์˜ ํด๋ž˜์Šค๋กœ ์ถ”์ถœํ•  ์ˆ˜ ์žˆ๋‹ค. Extract Method ๋ฉ”๋‰ด๋ฅผ ํด๋ฆญํ•˜๋ฉด ์„ ํƒ๋œ ์ฝ”๋“œ๋ฅผ ๋ฉ”์„œ๋“œ๋กœ ์ถ”์ถœํ•œ๋‹ค.

 

์ฐธ๊ณ ๋กœ โŒƒ T ๋‹จ์ถ•ํ‚ค๋ฅผ ๋ˆ„๋ฅด๋ฉด ๋‚˜์˜ค๋Š” Refactor This ๋ฉ”๋‰ด์—์„œ ์›ํ•˜๋Š” ๋ฆฌํŒฉํ† ๋ง ํ•ญ๋ชฉ์„ ์ง์ ‘ ์„ ํƒํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

 

 

Flutter ๊ธฐ์ดˆ ๋‚ด์šฉ ์ •๋ฆฌ


Hello World

// lib/main.dart

import 'package:flutter/material.dart';

void main() {
  runApp(App());
}

// StatelessWidget์„ ์ƒ์†๋ฐ›์•„ ์ƒํƒœ๊ฐ€ ์—†๋Š” Flutter ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ UI ์š”์†Œ๋ฅผ ์ •์˜ํ•œ๋‹ค.
class App extends StatelessWidget {
  @override
  // build ๋ฉ”์„œ๋“œ๋Š” ์œ„์ ฏ์˜ UI๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. Flutter๊ฐ€ ์ด ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•ด์„œ UI๋ฅผ ๊ตฌ์„ฑํ•œ๋‹ค
  Widget build(BuildContext context) {
    // MaterialApp์€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋ฃจํŠธ ์œ„์ ฏ์œผ๋กœ ์•ฑ์˜ ์ „๋ฐ˜์ ์ธ ๋””์ž์ธ, ํ…Œ๋งˆ, ๋ผ์šฐํŒ… ๋“ฑ์„ ๊ด€๋ฆฌํ•œ๋‹ค
    return MaterialApp(
      // Scaffold ์œ„์ ฏ์€ ํ™”๋ฉด์˜ ๊ธฐ๋ณธ์ ์ธ ๋ ˆ์ด์•„์›ƒ์„ ์ œ๊ณตํ•˜๋ฉฐ, Material ๋””์ž์ธ ๊ตฌ์กฐ๋ฅผ ๋”ฐ๋ฅธ๋‹ค
      // ์•ฑ๋ฐ”, ๋ณธ๋ฌธ, ํ”Œ๋กœํŒ… ์•ก์…˜ ๋ฒ„ํŠผ ๋“ฑ์„ ํฌํ•จํ•  ์ˆ˜ ์žˆ๋‹ค.
      home: Scaffold(
        // AppBar ์œ„์ ฏ์€ ์ƒ๋‹จ ํ—ค๋” ์˜์—ญ์„ ์ •์˜ํ•œ๋‹ค.
        appBar: AppBar(
          // Text ์œ„์ ฏ์„ ์‚ฌ์šฉํ•ด ํ™”๋ฉด์— ํ…์ŠคํŠธ๋ฅผ ํ‘œ์‹œํ•œ๋‹ค.
          title: Text('Hello Flutter'),
        ),
        body: Center(
          // Center ์œ„์ ฏ์€ ์ž์‹ ์œ„์ ฏ์„ ์ค‘์•™์— ๋ฐฐ์น˜ํ•œ๋‹ค.
          child: Text('Hello World'),
        ),
      ),
    );
  }
}

 

์ถœ๋ ฅ ํ™”๋ฉด

โ‘  ํ”Œ๋Ÿฌํ„ฐ๋Š” ๋ชจ๋“  ๊ฒƒ์ด ์œ„์ ฏ์œผ๋กœ ๊ตฌ์„ฑ๋ผ ์žˆ๋‹ค. ๋ ˆ๊ณ ์ฒ˜๋Ÿผ ์œ„์ ฏ์„ ์กฐ๋ฆฝํ•˜์—ฌ UI๋ฅผ ๊ตฌ์ถ•ํ•œ๋‹ค.

 

โ‘ก ์œ„์ ฏ์€ Flutter์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ๊ณผ ์ปค๋ฎค๋‹ˆํ‹ฐ์—์„œ ์ œ์ž‘๋œ ๊ฒƒ๋“ค์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

 

โ‘ข ์œ„์ ฏ์€ ํฌ๊ฒŒ ์ƒํƒœ๋ฅผ ๊ฐ€์ง€์ง€ ์•Š๋Š” StatelessWidget๊ณผ ์ƒํƒœ๋ฅผ ๊ฐ€์ง€๋Š” StatefulWidget์œผ๋กœ ๋‚˜๋‰œ๋‹ค.

 

โ‘ฃ ๋ฃจํŠธ ์œ„์ ฏ์€ ๋ณดํ†ต MaterialApp์ด๋‚˜ CupertinoApp์„ ์‚ฌ์šฉํ•œ๋‹ค.

  • MaterialApp: ๊ตฌ๊ธ€์˜ Material Design ๊ธฐ๋ฐ˜ (์ผ๋ฐ˜์ ์œผ๋กœ ๋งŽ์ด ์‚ฌ์šฉ)
  • CupertinoApp: Apple์˜ iOS ๋””์ž์ธ ์–ธ์–ด์ธ Cupertino ์Šคํƒ€์ผ ๊ธฐ๋ฐ˜

 

โ‘ค ์œ„์ ฏ ์ข…๋ฅ˜๊ฐ€ ์›Œ๋‚™ ๋‹ค์–‘ํ•ด์„œ ๋งค๋ฒˆ ๋ฌธ์„œ๋ฅผ ์ฐพ์•„๋ณด๊ธฐ ๋ฒˆ๊ฑฐ๋กญ๊ธฐ ๋•Œ๋ฌธ์— ํˆดํŒ(๋น ๋ฅธ ๋ฌธ์„œ)์„ ์ ๊ทน ํ™œ์šฉํ•œ๋‹ค.

AndroidStudio์—์„  `F1` ํ˜น์€ ๋งˆ์šฐ์Šค ์ปค์„œ๋ฅผ ์˜ฌ๋ฆฌ๋ฉด ๋น ๋ฅธ ๋ฌธ์„œ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

โ‘ฅ ์†์„ฑ ๊ฐ’์„ ์ž…๋ ฅํ•  ๋• ์ฝ”๋“œ ์ œ์•ˆ ๋‹จ์ถ•ํ‚ค๋ฅผ ํ™œ์šฉํ•˜๋ฉด ๋” ํŽธํ•˜๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

AndroidStudio์—์„  `โŒ˜` `Space` ๋‹จ์ถ•ํ‚ค๋ฅผ ๋ˆ„๋ฅด๋ฉด ์ฝ”๋“œ ์ œ์•ˆ ํŒ์—…์ด ํ‘œ์‹œ๋œ๋‹ค

โ‘ฆ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ชฉ๋ก ๋์— ํŠธ๋ ˆ์ผ๋ง ์ฝค๋งˆ๋ฅผ ๋ถ™์ด๊ณ  ์ €์žฅํ•˜๋ฉด ์ž๋™์œผ๋กœ ์ค„ ๋ฐ”๊ฟˆ ๋ผ์„œ ๋‹ค์Œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ ํŽธํ•ด์ง„๋‹ค.

 

Stateless Widgets

Header

// lib/main.dart

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        // ์ปค์Šคํ…€ ์ƒ‰์ƒ์„ ํ‘œํ˜„ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” Color ํด๋ž˜์Šค
        backgroundColor: Color(0xFF181818),
        // ํ™”๋ฉด ์•ˆ์ชฝ ์—ฌ๋ฐฑ์„ ์ฃผ๋Š” Padding ์œ„์ ฏ
        body: Padding(
          // ์ˆ˜ํ‰, ์ˆ˜์ง ๊ฐ๊ฐ ์—ฌ๋ฐฑ์„ ์ค„ ์ˆ˜ ์žˆ๋Š” EdgeInsets.symmetric ์ƒ์„ฑ์ž
          padding: EdgeInsets.symmetric(horizontal: 40),
          child: Column(children: [
            // ํŠน์ • ํฌ๊ธฐ์˜ ๋นˆ ๊ณต๊ฐ„์„ ๋งŒ๋“œ๋Š” SizedBox ์œ„์ ฏ
            SizedBox(height: 80),
            // ์ž์‹๋“ค์„ ๊ฐ€๋กœ(์ˆ˜ํ‰)๋กœ ๋ฐฐ์น˜ํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” Row ์œ„์ ฏ
            // mainAxisAlignment: ์ˆ˜ํ‰ ์ •๋ ฌ, crossAxisAlignment: ์ˆ˜์ง ์ •๋ ฌ
            Row(mainAxisAlignment: MainAxisAlignment.end, children: [
              // ์ž์‹๋“ค์„ ์„ธ๋กœ(์ˆ˜์ง)๋กœ ๋ฐฐ์น˜ํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” Column ์œ„์ ฏ
              // crossAxisAlignment: ์ˆ˜ํ‰ ์ •๋ ฌ, mainAxisAlignment: ์ˆ˜์ง ์ •๋ ฌ
              Column(crossAxisAlignment: CrossAxisAlignment.end, children: [
                Text(
                  'Hey, Selena',
                  style: TextStyle(
                    color: Colors.white,
                    fontSize: 28,
                    fontWeight: FontWeight.w800,
                  ),
                ),
                Text(
                  'Welcome back',
                  style: TextStyle(
                    // ๊ธฐ์กด ์ƒ‰์ƒ์— ํˆฌ๋ช…๋„๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” withOpacity ๋ฉ”์„œ๋“œ
                    color: Colors.white.withOpacity(0.8),
                    fontSize: 18,
                  ),
                ),
              ])
            ])
          ]),
        ),
      ),
    );
  }
}

 

์ถœ๋ ฅ ํ™”๋ฉด

  • Color : ์ƒ‰์ƒ์„ ํ‘œํ˜„ํ•˜๊ธฐ ์œ„ํ•œ ํด๋ž˜์Šค. 32๋น„ํŠธ ์ •์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ƒ‰์ƒ ์ •์˜.
    • Color.fromARGB(int a, int r, int g, int b)
      Alpha, Red, Green, Blue ๊ฐ’์„ ๊ฐœ๋ณ„์ ์œผ๋กœ ์ง€์ •ํ•˜์—ฌ ์ƒ‰์ƒ ์ƒ์„ฑ
    • Color.fromRGBO(int r, int g, int b, double opacity)
      RGB ๊ฐ’๊ณผ ํˆฌ๋ช…๋„๋ฅผ ์ง€์ •ํ•˜์—ฌ ์ƒ‰์ƒ ์ƒ์„ฑ
  • Padding : ์•ˆ์ชฝ ์—ฌ๋ฐฑ์„ ์ถ”๊ฐ€ํ•˜๋Š” ์œ„์ ฏ. padding ์†์„ฑ์— EdgeInsets๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์—ฌ๋ฐฑ ๊ฐ’ ์ง€์ •
  • EdgeInsets : ์—ฌ๋ฐฑ์„ ์ •์˜ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉํ•˜๋Š” ํด๋ž˜์Šค
    • EdgeInsets.all(double value)
      ์ƒํ•˜์ขŒ์šฐ ๋™์ผํ•œ ํฌ๊ธฐ์˜ ์—ฌ๋ฐฑ ์ ์šฉ
    • EdgeInsets.symmetric({double vertical, double horizontal}):
      ์ˆ˜ํ‰ ๋ฐ ์ˆ˜์ง ๋ฐฉํ–ฅ์— ๊ฐ๊ฐ ๋‹ค๋ฅธ ํฌ๊ธฐ์˜ ์—ฌ๋ฐฑ ์ ์šฉ
    • EdgeInsets.only({double left, double top, double right, double bottom})
      ๊ฐ ๋ฐฉํ–ฅ์— ๊ฐœ๋ณ„์ ์œผ๋กœ ์—ฌ๋ฐฑ ์ ์šฉ
    • EdgeInsets.zero
      ๋ชจ๋“  ๋ฐฉํ–ฅ์— 0 ํฌ๊ธฐ์˜ ์—ฌ๋ฐฑ ์ ์šฉ
  • SizedBox : ํŠน์ • ํฌ๊ธฐ์˜ ๋นˆ ๊ณต๊ฐ„์„ ๋งŒ๋“œ๋Š” ์œ„์ ฏ
  • Row : ์ž์‹๋“ค์„ ๊ฐ€๋กœ(์ˆ˜ํ‰)๋กœ ๋ฐฐ์น˜ํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ์œ„์ ฏ
    • mainAxisAlignment ์†์„ฑ : ์ˆ˜ํ‰ ์ •๋ ฌ ๋ฐฉ์‹ ์ง€์ •
    • crossAxisAlignment ์†์„ฑ : ์ˆ˜์ง ์ •๋ ฌ ๋ฐฉ์‹ ์ง€์ •
  • Column : ์ž์‹๋“ค์„ ์„ธ๋กœ(์ˆ˜์ง)๋กœ ๋ฐฐ์น˜ํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ์œ„์ ฏ
    • mainAxisAlignment ์†์„ฑ : ์ˆ˜์ง ์ •๋ ฌ ๋ฐฉ์‹ ์ง€์ •
    • crossAxisAlignment ์†์„ฑ : ์ˆ˜ํ‰ ์ •๋ ฌ ๋ฐฉ์‹ ์ง€์ •
  • Text : ํ…์ŠคํŠธ๋ฅผ ํ™”๋ฉด์— ํ‘œ์‹œํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๊ธฐ๋ณธ์ ์ธ ์œ„์ ฏ
    • data (์ฒซ ๋ฒˆ์งธ ์ธ์ž) : ํ‘œ๊ธฐํ•  ํ…์ŠคํŠธ ๋ฌธ์ž์—ด
    • style ์†์„ฑ : ํ…์ŠคํŠธ ์Šคํƒ€์ผ ์ •์˜. ์Šคํƒ€์ผ ์ •์˜์—” TextStyle ๊ฐ์ฒด ์‚ฌ์šฉ
      • color ์†์„ฑ : ํ…์ŠคํŠธ ์ƒ‰์ƒ ์ง€์ • e.g., Colors.black
      • fontWeight ์†์„ฑ : ํ…์ŠคํŠธ ๊ตต๊ธฐ ์ง€์ • e.g., FontWeight.bold
      • fontSize ์†์„ฑ : ํ…์ŠคํŠธ ํฌ๊ธฐ ์ง€์ •
    • textAlign ์†์„ฑ : ํ…์ŠคํŠธ ์ •๋ ฌ ์ง€์ • e.g., TextAlign.left
    • overflow ์†์„ฑ : ์˜์—ญ ์ดˆ๊ณผ ์‹œ ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ• ์ง€ ๊ฒฐ์ • e.g., TextOverflow.ellipsis
  • withOpacity : ๊ธฐ์กด ์ƒ‰์ƒ์— ํˆฌ๋ช…๋„๋ฅผ ์ถ”๊ฐ€/์ˆ˜์ •ํ•˜๋Š” ๋ฉ”์„œ๋“œ. 0.0~1.0 ์‚ฌ์ด์˜ ๊ฐ’ ์‚ฌ์šฉ

 

Button

// lib/main.dart

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        backgroundColor: Color(0xFF181818),
        body: Padding(
          padding: EdgeInsets.symmetric(horizontal: 40),
          child:
          Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
            // ...์ƒ๋žต

            Row(children: [
              // div ํƒœ๊ทธ์ฒ˜๋Ÿผ ์ฝ˜ํ…์ธ ๋ฅผ ๊ฐ์‹ธ๋Š” Container ์œ„์ ฏ
              // decoration ์†์„ฑ์„ ํ†ตํ•ด Container๋ฅผ ์Šคํƒ€์ผ๋งํ•  ์ˆ˜ ์žˆ๋‹ค
              Container(
                // ์ปจํ…Œ์ด๋„ˆ ์œ„์ ฏ์„ ๋””์ž์ธํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๋ฐ์ฝ”๋ ˆ์ด์…˜ ํด๋ž˜์Šค BoxDecoration
                decoration: BoxDecoration(
                  color: Colors.amber,
                  // ์œ„์ ฏ์˜ ๋ชจ์„œ๋ฆฌ๋ฅผ ๋‘ฅ๊ธ€๊ฒŒ ๋งŒ๋“ค ๋•Œ ์‚ฌ์šฉํ•˜๋Š” BorderRadius ํด๋ž˜์Šค
                  borderRadius: BorderRadius.circular(45),
                ),
                child: Padding(
                  padding: EdgeInsets.symmetric(
                    vertical: 15,
                    horizontal: 50,
                  ),
                  child: Text('Transfer', style: TextStyle(fontSize: 22)),
                ),
              )
            ])
          ]),
        ),
      ),
    );
  }
}

 

์ถœ๋ ฅ ํ™”๋ฉด

  • Container : ์ฝ˜ํ…์ธ ๋ฅผ ๊ฐ์‹ธ์„œ ๋ ˆ์ด์•„์›ƒ๊ณผ ์Šคํƒ€์ผ๋ง์„ ๋™์‹œ์— ์ฒ˜๋ฆฌํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ์œ„์ ฏ(div ํƒœ๊ทธ์™€ ์œ ์‚ฌ)
    • padding ์†์„ฑ : ์•ˆ์ชฝ ์—ฌ๋ฐฑ ์ง€์ •. EdgeInsets ํด๋ž˜์Šค๋กœ ๊ฐ’ ์„ค์ •
    • decoration ์†์„ฑ : ์ปจํ…Œ์ด๋„ˆ ์Šคํƒ€์ผ ์ง€์ •. BoxDecoration ํด๋ž˜์Šค๋กœ ๊ฐ’ ์„ค์ •
    • clipBehavior ์†์„ฑ : ๋ถ€๋ชจ ๊ฒฝ๊ณ„๋ฅผ ๋„˜์–ด์„  ์ž์‹ ์œ„์ ฏ์˜ ๋„˜์นœ ๋ถ€๋ถ„์„ ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ• ์ง€ ์ง€์ •
  • BoxDecoration : Container ์œ„์ ฏ์„ ์Šคํƒ€์ผ๋งํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๋ฐ์ฝ”๋ ˆ์ด์…˜ ํด๋ž˜์Šค
  • BorderRadius : ์œ„์ ฏ์˜ ๋ชจ์„œ๋ฆฌ๋ฅผ ๋‘ฅ๊ธ€๊ฒŒ ๋งŒ๋“ค ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ํด๋ž˜์Šค
    • BorderRadius.circular(double radius)
    • ๋ชจ๋“  ๋ชจ์„œ๋ฆฌ๋ฅผ ๋™์ผํ•œ ๋ฐ˜์ง€๋ฆ„์œผ๋กœ ๋‘ฅ๊ธ€๊ฒŒ ์„ค์ •
    • BorderRadius.all(Radius radius)
    • ๋ชจ๋“  ๋ชจ์„œ๋ฆฌ๋ฅผ ๋™์ผํ•œ Radius ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋‘ฅ๊ธ€๊ฒŒ ์„ค์ •
    • BorderRadius.only({Radius topLeft, Radius topRight, Radius bottomLeft, Radius bottomRight})
    • ๊ฐ ๋ชจ์„œ๋ฆฌ๋ฅผ ๊ฐœ๋ณ„์ ์œผ๋กœ ๋‘ฅ๊ธ€๊ฒŒ ์„ค์ •
    • BorderRadius.horizontal({Radius left, Radius right})
    • ์ˆ˜ํ‰ ๋ฐฉํ–ฅ(์ขŒ/์šฐ)์œผ๋กœ ๋‘ฅ๊ธ€๊ฒŒ ์„ค์ •
    • BorderRadius.vertical({Radius top, Radius bottom})
    • ์ˆ˜์ง ๋ฐฉํ–ฅ(์ƒ/ํ•˜)์œผ๋กœ ๋‘ฅ๊ธ€๊ฒŒ ์„ค์ •

 

const ํ‚ค์›Œ๋“œ

const ํ‚ค์›Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํ•ด๋‹น ๊ฐ์ฒด๊ฐ€ ์ปดํŒŒ์ผ ํƒ€์ž„์— ์ƒ์ˆ˜๋กœ ํ‰๊ฐ€๋œ๋‹ค. ์ด๋Š” ํ•ด๋‹น ๊ฐ์ฒด๊ฐ€ ๋ฉ”๋ชจ๋ฆฌ์— ํ•œ ๋ฒˆ๋งŒ ํ• ๋‹น๋˜๊ณ , ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์ค‘์—๋Š” ์ ˆ๋Œ€ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•œ๋‹ค. Flutter์—์„œ ์œ„์ ฏ์€ ์ฃผ๋กœ ๋ถˆ๋ณ€์„ฑ์„ ๊ฐ€์ง€๊ธฐ ๋•Œ๋ฌธ์— const ํ‚ค์›Œ๋“œ๋ฅผ ํ™œ์šฉํ•ด ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค.

const Text('Hello World');

 

์œ„ ์ฝ”๋“œ์—์„œ Text ์œ„์ ฏ์€ const ํ‚ค์›Œ๋“œ๋กœ ์„ ์–ธํ•ด์„œ ๋ฉ”๋ชจ๋ฆฌ์— ํ•œ ๋ฒˆ๋งŒ ์ƒ์„ฑ๋œ๋‹ค. ๋™์ผํ•œ ๋‚ด์šฉ์„ ๊ฐ€์ง„ ๋˜ ๋‹ค๋ฅธ const Text('Hello World') ์œ„์ ฏ์ด ์žˆ๋‹ค๋ฉด ์ด์ „์— ์ƒ์„ฑ๋œ ์ƒ์ˆ˜ ๊ฐ์ฒด๋ฅผ ์žฌ์‚ฌ์šฉํ•œ๋‹ค. ์ƒํƒœ ๋ณ€ํ™”๋กœ ์œ„์ ฏ ํŠธ๋ฆฌ๊ฐ€ ์žฌ๊ตฌ์„ฑ๋  ๋•Œ(build ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ)๋„ ์ด์ „์— ์ƒ์„ฑํ–ˆ๋˜ ๋‚ด์šฉ์„ ์žฌ์‚ฌ์šฉํ•œ๋‹ค.

 

ํ•˜์ง€๋งŒ ์‚ฌ์šฉ์ž ์ž…๋ ฅ๊ฐ’, ์ƒํƒœ ๋ณ€ํ™”, withOpacity ๋ฉ”์„œ๋“œ์ฒ˜๋Ÿผ ๋Ÿฐํƒ€์ž„์— ๊ฒฐ์ •๋˜๋Š” ๊ฐ’์ด ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฉด const ํ‚ค์›Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋‹ค.

 

const ํ‚ค์›Œ๋“œ๊ฐ€ ์ ์šฉ๋  ์ˆ˜ ์žˆ๋Š” ๊ณณ์„ ์ผ์ผ์ด ํŒŒ์•…ํ•˜๊ธฐ ํž˜๋“ค๊ธฐ ๋•Œ๋ฌธ์— IDE์—์„œ ์ œ๊ณตํ•˜๋Š” ์ฝ”๋“œ ์•ก์…˜์ด๋‚˜ ๋ฆฌํŒฉํ† ๋ง ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ž๋™์œผ๋กœ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

๐Ÿ’ก ์œ„์ ฏ ํŠธ๋ฆฌ์—์„œ ์ƒ์œ„ ์œ„์ ฏ์— const ํ‚ค์›Œ๋“œ๋ฅผ ๋ถ™์ด๋ฉด, ํ•˜์œ„์— ์žˆ๋Š” ์œ„์ ฏ๋“ค ์ค‘ const ์ƒ์„ฑ์ž๋ฅผ ๊ฐ€์ง„ ์œ„์ ฏ๋“ค์€(Text, Container ์œ„์ ฏ ๋“ฑ) ์ž๋™์œผ๋กœ const ์ฒ˜๋ฆฌ๋œ๋‹ค.

 

Reusable Button

๋ฆฌ์•กํŠธ์—์„œ ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ •์˜ํ•ด์„œ ์žฌ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ, Flutter์—์„œ๋„ ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” UI ์š”์†Œ๋ฅผ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์œ„์ ฏ์œผ๋กœ ๋งŒ๋“ค์–ด์„œ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

// lib/widgets/button.dart

class Button extends StatelessWidget {
  final String text;
  final Color bgColor, textColor;

  // ์ƒ์„ฑ์ž ์ดˆ๊ธฐํ™”
  const Button({
    super.key, // ๋ถ€๋ชจ ํด๋ž˜์Šค์ธ StatelessWidget์— key ์ „๋‹ฌ
    required this.text,
    required this.bgColor,
    required this.textColor,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: bgColor, // ์ง€์ •๋œ ๋ฐฐ๊ฒฝ์ƒ‰ ์‚ฌ์šฉ
        borderRadius: BorderRadius.circular(45),
      ),
      child: Padding(
        padding: const EdgeInsets.symmetric(
          vertical: 20,
          horizontal: 45,
        ),
        child: Text(text, // ์ง€์ •๋œ ๋ผ๋ฒจ ํ…์ŠคํŠธ ์‚ฌ์šฉ
            style: TextStyle(
              fontSize: 20,
              color: textColor, // ์ง€์ •๋œ ํ…์ŠคํŠธ ์ƒ‰์ƒ ์‚ฌ์šฉ
            )),
      ),
    );
  }
}

 

๊ณตํ†ต ์œ„์ ฏ ์‚ฌ์šฉ ์˜ˆ์‹œ โ–ผ

// lib/main.dart

class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        backgroundColor: const Color(0xFF181818),
        body: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 20),
          child:
              Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
            // ...์ƒ๋žต
            const Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  // ๊ณตํ†ต ์œ„์ ฏ ์žฌ์‚ฌ์šฉ
                  Button(
                    text: 'Transfer',
                    bgColor: Color(0xFFF1B33B),
                    textColor: Colors.black,
                  ),
                  Button(
                    text: 'Request',
                    bgColor: Color(0xFF1F2123),
                    textColor: Colors.white,
                  ),
                ])
          ]),
        ),
      ),
    );
  }
}

 

์ถœ๋ ฅ ํ™”๋ฉด

Reusable Card

๐Ÿ’ก Dart/Flutter ๋„ค์ด๋ฐ ๊ทœ์น™

  • ํŒŒ์ผ/ํด๋”๋ช… : snake_case
  • ํด๋ž˜์Šค : PascalCase
  • ๋ณ€์ˆ˜/ํ•จ์ˆ˜๋ช… : camelCase

 

// lib/widgets/currency_card.dart

class CurrencyCard extends StatelessWidget {
  final String name, code, amount;
  final IconData icon;
  final bool isInverted;
  final double order;

  final _blackColor = const Color(0xFF1F2123);
  final _baseOffset = -20;

  const CurrencyCard({
    super.key,
    required this.name,
    required this.code,
    required this.amount,
    required this.icon,
    required this.isInverted,
    required this.order,
  });

  @override
  Widget build(BuildContext context) {
    // ์œ„์ ฏ์˜ ์œ„์น˜๋ฅผ ํŠน์ • ๋ฐฉํ–ฅ์œผ๋กœ ์ด๋™์‹œํ‚ฌ ๋•Œ ์‚ฌ์šฉํ•˜๋Š” Transform.translate
    // ๋ถ€๋ชจ ์œ„์ ฏ์˜ ํฌ๊ธฐ๋‚˜ ๋ ˆ์ด์•„์›ƒ์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š์Œ
    return Transform.translate(
      // ์ขŒํ‘œ๋‚˜ ๊ฑฐ๋ฆฌ ๋“ฑ์„ ๋‚˜ํƒ€๋‚ด๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” Offset ํด๋ž˜์Šค
      offset: Offset(0, (order - 1) * _baseOffset),
      child: Container(
          // ์œ„์ ฏ์˜ ์ž˜๋ฆผ ๋ฐฉ์‹์„ ์ •์˜ํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” clipBehavior ์†์„ฑ
          // Clip.hardEdge๋Š” ์œ„์ ฏ์˜ ๊ฒฝ๊ณ„๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ฆ‰์‹œ ์ž˜๋ผ๋‚ด๋Š” ๋ฐฉ์‹
          clipBehavior: Clip.hardEdge,
          decoration: BoxDecoration(
            color: isInverted ? Colors.white : _blackColor,
            borderRadius: const BorderRadius.only(
                topLeft: Radius.circular(20), topRight: Radius.circular(20)),
          ),
          child: Padding(
            padding: const EdgeInsets.all(30),
            child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  // ...์ƒ๋žต

                  // ์ž์‹ ์œ„์ ฏ์˜ ํฌ๊ธฐ๋ฅผ ํ™•๋Œ€/์ถ•์†Œํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” Transform.scale
                  // ์ž์‹ ์œ„์ ฏ์˜ ํฌ๊ธฐ๋งŒ ๋ณ€๊ฒฝํ•˜๊ณ  ๋ ˆ์ด์•„์›ƒ์—๋Š” ์˜ํ–ฅ ์•ˆ์คŒ
                  Transform.scale(
                      scale: 2.2,
                      child: Transform.translate(
                        offset: const Offset(-5, 12),
                        // ์•„์ด์ฝ˜์„ ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” Icon ์œ„์ ฏ
                        child: Icon(
                          icon,
                          color: isInverted ? _blackColor : Colors.white,
                          size: 88,
                        ),
                      ))
                ]),
          )),
    );
  }
}

 

๋”๋ณด๊ธฐ
// lib/widgets/currency_card.dart

class CurrencyCard extends StatelessWidget {
  final String name, code, amount;
  final IconData icon;
  final bool isInverted;
  final double order;

  final _blackColor = const Color(0xFF1F2123);
  final _baseOffset = -20;

  const CurrencyCard({
    super.key,
    required this.name,
    required this.code,
    required this.amount,
    required this.icon,
    required this.isInverted,
    required this.order,
  });

  @override
  Widget build(BuildContext context) {
    // ์œ„์ ฏ์˜ ์œ„์น˜๋ฅผ ํŠน์ • ๋ฐฉํ–ฅ์œผ๋กœ ์ด๋™์‹œํ‚ฌ ๋•Œ ์‚ฌ์šฉํ•˜๋Š” Transform.translate
    // ๋ถ€๋ชจ ์œ„์ ฏ์˜ ํฌ๊ธฐ๋‚˜ ๋ ˆ์ด์•„์›ƒ์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š์Œ
    return Transform.translate(
      // ์ขŒํ‘œ๋‚˜ ๊ฑฐ๋ฆฌ ๋“ฑ์„ ๋‚˜ํƒ€๋‚ด๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” Offset ํด๋ž˜์Šค
      offset: Offset(0, (order - 1) * _baseOffset),
      child: Container(
          // ์œ„์ ฏ์˜ ์ž˜๋ฆผ ๋ฐฉ์‹์„ ์ •์˜ํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” clipBehavior ์†์„ฑ
          // Clip.hardEdge๋Š” ์œ„์ ฏ์˜ ๊ฒฝ๊ณ„๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ฆ‰์‹œ ์ž˜๋ผ๋‚ด๋Š” ๋ฐฉ์‹
          clipBehavior: Clip.hardEdge,
          decoration: BoxDecoration(
            color: isInverted ? Colors.white : _blackColor,
            borderRadius: const BorderRadius.only(
                topLeft: Radius.circular(20), topRight: Radius.circular(20)),
          ),
          child: Padding(
            padding: const EdgeInsets.all(30),
            child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                children: [
                  Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(name,
                            style: TextStyle(
                              color: isInverted ? _blackColor : Colors.white,
                              fontSize: 32,
                              fontWeight: FontWeight.w600,
                            )),
                        const SizedBox(height: 10),
                        Row(
                          children: [
                            Text(amount,
                                style: TextStyle(
                                  color:
                                      isInverted ? _blackColor : Colors.white,
                                  fontSize: 20,
                                )),
                            const SizedBox(width: 5),
                            Text(code,
                                style: TextStyle(
                                  color: isInverted
                                      ? _blackColor
                                      : Colors.white.withOpacity(0.8),
                                  fontSize: 20,
                                )),
                          ],
                        )
                      ]),
                  // ์ž์‹ ์œ„์ ฏ์˜ ํฌ๊ธฐ๋ฅผ ํ™•๋Œ€/์ถ•์†Œํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” Transform.scale
                  // ์ž์‹ ์œ„์ ฏ์˜ ํฌ๊ธฐ๋งŒ ๋ณ€๊ฒฝํ•˜๊ณ  ๋ ˆ์ด์•„์›ƒ์—๋Š” ์˜ํ–ฅ ์•ˆ์คŒ
                  Transform.scale(
                      scale: 2.2,
                      child: Transform.translate(
                        offset: const Offset(-5, 12),
                        // ์•„์ด์ฝ˜์„ ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” Icon ์œ„์ ฏ
                        child: Icon(
                          icon,
                          color: isInverted ? _blackColor : Colors.white,
                          size: 88,
                        ),
                      ))
                ]),
          )),
    );
  }
}

main.dart์— ์ ์šฉ ํ›„ ์ถœ๋ ฅ ํ™”๋ฉด

  • Transform.translate : ์ž์‹ ์œ„์ ฏ์˜ ์œ„์น˜๋ฅผ ํŠน์ • ๋ฐฉํ–ฅ์œผ๋กœ ์ด๋™์‹œํ‚ฌ ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ์œ„์ ฏ
    • offset ์†์„ฑ์— Offset ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ด๋™ํ•  x/y์ถ• ๊ฐ’ ์ง€์ •
    • ์œ„์ ฏ์˜ ์‹ค์ œ ์œ„์น˜๋Š” ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์ง€๋งŒ, ์‹œ๊ฐ์ ์œผ๋กœ ์œ„์น˜๊ฐ€ ๋ณ€๊ฒฝ๋œ ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ž„
    • ์ฆ‰, ์ž์‹ ์œ„์ ฏ์˜ ์‹œ๊ฐ์  ์œ„์น˜๋งŒ ๋ณ€๊ฒฝํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ถ€๋ชจ ์œ„์ ฏ์˜ ํฌ๊ธฐ๋‚˜ ๋ ˆ์ด์•„์›ƒ์— ์˜ํ–ฅ ์•ˆ ์คŒ
  • Offset(double dx, double dy) : ์ขŒํ‘œ๋‚˜ ๊ฑฐ๋ฆฌ ๋“ฑ์„ ๋‚˜ํƒ€๋‚ด๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” ํด๋ž˜์Šค
  • Clip : ๋„˜์นœ ๋ถ€๋ถ„์„ ์–ด๋–ป๊ฒŒ ์ฒ˜๋ฆฌํ• ์ง€ ์ •์˜ํ•œ ์—ด๊ฑฐํ˜•. ์œ„์ ฏ์˜ clipBehavior ์†์„ฑ์—์„œ ์‚ฌ์šฉ.
    • Clip.none : ์ž๋ฅด๊ธฐ ์•ˆ ํ•จ
    • Clip.hardEdge : ๋„˜์นœ ๋ถ€๋ถ„์„ ์ž˜๋ผ๋ƒ„
    • Clip.antiAlias : ๋„˜์นœ ๋ถ€๋ถ„์„ ์ž˜๋ผ๋‚ด๊ณ  ๊ฐ€์žฅ์ž๋ฆฌ๋ฅผ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ(์•ˆํ‹ฐ ์•จ๋ฆฌ์–ด์‹ฑ) ์ฒ˜๋ฆฌ
    • Clip.antiAliasWithSaveLayer : ์ž˜๋ฆฐ ๋ถ€๋ถ„์„ ์ƒˆ ๋ ˆ์ด์–ด์— ์ €์žฅํ•˜์—ฌ ๋” ๋†’์€ ํ’ˆ์งˆ ๋ณด์žฅ
  • Transform.scale : ์ž์‹ ์œ„์ ฏ์˜ ํฌ๊ธฐ๋ฅผ ์‹œ๊ฐ์ ์œผ๋กœ ํ™•๋Œ€/์ถ•์†Œํ•  ๋•Œ ์‚ฌ์šฉ
    • scale ์†์„ฑ์„ ํ†ตํ•ด ์œ„์ ฏ์˜ ํฌ๊ธฐ๋ฅผ ๋ช‡ ๋ฐฐ๋กœ ํ™•๋Œ€/์ถ•์†Œํ• ์ง€ ์ง€์ •
    • ์ž์‹ ์œ„์ ฏ์˜ ์‹œ๊ฐ์  ํฌ๊ธฐ๋งŒ ๋ณ€๊ฒฝํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ถ€๋ชจ ์œ„์ ฏ์˜ ํฌ๊ธฐ๋‚˜ ๋ ˆ์ด์•„์›ƒ์— ์˜ํ–ฅ ์•ˆ ์คŒ
  • Icon : ์•„์ด์ฝ˜์„ ํ‘œ์‹œํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ์œ„์ ฏ. IconData ๊ฐ์ฒด๋ฅผ ๋ฐ›์•„ ํ•ด๋‹นํ•˜๋Š” ์•„์ด์ฝ˜์„ ํ‘œ์‹œํ•˜๋ฉฐ, ์ผ๋ฐ˜์ ์œผ๋กœ Icons ํด๋ž˜์Šค์— ์ •์˜๋œ ์•„์ด์ฝ˜๋“ค์„ ์‚ฌ์šฉํ•จ.

 

Stateful Widgets

Stateless Widget์€ build ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ๋‹จ์ˆœํžˆ UI๋ฅผ ์ถœ๋ ฅํ•˜๋Š” ์—ญํ• ์„ ํ•œ๋‹ค. ์ƒํƒœ๊ฐ€ ๋ณ€ํ•˜์ง€ ์•Š์œผ๋ฉฐ, ์™ธ๋ถ€์—์„œ ์ œ๊ณต๋œ ๋ฐ์ดํ„ฐ์— ๋”ฐ๋ผ UI๋ฅผ ๋ Œ๋”๋ง ํ•œ๋‹ค. ์ƒํƒœ ๋ณ€ํ™”๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ์— build ๋ฉ”์„œ๋“œ๊ฐ€ ํ•œ ๋ฒˆ ํ˜ธ์ถœ๋ผ์„œ ์œ„์ ฏ์ด ๋ Œ๋”๋ง ๋˜๋ฉด ๊ทธ ์ดํ›„๋กœ๋Š” ๋ณ€๊ฒฝ๋˜์ง€ ์•Š๋Š”๋‹ค.

 

๋ฐ˜๋ฉด Stateful ์œ„์ ฏ์€ ์ด๋ฆ„ ๊ทธ๋Œ€๋กœ ์ƒํƒœ(state)๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์œผ๋ฉฐ, ์ƒํƒœ์— ๋”ฐ๋ผ UI๋ฅผ ๋™์ ์œผ๋กœ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ๋‹ค. Stateful Widget์€ Stateful Widget ํด๋ž˜์Šค์™€ State ํด๋ž˜์Šค๊ฐ€ ๊ฒฐํ•ฉ๋œ ํ˜•ํƒœ๋กœ ๊ตฌ์„ฑ๋œ๋‹ค. Stateful Widget ํด๋ž˜์Šค๋Š” ์œ„์ ฏ์˜ ์™ธํ˜•์„ ์ •์˜ํ•˜๊ณ , State ํด๋ž˜์Šค๋Š” ์œ„์ ฏ์˜ ์ƒํƒœ์™€ ์ƒํƒœ ๋ณ€ํ™”๋ฅผ ์ฒ˜๋ฆฌํ•œ๋‹ค.

 

State

์ƒํƒœ๋ฅผ ๊ฐ€์ง„ ํด๋ž˜์Šค๋Š” ์•„๋ž˜ ๊ตฌ์กฐ๋กœ ์ •์˜ํ•œ๋‹ค. State ํด๋ž˜์Šค๋Š” ์ƒํƒœ์™€ UI๋ฅผ ๋ชจ๋‘ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์„œ ์ƒํƒœ ๋ณ€ํ™”์— ๋”ฐ๋ผ UI๋ฅผ ๋™์ ์œผ๋กœ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ๋‹ค. setState ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉด build ๋ฉ”์„œ๋“œ๋ฅผ ๋‹ค์‹œ ์‹คํ–‰์‹œ์ผœ์„œ ๋ฆฌ๋ Œ๋”๋ง ๋œ๋‹ค. setState๋งŒ ์ •์ƒ์ ์œผ๋กœ ํ˜ธ์ถœ๋œ๋‹ค๋ฉด, ์ƒํƒœ ์—…๋ฐ์ดํŠธ ๋กœ์ง์„ setState ์™ธ๋ถ€์— ์ •์˜ํ•ด๋„ ๋ฌด๋ฐฉํ•˜๋‹ค.

// StatefulWidget์„ ์ƒ์†๋ฐ›์•„ ์ƒํƒœ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋Š” App ํด๋ž˜์Šค ์ •์˜

class App extends StatefulWidget {
  const App({super.key});

  // createState ๋ฉ”์„œ๋“œ๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋“œํ•˜์—ฌ App ์œ„์ ฏ์˜ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” State ๊ฐ์ฒด ์ƒ์„ฑ/๋ฐ˜ํ™˜
  @override
  State<App> createState() => _AppState();
}

// State<App>์„ ํ™•์žฅํ•˜์—ฌ App ์œ„์ ฏ์˜ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” _AppState ํด๋ž˜์Šค ์ •์˜
class _AppState extends State<App> {
  // ์ƒํƒœ๋กœ ์‚ฌ์šฉํ•  counter ๋ณ€์ˆ˜ ์ •์˜. ์ƒํƒœ๋Š” ํด๋ž˜์Šค ํ•„๋“œ๋กœ ์ •์˜ํ•œ๋‹ค.
  int counter = 0;

  // ์นด์šดํ„ฐ ์ฆ๊ฐ€ ํ•ธ๋“ค๋Ÿฌ
  void onClicked() {
    // State ํด๋ž˜์Šค์—๊ฒŒ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋๋‹ค๊ณ  ์•Œ๋ฆฌ๋Š” ํ•จ์ˆ˜
    // setState ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉด build ๋ฉ”์„œ๋“œ๋ฅผ ๋‹ค์‹œ ์‹คํ–‰์‹œ์ผœ์„œ ๋ฆฌ๋ Œ๋”๋ง
    setState(() => counter++);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        backgroundColor: const Color(0xFFF4EDDB),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Text('Click Count', style: TextStyle(fontSize: 30)),
              // counter ๊ฐ’์„ ์ถœ๋ ฅํ•˜๋Š” ํ…์ŠคํŠธ ์œ„์ ฏ
              Text('$counter', style: const TextStyle(fontSize: 30)),
              IconButton(
                iconSize: 60,
                onPressed: onClicked, // ํ•ธ๋“ค๋Ÿฌ ํ• ๋‹น
                icon: const Icon(Icons.add_box_rounded),
              )
            ],
          ),
        ),
      ),
    );
  }
}

 

๋ฒ„ํŠผ์„ ํด๋ฆญํ•  ๋•Œ๋งˆ๋‹ค counter๊ฐ€ ์ฆ๊ฐ€ํ•œ๋‹ค

๐Ÿ’ก ์ฝ”๋“œ ์•ก์…˜์„ ํ†ตํ•ด ๊ธฐ์กด StatelessWidget์„ StatefulWidget์œผ๋กœ ๋ณ€ํ™˜ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

 

BuildContext

BuildContext๋Š” Flutter ์œ„์ ฏ ํŠธ๋ฆฌ ๋‚ด์—์„œ ์œ„์ ฏ์˜ ์œ„์น˜ ์ •๋ณด๋ฅผ ๋‹ด๊ณ  ์žˆ๋Š” ๊ฐ์ฒด๋‹ค. ๋ชจ๋“  ์œ„์ ฏ์€ build ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ์ž์‹ ์˜ UI๋ฅผ ๊ตฌ์„ฑํ•˜๋ฉฐ, ์ด ๋ฉ”์„œ๋“œ๋Š” BuildContext๋ฅผ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ๋ฐ›์•„ ํ˜„์žฌ ์œ„์ ฏ์ด ํŠธ๋ฆฌ์—์„œ ์–ด๋Š ์œ„์น˜์— ์žˆ๋Š”์ง€ ํŒŒ์•…ํ•œ๋‹ค.

 

BuildContext๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์œ„์ ฏ ํŠธ๋ฆฌ ์ƒ์œ„์— ์žˆ๋Š” ๋‹ค๋ฅธ ์œ„์ ฏ๋“ค์˜ ๋ฐ์ดํ„ฐ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, Flutter ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ ์ œ๊ณตํ•˜๋Š” ์ „์—ญ์ ์ธ ๋ฐ์ดํ„ฐ(Theme, MediaQuery)์—๋„ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด๋Ÿฌํ•œ ๊ธฐ๋Šฅ์€ ์œ„์ ฏ ํŠธ๋ฆฌ ๋‚ด์—์„œ ์ƒํƒœ ๊ด€๋ฆฌ๋‚˜ ์Šคํƒ€์ผ๋ง, ํ™”๋ฉด ํฌ๊ธฐ ๋“ฑ์— ์œ ์—ฐํ•˜๊ฒŒ ๋Œ€์‘ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค€๋‹ค.

class App extends StatefulWidget {
  const App({super.key});

  @override
  State<App> createState() => _AppState();
}

class _AppState extends State<App> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // theme ์†์„ฑ์€ MaterialApp์—์„œ ์ „์ฒด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ํ…Œ๋งˆ๋ฅผ ์ •์˜ํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค
      // ThemeData ๊ฐ์ฒด๋ฅผ ํ†ตํ•ด ์•ฑ์—์„œ ๊ณตํ†ต์ ์œผ๋กœ ์ ์šฉํ•  ํ…Œ๋งˆ ์†์„ฑ์„ ์„ค์ •ํ•œ๋‹ค
      theme: ThemeData(
        // TextTheme ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•ด ์•ฑ์—์„œ ์‚ฌ์šฉํ•  ํ…์ŠคํŠธ ์Šคํƒ€์ผ์„ ์ •์˜ํ•œ๋‹ค
        textTheme: const TextTheme(
          // titleLarge ์†์„ฑ์— ํฐ ํƒ€์ดํ‹€์— ์‚ฌ์šฉํ•  ํ…์ŠคํŠธ ์Šคํƒ€์ผ์„ ์ง€์ •ํ•œ๋‹ค
          titleLarge: TextStyle(color: Colors.blueAccent),
        ),
      ),
      home: const Scaffold(
        backgroundColor: Color(0xFFF4EDDB),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [LargeTitle()],
          ),
        ),
      ),
    );
  }
}

// ์ƒํƒœ๋ฅผ ๊ฐ€์ง€์ง€ ์•Š๋Š” StatelessWidget
class LargeTitle extends StatelessWidget {
  const LargeTitle({super.key});

  @override
  Widget build(BuildContext context) {
    return Text(
      'Large Title',
      style: TextStyle(
          fontSize: 30,
          // Theme.of๋ฅผ ํ†ตํ•ด ํ˜„์žฌ ์œ„์ ฏ ํŠธ๋ฆฌ์˜ ์ƒ์œ„์— ์ •์˜๋œ ThemeData ๊ฐ์ฒด์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค
          // ์•„๋ž˜์—์„  MaterialApp ํ…Œ๋งˆ์— ์ ‘๊ทผํ•˜์—ฌ titleLarge ์ƒ‰์ƒ์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค
          color: Theme.of(context).textTheme.titleLarge?.color),
    );
  }
}

 

์ถœ๋ ฅ ํ™”๋ฉด

Widget Lifecycle

Flutter ์œ„์ ฏ๋„ React ์ปดํฌ๋„ŒํŠธ์ฒ˜๋Ÿผ ์ƒ์„ฑ๋ถ€ํ„ฐ ์†Œ๋ฉธ๊นŒ์ง€์˜ ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค. StatelessWidget์€ ์ƒํƒœ๋ฅผ ๊ฐ€์ง€์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์ƒ๋ช…์ฃผ๊ธฐ ๋ฉ”์„œ๋“œ๊ฐ€ ๋”ฐ๋กœ ์—†๊ณ , ํ™”๋ฉด์— ๊ทธ๋ ค์งˆ ๋•Œ ํ˜ธ์ถœ๋˜๋Š” build() ๋ฉ”์„œ๋“œ๋งŒ ์กด์žฌํ•œ๋‹ค. ๋ฐ˜๋ฉด, StatefulWidget์€ ์ƒํƒœ ๋ณ€๊ฒฝ์— ๋”ฐ๋ฅธ ๋‹ค์–‘ํ•œ ์ƒ๋ช…์ฃผ๊ธฐ ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ณตํ•œ๋‹ค.

 

  1. createState()
    • StatefulWidget์ด ์ฒ˜์Œ ์ƒ์„ฑ๋  ๋•Œ ํ˜ธ์ถœ.
    • ์ด ๋‹จ๊ณ„์—์„œ ์œ„์ ฏ์˜ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” State ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
  2. initState()
    • State ๊ฐ์ฒด๊ฐ€ ์ฒ˜์Œ ์ƒ์„ฑ๋œ ํ›„ ์œ„์ ฏ์ด ํŠธ๋ฆฌ์— ์‚ฝ์ž…๋์„ ๋•Œ ํ˜ธ์ถœ.
    • ์œ„์ ฏ์ด ์ฒ˜์Œ ๋นŒ๋“œ ๋  ๋•Œ ํ•œ ๋ฒˆ๋งŒ ํ˜ธ์ถœ๋˜๋ฉฐ, ์ด ๋‹จ๊ณ„์—์„œ ์ดˆ๊ธฐํ™” ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.
    • ๋ถ€๋ชจ ํด๋ž˜์Šค ์ดˆ๊ธฐํ™”๋ฅผ ์œ„ํ•ด ๋ฐ˜๋“œ์‹œ super.initState()๋ฅผ ํ˜ธ์ถœํ•ด์•ผ ํ•œ๋‹ค.
  3. didChangeDependencies()
    • ์œ„์ ฏ์ด ์˜์กดํ•˜๋Š” inheritedWidget ํ˜น์€ ๋‹ค๋ฅธ ์ข…์†์„ฑ์ด ๋ณ€๊ฒฝ๋  ๋•Œ ํ˜ธ์ถœ.
    • initState() ํ˜ธ์ถœ ์งํ›„์— ํ•œ ๋ฒˆ ํ˜ธ์ถœ๋˜๊ณ , ์ดํ›„๋ถ€ํ„ด ์œ„์ ฏ์˜ ์˜์กด์„ฑ์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ํ˜ธ์ถœ๋œ๋‹ค.
  4. build()
    • ์œ„์ ฏ์˜ UI๋ฅผ ๋ Œ๋”๋ง ํ•˜๋Š” ๋ฉ”์„œ๋“œ.
    • ์œ„์ ฏ์„ ์ฒ˜์Œ ๋นŒ๋“œํ•  ๋•Œ์™€, ์ƒํƒœ ๋ณ€๊ฒฝ ๋“ฑ์œผ๋กœ ์œ„์ ฏ์ด ๋‹ค์‹œ ๋นŒ๋“œ๋  ๋•Œ๋งˆ๋‹ค ํ˜ธ์ถœ๋œ๋‹ค.
  5. didUpdateWidget()
    • ๋ถ€๋ชจ ์œ„์ ฏ์ด ์žฌ๊ตฌ์„ฑ๋˜์–ด ํ˜„์žฌ ์œ„์ ฏ์˜ ์„ค์ •์ด ๋ณ€๊ฒฝ๋์„ ๋•Œ ํ˜ธ์ถœ.
    • ์ฆ‰, ๋ถ€๋ชจ ์œ„์ ฏ์—์„œ ์ „๋‹ฌ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋์„ ๋•Œ ํ˜ธ์ถœ๋œ๋‹ค.
  6. deactivate()
    • ์œ„์ ฏ์ด ํŠธ๋ฆฌ์—์„œ ์ œ๊ฑฐ๋˜๊ธฐ ์ง์ „์— ํ˜ธ์ถœ.
    • ์ด ๋ฉ”์„œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋ผ๋„ State ๊ฐ์ฒด๋Š” ์—ฌ์ „ํžˆ ๋ฉ”๋ชจ๋ฆฌ์— ๋‚จ์•„ ์žˆ๋‹ค.
  7. dispose()
    • State ๊ฐ์ฒด๊ฐ€ ์™„์ „ํžˆ ์†Œ๋ฉธ๋˜๊ธฐ ์ง์ „์— ํ˜ธ์ถœ.
    • ์ด ๋‹จ๊ณ„์—์„œ ๋„คํŠธ์›Œํฌ ์š”์ฒญ ์ทจ์†Œ, ์ด๋ฒคํŠธ ๊ตฌ๋… ํ•ด์ œ ๋“ฑ ์ •๋ฆฌ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.
    • ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด ๋ฐ˜๋“œ์‹œ super.dispose()๋ฅผ ํ˜ธ์ถœํ•ด์•ผ ํ•œ๋‹ค.

 

class _LargeTitleState extends State<LargeTitle> {
  // ์œ„์ ฏ์ด ํŠธ๋ฆฌ์— ์ฒ˜์Œ ์‚ฝ์ž…๋  ๋•Œ ํ•œ ๋ฒˆ๋งŒ ํ˜ธ์ถœ๋œ๋‹ค(build ์ด์ „์— ํ˜ธ์ถœ).
  // ์ƒํƒœ ๋ณ€์ˆ˜๊ฐ€ ์œ„์ ฏ์˜ ์ปจํ…์ŠคํŠธ์— ์˜์กดํ•˜๊ฑฐ๋‚˜, ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ปจํŠธ๋กค๋Ÿฌ ์ดˆ๊ธฐํ™”,
  // Stream ๊ตฌ๋…๊ณผ ๊ฐ™์€ ์ดˆ๊ธฐํ™” ์ž‘์—…์ด ํ•„์š”ํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค.
  @override
  void initState() {
    // ํ•ญ์ƒ super.initState()๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๋ถ€๋ชจ ํด๋ž˜์Šค์˜ ์ดˆ๊ธฐํ™” ์ž‘์—…์ด ๋จผ์ € ์ด๋ค„์ ธ์•ผ ํ•œ๋‹ค.
    super.initState();
    print('initState');
  }

  // dispose ๋ฉ”์„œ๋“œ๋Š” ์œ„์ ฏ์ด ์™„์ „ํžˆ ์ œ๊ฑฐ๋˜๊ธฐ ์ง์ „์— ํ˜ธ์ถœ๋œ๋‹ค.
  // API ํ˜ธ์ถœ ์ทจ์†Œ, ์ด๋ฒคํŠธ ๊ตฌ๋… ํ•ด์ œ, ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ปจํŠธ๋กค๋Ÿฌ ํ•ด์ œ ๋“ฑ ์ •๋ฆฌ ์ž‘์—…์— ์‚ฌ์šฉํ•œ๋‹ค.
  @override
  void dispose() {
    // ๋ฐ˜๋“œ์‹œ super.dispose()๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๋ถ€๋ชจ ํด๋ž˜์Šค์—์„œ ํ•„์š”ํ•œ ์ •๋ฆฌ ์ž‘์—…์ด ์ˆ˜ํ–‰๋˜๋„๋ก ํ•ด์•ผ ํ•œ๋‹ค.
    super.dispose();
    print('dispose!');
  }

  // build ๋ฉ”์„œ๋“œ๋Š” ์œ„์ ฏ์˜ UI๋ฅผ ๋ Œ๋”๋งํ•˜๋ฉฐ, ์œ„์ ฏ ํŠธ๋ฆฌ์˜ ์ž์‹ ์š”์†Œ๋“ค์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
  // build๋Š” initState ์ดํ›„์— ํ˜ธ์ถœ๋˜๊ณ , ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ(setState ํ˜ธ์ถœ ๋“ฑ)๋  ๋•Œ๋งˆ๋‹ค ํ˜ธ์ถœ๋œ๋‹ค.
  // build ๋ฉ”์„œ๋“œ๋Š” ๋ถ€์ˆ˜ ํšจ๊ณผ ์—†์ด ์ž…๋ ฅ๊ฐ’์—๋งŒ ์˜์กดํ•˜๋Š” ์ˆœ์ˆ˜ ํ•จ์ˆ˜์ฒ˜๋Ÿผ ๋™์ž‘ํ•ด์•ผ ํ•œ๋‹ค.
  @override
  Widget build(BuildContext context) {
    print('build');
    return Text(
      'Large Title',
      style: TextStyle(
        fontSize: 30,
        color: Theme.of(context).textTheme.titleLarge?.color,
      ),
    );
  }
}

// ์ฝ˜์†” ์ถœ๋ ฅ ๊ฒฐ๊ณผ
// flutter: initState
// flutter: build
// flutter: dispose! (LargeTitle ์œ„์ ฏ์ด ์ œ๊ฑฐ๋์„ ๋•Œ)

 

๋”๋ณด๊ธฐ
import 'package:flutter/material.dart';

class LifeCycle extends StatefulWidget {
  const LifeCycle({super.key});

  @override
  State<LifeCycle> createState() => _LifeCycleState();
}

// State
class _LifeCycleState extends State<LifeCycle> {
  bool showTitle = true;
  List<int> numbers = [];

  void toggleTitle() {
    setState(() => showTitle = !showTitle);
  }

  void onClicked() {
    // State ํด๋ž˜์Šค์—๊ฒŒ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋๋‹ค๊ณ  ์•Œ๋ฆฌ๋Š” ํ•จ์ˆ˜
    // setState ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉด build ๋ฉ”์„œ๋“œ๋ฅผ ๋‹ค์‹œ ์‹คํ–‰์‹œ์ผœ์„œ ๋ฆฌ๋ Œ๋”๋ง
    // ์ƒ๊ฐ๋ณด๋‹ค ํ”Œ๋Ÿฌํ„ฐ์—์„  State๋ฅผ ๋งŽ์ด ์‚ฌ์šฉํ•˜์ง€๋Š” ์•Š์Œ(๋ฆฌ์•กํŠธ์— ๋น„ํ•˜๋ฉด)
    setState(() => numbers.add(numbers.length));
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // theme ์†์„ฑ์€ MaterialApp์—์„œ ์ „์ฒด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ํ…Œ๋งˆ๋ฅผ ์ •์˜ํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค
      // ThemeData ๊ฐ์ฒด๋ฅผ ํ†ตํ•ด ์•ฑ์—์„œ ๊ณตํ†ต์ ์œผ๋กœ ์ ์šฉํ•  ํ…Œ๋งˆ ์†์„ฑ์„ ์„ค์ •ํ•œ๋‹ค
      theme: ThemeData(
        // TextTheme ํด๋ž˜์Šค๋ฅผ ์‚ฌ์šฉํ•ด ์•ฑ์—์„œ ์‚ฌ์šฉํ•  ํ…์ŠคํŠธ ์Šคํƒ€์ผ์„ ์ •์˜ํ•œ๋‹ค
        textTheme: const TextTheme(
          // titleLarge ์†์„ฑ์— ํฐ ํƒ€์ดํ‹€์— ์‚ฌ์šฉํ•  ํ…์ŠคํŠธ ์Šคํƒ€์ผ์„ ์ง€์ •ํ•œ๋‹ค
          titleLarge: TextStyle(color: Colors.red),
        ),
      ),
      home: Scaffold(
        backgroundColor: const Color(0xFFF4EDDB),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              showTitle ? const LargeTitle() : const Text('nothing to see'),
              IconButton(
                  iconSize: 100,
                  onPressed: toggleTitle,
                  icon: const Icon(Icons.remove_red_eye))
            ],
          ),
        ),
      ),
    );
  }
}

class LargeTitle extends StatefulWidget {
  const LargeTitle({super.key});

  @override
  State<LargeTitle> createState() => _LargeTitleState();
}

class _LargeTitleState extends State<LargeTitle> {
  // ์œ„์ ฏ์ด ํŠธ๋ฆฌ์— ์ฒ˜์Œ ์‚ฝ์ž…๋  ๋•Œ ํ•œ ๋ฒˆ๋งŒ ํ˜ธ์ถœ๋œ๋‹ค(build ์ด์ „์— ํ˜ธ์ถœ).
  // ์ƒํƒœ ๋ณ€์ˆ˜๊ฐ€ ์œ„์ ฏ์˜ ์ปจํ…์ŠคํŠธ์— ์˜์กดํ•˜๊ฑฐ๋‚˜, ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ปจํŠธ๋กค๋Ÿฌ ์ดˆ๊ธฐํ™”,
  // Stream ๊ตฌ๋…๊ณผ ๊ฐ™์€ ์ดˆ๊ธฐํ™” ์ž‘์—…์ด ํ•„์š”ํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค.
  @override
  void initState() {
    // ํ•ญ์ƒ super.initState()๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๋ถ€๋ชจ ํด๋ž˜์Šค์˜ ์ดˆ๊ธฐํ™” ์ž‘์—…์ด ๋จผ์ € ์ด๋ค„์ ธ์•ผ ํ•œ๋‹ค.
    super.initState();
    print('initState');
  }

  // dispose ๋ฉ”์„œ๋“œ๋Š” ์œ„์ ฏ์ด ์™„์ „ํžˆ ์ œ๊ฑฐ๋˜๊ธฐ ์ง์ „์— ํ˜ธ์ถœ๋œ๋‹ค.
  // API ํ˜ธ์ถœ ์ทจ์†Œ, ์ด๋ฒคํŠธ ๊ตฌ๋… ํ•ด์ œ, ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ปจํŠธ๋กค๋Ÿฌ ํ•ด์ œ ๋“ฑ ์ •๋ฆฌ ์ž‘์—…์— ์‚ฌ์šฉํ•œ๋‹ค.
  @override
  void dispose() {
    // ๋ฐ˜๋“œ์‹œ super.dispose()๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๋ถ€๋ชจ ํด๋ž˜์Šค์—์„œ ํ•„์š”ํ•œ ์ •๋ฆฌ ์ž‘์—…์ด ์ˆ˜ํ–‰๋˜๋„๋ก ํ•ด์•ผ ํ•œ๋‹ค.
    super.dispose();
    print('dispose!');
  }

  // build ๋ฉ”์„œ๋“œ๋Š” ์œ„์ ฏ์˜ UI๋ฅผ ๋ Œ๋”๋งํ•˜๋ฉฐ, ์œ„์ ฏ ํŠธ๋ฆฌ์˜ ์ž์‹ ์š”์†Œ๋“ค์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
  // build๋Š” initState ์ดํ›„์— ํ˜ธ์ถœ๋˜๊ณ , ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ(setState ํ˜ธ์ถœ ๋“ฑ)๋  ๋•Œ๋งˆ๋‹ค ํ˜ธ์ถœ๋œ๋‹ค.
  // build ๋ฉ”์„œ๋“œ๋Š” ๋ถ€์ˆ˜ ํšจ๊ณผ ์—†์ด ์ž…๋ ฅ๊ฐ’์—๋งŒ ์˜์กดํ•˜๋Š” ์ˆœ์ˆ˜ ํ•จ์ˆ˜์ฒ˜๋Ÿผ ๋™์ž‘ํ•ด์•ผ ํ•œ๋‹ค.
  @override
  Widget build(BuildContext context) {
    print('build');
    return Text(
      'Large Title',
      style: TextStyle(
        fontSize: 30,
        // Theme.of๋ฅผ ํ†ตํ•ด ํ˜„์žฌ ์œ„์ ฏ ํŠธ๋ฆฌ์˜ ์ƒ์œ„์— ์ •์˜๋œ ThemeData ๊ฐ์ฒด์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋‹ค
        // ์•„๋ž˜์—์„  MaterialApp ํ…Œ๋งˆ์— ์ ‘๊ทผํ•˜์—ฌ titleLarge ์ƒ‰์ƒ์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค
        color: Theme
            .of(context)
            .textTheme
            .titleLarge
            ?.color,
      ),
    );
  }
}

// ์ฝ˜์†” ์ถœ๋ ฅ ๊ฒฐ๊ณผ
// flutter: initState
// flutter: build
// flutter: dispose! (LargeTitle ์œ„์ ฏ์ด ์ œ๊ฑฐ๋์„ ๋•Œ)

 

Flexible / Expanded

CSS์—์„œ ํ™”๋ฉด ๋ ˆ์ด์•„์›ƒ์„ ๊ตฌ์„ฑํ•  ๋•Œ ์œ ์—ฐ์„ฑ์„ ์ œ๊ณตํ•˜๋Š” flex์ฒ˜๋Ÿผ ํ”Œ๋Ÿฌํ„ฐ๋„ ์ด์™€ ๋น„์Šทํ•œ Flexible ์œ„์ ฏ์„ ์ œ๊ณตํ•œ๋‹ค. Flexible ์œ„์ ฏ์€ ์ฃผ๋กœ Row, Column ์œ„์ ฏ ๋‚ด์—์„œ ์‚ฌ์šฉํ•˜์—ฌ ์ž์‹ ์œ„์ ฏ์ด ์ฐจ์ง€ํ•˜๋Š” ๊ณต๊ฐ„์„ ์กฐ์ ˆํ•œ๋‹ค.

class _HomeScreenState extends State<HomeScreen> {
  // ...

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Theme.of(context).scaffoldBackgroundColor,
      body: Column(
        children: [
          Flexible(
            flex: 1, // ์ „์ฒด ๋‚จ์€ ๊ณต๊ฐ„์—์„œ 1 ๋น„์œจ๋งŒํผ ์ฐจ์ง€
            child: Container(
              decoration: const BoxDecoration(color: Colors.red),
            ),
          ),
          Flexible(
            flex: 2, // ์ „์ฒด ๋‚จ์€ ๊ณต๊ฐ„์—์„œ 2 ๋น„์œจ๋งŒํผ ์ฐจ์ง€
            child: Container(
              decoration: const BoxDecoration(color: Colors.green),
            ),
          ),
          Flexible(
            flex: 1, // ์ „์ฒด ๋‚จ์€ ๊ณต๊ฐ„์—์„œ 1 ๋น„์œจ๋งŒํผ ์ฐจ์ง€
            child: Container(
              decoration: const BoxDecoration(color: Colors.blue),
            ),
          ),
        ],
      ),
    );
  }
}

 

์ถœ๋ ฅ ํ™”๋ฉด

Flexible ์œ„์ ฏ fit ์†์„ฑ์— FlexFit.tight ๊ฐ’์„ ์‚ฌ์šฉํ•˜๋ฉด ์ž์‹ ์œ„์ ฏ์ด ๋‚จ์€ ๊ณต๊ฐ„์„ ๋ชจ๋‘ ์ฑ„์šฐ๋„๋ก ๋งŒ๋“ ๋‹ค. Expanded ์œ„์ ฏ์€ FlexFit.tight๋กœ ์„ค์ •๋œ Flexible์˜ ๋‹จ์ถ• ํ˜•ํƒœ๋กœ ๊ฐ€๋Šฅํ•œ ๋ชจ๋“  ๊ณต๊ฐ„์„ ์ฑ„์šด๋‹ค.

class _HomeScreenState extends State<HomeScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Theme.of(context).scaffoldBackgroundColor,
      body: Column(
        children: [
          // ...์ƒ๋žต
          // ์•„๋ž˜๋ถ€ํ„ด ๊ฐ€์žฅ ์•„๋ž˜์— ์žˆ๋Š” ํฐ์ƒ‰ ๋ฐฐ๊ฒฝ ์นด๋“œ ๋ถ€๋ถ„
          Flexible(
            flex: 1,
            child: Row(
              children: [
                // Expanded ์œ„์ ฏ์„ ์‚ฌ์šฉํ–ˆ์œผ๋ฏ€๋กœ ์ˆ˜ํ‰์œผ๋กœ ๊ฐ€๋Šฅํ•œ ๊ณต๊ฐ„์„ ๋ชจ๋‘ ์ฑ„์šด๋‹ค.
                // Flexible ์œ„์ ฏ์„ ์‚ฌ์šฉํ•˜๊ณ  fit: FlexFit.tight ์†์„ฑ์„ ์ถ”๊ฐ€ํ•ด๋„ ๊ฒฐ๊ณผ๋Š” ๊ฐ™๋‹ค.
                Expanded(
                  child: Container(
                    decoration: BoxDecoration(
                      color: Theme.of(context).cardColor, // ํฐ์ƒ‰ ๋ฐฐ๊ฒฝ
                    ),
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Text('Pomodoros', style: TextStyle(/* ... */)),
                        Text('0', style: TextStyle(/* ... */)),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

 

๋”๋ณด๊ธฐ
import 'package:flutter/material.dart';
import 'package:practice_flutter/screens/home.dart';

void main() {
  runApp(const App());
}

class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        scaffoldBackgroundColor: const Color(0xffE7626C),
        textTheme: const TextTheme(
          headlineLarge: TextStyle(
            color: Color(0xff232B55),
          ),
        ),
        cardColor: const Color(0xffF4EDDB),
      ),
      home: const HomeScreen(),
    );
  }
}

 

(์ขŒ) Expanded ์œ„์ ฏ์„ ์‚ฌ์šฉํ–ˆ์„ ๋•Œ, (์šฐ) Expanded ์œ„์ ฏ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•˜์„ ๋•Œ

Timer

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<StatefulWidget> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  int totalSeconds = 1500; // 25๋ถ„
  bool isRunning = false; // ํƒ€์ด๋จธ ์ง„ํ–‰์ค‘ ์—ฌ๋ถ€
  late Timer timer; // ํ˜„์žฌ ์‹คํ–‰ ์ค‘์ธ ํƒ€์ด๋จธ ๊ฐ์ฒด

  void onTick(Timer timer) {
    setState(() => totalSeconds--);
  }

  // isRunning์ด false์ผ ๋•Œ ์‹คํ–‰๋˜๋Š” ํ•ธ๋“ค๋Ÿฌ
  void onStartPressed() {
    // 1์ดˆ ๊ฐ„๊ฒฉ์œผ๋กœ onTick์„ ์‹คํ–‰์‹œ์ผœ์„œ totalSeconds 1์”ฉ ๊ฐ์†Œ
    timer = Timer.periodic(const Duration(seconds: 1), onTick);
    setState(() => isRunning = true);
  }

  // isRunning์ด true์ผ ๋•Œ ์‹คํ–‰๋˜๋Š” ํ•ธ๋“ค๋Ÿฌ
  void onPausePressed() {
    timer.cancel(); // ํƒ€์ด๋จธ ์ค‘๋‹จ
    setState(() => isRunning = false);
  }

  // ...

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Theme.of(context).scaffoldBackgroundColor,
      body: Column(
        children: [
          Flexible(/* ... */),
          Flexible(
            flex: 2,
            child: Center(
              child: IconButton(
                iconSize: 120,
                color: Theme.of(context).cardColor,
                icon: Icon(isRunning
                    ? Icons.pause_circle_outline
                    : Icons.play_circle_outline),
                // isRunning ์ƒํƒœ์— ๋”ฐ๋ผ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ์กฐ๊ฑด๋ถ€ ์‹คํ–‰
                onPressed: isRunning ? onPausePressed : onStartPressed,
              ),
            ),
          ),
          Flexible(/* ... */),
        ],
      ),
    );
  }
}

 

๋”๋ณด๊ธฐ
import 'dart:async';

import 'package:flutter/material.dart';

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<StatefulWidget> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  static const twentyFiveMinutes = 1500;
  int totalSeconds = twentyFiveMinutes;
  bool isRunning = false;
  int totalPomodoros = 0;
  late Timer timer;

  void onTick(Timer timer) {
    setState(() {
      if (totalSeconds == 0) {
        totalPomodoros++;
        isRunning = false;
        totalSeconds = twentyFiveMinutes;
        timer.cancel();
      } else {
        totalSeconds--;
      }
    });
  }

  void onStartPressed() {
    if (isRunning) return;
    timer = Timer.periodic(const Duration(seconds: 1), onTick);
    setState(() => isRunning = true);
  }

  void onPausePressed() {
    timer.cancel();
    setState(() => isRunning = false);
  }

  void onResetPressed() {
    onPausePressed();
    totalSeconds = twentyFiveMinutes;
  }

  String format(int sec) {
    // 0:24:59.000000, 0:24:58.000000, ...
    var duration = Duration(seconds: sec);
    var formatted = duration.toString().substring(2, 7);
    return formatted;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Theme
          .of(context)
          .scaffoldBackgroundColor,
      body: Column(
        children: [
          Flexible(
            flex: 1,
            child: Container(
              alignment: Alignment.bottomCenter,
              child: Text(
                format(totalSeconds),
                style: TextStyle(
                    color: Theme
                        .of(context)
                        .cardColor,
                    fontSize: 89,
                    fontWeight: FontWeight.w700),
              ),
            ),
          ),
          Flexible(
            flex: 2,
            fit: FlexFit.tight,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                IconButton(
                  iconSize: 120,
                  color: Theme
                      .of(context)
                      .cardColor,
                  icon: Icon(isRunning
                      ? Icons.pause_circle_outline
                      : Icons.play_circle_outline),
                  onPressed: isRunning ? onPausePressed : onStartPressed,
                ),
                IconButton(
                  iconSize: 120,
                  color: Theme
                      .of(context)
                      .cardColor,
                  icon: const Icon(Icons.restart_alt),
                  onPressed: onResetPressed,
                ),
              ],
            ),
          ),
          Flexible(
            flex: 1,
            child: Row(
              children: [
                Expanded(
                  child: Container(
                    decoration: BoxDecoration(
                        color: Theme
                            .of(context)
                            .cardColor,
                        borderRadius: BorderRadius.circular(50)),
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        Text(
                          'Pomodoros',
                          style: TextStyle(
                            fontSize: 20,
                            fontWeight: FontWeight.w600,
                            color: Theme
                                .of(context)
                                .textTheme
                                .headlineLarge
                                ?.color,
                          ),
                        ),
                        Text(
                          '$totalPomodoros',
                          style: TextStyle(
                            fontSize: 58,
                            fontWeight: FontWeight.w600,
                            color: Theme
                                .of(context)
                                .textTheme
                                .headlineLarge
                                ?.color,
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

 

Timer.periodic ์ƒ์„ฑ์ž๋Š” ์ž‘์—…์„ ์ฃผ๊ธฐ์ ์œผ๋กœ ์‹คํ–‰ํ•˜๊ธฐ ์œ„ํ•œ ํƒ€์ด๋จธ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. ๋ธŒ๋ผ์šฐ์ € WebAPI์—์„œ ์ œ๊ณตํ•˜๋Š” setInterval ๋ฉ”์„œ๋“œ์™€ ๋น„์Šทํ•˜๋‹ค๊ณ  ๋ณด๋ฉด ๋œ๋‹ค.

 

Timer.periodic(Duration duration, void callback(Timer timer))

 

Timer.periodic์„ ํ˜ธ์ถœํ•˜๋ฉด Timer ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ณ , cancel() ๋ฉ”์„œ๋“œ๋กœ ํƒ€์ด๋จธ๋ฅผ ์ค‘์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ฒซ ๋ฒˆ์งธ ์ธ์ž์ธ Duration์€ ์‹œ๊ฐ„ ๊ฐ„๊ฒฉ์„ ๋‚˜ํƒ€๋‚ด๋Š” ํด๋ž˜์Šค๋กœ ๋ฐ€๋ฆฌ์ดˆ, ์ดˆ, ๋ถ„, ์‹œ๊ฐ„ ๋˜๋Š” ์ผ(days) ๋‹จ์œ„๋ฅผ ์ง€์›ํ•œ๋‹ค.

Duration duration = Duration(minutes: 2, seconds: 30);
print(duration); // 0:02:30.000000

// 2๋ถ„ 30์ดˆ ๊ฐ„๊ฒฉ์œผ๋กœ ์ฝœ๋ฐฑ ์‹คํ–‰
Timer timer = Timer.periodic(duration, (Timer timer) { /* ... */ });
timer.cancel(); // ํƒ€์ด๋จธ ์ค‘์ง€

 

duration ๊ฐ์ฒด์˜ ์‹œ๊ฐ„์€ ๋‹ค์–‘ํ•œ ๋‹จ์œ„๋กœ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์žˆ๊ณ , toString()์„ ์ด์šฉํ•ด ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

Duration duration = Duration(hours: 1, minutes: 2, seconds: 30);

// ์ „์ฒด ์‹œ๊ฐ„ ์ค‘ '์‹œ๊ฐ„' ๋‹จ์œ„ ๊ฐ’๋งŒ ๋ฐ˜ํ™˜, ๋‚จ์€ ๋ถ„/์ดˆ๋Š” ๋ฒ„๋ฆผ. ์ถœ๋ ฅ: 1
print(duration.inHours);
// ์ „์ฒด ์‹œ๊ฐ„ ์ค‘ '๋ถ„' ๋‹จ์œ„ ๊ฐ’๋งŒ ๋ฐ˜ํ™˜, ๋‚จ์€ ์ดˆ๋Š” ๋ฒ„๋ฆผ. ์ถœ๋ ฅ: 62
print(duration.inMinutes);
// ์ „์ฒด ์‹œ๊ฐ„ ์ค‘ '์ดˆ' ๋‹จ์œ„ ๊ฐ’๋งŒ ๋ฐ˜ํ™˜, ๋‚จ์€ ๋ฐ€๋ฆฌ์ดˆ๋Š” ๋ฒ„๋ฆผ. ์ถœ๋ ฅ: 3750
print(duration.inSeconds);
// toString() ๋ฉ”์„œ๋“œ๋ฅผ ์ด์šฉํ•ด ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•  ์ˆ˜๋„ ์žˆ๋‹ค. ์ถœ๋ ฅ: 02:30
print(duration.toString().substring(2, 7));

 

Timer ๊ฐ์ฒด๋Š” cancel() ๋ฉ”์„œ๋“œ ์™ธ์—๋„ ํƒ€์ด๋จธ์˜ ์‹คํ–‰ ์—ฌ๋ถ€๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” isActive ์†์„ฑ๊ณผ, ์ฃผ๊ธฐ์ ์œผ๋กœ ์‹คํ–‰๋˜๋Š” ํƒ€์ด๋จธ์—์„œ ํ˜„์žฌ ๋ช‡ ๋ฒˆ์งธ ํ˜ธ์ถœ(tick ์ˆ˜)์ธ์ง€ ๋‚˜ํƒ€๋‚ด๋Š” tick ์†์„ฑ์„ ๊ฐ€์ง„๋‹ค.

Timer.periodic(Duration(seconds: 1), (Timer timer) {
  print(timer.tick); // 1 2 3 4 ...
});

 

์ผ์ • ์‹œ๊ฐ„ ํ›„์— ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰ํ•˜๋ ค๋ฉด Timer ํด๋ž˜์Šค์˜ ์ƒ์„ฑ์ž๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

Timer(Duration(seconds: 5), () {
  print('hello'); // 5์ดˆ ๋’ค hello 1๋ฒˆ ์ถœ๋ ฅ
});

 


๊ธ€ ์ˆ˜์ •์‚ฌํ•ญ์€ ๋…ธ์…˜ ํŽ˜์ด์ง€์— ๊ฐ€์žฅ ๋น ๋ฅด๊ฒŒ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”
 
 
๋ฐ˜์‘ํ˜•