Android MVVM vs MVI: 2026๋…„์— ์–ด๋–ค ์•„ํ‚คํ…์ฒ˜๋ฅผ ์„ ํƒํ•ด์•ผ ํ• ๊นŒ?

Android์—์„œ MVVM๊ณผ MVI์˜ ์‹ฌ์ธต ๋น„๊ต: ์žฅ๋‹จ์ , ์‚ฌ์šฉ ์‚ฌ๋ก€, ๊ทธ๋ฆฌ๊ณ  2026๋…„์— ์˜ฌ๋ฐ”๋ฅธ ์•„ํ‚คํ…์ฒ˜๋ฅผ ์„ ํƒํ•˜๊ธฐ ์œ„ํ•œ ์‹ค์šฉ์ ์ธ ๊ฐ€์ด๋“œ.

Android์šฉ MVVM๊ณผ MVI ์•„ํ‚คํ…์ฒ˜ ๋น„๊ต

์˜ฌ๋ฐ”๋ฅธ ์•„ํ‚คํ…์ฒ˜ ์„ ํƒ์€ Android ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์œ ์ง€๋ณด์ˆ˜์„ฑ, ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ์„ฑ, ํ™•์žฅ์„ฑ์— ์ง์ ‘์ ์ธ ์˜ํ–ฅ์„ ๋ฏธ์น˜๋Š” ์ค‘์š”ํ•œ ๊ฒฐ์ •์ž…๋‹ˆ๋‹ค. 2026๋…„์—๋Š” ๋‘ ๊ฐ€์ง€ ํŒจํ„ด์ด ์ƒํƒœ๊ณ„๋ฅผ ์ฃผ๋„ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์‚ฐ์—… ํ‘œ์ค€์ธ MVVM๊ณผ Jetpack Compose์™€ ํ•จ๊ป˜ ์ฃผ๋ชฉ๋ฐ›๊ณ  ์žˆ๋Š” ๋ฐ˜์‘ํ˜• ์ ‘๊ทผ ๋ฐฉ์‹์ธ MVI์ž…๋‹ˆ๋‹ค.

์„ ํƒ์˜ ์ค‘์š”์„ฑ

์ž˜๋ชป๋œ ์•„ํ‚คํ…์ฒ˜ ์„ ํƒ์€ ๊ธฐ์ˆ  ๋ถ€์ฑ„, ์žฌํ˜„ํ•˜๊ธฐ ์–ด๋ ค์šด ๋ฒ„๊ทธ, ๊ทธ๋ฆฌ๊ณ  ๊ณ ํ†ต์Šค๋Ÿฌ์šด ๋ฆฌํŒฉํ† ๋ง์œผ๋กœ ์ด์–ด์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐ ์ ‘๊ทผ ๋ฐฉ์‹์˜ ๊ฐ•์ ๊ณผ ์•ฝ์ ์„ ์ดํ•ดํ•˜๋ฉด ์žฅ๊ธฐ์ ์œผ๋กœ ๋งŽ์€ ๋ฌธ์ œ๋ฅผ ์˜ˆ๋ฐฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

MVVM ์ดํ•ดํ•˜๊ธฐ: ํ™•๋ฆฝ๋œ ํ‘œ์ค€

MVVM(Model-View-ViewModel)์€ Jetpack ๋„์ž… ์ดํ›„ Google์ด ๊ถŒ์žฅํ•ด ์˜จ ์•„ํ‚คํ…์ฒ˜์ž…๋‹ˆ๋‹ค. ์ฑ…์ž„์„ ์„ธ ๊ฐœ์˜ ๋ช…ํ™•ํ•œ ๋ ˆ์ด์–ด๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ์ฝ”๋“œ๋ฅผ ๋” ์ฒด๊ณ„์ ์ด๊ณ  ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์‰ฝ๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

MVVM์˜ ํ•ต์‹ฌ ์›์น™

MVVM ํŒจํ„ด์€ ๋ช…ํ™•ํ•œ ๋ถ„๋ฆฌ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•ฉ๋‹ˆ๋‹ค. Model์€ ๋ฐ์ดํ„ฐ์™€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๊ด€๋ฆฌํ•˜๊ณ , View๋Š” UI๋ฅผ ํ‘œ์‹œํ•˜๋ฉฐ, ViewModel์€ ๊ด€์ฐฐ ๊ฐ€๋Šฅํ•œ ์ƒํƒœ๋ฅผ ์ œ๊ณตํ•˜์—ฌ ๋‘˜์„ ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค.

์•„๋ž˜์—์„œ MVVM์„ ์‚ฌ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ํ™”๋ฉด์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. ์ด ์ฒซ ๋ฒˆ์งธ ์˜ˆ์‹œ๋Š” ๊ด€์ฐฐ ๊ฐ€๋Šฅํ•œ ์ƒํƒœ์™€ ์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ์„ ์œ„ํ•œ ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ณตํ•˜๋Š” ViewModel์˜ ๊ธฐ๋ณธ ๊ตฌ์กฐ๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

