다양한 디스플레이 크기 지원

다양한 디스플레이 크기를 지원하면 최대한 많은 사용자가 다양한 기기에서 앱에 액세스할 수 있습니다.

다양한 기기 화면 또는 멀티 윈도우 모드의 여러 앱 창 등 최대한 많은 디스플레이 크기를 지원하려면 앱 레이아웃을 반응형 및 적응형으로 디자인합니다. 반응형/적응형 레이아웃은 디스플레이 크기와 관계없이 최적화된 사용자 환경을 제공하여 앱이 스마트폰과 태블릿, 폴더블, ChromeOS 기기, 세로 및 가로 모드, 크기 조절 가능한 디스플레이 구성(예: 스크린 분할 모드 및 데스크톱 창)을 수용할 수 있습니다.

반응형/적응형 레이아웃은 사용 가능한 디스플레이 공간에 따라 변경됩니다. 변경사항은 공간을 채우는 작은 레이아웃 조정 (반응형 디자인)부터 앱이 다양한 디스플레이 크기를 가장 잘 수용할 수 있도록 한 레이아웃을 다른 레이아웃으로 완전히 대체하는 것 (적응형 디자인)까지 다양합니다.

Jetpack Compose는 선언형 UI 도구 키트로, 다양한 디스플레이 크기에 맞게 콘텐츠를 다르게 렌더링하도록 동적으로 변경되는 레이아웃을 설계하고 구현하는 데 이상적입니다.

콘텐츠 수준 컴포저블의 대규모 레이아웃 변경을 명시하기

앱 수준 및 콘텐츠 수준 컴포저블은 앱에서 사용 가능한 모든 디스플레이 공간을 차지합니다. 이러한 유형의 컴포저블의 경우 대형 디스플레이에서 앱의 전체 레이아웃을 변경하는 것이 좋습니다.

레이아웃을 결정할 때 실제 하드웨어 값을 사용하지 마세요. 실재하는 고정된 값을 기반으로 결정하고 싶을 수 있지만 (기기가 태블릿인가요? 실제 화면의 가로세로 비율이 일정한가요?), 이러한 질문에 대한 답변은 UI가 사용할 수 있는 공간을 결정하는 데 유용하지 않을 수 있습니다.

그림 1. 휴대전화, 폴더블, 태블릿, 노트북 폼 팩터

태블릿에서는 앱이 멀티 윈도우 모드에서 실행될 수 있으며, 이는 앱이 다른 앱과 화면을 분할하고 있을 수 있다는 의미입니다. 데스크톱 창 모드 또는 ChromeOS에서는 앱이 크기 조절이 가능한 창에서 실행될 수 있습니다. 폴더블 기기와 같이 실제 화면이 두 개 이상 있을 수도 있습니다. 이러한 경우는 모두 실제 화면 크기와 콘텐츠 표시 방법을 결정하는 것 사이에 아무 관련이 없습니다.

대신 Jetpack WindowManager 라이브러리에서 제공하는 현재 창 측정항목으로 설명된 앱에 할당된 화면의 실제 부분을 기반으로 결정해야 합니다. Compose 앱에서 WindowManager를 사용하는 방법의 예는 JetNews 샘플을 참고하세요.

레이아웃을 사용 가능한 디스플레이 공간에 맞게 조정하면 ChromeOS와 같은 플랫폼과 태블릿 및 폴더블과 같은 폼 팩터를 지원하는 데 필요한 특수 처리의 양도 줄어듭니다.

앱에 사용할 수 있는 공간의 측정항목을 결정한 후에는 창 크기 클래스 사용에 설명된 대로 원시 크기를 창 크기 클래스로 변환합니다. 창 크기 클래스는 앱 로직 단순성과 대부분의 디스플레이 크기에 맞게 앱을 최적화하는 유연성 사이에서 균형을 이루도록 설계된 중단점입니다. 창 크기 클래스는 앱의 전체 창을 참조하므로 전체 앱 레이아웃에 영향을 주는 레이아웃 결정에 이 클래스를 사용합니다. 창 크기 클래스를 상태로 전달하거나 추가 로직을 실행하여 중첩된 컴포저블에 전달하기 위한 파생 상태를 만들 수 있습니다.

