태블릿 화면 지원하기

K_Gs·2025년 3월 23일
5
post-thumbnail

문제 인식

이번에 개발중인 요약쏙은 사용자가 PDF를 올리면 해당 PDF를 요약해주고 문제를 만들어서 제공해주는 앱이에요. 현재는 Android로 개발하고 있지만, 추후 iOS, 데스크탑앱으로도 제공할 계획을 하고 있죠!

열심히 개발하던 중 파일 업로드 기능을 서버와 연결하고 테스트하던 날이였어요.

실제로 파일을 업로드 해야했기에 제가 대학교에서 썼던 PDF 파일을 찾는데 잘 보이질 않는거에요. 왜 없지..? 하던 도중 떠올리게 되었습니다

아 PDF는 다 태블릿이나 컴퓨터에 있구나!

생각해보니 저도, 주변의 다른 사람들도 공부를 할 때 보통 노트북 혹은 태블릿으로 PDF를 보고, 핸드폰으로는 앞에 말한 두가지를 쓰기 어려운 경우에만 사용했었습니다.

이 생각에 이어서 그렇다면 우리 앱은 태블릿에서는 잘 실행될까? 라는 생각이 들기 시작했어요.

태블릿도 Android 운영체제라면 문제 없이 앱을 실행시킬 수 있었지만, 앱이 켜진다사용할 수 있다는 별개이기에 실제 태블릿으로 테스트를 해보았어요.

확인 결과, 사용에 큰 문제는 없었지만 태블릿은 일반 스마트폰 보다 훨씬 큰 화면이기에 너무 비어보여 다른 정보를 더 표시해도 괜찮을 것 같다는 생각이 들었어요.

이를 위해 태블릿 만의 화면을 구현하여 제공하기로 하였습니다.

접근 방식

일단 시작에 앞서 다른 앱들에서는 태블릿 화면을 어떻게 지원하고 있는지 살펴보았어요

대부분의 앱에서 스마트폰일때의 화면과 크게 다르지 않았지만, 일부 앱에서 다른 구조를 발견했습니다.

카카오톡 시계 앱

카카오톡의 경우 왼쪽에 리스트가 고정되어있고, 항목을 눌렀을때 오른쪽에 상세정보(채팅방)이 보이는 구조였어요.

시계의 경우 리스트가 있을때 화면의 비율에 따라 한줄의 아이템의 개수를 1~3개로 조정하는 모습이 보였어요.

저희 앱의 경우 카카오톡 처럼 리스트가 있고, 눌렀을때 상세정보가 보이는 구조여서 카카오 톡처럼 적용할 수도, 리스트의 항목이 복잡하지 않기에 시계와 같이 한줄에 보이는 항목의 개수를 조정할 수도 있었어요.

하지만 리스트에 드래그 앤 드랍 기능이 존재하여서 항목의 개수를 조정하게 된다면 너무 큰 수정이 일어나야했기에 카카오톡과 같은 방식으로 적용하게 되었습니다.


기존 화면을 태블릿 화면으로 바꾸는 것은 간단하게 기존 리스트를 화면 전체의 1/3에 배치하고 상세 정보를 눌렀을때 화면을 나머지 2/3에 배치하기로 하였습니다.

홈화면 리스트 상세정보 화면
태블릿 화면

구현

이후에는 바로 구현에 들어갔어요.

가장 먼저 해당 구조와 같이 1 : 2로 표현하는 경우 화면이 세로라면 오히려 기존보다 더 좁아져 불편함이 클 것이라 생각하고 대형화면의 기준인 600dp 부터는 자동으로 가로모드로 변경되게 해주었어요.

//MainActivity.kt
if (resources.configuration.smallestScreenWidthDp >= 600) {
    requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}

다음으로는 홈화면에서 기존 ListfillMaxWidth를 적용한 Row로 감싸고 weight(1f)를 주었어요.
이렇게 하게되면 만약 Row안에 리스트를 제외한 아이템이 없다면 전체를 차지하게 되고, 다른 아이템이 있다면 weight 비율에 따라 차지하게 돼요.