UserProfileViewModel.ktkotlin
// Classic MVVM ViewModel for a user profile screen
// It exposes observable state and methods for actions
class UserProfileViewModel(
    private val userRepository: UserRepository,
    private val analyticsTracker: AnalyticsTracker
) : ViewModel() {

    // Observable state with StateFlow - the View observes these changes
    private val _uiState = MutableStateFlow(UserProfileState())
    val uiState: StateFlow<UserProfileState> = _uiState.asStateFlow()

    // Separate loading state - MVVM allows multiple flows
    private val _isLoading = MutableStateFlow(false)
    val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()

    // One-shot error messages
    private val _errorMessage = MutableSharedFlow<String>()
    val errorMessage: SharedFlow<String> = _errorMessage.asSharedFlow()

    // Initial profile loading
    fun loadProfile(userId: String) {
        viewModelScope.launch {
            _isLoading.value = true

            try {
                // Repository call to fetch data
                val user = userRepository.getUser(userId)

                // Update state with new data
                _uiState.update { currentState ->
                    currentState.copy(
                        user = user,
                        isEditing = false
                    )
                }

                // Analytics tracking
                analyticsTracker.trackProfileViewed(userId)

            } catch (e: Exception) {
                // Emit one-shot error message
                _errorMessage.emit("Unable to load profile")
            } finally {
                _isLoading.value = false
            }
        }
    }

    // Enable edit mode
    fun enableEditMode() {
        _uiState.update { it.copy(isEditing = true) }
    }

    // Save profile changes
    fun saveProfile(name: String, bio: String) {
        viewModelScope.launch {
            _isLoading.value = true

            try {
                val updatedUser = userRepository.updateUser(
                    _uiState.value.user?.id ?: return@launch,
                    name = name,
                    bio = bio
                )

                _uiState.update {
                    it.copy(user = updatedUser, isEditing = false)
                }

            } catch (e: Exception) {
                _errorMessage.emit("Failed to save profile")
            } finally {
                _isLoading.value = false
            }
        }
    }
}

// Data class representing the screen state
data class UserProfileState(
    val user: User? = null,
    val isEditing: Boolean = false
)

์ด ViewModel์€ ์ „ํ˜•์ ์ธ MVVM ์ ‘๊ทผ ๋ฐฉ์‹์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๊ด€์ฐฐ ๊ฐ€๋Šฅํ•œ flow(์ฃผ ์ƒํƒœ, ๋กœ๋”ฉ, ์˜ค๋ฅ˜)์™€ ๊ฐ ์‚ฌ์šฉ์ž ์•ก์…˜์— ๋Œ€ํ•œ ๊ณต๊ฐœ ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

MVVM์˜ ์žฅ์ 

MVVM์€ ๊ด‘๋ฒ”์œ„ํ•œ ์ฑ„ํƒ์„ ์„ค๋ช…ํ•˜๋Š” ์—ฌ๋Ÿฌ ๊ฐ•์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

  • ์นœ์ˆ™ํ•จ: ๋Œ€๋ถ€๋ถ„์˜ Android ๊ฐœ๋ฐœ์ž๊ฐ€ ์ด ํŒจํ„ด์„ ์•Œ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค
  • ์œ ์—ฐ์„ฑ: ์ƒํƒœ๋ฅผ ์›ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌ์กฐํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค
  • ์ƒํƒœ๊ณ„: Jetpack(LiveData, StateFlow, Hilt)๊ณผ ์™„๋ฒฝํ•˜๊ฒŒ ํ†ตํ•ฉ๋ฉ๋‹ˆ๋‹ค
  • ๋‹จ์ˆœ์„ฑ: ์ดˆ๋ณด์ž๋ฅผ ์œ„ํ•œ ์™„๋งŒํ•œ ํ•™์Šต ๊ณก์„ ์ž…๋‹ˆ๋‹ค

MVVM์€ ๋‹ค์–‘ํ•œ ๊ฒฝํ—˜ ์ˆ˜์ค€์˜ ๊ฐœ๋ฐœ์ž๋กœ ๊ตฌ์„ฑ๋œ ํ˜ผํ•ฉ ํŒ€์— ํŠนํžˆ ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค. ๊ฐœ๋…์  ๋‹จ์ˆœ์„ฑ์ด ์˜จ๋ณด๋”ฉ์„ ์šฉ์ดํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

MVVM์˜ ํ•œ๊ณ„

๊ทธ๋Ÿฌ๋‚˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์„ฑ์žฅํ•จ์— ๋”ฐ๋ผ MVVM์€ ํ•œ๊ณ„๋ฅผ ๋“œ๋Ÿฌ๋ƒ…๋‹ˆ๋‹ค. ์ฃผ์š” ๋ฌธ์ œ๋Š” ๋ถ„์‚ฐ๋œ ์ƒํƒœ ๊ด€๋ฆฌ์ž…๋‹ˆ๋‹ค. ๋‹ค์Œ ์˜ˆ์‹œ๋Š” ์ด ์ผ๋ฐ˜์ ์ธ ๋ฌธ์ œ๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

ProblematicViewModel.ktkotlin
// Example MVVM ViewModel with fragmented state
// This pattern becomes problematic as the screen grows in complexity
class CheckoutViewModel : ViewModel() {

    // Problem: state scattered across multiple flows
    private val _cart = MutableStateFlow<List<CartItem>>(emptyList())
    private val _selectedAddress = MutableStateFlow<Address?>(null)
    private val _selectedPayment = MutableStateFlow<PaymentMethod?>(null)
    private val _promoCode = MutableStateFlow<String?>(null)
    private val _isLoading = MutableStateFlow(false)
    private val _error = MutableStateFlow<String?>(null)

    // Each modification can create temporary inconsistent states
    fun applyPromoCode(code: String) {
        viewModelScope.launch {
            _isLoading.value = true
            _error.value = null  // Reset error

            try {
                val discount = promoRepository.validate(code)
                _promoCode.value = code
                // Cart state also needs updating...
                // but there's a delay between the two updates
                recalculateCart()
            } catch (e: Exception) {
                _error.value = e.message
                _promoCode.value = null
            } finally {
                _isLoading.value = false
            }
        }
    }

    // Hard to guarantee consistency across all these states
    private fun recalculateCart() {
        // Complex logic depending on multiple states...
    }
}

