๊ฐœ๋ฐœ ์ผ์ง€ ๐Ÿ’ป/Swemo

Swemo - Iteration 2: UI ๋””์ž์ธ ๋ฐฉํ–ฅ ํƒ์ƒ‰(Exploration)

interfacer_han 2026. 1. 13. 13:40

#1 ๊ฐœ๋ฐœ ๋ชฉํ‘œ (Iteration Goal)

#1-1 ์ด์ƒ(Ideal)๊ณผ ์ง•๊ฒ€๋‹ค๋ฆฌ

์ด์ƒ(Ideal)

์ง์ „ Iteration์—์„œ ์•ฑ์˜ ์•„์ฃผ ๊ธฐ์ดˆ์ ์ธ ํ‹€์„ ์žก์•˜๋‹ค. ์ด์ œ ๋‚จ์€ ๊ฒƒ์€ ์—ฌ๊ธฐ์— ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ๊ตฌํ˜„์„ ์Œ“์•„ ์˜ฌ๋ฆฌ๋Š” ๊ฒƒ์ด๋‹ค. ๋จผ์ € ๋“  ์ƒ๊ฐ์€ "๋„๋ฉ”์ธ ์˜์—ญ์„ ์ผ๋‹จ ๊ตฌํ˜„ํ•˜์ž"์˜€๋‹ค. ํ•˜์ง€๋งŒ, ๋„๋ฉ”์ธ๋„ ๊ฒฐ๊ตญ ๋‚ด๊ฐ€ UI์—์„œ ์–ด๋–ป๊ฒŒ ํ‘œํ˜„ํ•˜๋А๋ƒ์— ๋”ฐ๋ผ ์ขŒ์ง€์šฐ์ง€๋  ๊ฒƒ์ด๋ผ๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค. ์™œ๋ƒํ•˜๋ฉด Swemo๋Š” UI๋กœ '์Šน๋ถ€'๋ฅผ ๋ณผ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์€ ์•ฑ์ด๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ๋งŒ์•ฝ Swemo ์‚ฌ๋žŒ๋“ค์—๊ฒŒ '์ข‹์€ ์•ฑ'์œผ๋กœ ์ธ์‹๋œ๋‹ค๋ฉด, ๊ทธ ์ด์œ ๋Š” (๊ธฐ์กด ๋ฉ”๋ชจ ์•ฑ๋“ค์— ๋น„ํ•ด) ์ข‹์€ UI์ผ ๊ฒƒ์ด๋‹ค. ๊ฒฐ๋ก ์€, ๋ฌด์—‡์„ ์–ด๋–ป๊ฒŒ ๋ณด์—ฌ์ฃผ๋А๋ƒ๊ฐ€ ๊ฐ€์žฅ ๊ทผ๋ณธ์ ์ธ ์„ค๊ณ„์˜ ํ•ต์‹ฌ์ด๋‹ค. Iteration 2์—์„œ ๊ตฌ์ฒดํ™”ํ•œ UI๋ฅผ ํ†ตํ•ด '์ด์ƒ(Ideal)'์˜ ์œค๊ณฝ๋„ ๋ณด์ด๋ฆฌ๋ผ ๋ฏฟ๋Š”๋‹ค.

 

Iteration 2: UI ๋””์ž์ธ ๋ฐฉํ–ฅ ํƒ์ƒ‰(Exploration)

์ •๋ง UI๋งŒ ๊ฐœ๋ฐœํ•  ๊ฒƒ์ด๋‹ค. ์•ฑ ์ปดํŒŒ์ผ๋„ ์•ˆ ํ•˜๊ณ  ๋Œ€๋ถ€๋ถ„ Compose Preview๋งŒ์œผ๋กœ ์ž‘์—…ํ•  ๊ฒƒ์ด๋‹ค. ๋ฌผ๋ก  UI ํ™•์ธ์„ ์œ„ํ•ด์„œ๋ผ๋„ ๊ธฐ์ดˆ์ ์ธ ์ƒํ˜ธ์ž‘์šฉ ์ •๋„๋Š” ์žˆ์–ด์•ผ ํ•˜๋ฏ€๋กœ ํ…Œ์ŠคํŠธ์šฉ State๋Š” ๋ช‡ ๊ฐœ ๋„ฃ์–ด์ค„ ๊ฒƒ์ด๋‹ค.

 

'Bubble' ๊ฐœ๋…๋„ ์—ฌ๊ธฐ์„œ๋ถ€ํ„ฐ ์กฐ๊ธˆ์”ฉ ๋„์ž…ํ•ด ๋‚˜๊ฐ„๋‹ค. ์‚ฌ์‹ค ์ด ๊ฐœ๋…์€ ๋ง๋กœ ํ‘œํ˜„ํ•˜๊ธฐ๊ฐ€ ์–ด๋ ต๋‹ค. ๊ทธ๋ž˜์„œ ๊ตฌ๊ตฌ์ ˆ์ ˆ ์Š๊ธฐ๋ณด๋‹ค๋Š”, ์ฝ”๋“œ ๊ทธ๋ฆฌ๊ณ  ์Šคํฌ๋ฆฐ์ƒท์œผ๋กœ ๋ณด์—ฌ์ฃผ๋ ค๊ณ  ํ•œ๋‹ค.

 

#1-2 ๊ฐœ๋ฐœ ๋ฒ”์œ„

