Scaffold | CoUI
LogoCoUI

Scaffold

헤더 / 본문 / 푸터 섹션과 사이드바·플로팅 액션 버튼·로딩 인디케이터를 제공하는 통합 화면 레이아웃 컴포넌트

Scaffold#

Scaffold는 화면의 기본 구조를 잡는 통합 레이아웃 컴포넌트입니다. 위로 쌓이는 헤더 섹션, 중앙 본문, 아래로 쌓이는 푸터 섹션을 제공하며, 선택적으로 사이드바, 플로팅 액션 버튼, 로딩 진행 인디케이터를 함께 표시할 수 있습니다. 헤더 영역은 AppBar로 leading / 중앙 / trailing 클러스터를 손쉽게 구성합니다.

Live Preview#

사용 시기 (When to Use)#

이 컴포넌트를 사용하세요:

  • 헤더 / 본문 / 푸터로 구성된 일관된 화면 골격이 필요할 때
  • 본문 옆에 사이드바를 두는 데스크톱 레이아웃
  • 본문 위에 떠 있는 플로팅 액션 버튼이나 상단 로딩 인디케이터가 필요할 때

대신 다른 컴포넌트를 사용하세요:

  • Card: 화면 일부의 콘텐츠 박스가 필요할 때
  • NavigationBar: 헤더가 아닌 주요 섹션 이동 네비게이션이 필요할 때

기본 사용법 (Basic Usage)#

Scaffold(
  headers: [
    AppBar(
      title: const Text('Dashboard').titleMedium,
      trailing: const [Icon(LucideIcons.settings)],
    ),
  ],
  footers: [
    AppBar(
      title: const Text('Ready').bodySmall.onSurfaceVariant,
    ),
  ],
  child: const Center(child: Text('Main content area')),
)
Scaffold(
  headers: [
    AppBar(
      title: const Text('Dashboard').titleMedium,
      trailing: const [Icon(LucideIcons.settings)],
    ),
  ],
  footers: [
    AppBar(
      title: const Text('Ready').bodySmall.onSurfaceVariant,
    ),
  ],
  child: div(
    [const Text('Main content area')],
    classes: 'flex items-center justify-center',
  ),
)

빠른 오버라이드 (Chain)#

이미 만든 Scaffold 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다. .surfaceContainer처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(surfaceContainer == CoreColors.surfaceContainer) 어느 컴포넌트에서 써도 뜻이 갈리지 않으며, 위 예시처럼 withStyle 뒤에 이어붙일 수도 있습니다.

class ScaffoldChainExample extends StatelessWidget {
  const ScaffoldChainExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
          headers: [
            AppBar(
              title: const Text('Dashboard').titleMedium,
              trailing: const [Icon(LucideIcons.settings)],
            ),
          ],
          footers: [
            AppBar(
              title: const Text('Ready').bodySmall.onSurfaceVariant,
            ),
          ],
          child: const Center(child: Text('Main content area')),
        )
        .withStyle(
          const CoreScaffoldStyle(
            headerBorderColor: CoreColor.token(CoreColors.outline),
            headerBorderWidth: CoreStrokeWidth.stroke2,
          ),
        )
        .surfaceContainer;
  }
}
class ScaffoldChainExample extends StatelessComponent {
  const ScaffoldChainExample({super.key});

  @override
  Component build(BuildContext context) {
    // Fixed-height frame (matches the Flutter preview canvas height)
    // so the scaffold's `min-h-full` has a definite parent to fill —
    // the Flutter twin fills its canvas constraints the same way.
    return div(
      [
        Scaffold(
              headers: [
                AppBar(
                  title: const Text('Dashboard').titleMedium,
                  trailing: const [Icon(LucideIcons.settings)],
                ),
              ],
              footers: [
                AppBar(
                  title: const Text('Ready').bodySmall.onSurfaceVariant,
                ),
              ],
              // `flex-1` fills the scaffold's body column (Flutter
              // `Center` fills the available body the same way).
              child: div(
                [const Text('Main content area')],
                classes: 'flex items-center justify-center flex-1',
              ),
            )
            .withStyle(
              const CoreScaffoldStyle(
                headerBorderColor: CoreColor.token(CoreColors.outline),
                headerBorderWidth: CoreStrokeWidth.stroke2,
              ),
            )
            .surfaceContainer,
      ],
      classes: 'w-full',
      styles: Styles(raw: const {'height': '${400 / 16}rem'}),
    );
  }
}