์ด ์˜ˆ์‹œ๋Š” MVVM์—์„œ ์ƒํƒœ๊ฐ€ ์–ด๋–ป๊ฒŒ ๋ถ„์‚ฐ๋  ์ˆ˜ ์žˆ๋Š”์ง€, ๊ทธ๋ฆฌ๊ณ  ์ƒํƒœ ์ „ํ™˜์„ ์ถ”์ ํ•˜๊ณ  ๋ฒ„๊ทธ๋ฅผ ์žฌํ˜„ํ•˜๊ธฐ๊ฐ€ ์–ด๋ ค์›Œ์ง€๋Š”์ง€ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

MVI ์ดํ•ดํ•˜๊ธฐ: ๋‹จ๋ฐฉํ–ฅ ์ ‘๊ทผ ๋ฐฉ์‹

MVI(Model-View-Intent)๋Š” ๋‹ค๋ฅธ ์ฒ ํ•™์„ ์ฑ„ํƒํ•ฉ๋‹ˆ๋‹ค. ๋‹จ๋ฐฉํ–ฅ ๋ฐ์ดํ„ฐ ํ๋ฆ„๊ณผ ๋‹จ์ผ ๋ถˆ๋ณ€ ์ƒํƒœ์ž…๋‹ˆ๋‹ค. Redux์—์„œ ์˜๊ฐ์„ ๋ฐ›์€ ์ด ์ ‘๊ทผ ๋ฐฉ์‹์€ ์ผ๊ด€์„ฑ ์—†๋Š” ์ƒํƒœ ๋ฌธ์ œ๋ฅผ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค.

MVI์˜ ํ•ต์‹ฌ ์›์น™

MVI์—์„œ๋Š” ๋ชจ๋“  ๊ฒƒ์ด ๋ช…ํ™•ํ•œ ์‚ฌ์ดํด์„ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ Intent(์•ก์…˜)๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋ฉด, Reducer๊ฐ€ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ์ƒˆ ์ƒํƒœ๋กœ ๋ณ€ํ™˜ํ•˜๊ณ , View๋Š” ๊ทธ ๋‹จ์ผ ์ƒํƒœ๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ์ธก ๊ฐ€๋Šฅํ•˜๊ณ , ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•˜๋ฉฐ, ๋””๋ฒ„๊น…ํ•˜๊ธฐ ์‰ฝ์Šต๋‹ˆ๋‹ค.

์•„๋ž˜์—์„œ MVI๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋™์ผํ•œ ํ”„๋กœํ•„ ํ™”๋ฉด์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. ์ƒํƒœ๊ฐ€ ์ค‘์•™ ์ง‘์ค‘ํ™”๋˜๊ณ  ์•ก์…˜์ด ๋ช…์‹œ์ ์œผ๋กœ ํƒ€์ž…์ด ์ง€์ •๋˜๋Š” ๋ฐฉ์‹์— ์ฃผ๋ชฉํ•˜์‹ญ์‹œ์˜ค.

UserProfileMviViewModel.ktkotlin
// MVI ViewModel for the same user profile screen
// Note the structure: Intent โ†’ Reducer โ†’ Single State
class UserProfileMviViewModel(
    private val userRepository: UserRepository,
    private val analyticsTracker: AnalyticsTracker
) : ViewModel() {

    // Single, immutable state - the absolute source of truth
    private val _state = MutableStateFlow(UserProfileState())
    val state: StateFlow<UserProfileState> = _state.asStateFlow()

    // Channel for side effects (navigation, snackbar)
    private val _sideEffect = Channel<UserProfileSideEffect>()
    val sideEffect: Flow<UserProfileSideEffect> = _sideEffect.receiveAsFlow()

    // Single entry point for all user actions
    fun onIntent(intent: UserProfileIntent) {
        when (intent) {
            is UserProfileIntent.LoadProfile -> loadProfile(intent.userId)
            is UserProfileIntent.EnableEditMode -> enableEditMode()
            is UserProfileIntent.SaveProfile -> saveProfile(intent.name, intent.bio)
            is UserProfileIntent.CancelEdit -> cancelEdit()
        }
    }

    private fun loadProfile(userId: String) {
        viewModelScope.launch {
            // Transition to loading state
            _state.update { it.copy(isLoading = true, error = null) }

            try {
                val user = userRepository.getUser(userId)

                // Single atomic state update
                _state.update {
                    it.copy(
                        user = user,
                        isLoading = false,
                        error = null
                    )
                }

                analyticsTracker.trackProfileViewed(userId)

            } catch (e: Exception) {
                // Error state is part of the main state
                _state.update {
                    it.copy(
                        isLoading = false,
                        error = "Unable to load profile"
                    )
                }
            }
        }
    }

    private fun enableEditMode() {
        // Simple, predictable update
        _state.update { it.copy(isEditing = true) }
    }

    private fun saveProfile(name: String, bio: String) {
        viewModelScope.launch {
            val currentUser = _state.value.user ?: return@launch

            _state.update { it.copy(isLoading = true) }

            try {
                val updatedUser = userRepository.updateUser(
                    currentUser.id,
                    name = name,
                    bio = bio
                )

                _state.update {
                    it.copy(
                        user = updatedUser,
                        isEditing = false,
                        isLoading = false
                    )
                }

                // Side effect to notify the user
                _sideEffect.send(UserProfileSideEffect.ShowSuccess("Profile updated"))

            } catch (e: Exception) {
                _state.update {
                    it.copy(isLoading = false, error = "Failed to save profile")
                }
            }
        }
    }

    private fun cancelEdit() {
        _state.update { it.copy(isEditing = false) }
    }
}