์ด๋ฒˆ ๊ฐœ๋ฐœ ๋ฒ”์œ„ (In Scope)

  1. ๋ฉ”๋ชจ ๋ฆฌ์ŠคํŠธ๋ฅผ ์ฝ๊ธฐ ์œ„ํ•œ ํ™”๋ฉด UI
  2. ๋ฉ”๋ชจ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ธฐ ์œ„ํ•œ ํ™”๋ฉด UI
  3. ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ์„ ํƒํ•˜๊ธฐ ์œ„ํ•œ ํ™”๋ฉด UI
  4. ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ธฐ ์œ„ํ•œ ํ™”๋ฉด UI
  5. Compose Runtime๋งŒ์œผ๋กœ๋„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋Š” UI (Compose Preview๋งŒ ๋ณด๊ณ  ๊ฐœ๋ฐœ)
  6. Compose State๋ฅผ ์œ„ํ•œ ๋„๋ฉ”์ธ ๋ชจ๋ธ ์ •๋ฆฝ
  7. 'Bubble' ๊ฐœ๋…์ด UI์—์„œ ํ‘œ์ถœ๋˜๋Š” ๋ฐฉ์‹

 

๋ฒ”์œ„ ์™ธ (Out of Scope)

  1. Compose Runtime๋งŒ์œผ๋กœ๋Š” ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์—†๋Š” UI (Compose Preview๋งŒ์œผ๋กœ๋Š” ํ™•์ธ ๋ถˆ๊ฐ€๋Šฅํ•œ ๊ธฐ๋Šฅ)
  2. ์‹œ์Šคํ…œ ์ปดํฌ๋„ŒํŠธ(Navigation Bar, Status Bar ๋“ฑ)์„ ๊ณ ๋ คํ•œ UI
  3. UI ์™ธ์ ์ธ ๊ธฐ๋Šฅ
  4. 'Bubble' ๊ฐœ๋…์ด ์‹ค์ œ๋กœ (๋ฐฑ์—”๋“œ์—์„œ) ์ž‘๋™ํ•˜๋„๋ก ๊ตฌํ˜„
  5. ๊ธฐ๋ณธ(built-in) ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ œ์™ธํ•œ, ์ถ”๊ฐ€ ์• ๋‹ˆ๋ฉ”์ด์…˜

 

#2 ๋ณ€๊ฒฝ ๋‚ด์—ญ (์ปค๋ฐ‹ ๋ชฉ๋ก)

#2-1 Iteration 2-1: UI Wireframing

Jetpack Compose๋กœ ์ง์ ‘ ๊ตฌํ˜„ํ•˜๊ธฐ์— ์•ž์„œ ์™€์ด์–ดํ”„๋ ˆ์ด๋ฐ์„ ์ง„ํ–‰ํ•œ๋‹ค.

 

๊ด€๋ จ ๊ฐœ๋ฐœ ๋ฒ”์œ„

  • ๋ฉ”๋ชจ ๋ฆฌ์ŠคํŠธ๋ฅผ ์ฝ๊ธฐ ์œ„ํ•œ ํ™”๋ฉด UI
  • ๋ฉ”๋ชจ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ธฐ ์œ„ํ•œ ํ™”๋ฉด UI
  • ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ์„ ํƒํ•˜๊ธฐ ์œ„ํ•œ ํ™”๋ฉด UI
  • ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ธฐ ์œ„ํ•œ ํ™”๋ฉด UI
  • 'Bubble' ๊ฐœ๋…์ด UI์—์„œ ํ‘œ์ถœ๋˜๋Š” ๋ฐฉ์‹

 

์ฃผ์š” ๋ณ€๊ฒฝ์ 

์ˆœ์ˆ˜ ๋””์ž์ธ์ด๊ธฐ ๋•Œ๋ฌธ์— ์ฝ”๋“œ์ƒ์œผ๋กœ๋Š” ๋ณ€ํ•œ ์ ์ด ์—†๋‹ค. ํ•˜์ง€๋งŒ, ์ด ์ž‘์—…์— ๊ธฐ๋ฐ˜ํ•ด์„œ Iteration 2-2๊ฐ€ ์ง„ํ–‰๋  ๊ฒƒ์ด๋ฏ€๋กœ, Iteration 2-2์˜ ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€์— Iteration 2-1์˜ ์ž‘์—… ๋‚ด์šฉ ๋ฐ ์Šคํฌ๋ฆฐ์ƒท ๋งํฌ๋ฅผ ์ฒจ๋ถ€ํ•œ๋‹ค.

 

์Šคํฌ๋ฆฐ์ƒท - ๋ฉ”๋ชจ ๋ฆฌ์ŠคํŠธ ์ฝ๊ธฐ

Swemo์˜ ๋ฉ”๋ชจ ํ•˜๋‚˜ํ•˜๋‚˜๋Š” 'key-value์˜ ๋ฆฌ์ŠคํŠธ'๋‹ค. ๋”ฐ๋ผ์„œ ์™ผ์ชฝ์— ์žˆ๋Š” ์—‘์…€ ํ‘œ์™€ ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฅผ Swemo๋กœ ํ‘œํ˜„ํ•  ์ˆ˜๋„ ์žˆ๋‹ค. ๊ทธ๋ž˜์„œ ํ›„์— ์–ธ์  ๊ฐ€๋Š” ์—‘์…€ ↔ Swemo ์–‘๋ฐฉํ–ฅ ๋‚ด๋ณด๋‚ด๊ธฐ ๊ธฐ๋Šฅ์„ ๋งŒ๋“ค์–ด๋ณด๊ณ  ์‹ถ๋‹ค. ํ•˜์ง€๋งŒ ๊ธฐ๋ณธ๋ถ€ํ„ฐ ํ•˜๊ณ  ๋ฐ”๋ผ์•ผ ํ•  ๊ฒƒ์ด๋‹ค.

 

