Adaptive Layout: 핵심 개념, 사이즈 클래스 및 한정자

저자: IT Sectr 게시일: 2026-02-26 읽는 시간: 10 분

Adaptive Layout은 애플리케이션이 다양한 기기 유형과 화면 방향에 따라 다른 레이아웃을 사용하는 인터페이스 레이아웃 접근 방식입니다. 반응형 디자인과 달리 adaptive layout은 중단점에서 미리 준비된 레이아웃 간에 전환합니다. 이 글에서는 iOS의 Size Classes 및 UITraitCollection, Android의 sw600dp/layout-w600dp 한정자와 실용적인 예제를 다룹니다.

주요 포인트

  • Adaptive Layout — 화면 크기, 방향 및 기기 유형에 따른 개별 레이아웃 전환
  • Size Classes iOS에서 공간을 가로 및 세로로 Compact 또는 Regular로 분류
  • UITraitCollection — iOS 환경 특성 컨테이너 (크기, 배율, force touch, 색영역)
  • sw600dp — 태블릿(7\" 이상)용 Android 최소 너비 한정자
  • layout-w600dp — 현재 방향에서 충분한 공간이 있을 때 활성화되는 사용 가능 너비 한정자

Adaptive Layout이란?

Adaptive Layout은 개발자가 각 기기 유형에 대해 별도의 레이아웃을 만들고 화면 특성에 따라 전환하는 UI 전략입니다. 요소가 부드럽게 흐르는 반응형 디자인과 달리 adaptive layout은 개별 전환을 사용합니다. 휴대폰 — 하나의 레이아웃, 태블릿 — 다른 레이아웃, 데스크톱 — 세 번째 레이아웃입니다.

Apple은 iOS 8(2014)에서 유니버설 앱(iPhone과 iPad 모두에서 실행되는 바이너리)과 함께 Size Classes를 도입했습니다. Google은 Android 3.2(2011)에서 smallestWidth 한정자(swdp)를 발표했습니다. 두 메커니즘 모두 동일한 문제를 해결합니다: 개발자에게 코드 중복 없이 다양한 화면에 맞는 다른 UI를 만들 수 있는 도구를 제공합니다.

최신 Apple HIG 및 Google Material Design 가이드라인은 여러 화면 크기를 지원하는 앱에 adaptive layout이 필수적이라는 데 동의합니다. Google Play는 태블릿 적응 부족을 단점으로 표시합니다. App Store에는 공식 요구 사항이 없지만 iPad 사용자는 늘어난 iPhone 앱이 아닌 네이티브 경험을 기대합니다.

iOS Size Classes 및 UITraitCollection

Size Classes는 각 축에서 사용 가능한 공간을 Compact(컴팩트) 또는 Regular(레귤러)로 정의하는 iOS 특성입니다. UITraitCollection은 뷰 계층 구조를 통해 환경 특성을 전달하는 iOS 시스템입니다. 크기, 화면 배율, force touch 기능, 색영역(라이트/다크), 접근성 설정 등이 포함됩니다.

기기 / 방향가로세로
iPhone 세로(전 모델)CompactRegular
iPhone Plus/Pro Max 가로RegularCompact
iPhone SE / mini 가로CompactCompact
iPad 세로RegularRegular
iPad 가로RegularRegular
iPad Split View(화면 1/3)CompactRegular

UITraitCollection은 시스템에 의해 형성되며 UIApplication에서 UIWindow를 통해 각 UIView로 전달됩니다. 방향이나 창 크기(Split View)가 변경되면 시스템이 새 UITraitCollection을 생성하고 traitCollectionDidChange를 호출합니다. SwiftUI에서는 변경 사항이 Environment Values를 통해 추적되고 뷰가 자동으로 다시 그려집니다.

Swift 적응형 레이아웃 예제

UIKit에서 UITraitCollection을 통한 적응은 trait collection 변경을 구독하고 레이아웃을 재구성하여 구현됩니다. 아래 예제는 iPhone용 수직 스택과 iPad용 분할 레이아웃 간에 전환하는 컨트롤러를 보여줍니다.

swift
// Size Classes를 사용한 적응형 컨트롤러
import UIKit

final class AdaptiveViewController: UIViewController {

    private let compactStack = UIStackView()
    private let regularStack = UIStackView()
    private let sidebar = UIView()
    private let content = UIView()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupViews()
        updateLayout(for: traitCollection)
    }

    override func traitCollectionDidChange(
        _ previousTraitCollection: UITraitCollection?
    ) {
        super.traitCollectionDidChange(previousTraitCollection)
        if traitCollection.horizontalSizeClass
            != previousTraitCollection?.horizontalSizeClass {
            updateLayout(for: traitCollection)
        }
    }

    private func updateLayout(for traits: UITraitCollection) {
        if traits.horizontalSizeClass == .regular {
            showRegularLayout() // iPad: 사이드바 + 콘텐츠
        } else {
            showCompactLayout() // iPhone: 탭 바 + 푸시
        }
    }

    private func showRegularLayout() {
        view.subviews.forEach { $0.removeFromSuperview() }
        regularStack.addArrangedSubview(sidebar)
        regularStack.addArrangedSubview(content)
        regularStack.frame = view.bounds
        regularStack.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.addSubview(regularStack)
    }

    private func showCompactLayout() {
        view.subviews.forEach { $0.removeFromSuperview() }
        compactStack.addArrangedSubview(content)
        compactStack.frame = view.bounds
        compactStack.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.addSubview(compactStack)
    }

    private func setupViews() {
        sidebar.backgroundColor = .systemGray6
        content.backgroundColor = .systemBackground
        compactStack.axis = .vertical
        regularStack.axis = .horizontal
    }
}