// All possible actions, explicitly typed
sealed class UserProfileIntent {
    data class LoadProfile(val userId: String) : UserProfileIntent()
    object EnableEditMode : UserProfileIntent()
    data class SaveProfile(val name: String, val bio: String) : UserProfileIntent()
    object CancelEdit : UserProfileIntent()
}

// Single, complete screen state
data class UserProfileState(
    val user: User? = null,
    val isLoading: Boolean = false,
    val isEditing: Boolean = false,
    val error: String? = null
)

// One-shot side effects
sealed class UserProfileSideEffect {
    data class ShowSuccess(val message: String) : UserProfileSideEffect()
    data class NavigateTo(val destination: String) : UserProfileSideEffect()
}

์ฐจ์ด๊ฐ€ ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค. ๋‹จ์ผ ์ƒํƒœ ํ๋ฆ„, ๋ช…์‹œ์  ์•ก์…˜, ๊ทธ๋ฆฌ๊ณ  ์˜์†์  ์ƒํƒœ์™€ ์ผํšŒ์„ฑ ํšจ๊ณผ ์‚ฌ์ด์˜ ๊น”๋”ํ•œ ๋ถ„๋ฆฌ์ž…๋‹ˆ๋‹ค.

๋” ์‰ฌ์šด ๋””๋ฒ„๊น…

MVI๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ชจ๋“  Intent์™€ ๋ชจ๋“  ์ƒํƒœ ์ „ํ™˜์„ ๋กœ๊น…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฒ„๊ทธ๋ฅผ ์žฌํ˜„ํ•˜๋Š” ๊ฒƒ์ด ๊ฐ„๋‹จํ•ด์ง‘๋‹ˆ๋‹ค. Intent ์‹œํ€€์Šค๋ฅผ ๋‹ค์‹œ ์žฌ์ƒํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

Jetpack Compose์™€ MVI

MVI๋Š” Jetpack Compose์™€ ํ•จ๊ป˜ ํŠนํžˆ ๋น›์„ ๋ฐœํ•ฉ๋‹ˆ๋‹ค. ๋‘˜ ๋‹ค ๋ถˆ๋ณ€ ์ƒํƒœ์™€ ์„ ์–ธ์  UI๋ผ๋Š” ๋™์ผํ•œ ์ฒ ํ•™์„ ๊ณต์œ ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ViewModel์„ Compose ํ™”๋ฉด์— ์—ฐ๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

UserProfileScreen.ktkotlin
// Compose screen consuming MVI state
// The connection between ViewModel and UI is elegant and reactive
@Composable
fun UserProfileScreen(
    viewModel: UserProfileMviViewModel = hiltViewModel(),
    onNavigateBack: () -> Unit
) {
    // Collect the single state
    val state by viewModel.state.collectAsStateWithLifecycle()

    // Handle side effects
    LaunchedEffect(Unit) {
        viewModel.sideEffect.collect { effect ->
            when (effect) {
                is UserProfileSideEffect.ShowSuccess -> {
                    // Show snackbar
                }
                is UserProfileSideEffect.NavigateTo -> {
                    // Navigate
                }
            }
        }
    }

    // Purely declarative UI based on state
    UserProfileContent(
        state = state,
        onIntent = viewModel::onIntent
    )
}

@Composable
private fun UserProfileContent(
    state: UserProfileState,
    onIntent: (UserProfileIntent) -> Unit
) {
    Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {

        // Conditional rendering based on the single state
        when {
            state.isLoading -> {
                CircularProgressIndicator(
                    modifier = Modifier.align(Alignment.CenterHorizontally)
                )
            }
            state.error != null -> {
                ErrorMessage(
                    message = state.error,
                    onRetry = {
                        state.user?.id?.let {
                            onIntent(UserProfileIntent.LoadProfile(it))
                        }
                    }
                )
            }
            state.user != null -> {
                ProfileCard(
                    user = state.user,
                    isEditing = state.isEditing,
                    onEditClick = { onIntent(UserProfileIntent.EnableEditMode) },
                    onSaveClick = { name, bio ->
                        onIntent(UserProfileIntent.SaveProfile(name, bio))
                    },
                    onCancelClick = { onIntent(UserProfileIntent.CancelEdit) }
                )
            }
        }
    }
}

UI๋Š” ์ƒํƒœ์˜ ์ˆœ์ˆ˜ ํ•จ์ˆ˜๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. ์˜ˆ์ธก ๊ฐ€๋Šฅํ•˜๊ณ , ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•˜๋ฉฐ, ์ˆจ๊ฒจ์ง„ ๋ถ€์ˆ˜ ํšจ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

์ƒ์„ธ ๋น„๊ต

๋‘ ํŒจํ„ด์„ ์‹ค์ œ๋กœ ์‚ดํŽด๋ณธ ํ›„, ํ”„๋กœ๋•์…˜์—์„œ ์ •๋ง ์ค‘์š”ํ•œ ๊ธฐ์ค€์œผ๋กœ ๋น„๊ตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ƒํƒœ ๊ด€๋ฆฌ

๊ทผ๋ณธ์ ์ธ ์ฐจ์ด๋Š” ์ƒํƒœ ๊ด€๋ฆฌ์— ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ์ฐจ์ด๋Š” ์žฅ๊ธฐ์ ์ธ ์œ ์ง€๋ณด์ˆ˜์„ฑ์— ์ง์ ‘์ ์œผ๋กœ ์˜ํ–ฅ์„ ๋ฏธ์นฉ๋‹ˆ๋‹ค.

