2026년 Jetpack Compose 면접 질문 Top 20

Jetpack Compose 면접에서 가장 자주 출제되는 20가지 질문: 리컴포지션, 상태 관리, 내비게이션, 성능, 아키텍처 패턴.

Android 개발자를 위한 Jetpack Compose 면접 질문 가이드

Jetpack Compose는 Android UI 개발의 표준 툴킷이 되었습니다. 기술 면접에서는 리컴포지션 메커니즘부터 상태 관리, 성능 최적화까지 Compose 숙련도를 정기적으로 테스트합니다. 다음은 가장 자주 출제되는 20가지 질문과 상세한 답변 및 코드 예제입니다.

가이드 활용 방법

각 질문에는 구조화된 답변과 코드 예제가 포함되어 있습니다. 질문은 난이도 순으로 구성되어 있습니다: 기초, 중급, 고급 순서입니다.

Jetpack Compose 기초

1. Compose와 XML 뷰 시스템의 차이점은 무엇입니까?

Compose는 선언적 패러다임을 사용합니다. UI는 상태의 함수로 기술되며, 프레임워크가 업데이트를 자동으로 처리합니다. 기존 XML 시스템은 명령적이며, findViewById 또는 View Binding을 통한 수동 뷰 조작이 필요합니다.

DeclarativeExample.ktkotlin
// Compose: UI updates automatically when count changes
@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }  // Reactive state
    Button(onClick = { count++ }) {               // UI declaration
        Text("Clicks: $count")                    // Recomposed automatically
    }
}

Compose에서는 TextView 참조를 찾아 수동으로 업데이트할 필요가 없습니다. 리컴포지션이 모든 것을 처리합니다.

2. 리컴포지션이란 무엇입니까?

리컴포지션은 상태가 변경될 때 Compose가 @Composable 함수를 다시 호출하는 프로세스입니다. 매개변수가 변경된 함수만 다시 실행되어 성능을 최적화합니다.

RecompositionExample.ktkotlin
@Composable
fun UserCard(name: String, age: Int) {
    Column {
        Text("Name: $name")   // Recomposed only if name changes
        Text("Age: $age")     // Recomposed only if age changes
        StaticBadge()          // Not recomposed if its inputs remain the same
    }
}

@Composable
fun StaticBadge() {
    Text("Static badge")  // Compose knows this function is stable
}

면접 핵심 포인트: 리컴포지션은 낙관적(Compose는 취소 가능하다고 가정)이며 순서가 정해지지 않음(Composable의 실행 순서가 보장되지 않음)입니다.

3. remember는 무엇을 합니까?

remember는 리컴포지션 간에 값을 보존합니다. remember 없이는 매 리컴포지션마다 변수가 초기값으로 재설정됩니다.

RememberExample.ktkotlin
@Composable
fun InputField() {
    // ✅ Value survives recompositions
    var text by remember { mutableStateOf("") }

    // ❌ Without remember, text resets to "" on every recomposition
    // var text by mutableStateOf("")

    TextField(
        value = text,
        onValueChange = { text = it },  // Triggers recomposition
        label = { Text("Enter text") }
    )
}

4. rememberrememberSaveable의 차이점은 무엇입니까?

remember는 리컴포지션 간에 값을 보존하지만 구성 변경(화면 회전) 시 잃어버립니다. rememberSaveableSavedInstanceState 메커니즘을 사용하여 구성 변경을 통해 값을 영속화합니다.

RememberSaveableExample.ktkotlin
@Composable
fun SearchBar() {
    // Lost after screen rotation
    var query by remember { mutableStateOf("") }

    // Preserved after screen rotation
    var savedQuery by rememberSaveable { mutableStateOf("") }

    TextField(
        value = savedQuery,
        onValueChange = { savedQuery = it },
        placeholder = { Text("Search...") }
    )
}

Compose에서의 상태 관리

5. 상태 호이스팅이란 무엇입니까?

상태 호이스팅은 상태를 Composable에서 부모로 이동하는 것을 의미합니다. 자식 Composable은 상태가 없는 상태가 됩니다: 상태를 매개변수로 받고 콜백을 통해 변경을 알립니다. 이 패턴은 재사용 가능하고 테스트 가능한 컴포넌트를 구축하는 데 필수적입니다.

StateHoistingExample.ktkotlin
// ✅ Stateless composable, easy to test and reuse
@Composable
fun EmailInput(
    email: String,                    // State provided by parent
    onEmailChange: (String) -> Unit,  // Callback to parent
    modifier: Modifier = Modifier
) {
    TextField(
        value = email,
        onValueChange = onEmailChange,
        label = { Text("Email") },
        modifier = modifier
    )
}