์Šคํฌ๋ฆฐ์ƒท - ๋ฉ”๋ชจ ์ถ”๊ฐ€

key-value ์Œ์„ ๋‹จ ํ•˜๋‚˜๋งŒ ์“ด๋‹ค๋ฉด, ๊ทธ๊ฑด ์ผ๋ฐ˜์ ์ธ ๋ฉ”๋ชจ ์•ฑ๊ณผ ๋‹ค๋ฅผ ๋ฐ” ์—†๋‹ค. ๊ทธ๋ž˜์„œ ์ž…๋ ฅ ์‹œ key-value ์Œ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋Š” ์ถ”๊ฐ€ ์˜์—ญ์ด ํ•„์š”ํ•˜๋‹ค. ์Šคํฌ๋ฆฐ์ƒท์— ์žˆ๋Š” '๋‚˜์ด', '์ข…', 'ํ‚ค', '๋ถ„๋ฅ˜' ๋“ฑ์˜ ๋ฒ„ํŠผ์„ ๋‚˜๋Š” Bubble์ด๋ผ๊ณ  ๋ถ€๋ฅธ๋‹ค. ๋น„๋ˆ—๋ฐฉ์šธ์ฒ˜๋Ÿผ ๋™๊ธ€๋™๊ธ€ํ•œ ๋ชจ์–‘์ด๊ณ , ๋˜ ์‚ฌ์šฉ์ž๊ฐ€ ํ•œ๋ฒˆ ํด๋ฆญํ•˜๋ฉด ์‚ฌ๋ผ์ง€๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์œ„์˜ ์Šคํฌ๋ฆฐ์ƒท์—์„œ '์ข…' Bubble์„ ํด๋ฆญํ•˜๋ฉด, ('์ข…'์„ key๋กœ ๋‘๋Š” EditText๊ฐ€ ์ถ”๊ฐ€๋˜๋ฉฐ) '์ข…' Bubble์ด ์‚ฌ๋ผ์ง„๋‹ค.

 

์Šคํฌ๋ฆฐ์ƒท - ์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ ๋ฐ ์ถ”๊ฐ€

Iteration 1-3์—์„œ ๋งŒ๋“ค์–ด๋‘” ModalNavigationDrawer ์†์— ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ๊ตฌ์„ฑํ•œ๋‹ค.

 

์ฐธ๊ณ 

 

#2-2 Iteration 2-2: Domain Modeling

#2-1์˜ ๋””์ž์ธ์„ ๋ฐ”๋กœ ๋งŒ๋“ค๋ ค๋‹ค๊ฐ€๋Š” ๋ฐ˜๋“œ์‹œ ์‹คํŒจํ•œ๋‹ค. ๋„๋ฉ”์ธ ๋ชจ๋ธ๋ง(๊ฐ์ฒด ๊ฐ„์˜ ๊ด€๊ณ„ ์ฝ”๋“œํ™”)์„ ์ž‘์—…ํ•ด ์ฃผ์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ViewModel ๋ฐ UI๋Š” ๋„๋ฉ”์ธ ๋ชจ๋ธ๋ง์— '๋Œ๋ ค'๋‹ค๋‹Œ๋‹ค. ๋”ฐ๋ผ์„œ ์ง€๊ธˆ ํ™•์‹คํ•˜๊ฒŒ ์ •ํ•ด๋‘์ง€ ์•Š๋Š”๋‹ค๋ฉด, ๋‚˜์ค‘์— ๊ฐ€์„œ ๋ชจ๋“  ์ž‘์—…์„ ๊ฐˆ์•„์—Ž๊ณ  ๋‹ค์‹œ ํ•˜๊ฒŒ ๋  ์ˆ˜๋„ ์žˆ๋‹ค.

 

๊ด€๋ จ ๊ฐœ๋ฐœ ๋ฒ”์œ„

  • Compose State๋ฅผ ์œ„ํ•œ ๋„๋ฉ”์ธ ๋ชจ๋ธ ์ •๋ฆฝ

 

์ฃผ์š” ๋ณ€๊ฒฝ์ 

  • ๋„๋ฉ”์ธ ๋ชจ๋ธ ์ถ”๊ฐ€
  • ๊ธฐ์กด์˜ ์ž„์‹œ ๋„๋ฉ”์ธ ๋ชจ๋ธ์€ ์ œ๊ฑฐ
  • ์ƒˆ๋กœ ์ถ”๊ฐ€ํ•œ ๋„๋ฉ”์ธ ๋ชจ๋ธ์ด TestScreen๊ณผ ์ž˜ ํ˜ธํ™˜๋˜๋„๋ก ์—…๋ฐ์ดํŠธ

 

์ฝ”๋“œ ์Šค๋‹ˆํŽซ

// in MemoContent.kt
data class MemoContent(val label: String, val text: String)

---

// in Memo.kt
data class Memo(val categoryId: String, val id: String, val contents: List<MemoContent> = emptyList())

---

// in Category.kt
data class Category(val id: String, val name: String)