StateComparison.ktkotlin
// MVVM: potentially fragmented state
class MvvmViewModel : ViewModel() {
    // Multiple sources of truth - manual synchronization needed
    private val _users = MutableStateFlow<List<User>>(emptyList())
    private val _selectedUser = MutableStateFlow<User?>(null)
    private val _isLoading = MutableStateFlow(false)
    private val _searchQuery = MutableStateFlow("")

    // What happens if _selectedUser points to a user
    // that's no longer in _users after a refresh?
    // โ†’ Inconsistent state that's hard to detect
}

// MVI: consistent state by construction
class MviViewModel : ViewModel() {
    // Single source of truth - inconsistencies are impossible
    private val _state = MutableStateFlow(UsersState())

    data class UsersState(
        val users: List<User> = emptyList(),
        val selectedUser: User? = null,  // Always consistent with users
        val isLoading: Boolean = false,
        val searchQuery: String = ""
    )

    // Each update automatically maintains invariants
    private fun selectUser(userId: String) {
        _state.update { currentState ->
            currentState.copy(
                selectedUser = currentState.users.find { it.id == userId }
            )
        }
    }
}
์œ ๋ น ๋ฒ„๊ทธ

MVVM์—์„œ ์ผ๊ด€์„ฑ ์—†๋Š” ์ƒํƒœ๋Š” ์žฌํ˜„ํ•˜๊ธฐ ์–ด๋ ค์šด ๊ฐ„ํ—์  ๋ฒ„๊ทธ๋กœ ๋‚˜ํƒ€๋‚˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Šต๋‹ˆ๋‹ค. MVI์—์„œ๋Š” ์ƒํƒœ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์œผ๋ฉด ๊ฒฐ์ •๋ก ์ ์œผ๋กœ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์•„ํ‚คํ…์ฒ˜ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ์„ฑ

๋‘ ์•„ํ‚คํ…์ฒ˜ ๋ชจ๋‘ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•˜์ง€๋งŒ, MVI๋Š” ์˜ˆ์ธก ๊ฐ€๋Šฅ์„ฑ ๋•๋ถ„์— ์ƒ๋‹นํ•œ ์ด์ ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

TestComparison.ktkotlin
// MVVM test: requires verifying multiple flows
@Test
fun `loadUsers should update state correctly`() = runTest {
    val viewModel = MvvmViewModel(fakeRepository)

    // Observe multiple flows simultaneously
    val users = mutableListOf<List<User>>()
    val loadingStates = mutableListOf<Boolean>()

    val job1 = launch { viewModel.users.toList(users) }
    val job2 = launch { viewModel.isLoading.toList(loadingStates) }

    viewModel.loadUsers()
    advanceUntilIdle()

    // Assertions on different flows
    assertThat(users.last()).isEqualTo(expectedUsers)
    assertThat(loadingStates).containsExactly(false, true, false)

    job1.cancel()
    job2.cancel()
}

// MVI test: single flow to verify, clear state sequence
@Test
fun `LoadUsers intent should produce correct state sequence`() = runTest {
    val viewModel = MviViewModel(fakeRepository)

    // Collect all states in order
    val states = mutableListOf<UsersState>()
    val job = launch { viewModel.state.toList(states) }

    // Send the intent
    viewModel.onIntent(UsersIntent.LoadUsers)
    advanceUntilIdle()

    // Verify the exact state sequence
    assertThat(states).containsExactly(
        UsersState(),                                    // Initial
        UsersState(isLoading = true),                   // Loading
        UsersState(users = expectedUsers, isLoading = false)  // Success
    )

    job.cancel()
}

MVI๋Š” ์ •ํ™•ํ•œ ์ƒํƒœ ์ „ํ™˜ ์ˆœ์„œ๋ฅผ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋ฉฐ, ์ด๋Š” ๋งŽ์€ ์ƒํ˜ธ์ž‘์šฉ์ด ์žˆ๋Š” ๋ณต์žกํ•œ ํ™”๋ฉด์— ํŠนํžˆ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

๋ณต์žก์„ฑ๊ณผ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ

ํŠธ๋ ˆ์ด๋“œ์˜คํ”„์— ๋Œ€ํ•ด ์†”์งํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. MVI๋Š” ๋” ๋งŽ์€ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ์™€ ๊ฐœ๋…์— ๋Œ€ํ•œ ๋” ๊นŠ์€ ์ดํ•ด๋ฅผ ์š”๊ตฌํ•ฉ๋‹ˆ๋‹ค.

BoilerplateComparison.ktkotlin
// MVVM: quick start, less code
class SimpleViewModel : ViewModel() {
    private val _name = MutableStateFlow("")
    val name: StateFlow<String> = _name.asStateFlow()

    fun updateName(newName: String) {
        _name.value = newName
    }
}
// Total: ~10 lines

// MVI: more structure, more code
class SimpleMviViewModel : ViewModel() {
    private val _state = MutableStateFlow(SimpleState())
    val state: StateFlow<SimpleState> = _state.asStateFlow()

    fun onIntent(intent: SimpleIntent) {
        when (intent) {
            is SimpleIntent.UpdateName -> {
                _state.update { it.copy(name = intent.name) }
            }
        }
    }
}

data class SimpleState(val name: String = "")

sealed class SimpleIntent {
    data class UpdateName(val name: String) : SimpleIntent()
}
// Total: ~20 lines

๊ฐ„๋‹จํ•œ ํ™”๋ฉด์˜ ๊ฒฝ์šฐ MVI๊ฐ€ ๊ณผํ•˜๊ฒŒ ๋А๊ปด์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ํ™”๋ฉด์˜ ๋ณต์žก์„ฑ์ด ์ฆ๊ฐ€ํ• ์ˆ˜๋ก ์ด ๊ตฌ์กฐ๋Š” ๊ฐ€์น˜๋ฅผ ๋ฐœํœ˜ํ•ฉ๋‹ˆ๋‹ค.