// Parent manages the state
@Composable
fun LoginForm() {
    var email by remember { mutableStateOf("") }
    EmailInput(
        email = email,
        onEmailChange = { email = it }  // Parent controls state
    )
}

이 패턴은 Compose의 기본이며 면접에서 자주 출제됩니다.

6. derivedStateOf는 어떻게 작동합니까?

derivedStateOf는 계산 결과가 변경될 때만 리컴포지션을 트리거하는 파생 상태를 생성합니다. 소스의 모든 수정에서가 아닙니다.

DerivedStateExample.ktkotlin
@Composable
fun FilteredList(items: List<String>) {
    var searchQuery by remember { mutableStateOf("") }

    // Recalculated only when the filtered result actually changes
    val filteredItems by remember(items) {
        derivedStateOf {
            items.filter { it.contains(searchQuery, ignoreCase = true) }
        }
    }

    Column {
        TextField(value = searchQuery, onValueChange = { searchQuery = it })
        LazyColumn {
            items(filteredItems) { item -> Text(item) }
        }
    }
}
derivedStateOf 사용 시기

이 메커니즘은 상태가 자주 변경되지만 파생 결과는 드물게 변경되는 경우에 유용합니다(예: 필터링된 목록, 폼 유효성에 따른 버튼 활성화/비활성화).

7. StateFlow와 Compose State<T>의 차이점은 무엇입니까?

StateFlow(Kotlin 코루틴)는 ViewModel에서 오는 반응형 스트림입니다. State<T>는 리컴포지션을 트리거하는 Compose 네이티브 메커니즘입니다. 실제로 StateFlowcollectAsStateWithLifecycle()을 통해 Composable에서 수집됩니다. 코루틴 플로우에 대한 자세한 내용은 Kotlin 코루틴 완전 가이드를 참조하십시오.

StateFlowExample.ktkotlin
class UserViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(UserUiState())
    val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
}

@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
    // Converts StateFlow to State<T> for Compose
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    Text("Hello, ${uiState.userName}")
}

collectAsState() 대신 collectAsStateWithLifecycle() 사용이 권장됩니다. 라이프사이클을 존중하고 화면이 더 이상 보이지 않으면 수집을 중지하기 때문입니다.

사이드 이펙트와 라이프사이클

8. Compose의 주요 사이드 이펙트는 무엇입니까?

사이드 이펙트를 통해 비Composable 코드(네트워크 호출, 로깅, 내비게이션)를 제어된 방식으로 실행할 수 있습니다. 주요 사이드 이펙트에는 LaunchedEffect, DisposableEffect, SideEffect, 그리고 Compose 1.12에서 도입된 새로운 Keyed SideEffect가 포함됩니다.

SideEffectsExample.ktkotlin
@Composable
fun AnalyticsScreen(screenName: String) {
    // LaunchedEffect: runs once when screenName changes
    LaunchedEffect(screenName) {
        analyticsTracker.logScreenView(screenName)  // Suspended call
    }

    // DisposableEffect: with cleanup (like useEffect with cleanup)
    DisposableEffect(Unit) {
        val listener = onScrollListener()
        scrollView.addListener(listener)
        onDispose {
            scrollView.removeListener(listener)  // Cleanup guaranteed
        }
    }

    // SideEffect: runs after every successful recomposition
    SideEffect {
        logger.log("Screen recomposed")  // Non-suspended code
    }
}

Compose 1.12 업데이트: Keyed SideEffect는 특정 키가 변경될 때마다 일회성 사이드 이펙트를 위한 키 인수를 지원하는 새로운 오버로드를 제공합니다. 공식 벤치마크에 따르면, 서스펜드되지 않는 작업의 경우 LaunchedEffect보다 최대 90% 더 빠를 수 있습니다.

9. LaunchedEffectrememberCoroutineScope는 언제 사용합니까?

LaunchedEffect는 컴포지션에 연결됩니다: Composable이 컴포지션을 벗어나거나 키가 변경되면 코루틴이 취소됩니다. rememberCoroutineScope는 사용자 제어 스코프를 제공하며, 사용자 트리거 액션(버튼 클릭)에 유용합니다.