@Composable
fun MyApp(
    windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
) {
    // Perform logic on the size class to decide whether to show the top app bar.
    val showTopAppBar = windowSizeClass.windowHeightSizeClass != WindowHeightSizeClass.COMPACT

    // MyScreen knows nothing about window sizes, and performs logic based on a Boolean flag.
    MyScreen(
        showTopAppBar = showTopAppBar,
        /* ... */
    )
}

계층화된 접근 방식은 디스플레이 크기 로직을 동기화를 유지해야 하는 앱의 여러 위치에 분산하는 대신 단일 위치로 제한합니다. 단일 위치는 상태를 생성하며, 이 상태는 다른 앱 상태와 마찬가지로 다른 컴포저블에 명시적으로 전달될 수 있습니다. 명시적으로 상태를 전달하면 개별 컴포저블이 간소화됩니다. 컴포저블은 창 크기 클래스 또는 다른 데이터와 함께 지정된 구성을 사용하기 때문입니다.

유연한 중첩 컴포저블은 재사용 가능함

컴포저블은 다양한 위치에 배치할 수 있을 때 더 많이 재사용할 수 있습니다. 컴포저블을 특정 크기로 특정 위치에 배치해야 하는 경우 컴포저블을 다른 컨텍스트에서 재사용할 수 없을 가능성이 큽니다. 즉, 재사용 가능한 개별 컴포저블은 전역 디스플레이 크기 정보에 암시적으로 종속되는 것을 피해야 합니다.

목록 세부정보 레이아웃을 구현하는 중첩된 컴포저블이 있고 이 컴포저블이 하나의 창 또는 두 개의 창을 나란히 표시한다고 가정해 보세요.

두 창을 나란히 표시하는 앱
그림 2. 일반적인 목록-세부정보 레이아웃을 보여주는 앱. 1은 목록 영역이고 2는 세부정보 영역입니다.

목록-세부정보 결정은 앱의 전체 레이아웃의 일부여야 하므로 콘텐츠 수준 컴포저블에서 결정이 전달됩니다.

@Composable
fun AdaptivePane(
    showOnePane: Boolean,
    /* ... */
) {
    if (showOnePane) {
        OnePane(/* ... */)
    } else {
        TwoPane(/* ... */)
    }
}

대신 컴포저블이 사용 가능한 디스플레이 공간에 따라 독립적으로 레이아웃을 변경하도록 하려면 어떻게 해야 할까요? 예를 들어 공간이 허용되는 경우 추가 세부정보를 표시하는 카드가 있습니다. 사용 가능한 일부 디스플레이 크기를 기반으로 로직을 실행하려고 하지만 구체적으로 어떤 크기일까요?

그림 3. 아이콘과 제목만 보여주는 좁은 카드와 아이콘, 제목, 짧은 설명을 보여주는 더 넓은 카드

기기의 실제 화면 크기를 사용하지 마세요. 이 작업은 다양한 유형의 화면에서 정확하지 않을 수 있으며 앱이 전체 화면이 아닌 경우에도 정확하지 않을 수 있습니다.

이 컴포저블은 콘텐츠 수준 컴포저블이 아니므로 현재 창 측정항목을 직접 사용하지 마세요. 구성요소가 패딩과 함께 배치되거나 (예: 인셋) 앱에 탐색 레일 또는 앱 바와 같은 구성요소가 포함된 경우 컴포저블에 사용할 수 있는 디스플레이 공간의 양은 앱에 사용할 수 있는 전체 공간과 크게 다를 수 있습니다.

컴포저블이 컴포저블 자체를 렌더링하는 데 실제로 제공되는 너비를 사용합니다. 이 너비를 얻는 방법은 두 가지가 있습니다.

  • 콘텐츠가 표시되는 위치 또는 방법을 변경하려면 수정자 모음 또는 맞춤 레이아웃을 사용하여 레이아웃을 반응형으로 만드세요. 이는 하위 요소가 사용 가능한 공간을 모두 채우도록 하거나, 충분한 공간이 있는 경우 여러 개의 열로 하위 요소를 배치하는 작업만큼 간단한 작업일 수 있습니다.

  • 표시되는 내용을 변경하려면 더 강력한 대안으로 BoxWithConstraints를 사용하세요. BoxWithConstraints는 사용 가능한 디스플레이 공간에 따라 다양한 컴포저블을 호출하는 데 사용할 수 있는 측정 제약 조건을 제공합니다. 하지만 이는 알려진 제약 조건이 있을 때 BoxWithConstraints가 레이아웃 단계까지 구성을 지연하여 레이아웃 중에 더 많은 작업이 진행되도록 하므로 비용이 발생합니다.