traitCollectionDidChange 메서드는 모든 특성 변경 시 호출되지만 레이아웃 전환의 가장 중요한 지표인 horizontalSizeClass만 확인합니다. iOS 17+에서는 변경 사항을 구독하지 않고 반응형 업데이트를 위해 UIViewController.horizontalSizeClass를 계산된 속성으로 사용하는 것이 좋습니다.

Android 한정자: sw600dp 및 w600dp

Android는 두 가지 주요 너비 한정자를 제공합니다: swdp(smallestWidth) 및 wdp(available width). 차이는 중요합니다: sw는 방향과 관계없는 최소 화면 너비이고, w는 현재 방향을 고려한 사용 가능한 너비입니다. sw는 지정된 최소 너비를 물리적으로 가진 기기에 대해 레이아웃이 로드되도록 보장합니다. w는 회전에 반응합니다 — 가로 모드에서는 세로 너비가 360 dp여도 w600dp가 활성화될 수 있습니다.

한정자조건기기 예시
layout-sw600dp최소 너비 ≥ 600 dpiPad(768 dp), Pixel C(900 dp)
layout-w600dp현재 너비 ≥ 600 dpSplit View iPad(600 dp), 가로 모드 휴대폰
layout-land가로 방향가로로 회전된 모든 기기
layout-sw720dp최소 너비 ≥ 720 dpiPad Pro(833 dp), Galaxy Tab S9(800 dp)

Android 리소스 계층 구조: res/layout/activity_main.xml(휴대폰), res/layout-sw600dp/activity_main.xml(7\" 태블릿), res/layout-w600dp-land/activity_main.xml(가로 모드 휴대폰). Android는 현재 기기 구성과 일치하는 가장 구체적인 한정자를 선택합니다. 일치하는 파일이 없으면 res/layout/의 기본 리소스가 사용됩니다.

Kotlin 적응형 레이아웃 예제

최신 Android 개발에서는 리소스를 직접 읽는 대신 Jetpack WindowManager를 사용하여 창 크기를 결정합니다. 이는 화면 크기가 동적으로 변하는 폴더블 기기(Galaxy Fold가 6.2\"에서 7.6\"으로 펼쳐짐)에서 특히 중요합니다.

kotlin
// Jetpack WindowManager를 통한 적응
import androidx.window.core.layout.WindowHeightSizeClass
import androidx.window.core.layout.WindowSizeClass
import androidx.window.core.layout.WindowWidthSizeClass

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun AdaptiveScreen() {
    val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass

    FlowRow(
        verticalAlignment = Arrangement.Top,
        horizontalArrangement = when (windowSizeClass.windowWidthSizeClass) {
            WindowWidthSizeClass.EXPANDED -> Arrangement.SpaceEvenly
            else -> Arrangement.Start
        }
    ) {
        when (windowSizeClass.windowWidthSizeClass) {
            WindowWidthSizeClass.COMPACT -> {
                CompactContent()
            }
            WindowWidthSizeClass.MEDIUM -> {
                MediumContent()
            }
            WindowWidthSizeClass.EXPANDED -> {
                ExpandedContent()
            }
        }
    }
}

@Composable
private fun CompactContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        Text("휴대폰", style = MaterialTheme.typography.headlineSmall)
        ListContent()
    }
}