CoroutineScopeExample.ktkotlin
@Composable
fun DataScreen(userId: String) {
    // ✅ LaunchedEffect: automatic loading tied to lifecycle
    LaunchedEffect(userId) {
        loadUserData(userId)  // Re-launched if userId changes
    }

    // ✅ rememberCoroutineScope: one-off user action
    val scope = rememberCoroutineScope()
    Button(onClick = {
        scope.launch { refreshData() }  // Triggered manually
    }) {
        Text("Refresh")
    }
}

Android 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

레이아웃과 고급 컴포넌트

10. LazyColumn은 어떻게 작동하며 RecyclerView와 어떻게 다릅니까?

LazyColumn은 Compose에서 RecyclerView에 해당합니다. 화면에 보이는 요소만 컴포즈하고 가시 창에서 스크롤 아웃된 Composable을 재활용합니다.

LazyColumnExample.ktkotlin
@Composable
fun UserList(users: List<User>) {
    LazyColumn(
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)  // Spacing between items
    ) {
        items(
            items = users,
            key = { it.id }  // Stable key to optimize recompositions
        ) { user ->
            UserCard(user)
        }
    }
}

중요한 포인트: 정렬이나 요소 제거 시 불필요한 리컴포지션을 피하려면 항상 안정적인 key 매개변수를 제공하십시오.

11. 커스텀 레이아웃은 어떻게 만듭니까?

Compose는 Layout 함수를 통해 커스텀 레이아웃을 생성할 수 있습니다. 이는 뷰 시스템의 커스텀 ViewGroup 구현을 대체합니다.

CustomLayoutExample.ktkotlin
@Composable
fun OverlappingRow(
    overlapOffset: Dp = (-16).dp,  // Negative offset for overlap
    content: @Composable () -> Unit
) {
    Layout(content = content) { measurables, constraints ->
        val placeables = measurables.map { it.measure(constraints) }
        val width = placeables.sumOf { it.width } + (overlapOffset.roundToPx() * (placeables.size - 1))
        val height = placeables.maxOf { it.height }

        layout(width, height) {
            var xOffset = 0
            placeables.forEach { placeable ->
                placeable.placeRelative(xOffset, 0)
                xOffset += placeable.width + overlapOffset.roundToPx()
            }
        }
    }
}

12. MaterialTheme으로 커스텀 테마를 구현하는 방법은?

Compose의 테마는 CompositionLocal에 의존합니다. MaterialTheme은 Composable 트리 전체에서 접근 가능한 색상, 타이포그래피, 셰이프 값을 제공합니다.

CustomThemeExample.ktkotlin
// Custom color definitions
private val DarkColorScheme = darkColorScheme(
    primary = Color(0xFF6200EE),
    secondary = Color(0xFF03DAC6),
    background = Color(0xFF121212)
)

@Composable
fun AppTheme(content: @Composable () -> Unit) {
    MaterialTheme(
        colorScheme = DarkColorScheme,
        typography = AppTypography,    // Custom typography
        content = content
    )
}

// Usage in a composable
@Composable
fun ThemedCard() {
    Card(colors = CardDefaults.cardColors(
        containerColor = MaterialTheme.colorScheme.surface  // Theme access
    )) {
        Text(
            text = "Content",
            style = MaterialTheme.typography.bodyLarge  // Theme typography
        )
    }
}

Compose에서의 내비게이션

13. Compose Navigation은 어떻게 작동합니까?

Compose Navigation은 직렬화 가능한 타입으로 선언된 루트를 가진 NavHost를 사용합니다(Navigation 2.8 이상). Navigation 2.9부터 라이브러리는 커스텀 NavType 구현 없이 값 클래스와 List<Enum>을 인수 타입으로 지원합니다.

NavigationExample.ktkotlin
@Composable
fun AppNavigation() {
    val navController = rememberNavController()

    NavHost(navController = navController, startDestination = "home") {
        composable("home") {
            HomeScreen(onNavigateToDetail = { id ->
                navController.navigate("detail/$id")  // Navigation with argument
            })
        }
        composable(
            route = "detail/{userId}",
            arguments = listOf(navArgument("userId") { type = NavType.StringType })
        ) { backStackEntry ->
            val userId = backStackEntry.arguments?.getString("userId") ?: ""
            DetailScreen(userId = userId)
        }
    }
}

14. 화면 간에 데이터를 전달하는 방법은?

단순 인수(String, Int)는 루트를 통해 직접 전달합니다. 복잡한 객체의 경우 현재 권장 사항은 공유 ViewModel을 사용하거나 식별자만 전달하고 대상 화면에서 데이터를 로드하는 것입니다. Navigation 2.9에서는 리스트 및 배열과 같은 컬렉션 기반 인수를 위한 CollectionNavType<T>가 도입되었습니다.