@Composable
fun Card(/* ... */) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(/* ... */)
                Title(/* ... */)
            }
        } else {
            Row {
                Column {
                    Title(/* ... */)
                    Description(/* ... */)
                }
                Image(/* ... */)
            }
        }
    }
}

모든 데이터를 다양한 디스플레이 크기로 사용할 수 있는지 확인

추가 디스플레이 공간을 활용하는 컴포저블을 구현할 때는 효율적으로 구현하고 싶고 데이터를 현재 디스플레이 크기의 부작용으로 로드하려는 경향이 있을 수 있습니다.

그러나 이렇게 하면 데이터를 끌어올려 컴포저블에 제공하여 적절하게 렌더링할 수 있는 단방향 데이터 흐름의 원칙에 어긋납니다. 콘텐츠의 일부가 항상 사용되지는 않더라도 모든 디스플레이 크기에 맞는 충분한 콘텐츠가 컴포저블에 항상 있도록 충분한 데이터가 컴포저블에 제공되어야 합니다.

@Composable
fun Card(
    imageUrl: String,
    title: String,
    description: String
) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(imageUrl)
                Title(title)
            }
        } else {
            Row {
                Column {
                    Title(title)
                    Description(description)
                }
                Image(imageUrl)
            }
        }
    }
}

Card 예를 기반으로, 항상 descriptionCard에 전달됩니다. 너비가 설명을 표시하도록 허용하는 경우에만 description을 사용하더라도 사용할 수 있는 너비에 상관없이 Card는 항상 description을 요구합니다.

항상 충분한 콘텐츠를 전달하면 레이아웃을 덜 스테이트풀하게 만들어서 적응형 레이아웃을 더 간단하게 만들 수 있고 디스플레이 크기 간에 전환 시 창 크기 조절, 방향 변경 또는 기기를 접거나 펼치는 동작으로 인해 발생할 수 있는 부작용이 발생하지 않습니다.

또한, 이 원칙을 사용하면 레이아웃을 변경해도 상태를 유지할 수 있습니다. 일부 디스플레이 크기에서 사용되지 않을 수도 있는 정보를 호이스팅하면 레이아웃 크기가 변경될 때 앱 상태를 유지할 수 있습니다. 예를 들어 디스플레이 크기 조절로 인해 레이아웃이 콘텐츠 숨기기와 표시하기 간에 전환될 때 앱 상태가 유지되도록 showMore 불리언 플래그를 호이스팅할 수 있습니다.

@Composable
fun Card(
    imageUrl: String,
    title: String,
    description: String
) {
    var showMore by remember { mutableStateOf(false) }

    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(imageUrl)
                Title(title)
            }
        } else {
            Row {
                Column {
                    Title(title)
                    Description(
                        description = description,
                        showMore = showMore,
                        onShowMoreToggled = { newValue ->
                            showMore = newValue
                        }
                    )
                }
                Image(imageUrl)
            }
        }
    }
}

자세히 알아보기

Compose의 적응형 레이아웃에 관한 자세한 내용은 다음 리소스를 참고하세요.

샘플 앱

  • CanonicalLayouts는 대형 디스플레이에서 최적의 사용자 환경을 제공하는 검증된 디자인 패턴의 저장소입니다.
  • JetNews는 가용 디스플레이 공간을 활용할 수 있도록 UI를 조정하는 앱을 디자인하는 방법을 보여줍니다.
  • Reply는 모바일, 태블릿, 폴더블을 지원하는 적응형 샘플입니다.
  • Now in Android는 적응형 레이아웃을 사용하여 다양한 디스플레이 크기를 지원하는 앱입니다.

동영상