Category๊ฐ€ Memo๋ฅผ ํ”„๋กœํผํ‹ฐ๋กœ ๋ณด์œ ํ•˜๊ฒŒ ๋‘์ง€ ์•Š๋Š” ์ด์œ ๊ฐ€ ์žˆ๋‹ค. ์นดํ…Œ๊ณ ๋ฆฌ๋Š” '์ปจํ…Œ์ด๋„ˆ'๋ผ๊ธฐ๋ณด๋‹ค๋Š” (SNS ๊ฒŒ์‹œ๊ธ€์— ๋ถ™๋Š”) 'ํƒœ๊ทธ'์— ๋” ๊ฐ€๊นŒ์šด ๊ฐœ๋…์ด๋‹ค. ๋ณธ ์•ฑ์„ ์‚ฌ์šฉํ•  ์‚ฌ์šฉ์ž์˜ ํ–‰์œ„๋ฅผ ์ƒ์ƒํ•ด ๋ณด์•„๋„, "์นดํ…Œ๊ณ ๋ฆฌ ์ˆ˜์ •(ํŽธ์ง‘)"๊ณผ "๋ฉ”๋ชจ ์ˆ˜์ •(ํŽธ์ง‘)"์„ ์™„์ „ํžˆ ๋‹ค๋ฅธ ํ–‰์œ„๋กœ ์ธ์‹ํ•  ๊ฒƒ์ด ๊ทธ๋ ค์ง„๋‹ค.

๋ฐ˜๋Œ€๋กœ Memo๋Š” List<MemoContent>๋ฅผ ํ”„๋กœํผํ‹ฐ๋กœ ๋ณด์œ ํ•œ๋‹ค. MemoContent ๋Š” ๋ฐ˜๋“œ์‹œ ํŠน์ • Memo์— ๊ท€์†๋˜๋ฉฐ, ๋‹จ๋…์œผ๋กœ๋Š” ์•„๋ฌด๋Ÿฐ ์˜๋ฏธ๋ฅผ ๊ฐ€์ง€์ง€ ์•Š๋Š”๋‹ค. Memo์— ๊ท€์†๋˜์–ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— Memo๊ฐ€ ์‚ญ์ œ๋˜๋ฉด ๊ฐ™์ด ์‚ฌ๋ผ์ง„๋‹ค. ๋˜ "Memo๋ฅผ ์ˆ˜์ •ํ•œ๋‹ค"๋ผ๋Š” ํ–‰์œ„๋Š” "MemoContent๋ฅผ ์ˆ˜์ •ํ•œ๋‹ค"๋Š” ํ–‰์œ„์™€ ์‚ฌ์‹ค์ƒ์˜ ๊ฐ™์€ ํ–‰์œ„๋‹ค. ๋”ฐ๋ผ์„œ ๋ถ„๋ฆฌํ•˜์ง€ ์•Š์•˜๋‹ค.

 

Excel์˜ ๋„๋ฉ”์ธ ๋ชจ๋ธ๊ณผ Swemo์˜ ๋„๋ฉ”์ธ ๋ชจ๋ธ ๊ฐœ๋…์ ์œผ๋กœ ์—ฐ๊ฒฐ
์ถ”๊ฐ€๋กœ (Microsoft) Excel์˜ ๋„๋ฉ”์ธ ๋ชจ๋ธํ•˜๊ณ ๋„ ๊ฐœ๋…์ ์œผ๋กœ๋‚˜๋งˆ ์—ฐ๊ฒฐ ์ง€์–ด๋‘˜ ํ•„์š”๊ฐ€ ์žˆ๋‹ค. #2-1์—์„œ ๋งํ•œ ๋Œ€๋กœ (๋ฏธ๋ž˜์—) ์—‘์…€๊ณผ์˜ ์ƒํ˜ธ ํ˜ธํ™˜์„ ์œ„ํ•ด์„œ๊ธฐ๋„ ํ•˜์ง€๋งŒ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ๋ชจ์–‘์„ ํ•œ ์—‘์…€์˜ ๊ตฌ์กฐ๋ฅผ ๋”ฐ๋ผ๊ฐ€์„œ ๋‚˜์  ๊ฒƒ๋„ ์—†๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์•„๋ž˜๋Š” #2-1์— ์žˆ๋˜ ์—‘์…€ ์–‘์‹์ด๋‹ค. 

 

์ด ์—‘์…€ ์–‘์‹์„ ์ฐธ์กฐํ•˜๋ฉด, ์•„๋ž˜์— ์žˆ๋Š” Excel ๋„๋ฉ”์ธ ๋ชจ๋ธ vs Swemo ๋„๋ฉ”์ธ ๋ชจ๋ธ ํ‘œ๋ฅผ ์ฝ๊ธฐ ํŽธํ•  ๊ฒƒ์ด๋‹ค.

 

์—ฌ๊ธฐ์„œ๋Š” ์ด๋ ‡๊ฒŒ ์—ฐ๊ฒฐ์ ์„ ๋ฐํ˜€๋งŒ ๋†“๊ณ , ์•ž์œผ๋กœ ๊ฐœ๋ฐœํ•ด ๋‚˜๊ฐˆ ๋•Œ ๋‘๊ณ ๋‘๊ณ  ์ฐธ์กฐํ•˜๋Š” ์šฉ๋„๋กœ ์“ฐ๊ฒ ๋‹ค.

 

์ฐธ๊ณ 

 

#2-3 Iteration 2-3: TestScreen์˜ State ๊ฒฉ๋ฆฌ, @Preview ์‚ฌ์šฉ ์ค€๋น„