//HomeScreen.kt
Row(modifier = Modifier.fillMaxSize()) {
	Scaffold(
        modifier = Modifier.weight(1f),
        //....

그리고 리스트 내부 아이템의 클릭이벤트에 만약 dp가 600미만 이라면 기존과 같이 상세정보 화면으로 이동을 하고, 600 이상이라면 focusedDocument라는 상세정보를 옆쪽에 보여주기 위한 값을 설정하도록 하였어요.

onFileClick = { documentId ->
    if(screenWidthDp < 600) {
	    navigateToGetSummary(documentId)
    }else{
        focusedDocument = documentId
	}
},

이제 가장 중요한! 상세 정보를 dp 따라 Row에 선택적으로 추가하게 하면 됩니다. 이때 focusedItem이 없어도 공간은 차지하고, 있을때만 상세정보 화면이 보여지도록 하였어요.

if(screenWidthDp < 600) return@Row
Box(
    modifier = Modifier
        .fillMaxHeight()
        .weight(2f)
) {
    if (focusedDocument == null) return@Box
    GetSummaryScreen(
    	//focusedDocument 이용
    )

이렇게 해서 구현이 완료되었지만..! 몇가지 문제가 있었어요.

트러블 슈팅

네비게이션 문제

현재 요약쏙 앱의 Screen들은 네비게이션이동, 뷰모델관련 코드들을 전부 네비게이션 그래프에서 제공 받아요.

즉, 해당 구조에서는 상세정보인 GetSummaryScreen만 생성해서는 다른 관련 화면(문제 풀이)등으로 이동을 할 수 없고, 상태도 제공받을 수 없었어요.

이를 해결하기 위해 관련 코드(뷰모델, 상태)등을 생성하더라도, GetSummaryScreen만 생성한 경우는 네비게이션 그래프에 포함된 것으로 판정되지 않아, 다른 화면으로 이동할 수 없었습니다.

그렇기에 기존과 같은 기능을 하기위해서는 저 네비게이션 그래프를 그대로 이용해야 했어요.

방법을 고민하 새로운 네비게이션 그래프를 생성하기로 결정하였어요.

if (focusedDocument == null) return@Box

val navController: NavHostController = rememberNavController()
val navActions: ForeverNavActions = remember(navController) {
    ForeverNavActions(navController)
}

NavHost(navController = navController, startDestination = Screen.Detail.route) {
    detailGraph(navController = navController, navActions = navActions)
}

기존 네비게이션 그래프에서 브랜치를 생성하는 느낌인거죠!

단, 이렇게 하였을 때는 생성만 된 것이고, navArgs를 통한 정보가 제공되지 않은 상태이기에 LauncedEffect를 통해 컴포지션이 완료된 후 원래 보여져야할 문서의 Id를 넘겨주었어요.

LaunchedEffect(Unit) {
    navController.navigate(Screen.GetSummary.createRoute(focusedDocument!!)) {
        popUpTo(navController.graph.startDestinationId) {
            inclusive = true
        }
    }
}

이렇게 해서 문제 하나를 해결하였어요!

BackHandler문제

다음으로는 조금 특이한 이슈가 생겼어요,

원래는 뒤로가기 하였을때 FocusedDocument를 제거하기 위한 목적으로 BackHandler를 생성해서 상세 정보 컴포넌트에 달아두었어요.

BackHandler(enabled = focusedItem!=null){
	focusedItem = null
}

제가 생각한 로직은 다음과 같은거죠!

  • 만약 focusedItem이 있다면 제거된다. (상세정보 컴포넌트)
  • 아니라면 List의 폴더에서 빠져나오거나 앱을 종료한다(리스트 컴포넌트)

하지만 이상하게 아래와 같은 로직으로 동작하였어요.

  • focusItem이 설정되어있을때
    • 누른 이후 깊이 2 이상 더 들어가면 폴더에서 빠져나오는게 우선된다.
    • 아니라면 focusedItem이 제거된다.

또한 드래그 앤 드랍에서 탑 앱바에 닿았을때 BackDisptcher를 통해 뒤로가기를 호출하다보니, 깊이 2 미만이라면 아이템이 탑 앱바에 닿으면 상세정보 화면이 사라졌어요.


이유를 분석해보니 BackHandler의 우선순위에 따른 문제였어요.

BackHandler는 나중에 선언될수록 더 높은 우선순위를 지녀요.
즉, 활성화 여부와 관계없이 코드 생성 순서상 가장 마지막에 생긴 BackHandler가 가장 먼저 호출돼요.

상세정보 컴포넌트는 focusedItem이 존재여부에 따라 그때 즉시 생성되기에 BackHandler도 그때 마지막에 등록이 되는거죠

  1. 리스트(깊이 1)의 백핸들러(비활성, 우선순위 1) 생성
  2. 파일을 눌러 상세정보 생성 -> 백핸들러(활성, 우선순위 2) 생성
  3. 폴더를 눌러 깊이 2 리스트 생성 -> 깊이 1 리스트의 백핸들러 활성, 깊이 2의 백핸들러 생성(비활성, 우선순위 3)

여기서 뒤로가기를 누른다면 비활성인 3은 스킵, 2가 이벤트를 받아 상세정보 제거

  1. 폴더를 눌러 깊이 3 리스트 생성 -> 깊이 2 리스트의 백핸들러 활성, 깊이 3의 백핸들러 생성(비활성, 우선순위 4)

여기서 뒤로가기를 누른다면 비활성인 4는 스킵, 3이 이벤트를 받아, 폴더에서 빠져나감

이런 로직이 됩니다. 즉 깊이 1에서 상세정보를 생성한 후 , 깊이 3까지 가면 폴더가 활성화 되어 문제가 생기는 것이였죠!

상상도 못한 버그에 당황했지만, 일단 원인을 파악했으니 수정에 들어갔어요.

처음에는 focusedItem의 설정 여부를 넘겨, 설정되어있다면 리스트의 모든 백핸들러를 비활성화하고자 하였지만 그럼 리스트의 드래그앤 드랍에서 상위폴더로 이동할 수 없는 문제가 생겨 적용할 수 없었어요.

다음으로는 리스트의 상위폴더 이동 로직을BackHandler에서 다른 걸로 변경하려 하였지만, 그럼 뒤로가기를 통한 상위 폴더로 이동또한 막혀 문제가 생겼어요.

결국 최종적으로 뒤로가기를 통해 focusedItem을 제거하는 것으로 생기는 기능적 이점에 비해 수정 비용이 더 크다 판단하여 해당 기능을 넣지 않는 것으로 결정하게 되었어요.

배운 점

  • 큰 화면(UI) 고려

스마트폰에서 잘 돌아가는 것과 태블릿에서 진짜 ‘편하게 쓸 수 있는가’는 다른 문제라는 걸 알게 됐어요.

분명 다른 앱들도 태블릿에서 쓰는 일이 존재할텐데, 앞으로도 실기기를 통한 태블릿 테스트도 진행해야겠다 느꼈습니다.

  • BackHandler 우선순위

이건 정말 예상 못 했던 부분인데, 그전에는 백핸들러를 많이 사용하지 않기도 하였고, 정적인 컴포넌트를 많이 사용하여 백핸들러의 동작이 이런 구조라는 것을 전혀 알지 못하였어요. 이번 구현 덕분에 동적 컴포넌트 + BackHandler 조합은 생각보다 훨씬 복잡하다는 것과 우선 순위를 생각하는게 중요하다는 것을 깨달았어요.

  • 기능 구현과 구현 비용

뒤로가기로 상세정보를 닫는 UX가 있으면 편하긴 한데, 이 기능 추가를 위해 수정해야하는 코드가 너무 많아, 기능 구현의 이점보다 수정 비용이 커 기능을 넣지 않기로 하였어요.

바람직하진 않다 생각하지만, 현재는 이 기능보다 우선 순위가 높은 구현해야할 기능이 있기도하고, 후에 리스트 컴포넌트의 리팩토링이 예정되어있어 그때 같이 고려할 예정입니다.

다만 그래도 구현을 할때 좀 더 각각의 기능을 이해하고 구조를 확장성 있게 짜야할 것 같아요!

profile
아직도 모르는게 많으니, 알아가고 싶은 것도 많다

2개의 댓글

comment-user-thumbnail
2025년 3월 23일

우와 태블릿 모드 구현까지 대단하네요! 앱 나오면 다운받아볼래요!!

답글 달기
comment-user-thumbnail
2025년 3월 23일

root쌤은 문제해결과정이 항상 인상깊습니다 👍

답글 달기