Props / Parameters#

Scaffold#

속성타입기본값설명
child Widget / Component 필수 메인 본문 콘텐츠
headers List<Widget> / List<Component> [] 본문 위에 쌓이는 헤더 섹션
footers List<Widget> / List<Component> [] 본문 아래에 쌓이는 푸터 섹션
sidebar Widget? / Component? null 본문 옆 사이드바
floatingActionButton Widget? / Component? null 본문 위에 떠 있는 액션 버튼
floatingActionButtonLocation CoreScaffoldFabLocation endFloat FAB 위치 (endFloat / centerFloat / startFloat)
floatingHeader bool false 헤더가 본문 위에 떠서 레이아웃 공간을 차지하지 않음
floatingFooter bool false 푸터가 본문 위에 떠서 레이아웃 공간을 차지하지 않음
loadingProgress double? null 로딩 진행 값 (0.01.0)
loadingProgressIndeterminate bool false 무한 로딩 모드
resizeToAvoidBottomInset bool (Flutter 전용) true 온스크린 키보드(IME)가 뜨면 콘텐츠를 그 높이만큼 리사이즈 (아래 동작 스펙 참조)
variant CoreScaffoldVariant standard 시각 변형
scaffoldStyle CoreScaffoldStyle? null 크롬 / 치수 스타일 오버라이드

AppBar#

headers / footers 에 넣는 AppBar 는 자기 chrome(appBarStyle)과 variant 를 가진 별도 컴포넌트입니다 — 파라미터 표와 스타일 필드는 AppBar 페이지를 참고하세요.

스타일 시스템 (Style System)#

CoreScaffoldStyle 필드#

필드타입설명
backgroundColor CoreColor? Background colour of the scaffold body surface.
headerBackgroundColor CoreColor? Background colour of the header section.
footerBackgroundColor CoreColor? Background colour of the footer section.
headerBorderColor CoreColor? Colour of the header's bottom border.
headerBorderWidth double? Thickness of the header's bottom border (logical px).
headerPadding CoreEdgeInsets? Deprecated (since 0.131) — use CoreAppBarStyle.padding . Outer padding of the header / app-bar section.
headerSpacing double? Deprecated (since 0.131) — use CoreAppBarStyle.spacing . Spacing between the header's leading / centre / trailing clusters (logical px). Mirrors Flutter Row.spacing between leading, centre and trailing children.
headerActionSpacing double? Deprecated (since 0.131) — use CoreAppBarStyle.actionSpacing . Spacing between adjacent widgets inside the header's leading or trailing cluster (logical px). Mirrors Flutter Row.spacing applied to each action cluster.
fabPadding CoreEdgeInsets? Padding around a floating action button.

CoreScaffoldStyle 변형별 기본값 (CoreScaffoldVariantStyle)#

필드standard
backgroundColorsurface
headerBackgroundColorsurface
footerBackgroundColorsurface

동작 스펙 (Behavior)#

레이아웃#

  • 고정 모드: 헤더 / 푸터가 레이아웃 공간을 차지하며 본문을 밀어냄
  • 플로팅 모드: floatingHeader / floatingFootertrue면 본문 위에 겹쳐 표시
  • 사이드바: sidebar를 지정하면 본문과 가로로 나란히 배치

로딩 인디케이터#

  • loadingProgress를 지정하면 화면 최상단에 진행 막대 표시
  • loadingProgressIndeterminatetrue면 무한 로딩 막대 표시