NavigationArgsExample.ktkotlin
// Type-safe navigation with Kotlin Serialization (Navigation 2.8+)
@Serializable
data class ProfileRoute(val userId: String, val tab: String = "info")

// Declaration
composable<ProfileRoute> { backStackEntry ->
    val route = backStackEntry.toRoute<ProfileRoute>()
    ProfileScreen(userId = route.userId, tab = route.tab)
}

// Navigation
navController.navigate(ProfileRoute(userId = "123", tab = "stats"))
피해야 할 안티패턴

루트에 복잡한 직렬화된 객체를 전달하지 마십시오. ID를 전달하고 대상 화면에서 ViewModel을 통해 데이터를 로드하게 하십시오. 아키텍처 가이드는 MVVM vs MVI: 어떤 아키텍처를 선택할 것인가를 참조하십시오.

성능과 최적화

15. 불필요한 리컴포지션을 방지하는 방법은?

불필요한 리컴포지션을 최소화하기 위한 세 가지 주요 전략:

PerformanceExample.ktkotlin
// 1. Use stable classes (data class with immutable properties)
@Stable  // Tells Compose this class is stable
data class UserState(
    val name: String,
    val avatar: String
)

// 2. Extract lambdas with remember
@Composable
fun OptimizedList(onItemClick: (String) -> Unit) {
    val stableCallback = remember(onItemClick) { onItemClick }
    LazyColumn {
        items(100) { index ->
            ItemRow(onClick = { stableCallback("item_$index") })
        }
    }
}

// 3. Use key() to help Compose identify elements
@Composable
fun UserTabs(users: List<User>) {
    Column {
        users.forEach { user ->
            key(user.id) {    // Stable identity
                UserRow(user)
            }
        }
    }
}

16. Compose 앱 성능을 프로파일링하는 방법은?

Android Studio의 Layout Inspector는 Composable별 리컴포지션 수를 표시합니다. Compose 1.13 alpha에서는 리컴포지션 무효화 흐름을 기록하기 위한 RecompositionTracer가 도입되었습니다. debugInspectorInfo 플래그도 진단에 도움이 됩니다.

ProfilingExample.ktkotlin
// Enable recomposition counters in debug
@Composable
fun DebugRecomposition(tag: String, content: @Composable () -> Unit) {
    val recompositionCount = remember { mutableIntStateOf(0) }

    SideEffect {
        recompositionCount.intValue++  // Incremented on every recomposition
        Log.d("Recomposition", "$tag: ${recompositionCount.intValue} times")
    }

    content()
}

// Usage
DebugRecomposition("UserCard") {
    UserCard(user)
}

추가로 Compose Compiler Metrics는 스킵 가능하고 재시작 가능한 함수 및 안정/불안정 클래스에 대한 상세 리포트를 생성합니다.

17. Modifier란 무엇이며 왜 중요합니까?

Modifier는 Composable의 외관과 동작을 수정하는 순서화된 명령 체인입니다. Modifier의 순서는 렌더링에 직접적인 영향을 미칩니다.

ModifierOrderExample.ktkotlin
@Composable
fun ModifierOrderDemo() {
    // ❌ Padding THEN background = padding not colored
    Text(
        text = "Hello",
        modifier = Modifier
            .padding(16.dp)
            .background(Color.Red)
    )

    // ✅ Background THEN padding = padding is colored
    Text(
        text = "Hello",
        modifier = Modifier
            .background(Color.Red)
            .padding(16.dp)
    )
}

모범 사례: 부모로부터의 커스터마이징을 허용하기 위해 재사용 가능한 Composable에서는 항상 modifier: Modifier = Modifier 매개변수를 받으십시오.

아키텍처와 고급 패턴

18. ViewModel로 Compose 화면을 구조화하는 방법은?

권장 패턴은 UI 상태를 data class에, 이벤트를 sealed interface에 분리하고, ViewModel이 비즈니스 로직을 처리합니다.

ScreenArchitectureExample.ktkotlin
// UI State
data class ProfileUiState(
    val user: User? = null,
    val isLoading: Boolean = false,
    val error: String? = null
)

// User events
sealed interface ProfileEvent {
    data object Refresh : ProfileEvent
    data class UpdateName(val name: String) : ProfileEvent
}