Android Studio์˜ Preview ๊ธฐ๋Šฅ์„ ํ†ตํ•ด UI ๊ฐœ๋ฐœ์„ ํ•˜๊ธฐ๋กœ ํ–ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ ค๋ฉด ๊ฑธ๋ฆฌ์ ๊ฑฐ๋ฆฌ๋Š” State๋ฅผ ๋ถ„๋ฆฌํ•  ํ•„์š”๊ฐ€ ์žˆ๋‹ค. TestScreen()์„ (๋ฉ”์†Œ๋“œ ์˜ค๋ฒ„๋กœ๋”ฉ์„ ํ†ตํ•ด) ์•„๋ž˜์˜ 2๊ฐœ์˜ ํ•จ์ˆ˜๋กœ ๋‚˜๋ˆด๋‹ค.

 

  • TestScreen1
    • TestScreen(viewModel: ViewModel) 
    • Stateful Screen: ํ™”๋ฉด์ด ์‚ฌ๋ผ์ ธ๋„ ๊ณ„์† ์œ ์ง€ํ•ด์•ผ ํ•  State๋“ค์„ ๋ชจ์•„, ์•„๋ž˜์— ์žˆ๋Š” ํ•จ์ˆ˜์— ๋ณด๋‚ด์คŒ
  • TestScreen2
    • TestScreen(categories: List<Category>, memos: List<Memo>, ...)
    • Pure UI Screen: ๋ชจ๋“  State๋ฅผ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ์ฃผ์ž…๋ฐ›์Œ. ๋‹จ, ํ™”๋ฉด์ด ์‚ฌ๋ผ์ง€๋ฉด ์•„๋ฌด๋Ÿฐ ์˜๋ฏธ๊ฐ€ ์—†๋Š” UI ์ „์šฉ State(DrawerState ๋“ฑ)๋Š” ๋‚ด๋ถ€์—์„œ ๋ณด์œ 

 

Preview ๊ธฐ๋Šฅ์„ ์ด์šฉํ•ด ์ฃผ๋กœ ๊ฐœ๋ฐœํ•  ํ•จ์ˆ˜๋Š” TestScreen2๋‹ค. ์•ž์œผ๋กœ @Preview ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•  ๊ฒƒ์ด๋ฏ€๋กœ ์•„์ฃผ ๊ฐ„๋‹จํ•œ Preview ํ•˜๋‚˜๋„ ๋งŒ๋“ค์–ด๋ดค๋‹ค.

 

๊ด€๋ จ ๊ฐœ๋ฐœ ๋ฒ”์œ„

  • Compose Runtime๋งŒ์œผ๋กœ๋„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋Š” UI (Compose Preview๋งŒ ๋ณด๊ณ  ๊ฐœ๋ฐœ)

 

์ฃผ์š” ๋ณ€๊ฒฝ์ 

  • TestScreen์„ ๋ฉ”์†Œ๋“œ ์˜ค๋ฒ„๋กœ๋”ฉ์œผ๋กœ 2๋ถ„ํ• . ํ•˜๋‚˜๋Š” State๋ฅผ ์ฃผ์ž…ํ•˜๋Š” ์—ญํ• , ํ•˜๋‚˜๋Š” ์ˆœ์ˆ˜ํ•˜๊ฒŒ UI๋ฅผ ๊ทธ๋ฆฌ๋Š” ์—ญํ• 
  • ๊ฐ„๋‹จํ•œ @Preview ๊ตฌํ˜„ (@PreviewParameter๋กœ ๊ฐ’ ์ฃผ์ž…)

 

์ฝ”๋“œ ์Šค๋‹ˆํŽซ - TestScreen.kt

...

// state ์ฃผ์ž…์šฉ
@Composable
fun TestScreen(viewModel: ViewModel? = null) {
    ...

    // category
    ...
    val categories = listOf(allMemosCategory) + categoriesFromRepo
    var selectedCategory: Category by remember { mutableStateOf(allMemosCategory) }

    // memo
    ...
    val memos by memosFlow.collectAsState(initial = emptyList())

    TestScreen(
        categories = categories,
        selectedCategory = selectedCategory,
        memos = memos,
        // events
        onCategorySelected = { category: Category ->
            selectedCategory = category
        },
        ...
    )
}

// ์ˆœ์ˆ˜ UI
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TestScreen(
    categories: List<Category>,
    selectedCategory: Category,
    memos: List<Memo>,
    // events
    onCategorySelected: (Category) -> Unit,
    onAddMemoClick: () -> Unit,
) {
    ...
}

@DevicePreviews
@Composable
fun TestScreenPreview(
    @PreviewParameter(MemoPreviewParameterProvider::class)
    memos: List<Memo>,
) {
    SwemoTheme {
        TestScreen(
            categories = emptyList(),
            selectedCategory = Category(id = "-1", name = "์ „์ฒด ๋ฉ”๋ชจ"),
            memos = memos,
            // events
            onCategorySelected = {},
            onAddMemoClick = {},
        )
    }
}

2๋ถ„ํ• ๋œ TestScreen()๊ณผ TestScreenPreview()๋‹ค. ์œ„์˜ ์ฝ”๋“œ๊ฐ€ 100% ์™„์„ฑ๋œ ์•ฑ์˜ ์ฝ”๋“œ์˜€๋‹ค๋ฉด, ์ฒซ ๋ฒˆ์งธ TestScreen()์—์„œ State๋ฅผ Repository๊ฐ€ ์•„๋‹ˆ๋ผ ViewModel๋กœ๋ถ€ํ„ฐ ๋ฐ›์•„์˜ฌ ๊ฒƒ์ด๋‹ค (State Hoisting). ํ•˜์ง€๋งŒ, ๋ณธ Iteration์—์„œ๋Š” UI์— ์ง‘์ค‘ํ•˜๋ฏ€๋กœ ๋‚˜์ค‘์— ์—…๋ฐ์ดํŠธํ•œ๋‹ค.

 

