ResponsiveGrid
N × M 셀의 반응형 그리드 레이아웃 with col/row span
목차
지정된 columns 개의 균등 너비 셀에 자식을 그리드로 배치합니다. responsive: true 일 때 CoreBreakpoint에 맞춰 컬럼 수가 자동 감소합니다 (기본:
md 이하 1열, lg 이하 2열, lg 이상 columns열). GridItem으로 감싸
colSpan/rowSpan을 적용하면 여러 셀을 차지합니다.
기본 사용법 (Basic Usage)#
ResponsiveGrid(
columns: 3,
cellSpacing: CoreSpace.space16,
children: [
for (var i = 1; i <= 6; i++)
Card(
cardStyle: const CoreCardStyle(
padding: CoreEdgeInsets.all(CoreSpace.space16),
),
child: Text('Item $i'),
),
],
)
Live Preview#
코드 보기
class ResponsiveGridDefaultExample extends StatelessComponent {
const ResponsiveGridDefaultExample({super.key});
@override
Component build(BuildContext context) {
final cs = context.colorScheme;
final textClass = 'text-${CoreTextStyles.bodyMedium.name} text-${cs.onSurface}';
return ResponsiveGrid(
columns: 3,
cellSpacing: CoreSpace.space16,
children: [
for (var i = 1; i <= 5; i += 1)
Card(
cardStyle: const CoreCardStyle(padding: CoreEdgeInsets.all(CoreSpace.space16)),
child: div([Text('Item $i')], classes: textClass),
),
],
);
}
}
class ResponsiveGridDefaultExample extends StatelessWidget {
const ResponsiveGridDefaultExample({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textStyle = theme.typography.bodyMedium.toValue(theme: theme).copyWith(
color: theme.colorScheme.onSurface.toValue(),
);
return ResponsiveGrid(
columns: 3,
cellSpacing: CoreSpace.space16,
children: [
for (var i = 1; i <= 5; i += 1)
Card(
cardStyle: const CoreCardStyle(
padding: CoreEdgeInsets.all(CoreSpace.space16),
),
child: Text('Item $i', style: textStyle),
),
],
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 대시보드처럼 N × M 셀의 카드/모듈을 배치할 때
- 뷰포트 폭에 따라 열 수가 달라져야 할 때
Wrap이나 flex보다 명시적으로 열 수를 정하고 싶을 때
대신 다른 컴포넌트를 사용하세요:
ResponsiveContainer: 단일 콘텐츠를 화면 중앙에 max-width로 제약Row/flex: 1행 배치Wrap/ flex-wrap: 셀 너비가 content 기반인 경우
col/row span#
ResponsiveGrid(
columns: 4,
cellSpacing: CoreSpace.space16,
responsive: false,
children: [
GridItem(colSpan: 2, child: Card(child: Text('Wide'))),
Card(child: Text('Cell')),
Card(child: Text('Cell')),
GridItem(rowSpan: 2, child: Card(child: Text('Tall'))),
// ...
],
)
반응형 커스터마이징#
기본 breakpoint 매핑을 덮어쓰려면 breakpoints를 제공:
ResponsiveGrid(
columns: 6,
breakpoints: {
CoreBreakpoint.xs: 1,
CoreBreakpoint.sm: 2,
CoreBreakpoint.md: 3,
CoreBreakpoint.lg: 4,
CoreBreakpoint.xl: 5,
CoreBreakpoint.xxl: 6,
},
children: [...],
)
Props / Parameters#
W는 Flutter의 Widget, Web의 Component를 뜻하는 셀 콘텐츠 타입입니다. colSpan/rowSpan은 셀 개수, cellSpacing은 논리 px 단위입니다.
ResponsiveGrid#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
children | List<W> | 필수 | 그리드 셀 |
columns |
int? |
null → theme → 12 | 가장 넓은 breakpoint에서의 컬럼 수 |
cellSpacing |
double? |
null → theme → CoreSpace.space16 |
셀 사이 간격 (logical px) |
responsive |
bool? |
null → theme → true | breakpoint 기반 반응형 활성화 |
breakpoints |
Map<CoreBreakpoint, int>? |
null → theme → 기본 곡선 | 명시적 breakpoint → 컬럼 매핑 |
responsiveGridStyle |
CoreResponsiveGridStyle? |
null |
위 네 값의 테마 차원 default 를 담는 단일 슬롯 (
columns
/
cellSpacing
/
responsive
/
breakpoints
)
|
GridItem#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
child | W | 필수 | 셀 내용 |
colSpan | int? | null → 1 | 차지할 컬럼 수 |
rowSpan | int? | null → 1 | 차지할 행 수 |
스타일 시스템 (Style System)#
CoreResponsiveGridStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
columns |
int? |
Column count at the widest breakpoint override. null defers to [defaultColumns]. |
cellSpacing |
double? |
Inter-cell spacing override applied uniformly between every column and every row (logical px).
null
defers to [defaultCellSpacing]. N-sibling distribution —
xxxSpacing
name per CLAUDE.md (the rendered value is fed straight to
RenderGrid
/ CSS
gap
).
|
responsive |
bool? |
Whether responsive breakpoints are enabled override.
null
defers to [defaultResponsive].
|
breakpoints |
Map<CoreBreakpoint, int>? |
Optional explicit breakpoint-to-column mapping override.
null
defers to the computed default curve (1 col below
md
, 2 cols below
lg
, full [columns] at
lg
and above).
|
동작 스펙 (Behavior)#
배치 알고리즘#
자식을 순서대로 좌→우, 상→하로 스캔하며 colSpan × rowSpan이 완전히 비어있는 최초의 사각형 영역을 예약합니다 (row-major packing).
렌더링#
-
Flutter:
MultiChildRenderObjectWidget→RenderGrid(bit-field 점유 그리드로 span 계산) -
Web: CSS Grid (
display: grid,grid-template-columns: repeat(N, minmax(0, 1fr))) +@container (min-width: ...)스코프 블록으로 responsive 처리
반응형 breakpoint 기본값#
| Breakpoint | 기본 컬럼 수 |
|---|---|
| xs (< 640px) | 1 |
| sm (≥ 640px) | 1 |
| md (≥ 768px) | 2 |
| lg (≥ 1024px) | columns |
| xl (≥ 1280px) | columns |
| xxl (≥ 1536px) | columns |
사용 가이드라인 (Usage Guidelines)#
열 수와 셀 span을 가진 반응형 배치에는 ResponsiveGrid를 사용합니다. 한 묶음의 가로/세로 전환은 ResponsiveRowColumn, 최대 폭 제약은 ResponsiveContainer, 행·열 의미와 고정 셀을 가진 데이터 표는 Table를 선택합니다.
단계별 여유 폭을 패딩으로 계산해야 하면 StageContainer, 사용자가 pane 비율을 직접 조절해야 하면 Resizable를 사용합니다.
✅ Do#
고정 span 레이아웃이 필요하면 responsive: false로 컬럼 수를 고정
ResponsiveGrid(
columns: 4,
responsive: false, // 4열 고정 — colSpan 이 항상 columns 이하로 유지됨
children: [
GridItem(colSpan: 2, child: Card(child: Text('Wide'))),
Card(child: Text('Cell')),
Card(child: Text('Cell')),
],
)
colSpan/rowSpan을 가진 레이아웃을 breakpoint마다 컬럼 수가 바뀌는 채로 두면 좁은 화면에서 span 값이 실제 컬럼 수를 넘어설 수 있습니다. 레이아웃이 span 관계에 의존한다면 responsive: false로 컬럼 수를 고정하는 편이 안전합니다.
❌ Don't#
responsive: true인 채로 큰 colSpan을 주지 않기
// ❌ 반응형(columns 축소)인데 colSpan이 최대 columns에 가까움
ResponsiveGrid(
columns: 4,
responsive: true,
children: [
GridItem(colSpan: 4, child: Card(child: Text('Full width'))),
// ...
],
)
Flutter는 colSpan을 현재 컬럼 수로 자동 clamp 하지만, Web은 grid-column: span N을 그대로 inline CSS로 내보내 clamp 하지 않습니다 — 컬럼이 4 → 2 → 1로 줄어드는 좁은 breakpoint에서 두 플랫폼의 렌더 결과가 갈릴 수 있습니다.
접근성 (Accessibility)#
역할#
없습니다 — ResponsiveGrid 도 GridItem 도, 양 플랫폼 모두. 순수 배치 컨테이너이며 자식의 시맨틱이 그대로 통과합니다.
-
Flutter:
responsive_grid.dart·grid_item.dart·render_grid.dart어디에도Semantics호출이 없습니다.ResponsiveGrid는LayoutBuilder→GridMultiChild(MultiChildRenderObjectWidget)를 반환하고(responsive_grid.dart:106-122,render_grid.dart:81),GridItem은 렌더 박스를 하나도 추가하지 않는GridItemParentData입니다(grid_item.dart:45-51). -
Web: 루트는
display: gridinline 스타일만 실은<div>이고configureAttributes를 override 하지 않아role/aria-*가 0건입니다(responsive_grid.dart:132-139).GridItem도grid-column/grid-rowspan 만 실은<div>로 자식을 한 겹 감쌉니다(grid_item.dart:77-87).
responsive 가 켜지면 Web 은 @container 규칙을 담은 <style> 요소와 query-container 래퍼
<div> 를 추가하지만(responsive_grid.dart:149-156), <style>
은 접근성 트리에 노출되지 않고 래퍼에도 역할이 붙지 않습니다.
키보드#
처리하는 키가 없습니다. 두 플랫폼 어느 파일에도 키 핸들러가 없고, 그리드 자체는 포커스를 받지 않으므로 Tab 은 자식이 만든 정지점 사이만 이동합니다.
스크린 리더#
그리드라고 안내되지 않습니다 — role="grid" / role="table" 도, 행 · 열 좌표도 어느 쪽에서도 나오지 않습니다. 리더가 보는 것은 자식들이 나열된 평평한 순서뿐입니다.
Web 에서 그 순서는 DOM 순서, 즉 children 리스트 순서 그대로입니다 — CSS Grid 의 배치는 시각 위치만 바꾸고 DOM 을 재정렬하지 않습니다. 그래서
responsive 로 열 수가 4 → 2 → 1 로 줄어도 읽는 순서는 breakpoint 와 무관하게 일정합니다.
포커스 관리#
- 진입 — 오버레이가 아니라 포커스 이동이 없습니다.
- 이탈 — 해당 없음.
-
트랩 — 없습니다. 두 플랫폼 모두
FocusScope나 focus trap 이 없고, 그리드 자신은 포커스 정지점이 아닙니다(Flutter 에Focus없음, Web 에tabindex없음).
알려진 제약#
-
표 구조가 보조 기술에 노출되지 않습니다. 시각적으로는 N × M 행렬이지만 리더에게는 순서만 있는 목록이라, "3행 2열" 같은 위치도
colSpan/rowSpan이 만든 병합도 안내되지 않습니다. 데이터가 행 · 열 관계를 갖는다면ResponsiveGrid가 아니라Table을 쓰세요. 컨테이너 역할만 얹으면 되는 경우 Web 은ResponsiveGrid(attributes: {'role': ...})로 루트에 실을 수 있지만(responsive_grid.dart:35,:138), FlutterResponsiveGrid생성자에는attributes도Semantics슬롯도 없어(responsive_grid.dart:37-44) 호출부가 위젯을 직접Semantics로 감싸야 합니다 — 그래도 셀의 행 · 열 좌표는RenderGrid내부 값이라 어느 쪽에서도 낼 수 없습니다.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 클래스명 | ResponsiveGrid / GridItem | 동일 |
| 렌더링 | RenderGrid (custom RenderBox) |
<div class style="display:grid"> |
| col/row span | GridParentData + bit-mask packing |
CSS grid-column: span N / grid-row: span N |
| 반응형 | LayoutBuilder의 부모 가용 폭 |
@container (min-width: ...)의 컨테이너 폭 |
| 테마 | CoreResponsiveGridTheme | 동일 |
레거시 vs 통일 비교 (Migration Notes)#
이전 버전의 Web CoUI 를 참조하는 코드에는 이 컴포넌트가 Grid 라는 이름이었을 수 있습니다. Flutter 쪽 이름(ResponsiveGrid)에 맞춰 Web 도
ResponsiveGrid 로 개명되었습니다.
| 항목 | 레거시 (구 Web Grid) | 통일 ResponsiveGrid |
|---|---|---|
| API | 동일 named properties | 동일 named properties |
| 이름만 변경 | — | Flutter 와 동일 이름으로 통일, 반응형 성격을 이름에 명시 |
마이그레이션: Grid(...) → ResponsiveGrid(...) — 파라미터는 그대로이므로 클래스명만 바꾸면 됩니다.
관련 컴포넌트 (Related Components)#
- ResponsiveContainer: 단일 max-width 래퍼
- Basic: slot 기반 단일 영역 레이아웃