μλλ‘μ΄λ μ ν리μΌμ΄μ μ κ°λ°ν λ, μ½λμ μ μ§λ³΄μμ±κ³Ό νμ₯μ±μ λμ΄λ κ²μ΄ λ§€μ° μ€μνλ€. ꡬκΈμ μ΄λ₯Ό μν΄ μλλ‘μ΄λ μν€ν μ²μ λν λ€μν κΆμ₯μ¬νμ μ 곡νκ³ μμΌλ©°, μ΄λ₯Ό λ°λ₯΄λ©΄ μ±μ νμ§κ³Ό κ²¬κ³ μ±μ ν₯μμν¬ μ μλ€.
1. μν€ν μ² κΆμ₯μ¬νμ κ°μ
μλλ‘μ΄λ μν€ν μ² κΆμ₯μ¬νμ μ격ν κ·μΉμ΄ μλλΌ μ±μ νμ§μ λμ΄λ λ° λμμ΄ λλ κ°μ΄λλΌμΈμ΄κΈ° λλ¬Έμ μ±μ μꡬμ¬νκ³Ό νμ κ°λ° νκ²½μ κ³ λ €νμ¬ μ΄λ₯Ό μ μ ν μ‘°μ ν μ μλ€.
κΆμ₯μ¬νμ μ°μ μμμ λ°λΌ λ€μκ³Ό κ°μ΄ ꡬλΆλλ€.
- μ κ·Ή κΆμ₯λ¨: κΈ°λ³Έμ μΈ μμΉκ³Ό μΆ©λνμ§ μλ ν λ°λμ λ°λΌμΌ νλ κΆμ₯μ¬ν.
- κΆμ₯λ¨: μ±μ νμ§μ ν₯μμν€λ λ° ν° λμμ΄ λλ κΆμ₯μ¬ν.
- μ νμ¬ν: νΉμ μν©μμ μ μ©ν μ μλ κΆμ₯μ¬ν.
2. κ³μΈ΅νλ μν€ν μ² μ μ©νκΈ°
μλλ‘μ΄λ μ ν리μΌμ΄μ μμλ κ΄μ¬μ¬ λΆλ¦¬(Separation of Concerns) λ₯Ό μ μ©νμ¬ λͺ νν μν€ν μ² κ³μΈ΅μ μ€κ³νλ κ²μ΄ μ€μνλ€. ꡬκΈμ λ°μ΄ν° λͺ¨λΈμμ UIλ₯Ό ꡬλνλ©°, λ¨μΌ μ 보 μμ€ μμΉ(Single Source of Truth)κ³Ό λ¨λ°©ν₯ λ°μ΄ν° νλ¦(Unidirectional Data Flow)μ λ°λ₯΄λ μν€ν μ²λ₯Ό κΆμ₯νλ€.
1. λ°μ΄ν° λ μ΄μ΄ ꡬμΆ
λ°μ΄ν° λ μ΄μ΄λ μ ν리μΌμ΄μ λ°μ΄ν°λ₯Ό κ΄λ¦¬νκ³ λΉμ¦λμ€ λ‘μ§μ ν¬ν¨νλ κ³μΈ΅μ΄λ€.
- μ μ₯μ(Repository)λ₯Ό μ¬μ©νμ¬ λ°μ΄ν°λ₯Ό λ ΈμΆ (μ κ·Ή κΆμ₯λ¨)
- λ°μ΄ν° μμ€(DB, API, Sensor λ±)μ UI λ μ΄μ΄κ° μ§μ μνΈμμ©νμ§ μλλ‘ ν¨
- μμ μ±μμλ data ν¨ν€μ§μ λ°μ΄ν° κ΄λ ¨ ν΄λμ€λ₯Ό λ°°μΉ κ°λ₯
2. UI λ μ΄μ΄ ꡬμΆ
UI λ μ΄μ΄λ λ°μ΄ν°λ₯Ό νλ©΄μ νμνκ³ μ¬μ©μ μ λ ₯μ μ²λ¦¬νλ μν μ νλ€.
- ViewModelμ νμ©νμ¬ UI μν κ΄λ¦¬ (μ κ·Ή κΆμ₯λ¨)
- λ¨λ°©ν₯ λ°μ΄ν° νλ¦(UDF) μμΉμ λ°λ¦ (μ κ·Ή κΆμ₯λ¨)
- UIμμ μ§μ λ°μ΄ν° μμ€λ₯Ό νΈμΆνμ§ μκ³ , ViewModelμ ν΅ν΄ λ°μ΄ν°μ μνΈμμ©
3. ViewModel νμ© λ° UI μν κ΄λ¦¬
ViewModelμ UI μνλ₯Ό κ΄λ¦¬νκ³ , λ°μ΄ν°λ₯Ό λ ΈμΆνλ μν μ νλ€. ViewModelμ νμ©ν λ λ€μ κΆμ₯μ¬νμ λ°λ₯΄λ κ²μ΄ μ’λ€.
- Lifecycleμ ꡬμ λ°μ§ μλλ‘ μ€κ³ (μ κ·Ή κΆμ₯λ¨)
- μ½λ£¨ν΄ λ° Flow μ¬μ©νμ¬ λΉλκΈ° μμ μ²λ¦¬ (μ κ·Ή κΆμ₯λ¨)
- UI μνλ₯Ό StateFlowλ‘ κ΄λ¦¬ (κΆμ₯λ¨)
- ViewModelμμ UIλ‘ μ΄λ²€νΈλ₯Ό μ§μ μ μ‘νμ§ μμ (μ κ·Ή κΆμ₯λ¨)
@HiltViewModel
class BookmarksViewModel @Inject constructor(
newsRepository: NewsRepository
) : ViewModel() {
val feedState: StateFlow<NewsFeedUiState> =
newsRepository
.getNewsResourcesStream()
.mapToFeedState()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = NewsFeedUiState.Loading
)
}
4. λ¨μΌ μ‘ν°λΉν° & Jetpack Compose νμ©
μ±μ΄ μ¬λ¬ νλ©΄μ ν¬ν¨νλ κ²½μ° λ¨μΌ μ‘ν°λΉν° ꡬ쑰(Single Activity Architecture) λ₯Ό λ°λ₯΄λ κ²μ΄ κΆμ₯λλ€. λν, μ΅μ UI νλ μμν¬μΈ Jetpack Compose λ₯Ό νμ©νλ©΄ λ³΄λ€ μ μ°νκ³ μ μΈμ μΈ UI κ°λ°μ΄ κ°λ₯νλ€.
- Navigation Component νμ©νμ¬ νλ©΄ κ° μ΄λ κ΄λ¦¬ (κΆμ₯λ¨)
- Compose UI μνλ₯Ό collectAsStateWithLifecycleμΌλ‘ μμ§ (μ κ·Ή κΆμ₯λ¨)
@Composable
fun MyScreen(viewModel: MyViewModel) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
// UI ꡬμ±
}
5. μ’ μμ± κ΄λ¦¬ λ° DI νμ©
μλλ‘μ΄λ μ± κ°λ°μμλ μμ‘΄μ± μ£Όμ (Dependency Injection, DI) μ ν΅ν΄ κ°μ²΄ μμ±μ ν¨μ¨μ μΌλ‘ κ΄λ¦¬νλ κ²μ΄ μ€μνλ€.
- Hiltλ₯Ό μ¬μ©νμ¬ μμ‘΄μ± κ΄λ¦¬ (κΆμ₯λ¨)
- μμ±μ μ£Όμ (Constructor Injection) μ¬μ© (μ κ·Ή κΆμ₯λ¨)
- νμν κ²½μ° μ»΄ν¬λνΈ λ²μ μ§μ (μ κ·Ή κΆμ₯λ¨)
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
fun provideRepository(): NewsRepository {
return DefaultNewsRepository()
}
}
6. ν μ€νΈ μ λ΅
μ±μ νμ§μ 보μ₯νκΈ° μν΄μλ μ μ ν ν μ€νΈ μ λ΅μ΄ νμνλ€.
- ViewModel λ¨μ ν μ€νΈ μν (μ κ·Ή κΆμ₯λ¨)
- Flow λ° LiveData ν μ€νΈ (μ κ·Ή κΆμ₯λ¨)
- UI νμ ν μ€νΈ μ€ν (μ κ·Ή κΆμ₯λ¨)
- Mock보λ€λ Fake κ°μ²΄ νμ© (μ κ·Ή κΆμ₯λ¨)
@Test
fun testViewModelEmitsCorrectState() = runTest {
val viewModel = MyViewModel(FakeRepository())
assertEquals(expectedState, viewModel.uiState.value)
}
κ²°λ‘
- μλλ‘μ΄λ μν€ν μ² κΆμ₯μ¬νμ λ°λ₯΄λ©΄ μ±μ νμ§κ³Ό μ μ§λ³΄μμ±μ λν ν₯μμν¬ μ μλ€.
- νΉν λ¨λ°©ν₯ λ°μ΄ν° νλ¦μ μ μ§νκ³ , κ΄μ¬μ¬ λΆλ¦¬λ₯Ό λͺ νν νλ©°, ViewModelμ νμ©ν UI μν κ΄λ¦¬λ₯Ό μ² μ ν μ μ© νλ κ²μ΄ μ€μνλ€.
- λν, DI λ° ν μ€νΈ μ λ΅μ μ κ·Ή νμ© νμ¬ νμ₯μ±κ³Ό μμ μ±μ ν보νλ κ²μ΄ μ’λ€.
'π€ μλλ‘μ΄λ > μν€ν μ²' μΉ΄ν κ³ λ¦¬μ λ€λ₯Έ κΈ
[μλλ‘μ΄λ] UI μ΄λ²€νΈ (1) | 2025.03.29 |
---|---|
[μλλ‘μ΄λ] UI λ μ΄μ΄ μν€ν μ² (0) | 2025.03.28 |
[μλλ‘μ΄λ] μλλ‘μ΄λ μ’ μ νλͺ© μ½μ (DI) (0) | 2025.03.25 |
[μλλ‘μ΄λ] Android 4λ μ»΄ν¬λνΈλ₯Ό 맀λνμ€νΈ μμ μ μΈνλ λ°©λ² (0) | 2025.03.24 |
[μλλ‘μ΄λ] μλλ‘μ΄λ 4λ μ»΄ν¬λνΈ (0) | 2025.03.23 |
μλλ‘μ΄λ μ ν리μΌμ΄μ μ κ°λ°ν λ, μ½λμ μ μ§λ³΄μμ±κ³Ό νμ₯μ±μ λμ΄λ κ²μ΄ λ§€μ° μ€μνλ€. ꡬκΈμ μ΄λ₯Ό μν΄ μλλ‘μ΄λ μν€ν μ²μ λν λ€μν κΆμ₯μ¬νμ μ 곡νκ³ μμΌλ©°, μ΄λ₯Ό λ°λ₯΄λ©΄ μ±μ νμ§κ³Ό κ²¬κ³ μ±μ ν₯μμν¬ μ μλ€.
1. μν€ν μ² κΆμ₯μ¬νμ κ°μ
μλλ‘μ΄λ μν€ν μ² κΆμ₯μ¬νμ μ격ν κ·μΉμ΄ μλλΌ μ±μ νμ§μ λμ΄λ λ° λμμ΄ λλ κ°μ΄λλΌμΈμ΄κΈ° λλ¬Έμ μ±μ μꡬμ¬νκ³Ό νμ κ°λ° νκ²½μ κ³ λ €νμ¬ μ΄λ₯Ό μ μ ν μ‘°μ ν μ μλ€.
κΆμ₯μ¬νμ μ°μ μμμ λ°λΌ λ€μκ³Ό κ°μ΄ ꡬλΆλλ€.
- μ κ·Ή κΆμ₯λ¨: κΈ°λ³Έμ μΈ μμΉκ³Ό μΆ©λνμ§ μλ ν λ°λμ λ°λΌμΌ νλ κΆμ₯μ¬ν.
- κΆμ₯λ¨: μ±μ νμ§μ ν₯μμν€λ λ° ν° λμμ΄ λλ κΆμ₯μ¬ν.
- μ νμ¬ν: νΉμ μν©μμ μ μ©ν μ μλ κΆμ₯μ¬ν.
2. κ³μΈ΅νλ μν€ν μ² μ μ©νκΈ°
μλλ‘μ΄λ μ ν리μΌμ΄μ μμλ κ΄μ¬μ¬ λΆλ¦¬(Separation of Concerns) λ₯Ό μ μ©νμ¬ λͺ νν μν€ν μ² κ³μΈ΅μ μ€κ³νλ κ²μ΄ μ€μνλ€. ꡬκΈμ λ°μ΄ν° λͺ¨λΈμμ UIλ₯Ό ꡬλνλ©°, λ¨μΌ μ 보 μμ€ μμΉ(Single Source of Truth)κ³Ό λ¨λ°©ν₯ λ°μ΄ν° νλ¦(Unidirectional Data Flow)μ λ°λ₯΄λ μν€ν μ²λ₯Ό κΆμ₯νλ€.
1. λ°μ΄ν° λ μ΄μ΄ ꡬμΆ
λ°μ΄ν° λ μ΄μ΄λ μ ν리μΌμ΄μ λ°μ΄ν°λ₯Ό κ΄λ¦¬νκ³ λΉμ¦λμ€ λ‘μ§μ ν¬ν¨νλ κ³μΈ΅μ΄λ€.
- μ μ₯μ(Repository)λ₯Ό μ¬μ©νμ¬ λ°μ΄ν°λ₯Ό λ ΈμΆ (μ κ·Ή κΆμ₯λ¨)
- λ°μ΄ν° μμ€(DB, API, Sensor λ±)μ UI λ μ΄μ΄κ° μ§μ μνΈμμ©νμ§ μλλ‘ ν¨
- μμ μ±μμλ data ν¨ν€μ§μ λ°μ΄ν° κ΄λ ¨ ν΄λμ€λ₯Ό λ°°μΉ κ°λ₯
2. UI λ μ΄μ΄ ꡬμΆ
UI λ μ΄μ΄λ λ°μ΄ν°λ₯Ό νλ©΄μ νμνκ³ μ¬μ©μ μ λ ₯μ μ²λ¦¬νλ μν μ νλ€.
- ViewModelμ νμ©νμ¬ UI μν κ΄λ¦¬ (μ κ·Ή κΆμ₯λ¨)
- λ¨λ°©ν₯ λ°μ΄ν° νλ¦(UDF) μμΉμ λ°λ¦ (μ κ·Ή κΆμ₯λ¨)
- UIμμ μ§μ λ°μ΄ν° μμ€λ₯Ό νΈμΆνμ§ μκ³ , ViewModelμ ν΅ν΄ λ°μ΄ν°μ μνΈμμ©
3. ViewModel νμ© λ° UI μν κ΄λ¦¬
ViewModelμ UI μνλ₯Ό κ΄λ¦¬νκ³ , λ°μ΄ν°λ₯Ό λ ΈμΆνλ μν μ νλ€. ViewModelμ νμ©ν λ λ€μ κΆμ₯μ¬νμ λ°λ₯΄λ κ²μ΄ μ’λ€.
- Lifecycleμ ꡬμ λ°μ§ μλλ‘ μ€κ³ (μ κ·Ή κΆμ₯λ¨)
- μ½λ£¨ν΄ λ° Flow μ¬μ©νμ¬ λΉλκΈ° μμ μ²λ¦¬ (μ κ·Ή κΆμ₯λ¨)
- UI μνλ₯Ό StateFlowλ‘ κ΄λ¦¬ (κΆμ₯λ¨)
- ViewModelμμ UIλ‘ μ΄λ²€νΈλ₯Ό μ§μ μ μ‘νμ§ μμ (μ κ·Ή κΆμ₯λ¨)
@HiltViewModel
class BookmarksViewModel @Inject constructor(
newsRepository: NewsRepository
) : ViewModel() {
val feedState: StateFlow<NewsFeedUiState> =
newsRepository
.getNewsResourcesStream()
.mapToFeedState()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = NewsFeedUiState.Loading
)
}
4. λ¨μΌ μ‘ν°λΉν° & Jetpack Compose νμ©
μ±μ΄ μ¬λ¬ νλ©΄μ ν¬ν¨νλ κ²½μ° λ¨μΌ μ‘ν°λΉν° ꡬ쑰(Single Activity Architecture) λ₯Ό λ°λ₯΄λ κ²μ΄ κΆμ₯λλ€. λν, μ΅μ UI νλ μμν¬μΈ Jetpack Compose λ₯Ό νμ©νλ©΄ λ³΄λ€ μ μ°νκ³ μ μΈμ μΈ UI κ°λ°μ΄ κ°λ₯νλ€.
- Navigation Component νμ©νμ¬ νλ©΄ κ° μ΄λ κ΄λ¦¬ (κΆμ₯λ¨)
- Compose UI μνλ₯Ό collectAsStateWithLifecycleμΌλ‘ μμ§ (μ κ·Ή κΆμ₯λ¨)
@Composable
fun MyScreen(viewModel: MyViewModel) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
// UI ꡬμ±
}
5. μ’ μμ± κ΄λ¦¬ λ° DI νμ©
μλλ‘μ΄λ μ± κ°λ°μμλ μμ‘΄μ± μ£Όμ (Dependency Injection, DI) μ ν΅ν΄ κ°μ²΄ μμ±μ ν¨μ¨μ μΌλ‘ κ΄λ¦¬νλ κ²μ΄ μ€μνλ€.
- Hiltλ₯Ό μ¬μ©νμ¬ μμ‘΄μ± κ΄λ¦¬ (κΆμ₯λ¨)
- μμ±μ μ£Όμ (Constructor Injection) μ¬μ© (μ κ·Ή κΆμ₯λ¨)
- νμν κ²½μ° μ»΄ν¬λνΈ λ²μ μ§μ (μ κ·Ή κΆμ₯λ¨)
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
fun provideRepository(): NewsRepository {
return DefaultNewsRepository()
}
}
6. ν μ€νΈ μ λ΅
μ±μ νμ§μ 보μ₯νκΈ° μν΄μλ μ μ ν ν μ€νΈ μ λ΅μ΄ νμνλ€.
- ViewModel λ¨μ ν μ€νΈ μν (μ κ·Ή κΆμ₯λ¨)
- Flow λ° LiveData ν μ€νΈ (μ κ·Ή κΆμ₯λ¨)
- UI νμ ν μ€νΈ μ€ν (μ κ·Ή κΆμ₯λ¨)
- Mock보λ€λ Fake κ°μ²΄ νμ© (μ κ·Ή κΆμ₯λ¨)
@Test
fun testViewModelEmitsCorrectState() = runTest {
val viewModel = MyViewModel(FakeRepository())
assertEquals(expectedState, viewModel.uiState.value)
}
κ²°λ‘
- μλλ‘μ΄λ μν€ν μ² κΆμ₯μ¬νμ λ°λ₯΄λ©΄ μ±μ νμ§κ³Ό μ μ§λ³΄μμ±μ λν ν₯μμν¬ μ μλ€.
- νΉν λ¨λ°©ν₯ λ°μ΄ν° νλ¦μ μ μ§νκ³ , κ΄μ¬μ¬ λΆλ¦¬λ₯Ό λͺ νν νλ©°, ViewModelμ νμ©ν UI μν κ΄λ¦¬λ₯Ό μ² μ ν μ μ© νλ κ²μ΄ μ€μνλ€.
- λν, DI λ° ν μ€νΈ μ λ΅μ μ κ·Ή νμ© νμ¬ νμ₯μ±κ³Ό μμ μ±μ ν보νλ κ²μ΄ μ’λ€.
'π€ μλλ‘μ΄λ > μν€ν μ²' μΉ΄ν κ³ λ¦¬μ λ€λ₯Έ κΈ
[μλλ‘μ΄λ] UI μ΄λ²€νΈ (1) | 2025.03.29 |
---|---|
[μλλ‘μ΄λ] UI λ μ΄μ΄ μν€ν μ² (0) | 2025.03.28 |
[μλλ‘μ΄λ] μλλ‘μ΄λ μ’ μ νλͺ© μ½μ (DI) (0) | 2025.03.25 |
[μλλ‘μ΄λ] Android 4λ μ»΄ν¬λνΈλ₯Ό 맀λνμ€νΈ μμ μ μΈνλ λ°©λ² (0) | 2025.03.24 |
[μλλ‘μ΄λ] μλλ‘μ΄λ 4λ μ»΄ν¬λνΈ (0) | 2025.03.23 |