@Composable
private fun MediumContent() {
    Row(modifier = Modifier.padding(24.dp)) {
        NavigationRail { ... }
        Column { ListContent() }
    }
}

@Composable
private fun ExpandedContent() {
    Row(modifier = Modifier.padding(32.dp)) {
        PermanentNavigationDrawer { ... }
        Column {
            ListContent()
        }
        DetailPane()
    }
}

androidx.window 라이브러리의 currentWindowAdaptiveInfo() 구성 요소는 휴대폰, 태블릿, 폴더블, ChromeOS 등 모든 기기에 인터페이스를 적응시킵니다. 너비 클래스: COMPACT(0–600 dp), MEDIUM(600–840 dp), EXPANDED(>840 dp). 높이 클래스: COMPACT(0–480 dp), MEDIUM(480–900 dp), EXPANDED(>900 dp).

SwiftUI의 Adaptive Layout

SwiftUI는 Size Classes를 직접 사용하지 않고 적응형 레이아웃을 위한 내장 도구를 제공합니다. AnyLayout, ViewThatFits 및 GeometryReader를 사용하면 사용 가능한 공간에 자동으로 적응하는 인터페이스를 구축할 수 있습니다.

swift
// AnyLayout 및 ViewThatFits를 통한 SwiftUI 적응
import SwiftUI

struct AdaptiveGrid: View {
    @Environment(\.horizontalSizeClass) private var hSizeClass

    var body: some View {
        let layout = hSizeClass == .regular
            ? AnyLayout(HStackLayout())
            : AnyLayout(VStackLayout())

        layout {
            Label("즐겨찾기", systemImage: "star")
            Label("최근", systemImage: "clock")
            Label("설정", systemImage: "gear")
        }
        .padding()
    }
}

// ViewThatFits — 자동 레이아웃 선택
struct SmartLayout: View {
    var body: some View {
        ViewThatFits {
            HStack { // 우선순위 1: 가로
                CardView()
                CardView()
            }
            VStack { // 맞지 않으면 — 세로
                CardView()
                CardView()
            }
        }
    }
}

ViewThatFits는 잘림 없이 사용 가능한 공간에 맞는 첫 번째 하위 뷰를 자동으로 선택하는 강력한 SwiftUI 도구입니다. 간단한 적응형 전환을 위해 Size Classes를 명시적으로 확인할 필요가 없습니다. AnyLayout을 사용하면 뷰 본문에서 if-else 없이 레이아웃 유형(HStack/VStack)을 전환할 수 있습니다.

Jetpack Compose의 Adaptive Layout

Jetpack Compose는 적응형 레이아웃을 위해 WindowSizeClass(Material 3 adaptive 라이브러리)와 BoxWithConstraints를 사용합니다. XML 한정자와 달리 Compose는 런타임에 창 크기를 결정하고 구성 변경 시 반응형으로 UI를 재구성합니다.

kotlin
// BoxWithConstraints를 통한 Jetpack Compose 적응형
@Composable
fun AdaptiveList(items: List<String>) {
    BoxWithConstraints {
        val width = maxWidth

        if (width >= 600.dp) {
            // 태블릿: 두 열(list-detail)
            Row(modifier = Modifier.fillMaxSize()) {
                LazyColumn(modifier = Modifier.weight(1f)) {
                    items(items) { item ->
                        ListItem(text = item)
                    }
                }
                var selected by remember { mutableStateOf(items.first()) }
                DetailPanel(item = selected)
                    .weight(2f)
            }
        } else {
            // 휴대폰: 탐색 기능이 있는 한 열
            LazyColumn(modifier = Modifier.fillMaxSize()) {
                items(items) { item ->
                    ListItem(text = item, onClick = { navigateToDetail(item) })
                }
            }
        }
    }
}