// ViewModel
class ProfileViewModel(private val repo: UserRepository) : ViewModel() {
    private val _uiState = MutableStateFlow(ProfileUiState(isLoading = true))
    val uiState = _uiState.asStateFlow()

    fun onEvent(event: ProfileEvent) {
        when (event) {
            is ProfileEvent.Refresh -> loadProfile()
            is ProfileEvent.UpdateName -> updateName(event.name)
        }
    }
}

// Compose screen
@Composable
fun ProfileScreen(viewModel: ProfileViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    ProfileContent(
        uiState = uiState,
        onEvent = viewModel::onEvent  // Event delegation
    )
}

19. Composable을 테스트하는 방법은?

Compose는 UI 테스트를 위한 ComposeTestRule과 시맨틱 어설션을 포함한 테스트 라이브러리를 제공합니다. Compose 1.11부터 v2 테스트 API가 기본값이 되었으며, v1은 더 이상 사용되지 않습니다. 핵심 변경 사항은 UnconfinedTestDispatcher(즉시 실행)에서 StandardTestDispatcher(대기열 실행)로의 전환이며, 이제 테스트가 프로덕션 동작을 더 잘 반영합니다.

ComposableTestExample.ktkotlin
@get:Rule
val composeTestRule = createComposeRule()

@Test
fun counter_incrementsOnClick() {
    composeTestRule.setContent {
        Counter()  // The composable under test
    }

    // Verify initial state
    composeTestRule.onNodeWithText("Clicks: 0").assertIsDisplayed()

    // Simulate a click
    composeTestRule.onNodeWithText("Clicks: 0").performClick()

    // With v2 APIs, advance the clock explicitly
    composeTestRule.waitForIdle()

    // Verify new state
    composeTestRule.onNodeWithText("Clicks: 1").assertIsDisplayed()
}

상태가 없는 Composable의 단위 테스트의 경우, 표준 JUnit/Turbine 테스트로 ViewModel을 별도로 테스트하는 것이 더 효율적인 경우가 많습니다.

20. Compose를 기존 XML 기반 앱에 통합하는 방법은?

상호 운용성은 양방향입니다: ComposeView는 XML 내에 Compose를 삽입하고, AndroidView는 Compose 내에서 클래식 뷰를 사용합니다.

InteropExample.ktkotlin
// Compose in XML (in a Fragment or Activity)
class ProfileFragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        return ComposeView(requireContext()).apply {
            setViewCompositionStrategy(
                ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
            )
            setContent {
                AppTheme { ProfileScreen() }
            }
        }
    }
}

// XML View in Compose
@Composable
fun LegacyMapView() {
    AndroidView(
        factory = { context -> MapView(context).apply { onCreate(null) } },
        update = { mapView -> mapView.getMapAsync { /* config */ } }
    )
}
권장 마이그레이션 전략

가장 간단한 화면부터 시작하여 화면별로 마이그레이션합니다. 모든 새 화면은 완전히 Compose로 구축하고, 기존 화면은 점진적으로 마이그레이션합니다.

Android 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

소스

Jetpack Compose 면접 핵심 포인트

이 20가지 질문은 Jetpack Compose 면접을 위해 모든 Android 개발자가 마스터해야 하는 기초를 다룹니다. 실습을 위해 Jetpack Compose 면접 질문 모듈을 시도해 보십시오.

  • ✅ 리컴포지션과 그 낙관적 동작을 이해한다
  • remember, rememberSaveable, derivedStateOf를 마스터한다
  • ✅ 상태 호이스팅을 체계적으로 적용한다
  • ✅ 사이드 이펙트를 이해한다(LaunchedEffect, DisposableEffect, SideEffect, 새로운 Keyed SideEffect)
  • ✅ 성능을 최적화한다(안정 클래스, 키, 람다)
  • ✅ ViewModel + UiState + Events로 화면을 구조화한다
  • ✅ v2 테스트 API를 사용하여 ComposeTestRule로 Composable을 테스트한다
  • ✅ Compose/Views 상호 운용성을 처리한다

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

오늘의 챌린지

Android 코드의 버그를 찾을 수 있나요

실제 코드 한 조각, 숨은 버그 하나, 하루 한 번. 계정 없이 바로 도전할 수 있습니다.

Anthony Fillion-Maillet

작성자

Anthony Fillion-Maillet

SharpSkill 창업자

10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.

2026년 8월 20일 업데이트

태그

#jetpack compose
#android
#interview
#kotlin
#ui

공유

관련 기사