Android ๋ฉด์ ‘ ์ค€๋น„๊ฐ€ ๋˜์…จ๋‚˜์š”?

์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ, flashcards, ๊ธฐ์ˆ  ํ…Œ์ŠคํŠธ๋กœ ์—ฐ์Šตํ•˜์„ธ์š”.

MVVM์„ ์„ ํƒํ•ด์•ผ ํ•  ๋•Œ

MVVM์€ ์—ฌ๋Ÿฌ ์ƒํ™ฉ์—์„œ ์‹ค์šฉ์ ์ธ ์„ ํƒ์œผ๋กœ ๋‚จ์•„ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ์กด ํ”„๋กœ์ ํŠธ

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์ด๋ฏธ MVVM์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค๋ฉด, MVI๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•˜๋Š” ๊ฒƒ์€ ์ƒ๋‹นํ•œ ๋…ธ๋ ฅ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์กด MVVM ๊ตฌ์กฐ๋ฅผ ๊ฐœ์„ ํ•˜๋Š” ๊ฒƒ์ด ๋” ํ˜„๋ช…ํ•œ ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Šต๋‹ˆ๋‹ค.

Junior ๋˜๋Š” ํ˜ผํ•ฉ ํŒ€

MVVM์ด ๋” ์ ‘๊ทผํ•˜๊ธฐ ์‰ฝ์Šต๋‹ˆ๋‹ค. ์ดˆ๋ณด ๊ฐœ๋ฐœ์ž๋กœ ๊ตฌ์„ฑ๋œ ํŒ€์€ MVI๋ณด๋‹ค MVVM์œผ๋กœ ๋” ๋น ๋ฅด๊ฒŒ ์ƒ์‚ฐ์„ฑ์„ ๋ฐœํœ˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹จ์ˆœํ•œ ํ™”๋ฉด

์ƒํƒœ์™€ ์ƒํ˜ธ์ž‘์šฉ์ด ์ ์€ ํ™”๋ฉด์˜ ๊ฒฝ์šฐ MVI๋Š” ๋น„๋ก€์ ์ธ ์ด์  ์—†์ด ๋ณต์žก์„ฑ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

SettingsViewModel.ktkotlin
// For a simple settings screen, MVVM is plenty
class SettingsViewModel(
    private val preferencesRepository: PreferencesRepository
) : ViewModel() {

    val darkMode = preferencesRepository.darkModeFlow
        .stateIn(viewModelScope, SharingStarted.Lazily, false)

    val notificationsEnabled = preferencesRepository.notificationsFlow
        .stateIn(viewModelScope, SharingStarted.Lazily, true)

    fun toggleDarkMode() {
        viewModelScope.launch {
            preferencesRepository.setDarkMode(!darkMode.value)
        }
    }

    fun toggleNotifications() {
        viewModelScope.launch {
            preferencesRepository.setNotifications(!notificationsEnabled.value)
        }
    }
}

MVI๋ฅผ ์„ ํƒํ•ด์•ผ ํ•  ๋•Œ

MVI๋Š” ํŠน์ • ์ƒํ™ฉ์—์„œ ๊ทธ ๊ฐ€์น˜๋ฅผ ์ฆ๋ช…ํ•ฉ๋‹ˆ๋‹ค.

๋ณต์žกํ•œ ์ƒํƒœ๋ฅผ ๊ฐ€์ง„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜

ํ™”๋ฉด์— ์ƒํ˜ธ ์˜์กด์ ์ธ ์ƒํƒœ๊ฐ€ ๋งŽ์„ ๋•Œ, MVI๋Š” ์ผ๊ด€์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

CheckoutState.ktkotlin
// Checkout screen with complex state: MVI excels
data class CheckoutState(
    val cartItems: List<CartItem> = emptyList(),
    val selectedAddress: Address? = null,
    val selectedPayment: PaymentMethod? = null,
    val promoCode: PromoCode? = null,
    val deliveryOptions: List<DeliveryOption> = emptyList(),
    val selectedDelivery: DeliveryOption? = null,
    val subtotal: Money = Money.ZERO,
    val discount: Money = Money.ZERO,
    val deliveryFee: Money = Money.ZERO,
    val total: Money = Money.ZERO,
    val isLoading: Boolean = false,
    val error: CheckoutError? = null,
    val step: CheckoutStep = CheckoutStep.CART
) {
    // Verifiable invariants
    init {
        require(total == subtotal - discount + deliveryFee) {
            "Total inconsistent with components"
        }
    }
}

sealed class CheckoutIntent {
    data class AddItem(val item: CartItem) : CheckoutIntent()
    data class RemoveItem(val itemId: String) : CheckoutIntent()
    data class SelectAddress(val address: Address) : CheckoutIntent()
    data class SelectPayment(val method: PaymentMethod) : CheckoutIntent()
    data class ApplyPromo(val code: String) : CheckoutIntent()
    object RemovePromo : CheckoutIntent()
    data class SelectDelivery(val option: DeliveryOption) : CheckoutIntent()
    object ProceedToPayment : CheckoutIntent()
    object ConfirmOrder : CheckoutIntent()
}

์‹ค์‹œ๊ฐ„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜

WebSocket, ํ‘ธ์‹œ ์•Œ๋ฆผ, ๋˜๋Š” ์‹ค์‹œ๊ฐ„ ๋™๊ธฐํ™”๊ฐ€ ์žˆ๋Š” ์•ฑ์˜ ๊ฒฝ์šฐ MVI๋Š” ์—ฌ๋Ÿฌ ๋ฐ์ดํ„ฐ ํ๋ฆ„์„ ์šฐ์•„ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