@Composable
fun AdaptiveListMaterial3() {
    val windowClass = currentWindowAdaptiveInfo().windowSizeClass

    AdaptiveLayout(
        layout = windowClass.windowWidthSizeClass
    ) {
        when (windowClass.windowWidthSizeClass) {
            WindowWidthSizeClass.COMPACT -> {
                ListDetailScaffold(
                    isDetailOnly = false,
                    list = { ListPane() },
                    detail = { DetailPane() }
                )
            }
            else -> {
                ListDetailScaffold(
                    isDetailOnly = false,
                    list = { ListPane() },
                    detail = { DetailPane() }
                )
            }
        }
    }
}

Material 3 Adaptive Layouts는 즉시 사용 가능한 구성 요소를 제공합니다: ListDetailScaffold, SupportingPaneScaffold, NavigationSuiteScaffold. 이러한 구성 요소는 WindowWidthSizeClass에 자동으로 적응하여 스택(COMPACT)과 나란히(MEDIUM/EXPANDED) 레이아웃 간에 전환됩니다. 개발자는 패턴을 선택하고 콘텐츠 패널을 전달하기만 하면 됩니다.

자주 묻는 질문

sw600dp와 w600dp의 차이점은 무엇인가요?

sw600dp(smallestWidth) — 방향과 관계없는 최소 화면 너비. 7\"+ 태블릿에서 항상 활성화됩니다. w600dp(available width) — 현재 방향을 고려한 사용 가능한 너비. 너비가 600 dp를 초과할 수 있는 가로 모드 휴대폰에서 활성화됩니다.

iOS에서 UITraitCollection이란 무엇인가요?

UITraitCollection은 환경 특성을 포함하는 iOS 객체입니다: size classes(가로/세로), 디스플레이 배율, force touch 기능, 사용자 인터페이스 idiom(iPhone/iPad), 색영역, 접근성 설정. UIApplication에서 뷰 계층 구조를 통해 전달되며 방향이나 창 크기가 변경될 때 변경됩니다.

폴더블 기기에 앱을 어떻게 적응시키나요?

Jetpack WindowManager(Android) 및 UIScreen nativeBounds + traitCollection(iOS)을 사용하세요. 폴더블 기기는 펼칠 때 Size Class가 변경됩니다. Activity 재시작 없이 창 크기 변경을 반응형으로 구독하는 표준 레이아웃(list-detail, supporting pane) 기반 아키텍처가 권장됩니다.

iPad용 별도 storyboard를 만들어야 하나요?

아니요. Size Class 제약 조건 변형이 있는 단일 storyboard 또는 UIKit을 통한 프로그래밍 방식 레이아웃을 사용하세요. SwiftUI는 storyboard가 전혀 필요하지 않습니다. iPad용 별도 storyboard는 중복을 만들고 유지 관리를 복잡하게 합니다 — Size Classes를 통해 하나의 레이아웃을 적응시키는 것이 좋습니다.

Material 3의 표준 레이아웃(canonical layouts)이란 무엇인가요?

Canonical layouts은 Google의 기성 적응형 레이아웃 패턴입니다: list-detail(목록 + 상세), supporting pane(콘텐츠 + 작업 패널), feed(카드). 각 패턴은 세 가지 상태(compact/medium/expanded)를 지원하며 Material 3 Adaptive 라이브러리에서 ListDetailScaffold 및 SupportingPaneScaffold를 통해 구현됩니다.

요약

  • Adaptive Layout — 다양한 기기를 위한 중단점에서의 개별 레이아웃 전환
  • Size Classes iOS(Compact/Regular)가 iPhone, iPad 및 Split View의 레이아웃 정의
  • UITraitCollection — 뷰 계층 구조를 통해 환경 특성을 전달하는 메커니즘
  • sw600dp — 7\"+ 태블릿용 Android 최소 너비 한정자
  • w600dp — 가로로 회전 시 활성화되는 사용 가능 너비 한정자
  • Jetpack WindowManager가 WindowSizeClass 제공(WindowWidthSizeClass: COMPACT/MEDIUM/EXPANDED)
  • Material 3 Adaptive — 자동 적응 기능이 있는 기성 구성 요소(ListDetailScaffold)

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기