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

공유

관련 기사