์ฝ”๋“œ ์Šค๋‹ˆํŽซ - DevicePreviews.kt

package com.example.ui

import androidx.compose.ui.tooling.preview.Preview

/**
 * Multipreview annotation that represents various device sizes. Add this annotation to a composable
 * to render various devices.
 */
@Preview(name = "phone", device = "spec:shape=Normal,width=360,height=640,unit=dp,dpi=480")
@Preview(name = "landscape", device = "spec:shape=Normal,width=640,height=360,unit=dp,dpi=480")
@Preview(name = "foldable", device = "spec:shape=Normal,width=673,height=841,unit=dp,dpi=480")
@Preview(name = "tablet", device = "spec:shape=Normal,width=1280,height=800,unit=dp,dpi=480")
annotation class DevicePreviews

Now in Android์— ์žˆ๋Š” ์ฝ”๋“œ๋ฅผ ๊ทธ๋Œ€๋กœ ๊ฐ€์ ธ์™”๋‹ค. @DevicePreviews ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์ด๋ฉด, 4๊ฐœ์˜ ํ”„๋ฆฌ๋ทฐ ํ™”๋ฉด์ด ์ƒ์„ฑ๋œ๋‹ค.

 

์Šคํฌ๋ฆฐ์ƒท -  ์ปดํฌ๋„ŒํŠธ์˜ ๋ฐฐ๊ฒฝ์ด ํˆฌ๋ช…ํ•˜๊ฒŒ ๋‚˜์˜ค๋Š” ํ˜„์ƒ

์ฒ˜์Œ์—๋Š”, ์ด ํ™”๋ฉด์ด '๋…ผ๋ฆฌ์  ์—๋Ÿฌ'์— ๊ฐ€๊น๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค. ํ•˜์ง€๋งŒ, ๊ฐ€๋งŒ ์ƒ๊ฐํ•ด ๋ณด๋‹ˆ ์•„๋‹ˆ์—ˆ๋‹ค. Preview ํ™”๋ฉด์€ ํ˜„์žฌ ํŒŒ์ผ ์†์— ์žˆ๋Š” ์ฝ”๋“œ๋งŒ ์ฐธ์กฐํ•œ๋‹ค. TestScreen.kt ํŒŒ์ผ ์™ธ๋ถ€์˜ ์ฝ”๋“œ์—์„œ Scaffold(๊ฐ™์ด ๋ฐฐ๊ฒฝ์ƒ‰์ด ์กด์žฌํ•˜๋Š” ์ปจํ…Œ์ด๋„ˆ)๋ฅผ ์„ ์–ธํ•œ ๋‹ค์Œ์— TestScreen๋ฅผ ํ˜ธ์ถœํ•˜๋Š”, ์ด๋ฅธ๋ฐ” ์ •์„์ ์ธ ๋ฐฉ๋ฒ•์ด์–ด๋„ ์œ„์™€ ๊ฐ™์€ ํ™”๋ฉด์ด ์ •์ƒ์ด๋ผ๋Š” ๋ง์ด๋‹ค.

 

๋ฐฐ๊ฒฝ์ƒ‰์ด ๊ฒ€์€์ƒ‰์ธ ๊ฒƒ์ด ๋ญ”๊ฐ€ ๊ฑฐ์Šฌ๋ฆฐ๋‹ค๊ณ  @Preview๊ฐ€ ๋ถ™์€ ํ•จ์ˆ˜์— ๋ณ„๋„๋กœ Scaffold๋ฅผ ์„ ์–ธํ•œ๋‹ค? ์ด๋Ÿฌ๋ฉด ๊ฐ์ฒด์ง€ํ–ฅ์˜ ์˜๋ฏธ๊ฐ€ ์‚ฌ๋ผ์ ธ ๋ฒ„๋ฆฐ๋‹ค. Now in Android์— ๊ตฌํ˜„๋œ Preview๋“ค๋„ (์ปจํ…Œ์ด๋„ˆ๊ฐ€ ํŒŒ์ผ ๋‚ด ์—†๋Š” ๊ฒฝ์šฐ) ๋ฐฐ๊ฒฝ์ด ํˆฌ๋ช…ํ–ˆ๋‹ค.

 

์Šคํฌ๋ฆฐ์ƒท -  ๋ณด๊ธฐ ๋ฐฉ์‹ ๋ณ€๊ฒฝ

'์ƒ‰๋งน ๋ชจ๋“œ' ์„ค์ •์ด ์žˆ์—ˆ๋‹ค. Preview ํ™”๋ฉด์„ ๋ณด๊ธฐ ํž˜๋“ค ๋•Œ ์จ์•ผ๊ฒ ๋‹ค.

 

์ฐธ๊ณ 

 

#2-4 Iteration 2-4: TestScreen ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ ๋ถ„๋ฆฌ

// in TestScreen.kt
@Composable
fun TestScreen(
    ...
) {
    ...

    ModalNavigationDrawer(
        ...
        drawerContent = {
            // ์ด๋ฒˆ ์ปค๋ฐ‹์— ์ƒˆ๋กœ ์„ ์–ธํ•œ ์ปดํฌ๋„ŒํŠธ
            CategorySelector(
                categories = categories,
                ...
            )
        }
    ) {
        Column {
            ...

            // ์ด๋ฒˆ ์ปค๋ฐ‹์— ์ƒˆ๋กœ ์„ ์–ธํ•œ ์ปดํฌ๋„ŒํŠธ
            MemoFeed(
                memos = memos,
                ...
            )
        }
    }
}