키보드(IME) 인셋 회피 — resizeToAvoidBottomInset (Flutter 전용)#

  • true면 헤더 / 본문 / 푸터 스택(플로팅 오버레이·FAB 포함)을 MediaQuery.viewInsets.bottom 만큼 하단 패딩 — 채팅 입력바·폼 같은 하단 고정 콘텐츠가 소프트 키보드와 IME 액세서리 스트립(물리 키보드의 자동완성 툴바 등) 위로 올라옵니다. Material Scaffold.resizeToAvoidBottomInset과 동일한 의미입니다.
  • 소비한 인셋은 자식 MediaQuery에서 제거되어 중첩 Scaffold가 이중 적용하지 않습니다.
  • 기본값은 true — Material Scaffold 의 기본값과 같고, Web 쌍둥이와도 같습니다. 브라우저는 IME 에 맞춰 visual viewport 를 리사이즈하며 이를 끌 방법이 없으므로, Flutter 만 false 이면 같은 코드가 Web 에서는 키보드 위로 올라오고 Flutter 에서는 가려집니다.
  • 콘텐츠가 키보드 뒤로 이어져야 하는 화면(전면 미디어·지도)에서는 false 로 끕니다.
  • 본문 높이가 줄어들므로 스크롤 불가한 고정 높이 본문은 BOTTOM OVERFLOWED 를 냅니다 — 종전에 "가려짐"이던 것이 경고로 드러난 것이고, 본문을 스크롤 가능하게 만드는 것이 해법입니다.
  • Web에는 이 파라미터가 없습니다. 브라우저가 IME를 문서 레벨에서 native 처리하기 때문입니다 — 모바일 웹에서 키보드 회피가 필요하면 앱 진입 HTML의 viewport meta에 interactive-widget=resizes-content를 지정하거나 CSS 100dvh 단위를 사용하세요. 이는 Scaffold 인스턴스가 아니라 문서(document) 단위 설정입니다.

사용 가이드라인 (Usage Guidelines)#

✅ Do#

키보드 뒤로 이어져야 하는 화면에서만 resizeToAvoidBottomInset 끄기

Scaffold(
  resizeToAvoidBottomInset: false,
  child: fullBleedMapView,
)

기본값이 true라 채팅 입력바·폼처럼 하단에 고정된 콘텐츠는 별도 설정 없이 소프트 키보드 위로 올라옵니다 — Material Scaffold 및 Web 쌍둥이와 같은 동작입니다. 끄는 것은 전면 미디어·지도처럼 콘텐츠가 키보드 뒤까지 이어져야 하는 화면뿐입니다. Web에는 이 파라미터가 없으므로 모바일 웹 키보드 회피는 viewport meta interactive-widget=resizes-content 또는 100dvh로 문서 단위에서 처리합니다.


❌ Don't#

floatingHeader를 켠 채 본문에 상단 여백을 주지 않기

// ❌ floatingHeader가 레이아웃 공간을 차지하지 않아 헤더가 본문 상단과 겹침
Scaffold(
  floatingHeader: true,
  headers: [appBar],
  child: contentWithNoTopPadding,
)

floatingHeader: true면 헤더가 본문 위에 겹쳐 표시될 뿐 레이아웃 공간을 차지하지 않습니다. 본문 콘텐츠가 헤더 아래로 가려지지 않으려면 본문 쪽에 헤더 높이만큼의 여백을 직접 줘야 합니다.

접근성 (Accessibility)#

스크린 리더#

  • Flutter: 본문을 ColoredBox로 감싸 레이아웃만 담당 — 헤더 / 푸터의 시맨틱은 각 슬롯 위젯이 담당
  • Web: 루트 <div role="group" aria-label="Scaffold"> 적용

크로스 플랫폼 차이점 (Platform Differences)#

항목FlutterWeb
클래스명 Scaffold / AppBar Scaffold / AppBar
렌더 루트 ColoredBox + Column / Stack <div> (flex column, position: relative)
플로팅 헤더 Stack + Position 오버레이 position: sticky
FAB 위치 Align + Padding position: absolute
키보드(IME) 회피 기본 동작 — resizeToAvoidBottomInset: false 로 옵트아웃 파라미터 없음 — 브라우저 native (viewport meta interactive-widget=resizes-content / 100dvh )
  • NavigationBar: 주요 섹션 이동 네비게이션 컨테이너
  • Card: 화면 일부의 콘텐츠 박스