#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' ๊ฐ๋ ๋ ์ฌ๊ธฐ์๋ถํฐ ์กฐ๊ธ์ฉ ๋์ ํด ๋๊ฐ๋ค. ์ฌ์ค ์ด ๊ฐ๋ ์ ๋ง๋ก ํํํ๊ธฐ๊ฐ ์ด๋ ต๋ค. ๊ทธ๋์ ๊ตฌ๊ตฌ์ ์ ์๊ธฐ๋ณด๋ค๋, ์ฝ๋ ๊ทธ๋ฆฌ๊ณ ์คํฌ๋ฆฐ์ท์ผ๋ก ๋ณด์ฌ์ฃผ๋ ค๊ณ ํ๋ค (#2-5 ์ฐธ์กฐ).
#1-2 ๊ฐ๋ฐ ๋ฒ์
์ด๋ฒ ๊ฐ๋ฐ ๋ฒ์ (In Scope)
- ๋ฉ๋ชจ ๋ฆฌ์คํธ๋ฅผ ์ฝ๊ธฐ ์ํ ํ๋ฉด UI
- ๋ฉ๋ชจ๋ฅผ ์ถ๊ฐํ๊ธฐ ์ํ ํ๋ฉด UI
- ์นดํ ๊ณ ๋ฆฌ๋ฅผ ์ ํํ๊ธฐ ์ํ ํ๋ฉด UI
- ์นดํ ๊ณ ๋ฆฌ๋ฅผ ์ถ๊ฐํ๊ธฐ ์ํ ํ๋ฉด UI
- Compose Runtime๋ง์ผ๋ก๋ ํ ์คํธํ ์ ์๋ UI (Compose Preview๋ง ๋ณด๊ณ ๊ฐ๋ฐ)
- Compose State๋ฅผ ์ํ ๋๋ฉ์ธ ๋ชจ๋ธ ์ ๋ฆฝ
- 'Bubble' ๊ฐ๋ ์ด UI์์ ํ์ถ๋๋ ๋ฐฉ์
๋ฒ์ ์ธ (Out of Scope)
- Compose Runtime๋ง์ผ๋ก๋ ํ ์คํธํ ์ ์๋ UI (Compose Preview๋ง์ผ๋ก๋ ํ์ธ ๋ถ๊ฐ๋ฅํ ๊ธฐ๋ฅ)
- ์์คํ ์ปดํฌ๋ํธ(Navigation Bar, Status Bar ๋ฑ)์ ๊ณ ๋ คํ UI
- UI ์ธ์ ์ธ ๊ธฐ๋ฅ
- 'Bubble' ๊ฐ๋ ์ด ์ค์ ๋ก (๋ฐฑ์๋์์) ์๋ํ๋๋ก ๊ตฌํ
- ๊ธฐ๋ณธ(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 ํ๋ฉด์ ๋ณด๊ธฐ ํ๋ค ๋ ์จ์ผ๊ฒ ๋ค.
์ฐธ๊ณ
- [GitHub] ์ด ์์ ์ Commit
- [Tistory] [Android] Jetpack Compose - State Hoisting
- [GitHub] ForYouScreen.kt at b9a80fc038e8dd1e2dc08fff4ff9feb40d6f3ef3 · android/nowinandroid
#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๊ฐ ์์ฑ
๊ด๋ จ ๊ฐ๋ฐ ๋ฒ์
- ๋ฉ๋ชจ๋ฅผ ์ถ๊ฐํ๊ธฐ ์ํ ํ๋ฉด UI
- '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๋ฅผ ์ฒ์์ผ๋ก ์ฌ์ฉํด ๋ณด์๋ค. ๋ง์ ํด๋ณด๋๊น ๋ณ๊ฑฐ ์์๋ค!
์ฐธ๊ณ
- [GitHub] ์ด ์์ ์ Commit
- [Material Design 3] Chips
- [Kotlin Programming Language] Collection | Core API
- [Kotlin Programming Language] Sequence | Core API
- [Kotlin Programming Language] asSequence | Core API
- [velog] map, flatMap ์ ์ฐจ์ด
- [Kotlin Programming Language] Destructuring declarations
#2-6 Iteration 2-6: MemoEditor - ์ฌ๋ฌ ๋ฒํผ ์ถ๊ฐ

- MemoEditor๋ฅผ ์ผ๋ถ ๊ตฌํํ๋ค
- MemoEditor๋ ๋ณธ ํ๋ก์ ํธ์์ ์๋ง ์ ์ผ ๋ณต์กํ ์ปดํฌ๋ํธ์ด๊ธฐ ๋๋ฌธ์ ์ ์ ํ๊ฒ ๋๋ ์ค ํ์๊ฐ ์๋ค. ๊ทธ๋์ ํ์ ์ปดํฌ๋ํธ 2๊ฐ๋ฅผ ์๋ก ์ ์ธํ๋ค.
๊ด๋ จ ๊ฐ๋ฐ ๋ฒ์
- ๋ฉ๋ชจ๋ฅผ ์ถ๊ฐํ๊ธฐ ์ํ ํ๋ฉด UI
- 'Bubble' ๊ฐ๋ ์ด UI์์ ํ์ถ๋๋ ๋ฐฉ์
์ฃผ์ ๋ณ๊ฒฝ์
- MainActivity์ ์๋ 'IME์ ์ํด UI๊ฐ ๋ฐ๋ฆฌ์ง ์๊ฒ ํ๋ ์ฝ๋'๋ฅผ ์ ๊ฑฐ
- MemoEditor์ ํ์ ์ปดํฌ๋ํธ์ธ MemoEditorActionBar ์ถ๊ฐ
- EditableFieldList์ ํ์ ์ปดํฌ๋ํธ์ธ MemoContentTextField ์ถ๊ฐ
์คํฌ๋ฆฐ์ท

๋งจ ์๋์ ์๋ + ๋ฒํผ๊ณผ โท ๋ฒํผ์ MemoEditorActionBar ์์ ์๋ค.
์ฐธ๊ณ
#2-7 Iteration 2-7: MemoEditor - ํ ๊ธ ๋ฒํผ
ํ ๊ธ ๋ฒํผ์ ํตํด์ MemoEditor๋ฅผ ์ ๊ฑฐ๋ ํผ์น ์ ์๊ฒ ๋ง๋ค๋ ค ํ๋ค. ๊ทธ๋ฐ ๋์์ ์ํํ๋ ๋ฐ์ ์ธ๋งํ UI๋ FAB๋ค. FAB๋ฅผ ์์ํ๊ฒ ๋ฐฐ์นํ๊ธฐ ์ํด์ Scaffold๋ฅผ ๋์ ํ FAB์ ๊ตฌํํ๋ค.
๊ด๋ จ ๊ฐ๋ฐ ๋ฒ์
- ๋ฉ๋ชจ๋ฅผ ์ถ๊ฐํ๊ธฐ ์ํ ํ๋ฉด UI
์ฃผ์ ๋ณ๊ฒฝ์
- TestScreen์ Scaffold ์ ์ธ ํ Scaffold์ topBar, bottomBar, content ์ฌ๋กฏ์ ๊ฐ๊ฐ TopAppBar, MemoEditor, MemoFeed ๋ฐฐ์น
- Scaffold์ FAB ์ฌ๋กฏ์ ๋ฐฐ์นํ ๋ฒํผ ํด๋ฆญ ์ MemoEditor๋ฅผ ํผ์น๊ฑฐ๋ ์ ์ ์ ์๊ฒ ํจ
์คํฌ๋ฆฐ์ท - MemoEditor ์ ํ

์คํฌ๋ฆฐ์ท - MemoEditor ํผ์นจ

์ฝ๋ ์ค๋ํซ - Scaffold ๊ตฌ์กฐ
// state ์ฃผ์
์ฉ
@Composable
fun TestScreen(...) {
...
// etc
var isMemoEditorVisible by remember { mutableStateOf(false) }
TestScreen(
...
isMemoEditorVisible = isMemoEditorVisible,
...
onMemoEditorToggleButtonClick = {
isMemoEditorVisible = !isMemoEditorVisible
},
)
}
// ์์ UI
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TestScreen(
...
isMemoEditorVisible: Boolean,
...
onMemoEditorToggleButtonClick: () -> Unit,
) {
...
ModalNavigationDrawer(
...
) {
Scaffold(
topBar = {
TopAppBar(
...
)
},
bottomBar = {
if (isMemoEditorVisible) {
... // MemoEditor()
}
},
floatingActionButton = {
Button(
onClick = {
onMemoEditorToggleButtonClick()
}
) {
...
}
},
...
) { ...
MemoFeed(
...
)
}
}
}
์ป์ ๊ฒฝํ์น
- rememberSaveable๋ฅผ ์ค๋๋ง์ ๋ง์ฃผ์ณค๋ค(?). remember์๋ ๋ฌ๋ฆฌ ํ๋ฉด ํ์ ๋ฑ์ผ๋ก ์ธํ Compose UI์ ์ด๊ธฐํ ์์๋ ๊ฐ์ ์ ์งํ๋ค. ๋ฌผ๋ก ๋๋ ์ถํ ViewModel์ ์ฌ์ฉํ ๊ฒ์ด๋ฏ๋ก ์ฌ๊ธฐ์์ ์ฌ์ฉํ์ง ์์๋ค.
์ฐธ๊ณ
- [GitHub] ์ด ์์ ์ Commit
- [Tistory] [Android] Jetpack Compose - Scaffold
- [Material Design 3] FAB
- [Tistory] [Android] Jetpack Compose - State Remembering
#2-8 Iteration 2-8: AddCategoryDialog
์ ์นดํ ๊ณ ๋ฆฌ ์ถ๊ฐ ๋ฒํผ ๊ทธ๋ฆฌ๊ณ ๋ฒํผ์ ํด๋ฆญํ์ ๋ ์ ์นดํ ๊ณ ๋ฆฌ ์ด๋ฆ ์ ๋ ฅ์ ์ํ Dialog์ ๋ชจ์์ ์ก์๋ค.
๊ด๋ จ ๊ฐ๋ฐ ๋ฒ์
- ์นดํ ๊ณ ๋ฆฌ๋ฅผ ์ถ๊ฐํ๊ธฐ ์ํ ํ๋ฉด UI
์ฃผ์ ๋ณ๊ฒฝ์
- ์นดํ ๊ณ ๋ฆฌ ๋ฆฌ์คํธ์ '+' ๋ฒํผ ์ถ๊ฐ
- ์นดํ ๊ณ ๋ฆฌ ๋ฆฌ์คํธ์ '+' ๋ฒํผ ํด๋ฆญ ์, ์นดํ ๊ณ ๋ฆฌ๋ฅผ ์ถ๊ฐํ๊ธฐ ์ํ Dialog ๋จ๊ฒ ๋ง๋ฆ
์คํฌ๋ฆฐ์ท

์ฝ๋ ์ค๋ํซ - TestScreen()
@Composable
fun TestScreen(
...
isAddCategoryDialogVisible: Boolean,
...
onAddCategoryDialogVisibleChange: (Boolean) -> Unit,
) {
...
ModalNavigationDrawer(
...
drawerContent = {
CategorySelector(
...
onAddCategoryButtonClick = {
onAddCategoryDialogVisibleChange(true)
}
)
}
) {
Scaffold(
...
) { ...
MemoFeed(
...
)
}
}
if (isAddCategoryDialogVisible) {
AddCategoryDialog(
...
) {
...
}
}
}
์ฐธ๊ณ
#2-9 Iteration 2-9: ์ฝ๋ ์ ๋ฆฌ
Iteration 2์์ ์ค์ ์ ์ผ๋ก ๊ฐ๋ฐํ ๋ชจ๋์ :feature:test ๋ชจ๋์ด๋ค. ๊ฐ๋ฐ์ด ์งํ๋๋ฉด์ ์ด ๋ชจ๋์ ('UI์ ํฐ ํ'์ด๋ผ๋ ์ญํ ์ ์ป์ ๋์ ) 'test'๋ก์์ ์๋ฏธ๋ฅผ ์์ด๋ฒ๋ ธ๋ค. ๋ฐ๋ผ์ ์ด๋ฆ์ :feature:memo๋ก ๋ณ๊ฒฝํ๋ค. ๊ทธ๋ฆฌ๊ณ Now in Android์ ์ฝ๋์ ๊ธฐ๋ฐํด์, ์ด๋ฒ Iteration์์ ์ถ๊ฐํ ์ฝ๋๋ค์ ์ ๋ฆฌํ๋ค. ๊ธฐ๋ฅ์ ์ผ๋ก๋ ๋ฐ๋ ๊ฒ ์ ํ ์๋ค. ๋ง์ง๋ง์ผ๋ก Iteration 1-1์์ ์ ๊ฑฐํ์ง ์์๋ ์ฝ๋๋ค์ ์ ๊ฑฐํ๋ค. ๋ค์์ ๋ค์ ์ถ๊ฐํ ํ๋ฅ ์ด ๋์ ์ฝ๋์ง๋ง, ์๋ฌด๋ฐ ์๋ฏธ๋ ์๋ ์ฝ๋๋ฅผ ์ค๋ซ๋์ ๋ฐฉ์นํ๋ ์ํฉ์ด ์ซ์๋ค.
์ฃผ์ ๋ณ๊ฒฝ์
- :feature:test ๋ชจ๋์ ์ด๋ฆ์ :feature:memo๋ก ๋ณ๊ฒฝ
- TestScreen์ ์ด๋ฆ์ MemoScreen์ผ๋ก ๋ณ๊ฒฝ
- MemoScreen์์ ์ฌ์ฉํ๋ ์ฌ๋ฌ ์ปดํฌ๋ํธ๋ค์ components๋ผ๋ ์ด๋ฆ์ ํจํค์ง์ ๋ชฐ์ ๋ฃ์
- ์์ด์ฝ ํ์ผ๋ค์ :designsystem ๋ชจ๋๋ก ์ด๋์ํค๊ณ , object SwemoIcons๋ฅผ ํตํด ์ ๊ทผํ๋๋ก ๋ง๋ฆ
- :database, :common, :datastore ๋ชจ๋ ์ ๊ฑฐ
์ฐธ๊ณ
#4 ๊ฐ๋ฐ ๊ฒฐ๊ณผ (Iteration Outcome)
#4-1 ์ด๋ฒ Iteration ์์ฝ

Swemo์ ํ๋ก ํธ์๋์ ์ค๊ณฝ์ ์ก์๋ค. ์์ผ๋ก๋ ์์ ๋ชจ์ต์์ ํฌ๊ฒ ๋ฌ๋ผ์ง ์ผ์ ์์ ๊ฒ์ผ๋ก ์์ํ๋ค.
#4-2 ๋ค์ ๊ฐ๋ฐ์ ์ํ ์ํฉ ์ ๋ฆฌ

Iteration 1๊ณผ 2๋ ๊ฐ๋ฐ ๋ฐฉํฅ์ ๋ํด '๊ฐ์ ๋ณด๋' ์ฑ๊ฒฉ์ด ์ปธ๋ค. ์ดํ๋ก Swemo์ ์์ด์ ์ฑ์ฐ๋ ์์ ์ ํ๊ฒ ๋ ๊ฒ์ด๋ค. ๋ค์์ ๋ญ ํ ์ง๋ ์์ง ์ ํ์ง ์์์ง๋ง, ๋น์ฅ ์๊ฐ๋๋ ํ๋ณด๋ค์ ์๋์ ๊ฐ๋ค.
- ViewModel ๊ตฌ์กฐ ์ ๋น
- ํ์ฌ ํธ์์ UI ๋ ์ด์ด์ ์๋ฆฌ ์ก์ ์ํ(State)๋ค์ ViewModel๋ก ์ด๊ด
- ์ํ ๊ด๋ฆฌ ์ฑ ์์ ๋ ๋ช ํํ ๋ถ๋ฆฌ
- Preview ์ค์ฌ ๊ตฌํ์ ๋ฐํ์ ๋์ ๋ณด์ฅ
- Iteration 2์์ ๊ตฌ์ฑํ UI ๊ตฌ์กฐ๊ฐ Preview ํ๊ฒฝ์ ๊ตญํ๋์ง ์๊ณ , ์ค์ Android ๋ฐํ์ ํ๊ฒฝ์์๋ ๋์ผํ๊ฒ ๋์ํ๋๋ก ๋ณด์
- IME ๋์์ ๊ณ ๋ คํ UI ๊ตฌ์กฐ (๊ณ ๋ํ)
- Iteration 2์์ ์ค๊ณํ UI๋ (์๋์ ์ผ๋ก) IME ์์ญ์ ๊ณ ๋ คํ์ง ์์ ์ํ์ด๋ฏ๋ก, ํฅํ IME ํผ์นจใ์จ๊น ์ํฉ์์๋ ์์ฐ์ค๋ฝ๊ฒ ๋์ํ๋๋ก ๊ฐ์
'๊ฐ๋ฐ ์ผ์ง ๐ป > Swemo' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| Swemo - Iteration 3: ๋ฉ๋ชจ ํ๋ฉด์ State & Event ์ฒด๊ณ ์ ๋ฆฌ (0) | 2026.02.08 |
|---|---|
| Swemo - Iteration 1: ๋ฉ๋ชจ ์ฑ์ ์ฒญ์ฌ์ง(๊ธฐ์ด) (0) | 2025.12.28 |
| Swemo - Nutri Capture ๊ฒ์๊ธ ์๋ฆฌ์ฆ๋ก ์ด๋ (0) | 2025.12.09 |
| Nutri Capture - Swemo ๊ฒ์๊ธ ์๋ฆฌ์ฆ๋ก ์ด๋ (0) | 2025.12.09 |
| Nutri Capture - ๋ฐฉํฅ ์ฌ์ค๊ณ (ํ๋ก์ ํธ ์ด๋ฆ ๋ณ๊ฒฝ) (0) | 2025.12.02 |