---

// in MemoFeed.kt
@Composable
fun MemoFeed(
    memos: List<Memo>,
    ...
) {
    LazyColumn(
        ...
    ) {
        items(memos) { memo ->
            // ์ด๋ฒˆ ์ปค๋ฐ‹์— ์ƒˆ๋กœ ์„ ์–ธํ•œ ์ปดํฌ๋„ŒํŠธ
            MemoCard(memo = memo)
            ...
        }
    }
}

TestScreen์˜ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ๋“ค์„ ๋ถ„๋ฆฌํ•ด ๋ณ„๋„์˜ ์ปดํฌ๋„ŒํŠธ ํ•จ์ˆ˜๋กœ ๋งŒ๋“ค์—ˆ๋‹ค. ๋งŒ๋“  ์ปดํฌ๋„ŒํŠธ๋“ค์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

 

  • CategorySelector: List<Category>๋ฅผ ๋ฐ›์•„์„œ Category๋ฅผ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋Š” ํ™”๋ฉด์„ ๊ตฌํ˜„
  • MemoFeed: List<Memo>๋ฅผ ๋ฐ›์•„์„œ Memo ์•„์ดํ…œ๋“ค์ด ๋ชจ์—ฌ์žˆ๋Š” ํ™”๋ฉด์„ ๊ตฌํ˜„
  • MemoCard: Memo ์•„์ดํ…œ ํ•˜๋‚˜์˜ ์ •๋ณด๋ฅผ ํ‘œ์‹œ

 

๊ด€๋ จ ๊ฐœ๋ฐœ ๋ฒ”์œ„

  • ๋ฉ”๋ชจ ๋ฆฌ์ŠคํŠธ๋ฅผ ์ฝ๊ธฐ ์œ„ํ•œ ํ™”๋ฉด UI
  • ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ์„ ํƒํ•˜๊ธฐ ์œ„ํ•œ ํ™”๋ฉด UI
  • Compose Runtime๋งŒ์œผ๋กœ๋„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋Š” UI (Compose Preview๋งŒ ๋ณด๊ณ  ๊ฐœ๋ฐœ)

 

์ฃผ์š” ๋ณ€๊ฒฝ์ 

  • TestScreen์˜ ๋‚ด๋ถ€ ์ปดํฌ๋„ŒํŠธ๋“ค์„ ๋ถ„๋ฆฌํ•ด ๋ณ„๋„์˜ ์ปดํฌ๋„ŒํŠธ ํ•จ์ˆ˜ ์„ ์–ธ
  • Memo ์•„์ดํ…œ ํ•˜๋‚˜๋ฅผ Card๋กœ ๊ฐ์‹ผ ์ž„์‹œ UI (MemoCard())

 

์Šคํฌ๋ฆฐ์ƒท - TestScreen์˜ Preview

 

์ฐธ๊ณ 

 