์—„๊ฒฉํ•œ ๋””๋ฒ„๊น… ์š”๊ตฌ์‚ฌํ•ญ

๊ทœ์ œ๋œ ๋„๋ฉ”์ธ(ํ•€ํ…Œํฌ, ํ—ฌ์Šค์ผ€์–ด)์—์„œ ์ด๋ฒคํŠธ์˜ ์ •ํ™•ํ•œ ์‹œํ€€์Šค๋ฅผ ์žฌํ˜„ํ•˜๋Š” ๋Šฅ๋ ฅ์€ ๋งค์šฐ ์†Œ์ค‘ํ•ฉ๋‹ˆ๋‹ค.

MVI๋Š” "์‹œ๊ฐ„ ์—ฌํ–‰ ๋””๋ฒ„๊น…"์„ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ์ƒํƒœ๋ฅผ ๊ธฐ๋กํ•˜๊ณ  ์‚ฌ์šฉ์ž ์„ธ์…˜์„ ์žฌ์ƒํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์ ‘๊ทผ ๋ฐฉ์‹: ๋‘ ์„ธ๊ณ„์˜ ์ตœ์„ 

์‹ค์ œ๋กœ ๋งŽ์€ ํŒ€์ด ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์ ‘๊ทผ ๋ฐฉ์‹์„ ์ฑ„ํƒํ•ฉ๋‹ˆ๋‹ค. ๋ณต์žกํ•œ ํ™”๋ฉด์—๋Š” MVI๋ฅผ, ๋‹จ์ˆœํ•œ ํ™”๋ฉด์—๋Š” ๊ฐ„์†Œํ™”๋œ MVVM์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๊ถŒ์žฅ ํŒจํ„ด์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

MviViewModel.ktkotlin
// Base ViewModel with lightweight MVI structure
// Reusable for all screens
abstract class MviViewModel<S, I>(initialState: S) : ViewModel() {

    private val _state = MutableStateFlow(initialState)
    val state: StateFlow<S> = _state.asStateFlow()

    protected val currentState: S get() = _state.value

    // Single entry point for intents
    abstract fun onIntent(intent: I)

    // Helper to update state
    protected fun updateState(reducer: S.() -> S) {
        _state.update { it.reducer() }
    }
}

// Concrete implementation stays simple
class ProfileViewModel(
    private val userRepository: UserRepository
) : MviViewModel<ProfileState, ProfileIntent>(ProfileState()) {

    override fun onIntent(intent: ProfileIntent) {
        when (intent) {
            is ProfileIntent.Load -> load(intent.userId)
            is ProfileIntent.Refresh -> refresh()
            is ProfileIntent.ToggleFavorite -> toggleFavorite()
        }
    }

    private fun load(userId: String) {
        viewModelScope.launch {
            updateState { copy(isLoading = true) }

            val user = userRepository.getUser(userId)

            updateState {
                copy(user = user, isLoading = false)
            }
        }
    }

    private fun refresh() = load(currentState.user?.id ?: return)

    private fun toggleFavorite() {
        updateState {
            copy(user = user?.copy(isFavorite = !user.isFavorite))
        }
    }
}

์ด ์ ‘๊ทผ ๋ฐฉ์‹์€ ๊ณผ๋„ํ•œ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์—†์ด MVI์˜ ์ด์ (๋‹จ์ผ ์ƒํƒœ, ํƒ€์ž…์ด ์ง€์ •๋œ Intent)์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

2026๋…„ ๊ถŒ์žฅ ์‚ฌํ•ญ

์ƒํ™ฉ์— ๋”ฐ๋ผ ๋‘ ์•„ํ‚คํ…์ฒ˜ ์ค‘์—์„œ ์„ ํƒํ•˜๊ธฐ ์œ„ํ•œ ๊ถŒ์žฅ ์‚ฌํ•ญ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

Compose๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ƒˆ ํ”„๋กœ์ ํŠธ

์ฒ˜์Œ๋ถ€ํ„ฐ MVI๋ฅผ ๋„์ž…ํ•˜์‹ญ์‹œ์˜ค. Compose์™€ MVI๋Š” ๋™์ผํ•œ ์ฒ ํ•™์„ ๊ณต์œ ํ•˜๋ฉฐ, ์ดˆ๊ธฐ ํˆฌ์ž๋Š” ๋น ๋ฅด๊ฒŒ ํšŒ์ˆ˜๋ฉ๋‹ˆ๋‹ค.

๊ธฐ์กด View ๊ธฐ๋ฐ˜ ํ”„๋กœ์ ํŠธ

MVVM์„ ์œ ์ง€ํ•˜๋˜, MVI ๋ชจ๋ฒ” ์‚ฌ๋ก€๋ฅผ ์ ์ง„์ ์œผ๋กœ ๋„์ž…ํ•˜์‹ญ์‹œ์˜ค. ViewModel์˜ ๋‹จ์ผ ์ƒํƒœ, sealed class๋ฅผ ์‚ฌ์šฉํ•œ ํƒ€์ž…์ด ์ง€์ •๋œ ์•ก์…˜์ž…๋‹ˆ๋‹ค.

๋Œ€๊ทœ๋ชจ ํŒ€

ํ•˜๋‚˜์˜ ์ ‘๊ทผ ๋ฐฉ์‹์œผ๋กœ ํ‘œ์ค€ํ™”ํ•˜๊ณ  ๋ฌธ์„œํ™”ํ•˜์‹ญ์‹œ์˜ค. ์ฝ”๋“œ๋ฒ ์ด์Šค์˜ ์ผ๊ด€์„ฑ์ด ํŒจํ„ด ์„ ํƒ ์ž์ฒด๋ณด๋‹ค ๋” ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.

