Scaffold
헤더 / 본문 / 푸터 섹션과 사이드바·플로팅 액션 버튼·로딩 인디케이터를 제공하는 통합 화면 레이아웃 컴포넌트
목차
Scaffold는 화면의 기본 구조를 잡는 통합 레이아웃 컴포넌트입니다. 위로 쌓이는 헤더 섹션, 중앙 본문, 아래로 쌓이는 푸터 섹션을 제공하며, 선택적으로 사이드바, 플로팅 액션 버튼, 로딩 진행 인디케이터를 함께 표시할 수 있습니다. 헤더 영역은
AppBar로 leading / 중앙 / trailing 클러스터를 손쉽게 구성합니다.
기본 사용법 (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'}),
);
}
}
Live Preview#
코드 보기
class ScaffoldDefaultExample extends StatelessComponent {
const ScaffoldDefaultExample({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',
),
),
],
classes: 'w-full',
styles: Styles(raw: const {'height': '${400 / 16}rem'}),
);
}
}
class ScaffoldDefaultExample extends StatelessWidget {
const ScaffoldDefaultExample({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')),
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 헤더 / 본문 / 푸터로 구성된 일관된 화면 골격이 필요할 때
- 본문 옆에 사이드바를 두는 데스크톱 레이아웃
- 본문 위에 떠 있는 플로팅 액션 버튼이나 상단 로딩 인디케이터가 필요할 때
대신 다른 컴포넌트를 사용하세요:
Card: 화면 일부의 콘텐츠 박스가 필요할 때NavigationBar: 헤더가 아닌 주요 섹션 이동 네비게이션이 필요할 때
Props / Parameters#
W는 Flutter의 Widget, Web의 Component를 뜻하는 합성 콘텐츠 타입입니다.
Scaffold#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
child | W | 필수 | 메인 본문 콘텐츠 |
headers |
List<W> |
[] |
본문 위에 쌓이는 헤더 섹션 |
footers |
List<W> |
[] |
본문 아래에 쌓이는 푸터 섹션 |
sidebar | W? | null | 본문 옆 사이드바 |
floatingActionButton |
W? |
null |
본문 위에 떠 있는 액션 버튼 |
floatingActionButtonLocation |
CoreScaffoldFabLocation |
endFloat |
FAB 위치 (endFloat / centerFloat / startFloat) |
floatingHeader |
bool |
false |
헤더가 본문 위에 떠서 레이아웃 공간을 차지하지 않음 |
floatingFooter |
bool |
false |
푸터가 본문 위에 떠서 레이아웃 공간을 차지하지 않음 |
loadingProgress |
double? |
null |
로딩 진행 값 (0.0–1.0) |
loadingProgressIndeterminate |
bool |
false |
무한 로딩 모드 |
mainLandmark |
bool |
true |
가장 바깥 Scaffold의 본문 main 랜드마크 허용 |
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). |
fabPadding |
CoreEdgeInsets? |
Padding around a floating action button. |
CoreScaffoldStyle 변형별 기본값 (CoreScaffoldVariantStyle)#
| 필드 | standard |
|---|---|
backgroundColor | surface |
headerBackgroundColor | surface |
footerBackgroundColor | surface |
동작 스펙 (Behavior)#
레이아웃#
- 고정 모드: 헤더 / 푸터가 레이아웃 공간을 차지하며 본문을 밀어냄
-
플로팅 모드:
floatingHeader/floatingFooter가true면 본문 위에 겹쳐 표시 - 사이드바:
sidebar를 지정하면 본문과 가로로 나란히 배치
로딩 인디케이터#
loadingProgress를 지정하면 화면 최상단에 진행 막대 표시loadingProgressIndeterminate가true면 무한 로딩 막대 표시
키보드(IME) 인셋 회피 — resizeToAvoidBottomInset (Flutter 전용)#
-
true면 헤더 / 본문 / 푸터 스택(플로팅 오버레이·FAB 포함)을MediaQuery.viewInsets.bottom만큼 하단 패딩 — 채팅 입력바·폼 같은 하단 고정 콘텐츠가 소프트 키보드와 IME 액세서리 스트립(물리 키보드의 자동완성 툴바 등) 위로 올라옵니다. MaterialScaffold.resizeToAvoidBottomInset과 동일한 의미입니다. - 소비한 인셋은 자식
MediaQuery에서 제거되어 중첩 Scaffold가 이중 적용하지 않습니다. -
기본값은
true— MaterialScaffold의 기본값과 같고, Web 쌍둥이와도 같습니다. 브라우저는 IME 에 맞춰 visual viewport 를 리사이즈하며 이를 끌 방법이 없으므로, Flutter 만false이면 같은 코드가 Web 에서는 키보드 위로 올라오고 Flutter 에서는 가려집니다. - 콘텐츠가 키보드 뒤로 이어져야 하는 화면(전면 미디어·지도)에서는
false로 끕니다. -
본문 높이가 줄어들므로 스크롤 불가한 고정 높이 본문은
BOTTOM OVERFLOWED를 냅니다 — 종전에 "가려짐"이던 것이 경고로 드러난 것이고, 본문을 스크롤 가능하게 만드는 것이 해법입니다. -
Web에는 이 파라미터가 없습니다. 브라우저가 IME를 문서 레벨에서 native 처리하기 때문입니다 — 모바일 웹에서 키보드 회피가 필요하면 앱 진입 HTML의 viewport meta에
interactive-widget=resizes-content를 지정하거나 CSS100dvh단위를 사용하세요. 이는 Scaffold 인스턴스가 아니라 문서(document) 단위 설정입니다.
사용 가이드라인 (Usage Guidelines)#
헤더·본문·푸터·사이드바·FAB를 함께 배치하는 화면 셸에는 Scaffold를 사용합니다. 본문 안의 방향·간격 배치는 Frame, 콘텐츠 묶음의 표면은 Card, 여러 떠 있는 창의 상태 관리는 Window를 선택합니다. Scaffold의 loadingProgress는 전체 작업 표시이고 콘텐츠 자리의 로딩은 호출자가 Skeleton으로 구성합니다.
✅ Do#
키보드 뒤로 이어져야 하는 화면에서만 resizeToAvoidBottomInset 끄기
Scaffold(
resizeToAvoidBottomInset: false,
child: fullBleedMapView,
)
기본값이 true라 채팅 입력바·폼처럼 하단에 고정된 콘텐츠는 별도 설정 없이 소프트 키보드 위로 올라옵니다 — Material Scaffold 및 Web 쌍둥이와 같은 동작입니다. 끄는 것은 전면 미디어·지도처럼 콘텐츠가 키보드 뒤까지 이어져야 하는 화면뿐입니다. Web에는 이 파라미터가 없으므로 모바일 웹 키보드 회피는 viewport meta interactive-widget=resizes-content 또는 100dvh로 문서 단위에서 처리합니다.
헤더에 AppBar 를 넣을 때는 AppBar 디바이더를 끄기
Scaffold(
headers: [
AppBar(
title: const Text('Dashboard'),
showDivider: false,
),
],
child: body,
)
Scaffold 헤더 보더(CoreScaffoldStyle.headerBorderColor · headerBorderWidth)와 AppBar 하단 디바이더는 같은 자리(헤더 하단)에 그려집니다. 둘 다 켜 두면 선이 두 겹이 되고, 한쪽 색만 바꾸면 어긋난 두 줄로 드러납니다. 호출자가 AppBar 쪽을 showDivider: false 로 끄고, 헤더 경계선은 Scaffold 헤더 보더 한 곳에서 정합니다. Web · Flutter 동일합니다.
❌ Don't#
floatingHeader를 켠 채 본문에 상단 여백을 주지 않기
// ❌ floatingHeader가 레이아웃 공간을 차지하지 않아 헤더가 본문 상단과 겹침
Scaffold(
floatingHeader: true,
headers: [appBar],
child: contentWithNoTopPadding,
)
floatingHeader: true면 헤더가 본문 위에 겹쳐 표시될 뿐 레이아웃 공간을 차지하지 않습니다. 본문 콘텐츠가 헤더 아래로 가려지지 않으려면 본문 쪽에 헤더 높이만큼의 여백을 직접 줘야 합니다.
접근성 (Accessibility)#
역할#
Scaffold 는 루트와 본문 두 곳에만 역할을 붙이고, 나머지 슬롯은 그 안에 들어가는 위젯의 몫으로 둡니다.
-
Flutter: 루트는
Semantics(container: true, label: scaffoldLabel)로 이름 있는 컨테이너 노드가 되고(scaffold.dart:289-296), 본문은CoUISemantics(role: .main, label: mainLabel, container: true)로 감쌉니다(scaffold.dart:178-183)..main의 전제 조건이label인데 라벨을 함께 주므로 역할이 실제로 emit 됩니다. -
Web: 루트는
<div role="group" aria-label="{scaffoldLabel}">(scaffold.dart:242-246), 본문은role="main"+aria-label="{mainLabel}"(scaffold.dart:186-189).
main 랜드마크가 루트가 아니라 본문에 있는 것은 의도된 배치입니다 — 리더 사용자가 헤더와 내비게이션을 건너뛰려고 점프하는 지점이므로, 루트에 있으면 이미 있던 자리로 점프하게 됩니다.
sidebar 가 있으면 사이드바를 포함하는 행이 아니라 안쪽 본문 셀에 붙습니다(scaffold.dart:190-209).
headers / footers 섹션과 AppBar 자신에는 두 플랫폼 모두 역할이 없습니다 — Flutter 의
_ScaffoldSection 은 RepaintBoundary + DecoratedBox + Column
이고(scaffold.dart:408,472), Web 은 평범한 div 입니다(scaffold.dart:161-175).
loadingProgress 를 주면 그 자리의 Progress 가 Web 에서 role="progressbar"
+ aria-valuemin/max/now 를(progress.dart:141-144), Flutter 에서 백분율 문자열을 담은
Semantics(value:) 를 냅니다(progress.dart:113-116).
키보드#
처리하는 키가 없습니다. Flutter scaffold.dart 에 Shortcuts / Actions / LogicalKeyboardKey
가 0건이고 Web scaffold.dart 에 keydown 핸들러가 0건입니다. Tab 은 헤더 · 본문 · 푸터 · FAB 안의 각 슬롯 위젯이 만든 정지점 사이를 이동할 뿐
Scaffold 자신은 정지점이 아닙니다.
스크린 리더#
-
루트 이름은 로컬라이즈된
scaffoldLabel입니다(영어Scaffold/ 한국어스캐폴드). 양 플랫폼이 같은 키를 읽습니다. -
본문 이름은 로컬라이즈된
mainLabel입니다(영어Main content/ 한국어본문). - 헤더 · 푸터 · 사이드바 · FAB 의 이름은 각 슬롯 위젯이 책임집니다 —
Scaffold는 아무 이름도 얹지 않습니다. -
상태 통보는 없습니다.
loadingProgress가 바뀌어도 통보되지 않습니다 — FlutterProgress에liveRegion이 없고 WebProgress에aria-live도role="status"도 없어, 진행률은 사용자가 그 지점으로 이동해야만 읽힙니다.
포커스 관리#
- 진입 — 오버레이가 아니라 포커스 이동이 없습니다.
- 이탈 — 해당 없음.
- 트랩 — 없습니다. Flutter 에
FocusScope가 0건이고 Web 에도 focus trap 이 없습니다.
헤더를 건너뛰는 수단은 skip link 가 아니라 본문의 main 랜드마크입니다 — 랜드마크 점프를 쓰는 리더 사용자는 헤더를 건너뛸 수 있지만, 키보드만 쓰는 사용자에게는 헤더 안의 정지점을 차례로 지나가는 것 말고 다른 경로가 없습니다.
알려진 제약#
-
루트의
role="group"과 이름을 호출부가 바꿀 수 없습니다. Web 은role/aria-label을componentAttributes뒤에 병합해 컴포넌트가 callerattributes를 이깁니다(scaffold.dart:242-246) — 이 저장소의 일반적인 "caller 가 이긴다" 순서와 반대입니다. Flutter 에는 라벨 슬롯 자체가 없습니다(scaffold.dart:289-291). 한 문서에Scaffold가 여럿이면 전부 같은 이름으로 안내됩니다. 대안 없음. -
헤더 · 푸터가 랜드마크가 아닙니다.
banner/contentInfo역할이 어느 플랫폼에도 없어 리더의 랜드마크 목록에 페이지 머리말 · 꼬리말이 나타나지 않습니다. Web 은AppBar(attributes: {'role': 'banner'})로 호출부가 실을 수 있지만(scaffold.dart:381), FlutterAppBar생성자에는attributes도Semantics슬롯도 없어(scaffold.dart:490이하 — 파라미터는 슬롯과scaffoldStyle뿐) 호출부가AppBar를 직접Semantics로 감싸야 합니다. -
sidebar슬롯에 navigation 랜드마크가 붙지 않습니다. 두 구현의 주석은 "사이드바는 내비게이션" 이라고 말하지만 실제로 역할을 내는 코드는 어느 쪽에도 없습니다(scaffold.dart:184-193/scaffold.dart:190-203). 사이드바가 내비게이션이라면 그 위젯 쪽에서 역할을 붙이세요 — 예:Frame(a11y: CoreFrameA11y(role: .navigation, label: ...), child: sidebarContent). -
loadingProgress의 변화가 통보되지 않습니다. 위 "스크린 리더" 에 적은 대로 양 플랫폼 모두 live region 이 없어, 로딩이 시작되거나 끝나도 리더는 말하지 않습니다. 진행 상태를 알려야 하는 화면이라면Scaffold의 인디케이터에 의존하지 말고 통보되는 표면(Toast·Status등)을 함께 쓰세요. -
floatingHeader/floatingFooter를 켜면 두 플랫폼의 트리 순서가 갈립니다. Flutter 는 그 섹션을 세로 스택에서 빼내 본문 뒤 오버레이(Stacks)로 옮기고(scaffold.dart:197-230), Web 은 DOM 위치를 본문 앞에 그대로 둔 채position: sticky클래스만 바꿉니다(scaffold.dart:223/scaffold_style.dart:234). 읽기 · 포커스 순서를 두 플랫폼에서 같게 유지해야 한다면 고정 모드(기본값)를 쓰세요.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | 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
)
|
관련 컴포넌트 (Related Components)#
- NavigationBar: 주요 섹션 이동 네비게이션 컨테이너
- Card: 화면 일부의 콘텐츠 박스
-
BottomActionBar:
footers에 넘기는 화면 아래의 행동 바 — 주 행동 · 보조 행동과 아래 안전 영역을 맡습니다