#2-5 Iteration 2-5: MemoEditor ๋ผˆ๋Œ€ ๊ตฌํ˜„

  • ๋ณธ ๊ฒŒ์‹œ๊ธ€ ๋งจ ์œ„์—์„œ๋ถ€ํ„ฐ ์–ธ๊ธ‰๋˜๋Š” 'Bubble'์ด๋ผ๋Š” ๊ฐœ๋…์˜ ์ด๋ฆ„์„ 'Chip'์œผ๋กœ ๋ณ€๊ฒฝํ–ˆ๋‹ค.
    • Chip์ด๋ผ๋Š” UI ์ปดํฌ๋„ŒํŠธ ์–‘์‹์ด ์žˆ๋‹ค๋Š” ๊ฑธ ๋ชฐ๋ž๊ธฐ ๋•Œ๋ฌธ์— ๊ตณ์ด 'Bubble'์ด๋ผ๋Š” ์ด๋ฆ„์„ ๋‚ด๊ฐ€ ์ง€์–ด๋‚ด์„œ ์‚ฌ์šฉํ•ด ์™”๋‹ค.
    • ์ด์ œ Bubble๊ณผ (์™„์ „ํžˆ ๊ฐ™์ง„ ์•Š๋”๋ผ๋„) ๋น„์Šทํ•œ ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•˜๋Š” UI ์ปดํฌ๋„ŒํŠธ์˜ ์ผ๋ฐ˜์ ์ธ ์ด๋ฆ„์„ ์•Œ๊ฒŒ ๋˜์—ˆ์œผ๋‹ˆ, 'Bubble'์ด๋ผ๋Š” ์ด๋ฆ„์€ ์ด์ œ ์“ธ ์ด์œ ๊ฐ€ ์—†๋‹ค.
    • Chip ๋ฒ„ํŠผ ํ•˜๋‚˜๊ฐ€ label (#2-2) ํ•˜๋‚˜์— ๋Œ€์‘ํ•œ๋‹ค.
  • MemoEditor๋Š” ๋ณธ ์•ฑ์˜ ํ•ต์‹ฌ์ด ๋˜๋Š” UI ์ปดํฌ๋„ŒํŠธ๋‹ค. ๋งŽ์€ ์ •์„ฑ์„ ๋“ค์—ฌ์•ผ ํ•œ๋‹ค. ์ž‘์—…๋Ÿ‰์ด ๋งŽ์„ ๊ฒƒ์ด๋ฏ€๋กœ, ์šฐ์„  ๋ผˆ๋Œ€๋งŒ ์„ ์–ธํ–ˆ๋‹ค.
    • TestScreen1 (#2-3)์— editingMemo: Memo ์™€ allLables: Set<Memo>๋ฅผ ์„ ์–ธํ•ด์„œ TestScreen2์— ๋‚ด๋ ค์ฃผ์—ˆ๋‹ค.
    • MemoEditor์˜ ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ์ธ EditableFieldList ๋ฐ LabelChipGroup์€ ๊ฐ๊ฐ ์ˆœ์„œ๋Œ€๋กœ editingMemo์™€ allLabels๋ฅผ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ๋ฐ›๋Š”๋‹ค.
    • ์ดํ›„์˜ ๊ฐœ๋ฐœ ๋ฐฉํ–ฅ: LabelChipGroup์—์„œ ์ถ”๊ฐ€ํ•˜๊ณ ์ž ํ•˜๋Š” label์„ ์„ ํƒํ•˜๋ฉด editingMemo์— ํ•ด๋‹น label์— ๋Œ€์‘ํ•˜๋Š” TextField๊ฐ€ ์ƒ์„ฑ

 

๊ด€๋ จ ๊ฐœ๋ฐœ ๋ฒ”์œ„

  • 'Bubble' ๊ฐœ๋…์ด UI์—์„œ ํ‘œ์ถœ๋˜๋Š” ๋ฐฉ์‹

 

์ฃผ์š” ๋ณ€๊ฒฝ์ 

  • MemoEditor์˜ ๋ผˆ๋Œ€ ๊ตฌํ˜„
  • MemoEditor ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ(EditableFieldList, LabelChipGroup)๋“ค์˜ ๋ผˆ๋Œ€ ๊ตฌํ˜„
  • ์ˆœ์ˆ˜ UI๋ฅผ ๋‹ด๋‹นํ•˜๋Š” TestScreen()์˜ ๋งค๊ฐœ๋ณ€์ˆ˜์— editingMemo: Memo ๋ฐ allLables: Set<Memo> ์ถ”๊ฐ€

 

์Šคํฌ๋ฆฐ์ƒท

 

์ฝ”๋“œ ์Šค๋‹ˆํŽซ - label๋“ค์˜ Set ๋งŒ๋“ค๊ธฐ

val memosFlow = remember(selectedCategory.id) {
    repository.getMemosByCategory(selectedCategory.id)
}
val memos by memosFlow.collectAsState(initial = emptyList())
val allLabels by remember(memos) {
    derivedStateOf {
        memos
            .asSequence()
            .flatMap { it.contents }
            .map { it.label }
            .toSet()
    }
}

'ํ˜„์žฌ ์นดํ…Œ๊ณ ๋ฆฌ (selectedCategory)'์— ์กด์žฌํ•˜๋Š” ๋ชจ๋“  ๋ฉ”๋ชจ๊ฐ€ ๋ณด์œ ํ•œ label์˜ ๊ต์ง‘ํ•ฉ์ธ allLabels๋ฅผ ๊ตฌํ•˜๋Š” ์ฝ”๋“œ. allLabels๋Š” LabelChipGroup ์ปดํฌ๋„ŒํŠธ์— ์ „๋‹ฌ๋œ๋‹ค.

 

์–ป์€ ๊ฒฝํ—˜์น˜

๋”๋ณด๊ธฐ
  • Material Chip์˜ ์กด์žฌ๋ฅผ ์•Œ์•˜๋‹ค.
  • Sequence์˜ ์กด์žฌ๋ฅผ ์•Œ์•˜๋‹ค. Sequence๋Š” ๊ฒฐ๊ณผ๊ฐ€ ์‹ค์ œ๋กœ ํ•„์š”(์†Œ๋น„)ํ•ด์งˆ ๋•Œ๊นŒ์ง€ ๊ณ„์‚ฐ์„ ๋ฏธ๋ฃจ๊ธฐ ๋•Œ๋ฌธ์— (Collection์— ๋น„ํ•ด) ์ž์› ๋‚ญ๋น„๊ฐ€ ์ ๋‹ค.
  • asSequence()๋Š” Collection์„ Sequence๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค.
  • map()๊ณผ flatMap()์˜ ์ฐจ์ด๋Š” ํ‰ํƒ„ํ™”๋‹ค.
  • ๊ตฌ์กฐ ๋ถ„ํ•ด ์„ ์–ธ(Destructuring declaration)์œผ๋กœ ๋žŒ๋‹ค ํ•จ์ˆ˜๋ฅผ ๋” ์งง๊ฒŒ ์“ธ ์ˆ˜ ์žˆ๋‹ค.
  • ๊ฑฐ์Šฌ๋ฆฌ๋Š” ๋ณ€์ˆ˜ ์ด๋ฆ„์„ ๋ฐ”๊พธ๊ธฐ ์œ„ํ•ด์„œ, git rebase๋ฅผ ์ฒ˜์Œ์œผ๋กœ ์‚ฌ์šฉํ•ด ๋ณด์•˜๋‹ค. ๋ง‰์ƒ ํ•ด๋ณด๋‹ˆ๊นŒ ๋ณ„๊ฑฐ ์—†์—ˆ๋‹ค!

 

์ฐธ๊ณ 

 

#2-6 Iteration 2-6

์ถ”๊ฐ€ ์˜ˆ์ •.

 

#3 ํ…Œ์ŠคํŠธ

์ถ”๊ฐ€ ์˜ˆ์ •.

 

#4 ๊ฐœ๋ฐœ ๊ฒฐ๊ณผ (Iteration Outcome)

์ถ”๊ฐ€ ์˜ˆ์ •.