์ง„์ •ํ•œ ๊ธฐ์ค€

์ตœ์„ ์˜ ํŒจํ„ด์€ ํŒ€์ด ์ดํ•ดํ•˜๊ณ  ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ ์šฉํ•˜๋Š” ํŒจํ„ด์ž…๋‹ˆ๋‹ค. ์ž˜ ๊ตฌํ˜„๋œ MVVM์ด ์ž˜๋ชป ์ดํ•ด๋œ MVI๋ณด๋‹ค ๋‚ซ์Šต๋‹ˆ๋‹ค.

๊ฒฐ๋ก 

MVVM๊ณผ MVI๋Š” ๋ชจ๋‘ Android ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์•„ํ‚คํ…์ฒ˜ํ•˜๋Š” ๋ฐ ์œ ํšจํ•œ ์ ‘๊ทผ ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. MVVM์€ ๋‹จ์ˆœ์„ฑ๊ณผ ์นœ์ˆ™ํ•จ์„ ์ œ๊ณตํ•˜๊ณ , MVI๋Š” ์˜ˆ์ธก ๊ฐ€๋Šฅ์„ฑ๊ณผ ๋” ์‰ฌ์šด ๋””๋ฒ„๊น…์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

๊ฒฐ์ • ์ฒดํฌ๋ฆฌ์ŠคํŠธ

  • MVVM ์„ ํƒ ์กฐ๊ฑด: ์ฃผ๋‹ˆ์–ด ํŒ€, ๋‹จ์ˆœํ•œ ํ”„๋กœ์ ํŠธ, ๋น„์šฉ์ด ๋งŽ์ด ๋“œ๋Š” ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜
  • MVI ์„ ํƒ ์กฐ๊ฑด: ๋„ค์ดํ‹ฐ๋ธŒ Compose, ๋ณต์žกํ•œ ์ƒํƒœ, ์ค‘์š”ํ•œ ๋””๋ฒ„๊น… ํ•„์š”
  • ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์ ‘๊ทผ ๋ฐฉ์‹ ๊ถŒ์žฅ: ๋‹จ์ผ ์ƒํƒœ์˜ ๊ฒฝ๋Ÿ‰ MVI, ๊ณผ๋„ํ•œ ์—”์ง€๋‹ˆ์–ด๋ง ์—†์ด
  • ์ตœ์šฐ์„  ์‚ฌํ•ญ: ์ฝ”๋“œ๋ฒ ์ด์Šค ์ „๋ฐ˜์˜ ์ผ๊ด€์„ฑ

์—ฐ์Šต์„ ์‹œ์ž‘ํ•˜์„ธ์š”!

๋ฉด์ ‘ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ์™€ ๊ธฐ์ˆ  ํ…Œ์ŠคํŠธ๋กœ ์ง€์‹์„ ํ…Œ์ŠคํŠธํ•˜์„ธ์š”.

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

ํƒœ๊ทธ

#android
#mvvm
#mvi
#architecture
#jetpack compose

๊ณต์œ 

๊ด€๋ จ ๊ธฐ์‚ฌ

Android ๊ฐœ๋ฐœ์ž๋ฅผ ์œ„ํ•œ Jetpack Compose ๊ณ ๊ธ‰ ์• ๋‹ˆ๋ฉ”์ด์…˜

Jetpack Compose: ๊ณ ๊ธ‰ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋‹จ๊ณ„๋ณ„ ๊ฐ€์ด๋“œ

Compose ๊ณ ๊ธ‰ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์™„๋ฒฝ ๊ฐ€์ด๋“œ: ์ „ํ™˜, AnimatedVisibility, Animatable, ์ œ์Šค์ฒ˜, ๋ถ€๋“œ๋Ÿฌ์šด Android ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์œ„ํ•œ ์„ฑ๋Šฅ.

Android ๊ฐœ๋ฐœ์ž๋ฅผ ์œ„ํ•œ Jetpack Compose ๋ฉด์ ‘ ์งˆ๋ฌธ ๊ฐ€์ด๋“œ

Jetpack Compose ๋ฉด์ ‘ ์งˆ๋ฌธ 20์„  (2026๋…„)

Jetpack Compose ๋ฉด์ ‘์—์„œ ์ž์ฃผ ์ถœ์ œ๋˜๋Š” 20๊ฐ€์ง€ ์งˆ๋ฌธ์„ ํ•ด์„คํ•ฉ๋‹ˆ๋‹ค. ๋ฆฌ์ปดํฌ์ง€์…˜, ์ƒํƒœ ๊ด€๋ฆฌ, ์‚ฌ์ด๋“œ ์ดํŽ™ํŠธ, ๋‚ด๋น„๊ฒŒ์ด์…˜, ์„ฑ๋Šฅ ์ตœ์ ํ™”, ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด์„ ํฌ๊ด„์ ์œผ๋กœ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

Android Dependency Injection: Hilt vs Koin comparison guide

Android ์˜์กด์„ฑ ์ฃผ์ž…: Hilt vs Koin ์™„์ „ ๊ฐ€์ด๋“œ ๋ฐ ๋ฉด์ ‘ ์งˆ๋ฌธ 2026

Hilt์™€ Koin์„ ์ฝ”๋“œ ์˜ˆ์ œ, ์„ฑ๋Šฅ ๋ฒค์น˜๋งˆํฌ, ๋ฉด์ ‘ ์งˆ๋ฌธ์œผ๋กœ ์ฒ ์ €ํžˆ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค. Hilt 2.57, Koin 4.2 ๊ธฐ๋ฐ˜ 2026๋…„ ์ตœ์‹  ๊ฐ€์ด๋“œ.