Breakpoint#
Breakpoint는 "지금 화면이 mobile/tablet/desktop 중 무엇인가"를 답하는 non-widget 값 접근자입니다. 렌더 산출물이 없습니다 —
MediaQueryVisibility처럼 조건부로 child를 그리는 컴포넌트가 아니라, BuildContext
확장 getter(context.breakpointTier 등)로 값만 반환합니다. 값 분기(if (context.isMobile) ... else ...)는 직접 작성합니다.
Live Preview#
class BreakpointDefaultExample extends StatefulComponent {
const BreakpointDefaultExample({super.key});
@override
State<BreakpointDefaultExample> createState() => _BreakpointDefaultExampleState();
}
class _BreakpointDefaultExampleState extends State<BreakpointDefaultExample> {
void Function()? _disposeListener;
@override
void initState() {
super.initState();
_disposeListener = listenForViewportResize(() {
if (mounted) setState(() {});
});
}
@override
void dispose() {
_disposeListener?.call();
super.dispose();
}
@override
Component build(BuildContext context) {
final breakpoint = Breakpoint(context);
final cs = context.theme.colorScheme;
return div(
[
Text(
'width=${breakpoint.width.toStringAsFixed(0)} '
'tier=${breakpoint.tier.name} '
'coreBreakpoint=${breakpoint.coreBreakpoint.name}',
).bodyMedium.onSurface,
],
classes: 'w-full p-${CoreSpace.scale.space16} rounded-${CoreRadius.scale.radius8} bg-${cs.surfaceContainer}',
);
}
}
class BreakpointDefaultExample extends StatelessWidget {
const BreakpointDefaultExample({super.key});
@override
Widget build(BuildContext context) {
final breakpoint = Breakpoint(context);
return Container(
width: double.infinity,
padding: EdgeInsets.all(CoreSpace.space16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer.toValue(),
borderRadius: BorderRadius.circular(CoreRadius.radius8),
),
child: Text(
'width=${breakpoint.width.toStringAsFixed(0)} '
'tier=${breakpoint.tier.name} '
'coreBreakpoint=${breakpoint.coreBreakpoint.name}',
).bodyMedium.onSurface,
);
}
}
프리뷰의 두
width값이 다른 것은 정상입니다 — Breakpoint 는 자기 앱의 뷰포트를 측정하므로, Web 데모는 브라우저 창 너비를, Flutter 데모는 페이지에 임베드된 Flutter 뷰(작은 앱 창)의 너비를 각각 보고합니다. 실제 Flutter 앱에서는 OS 창 너비가 측정됩니다.
라이브 프리뷰는 현재 width/tier/coreBreakpoint를 한 줄로 보여줍니다. 브라우저 창(또는 에뮬레이터)을 리사이즈하면 값이 갱신됩니다.
사용 시기#
-
레이아웃을 mobile/tablet/desktop 3단계로 분기할 때 (
Scaffold의 sidebar 유무,NavigationBar↔NavigationBar(rail:)전환 등) - 컴포넌트 내부에서 이미 존재하는
CoreBreakpoint(Tailwind 6단계 스케일) 소비처와 상호운용할 때 -
responsive_framework의context.breakpoint...접근자를 CoUI 네이티브 접근자로 교체할 때 (아래 마이그레이션 가이드 참조)
값에 따라 다른 위젯/컴포넌트를 통째로 교체하고 싶다면 Breakpoint 대신 MediaQuery(MediaQueryVisibility)를 사용하세요 — 그쪽은
child/alternateChild 슬롯을 직접 그립니다.
기본 사용법#
// 주 조회 API — BuildContext 확장 getter
final tier = context.breakpointTier; // CoreBreakpointTier.mobile/.tablet/.desktop
if (context.isDesktop) {
// sidebar 있는 레이아웃
}
// width까지 필요하면 Breakpoint(context) 인스턴스
final breakpoint = Breakpoint(context);
Text('width=${breakpoint.width} tier=${breakpoint.tier.name}');
// 주 조회 API — BuildContext 확장 getter (Flutter와 멤버명 동일)
final tier = context.breakpointTier; // CoreBreakpointTier.mobile/.tablet/.desktop
if (context.isDesktop) {
// sidebar 있는 레이아웃
}
// width까지 필요하면 Breakpoint(context) 인스턴스
final breakpoint = Breakpoint(context);
Text('width=${breakpoint.width} tier=${breakpoint.tier.name}');
⚠️ Web은 resolveBreakpoint가 one-shot 관측입니다 — SSR-safe하게 window.innerWidth(또는 서버에서는 폴백값
0)를 그 호출 시점에 한 번 읽을 뿐, 내부적으로 ResizeObserver/rebuild를 걸지 않습니다. 리사이즈에 실시간으로 반응하는 UI가 필요하면(라이브 프리뷰가 하는 것처럼) 소비 컴포넌트가 직접 작은
StatefulComponent를 만들어 window resize 리스너에서 setState를 호출하세요 —
StageContainer/Window가 자기 로컬 측정에 쓰는 것과 동일한 패턴입니다. Flutter는 MediaQuery.sizeOf(context)가
InheritedWidget 의존성을 등록하므로 위젯이 리사이즈마다 자동으로 rebuild됩니다 — 별도 배선이 필요 없습니다.
Props / Parameters#
Breakpoint는 렌더 산출물이 없는 값 접근자이므로 chrome 슬롯(xxxStyle)도, named 파라미터도 갖지 않습니다. 생성자가 받는 것은 관측 대상 컨텍스트 하나뿐입니다.
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
context |
BuildContext |
필수 (positional) | 글로벌 viewport 를 관측할 컨텍스트. Flutter·Web 동일 |
실제 소비는 생성자보다 아래 BuildContext 확장 getter 를 씁니다.
API#
Breakpoint는 렌더링하지 않으므로 named-properties 생성자 파라미터가 아니라 BuildContext 확장 getter로 노출됩니다. Flutter/Web 양쪽에서 멤버 이름이 완전히 동일합니다.
| 멤버 | 타입 | 설명 |
|---|---|---|
context.breakpointTier |
CoreBreakpointTier |
현재 tier — mobile / tablet / desktop. 가장 자주 쓰는 조회 API. |
context.coreBreakpoint |
CoreBreakpoint |
현재 활성 breakpoint(Tailwind 6단계:
xs
/
sm
/
md
/
lg
/
xl
/
xxl
).
responsive_framework
의 동명
BuildContext.breakpoint
getter와의 충돌을 피하려
breakpoint
가 아니라
coreBreakpoint
로 명명됨.
|
context.isMobile |
bool |
context.breakpointTier == CoreBreakpointTier.mobile의 단축형. |
context.isTablet |
bool |
context.breakpointTier == CoreBreakpointTier.tablet의 단축형. |
context.isDesktop |
bool |
context.breakpointTier == CoreBreakpointTier.desktop의 단축형. |
Breakpoint(context).width |
double |
관측 시점의 글로벌 viewport 폭(logical px).
BuildContext
확장에는 없고
Breakpoint
인스턴스에만 있음.
|
Breakpoint(context).coreBreakpoint |
CoreBreakpoint |
context.coreBreakpoint
와 동일 —
Breakpoint
가
CoreBreakpointContract
를 구현하는 인스턴스 형태.
|
Breakpoint(context).tier |
CoreBreakpointTier |
context.breakpointTier와 동일. |
resolveBreakpoint — boundaries override#
내부 resolver resolveBreakpoint(context, {boundaries:})가 모든 접근자 뒤에서 호출됩니다. boundaries는
tier 경계를 재정의하는 유일한 진입점입니다 — 필요하면 직접 호출해 부분(partial) override할 수 있습니다(누락된 tier는 기본값으로 폴백).
boundaries는 값(BEHAVIOUR) 파라미터입니다 — 시각 chrome이 아니므로 CoreXxxStyle 슬롯이 아니라 CoreBreakpointContract.defaultBoundaries
위에 직접 얹힙니다.
final resolved = resolveBreakpoint(
context,
boundaries: {
// tablet 경계만 sm(640px)으로 낮춤 — mobile/desktop 경계는 기본값 유지
CoreBreakpointTier.tablet: CoreBreakpoint.sm,
},
);
resolved.tier; // 커스텀 경계로 판정된 CoreBreakpointTier
resolved.breakpoint; // CoreBreakpoint
resolved.width; // double
resolveBreakpoint/ResolvedBreakpoint는 Web(coui_web)의 resolver 파일에도 Flutter와 동일한 시그니처로 존재하지만, 현재
coui_web.dart 배럴은 상위 접근자(Breakpoint/context.breakpointTier 등)만 재노출하고 resolver 자체는 재노출하지 않습니다 — Web에서
boundaries를 override하려면 지금은 context.breakpointTier가 기본 경계로 판정한 값을 소비 측에서 직접 재해석해야 합니다.
resolveBreakpoint가 배럴에 재노출되면 이 문서는 Flutter와 동일한 코드 샘플로 갱신됩니다.
기본 tier 경계값#
| Tier | 기준 CoreBreakpoint | 최소 너비 (px) |
|---|---|---|
mobile | xs | 0 |
tablet | md | 768 |
desktop | lg | 1024 |
CoreBreakpoint 값 (6단계, Tailwind 표준)#
| 이름 | 최소 너비 (px) |
|---|---|
xs | 0 |
sm | 640 |
md | 768 |
lg | 1024 |
xl | 1280 |
xxl | 1536 |
마이그레이션 가이드 — responsive_framework → CoUI#
| 상황 | Before (responsive_framework) | After (CoUI) |
|---|---|---|
| 모바일 판정 | context.breakpoint.isMobile |
context.isMobile |
| 태블릿 판정 | context.breakpoint.isTablet |
context.isTablet |
| 데스크탑 판정 | context.breakpoint.isDesktop |
context.isDesktop |
| 특정 breakpoint보다 큼 (예: TABLET 초과) | context.breakpoint.largerThan(TABLET) |
context.isDesktop (3-tier 모델에서 "TABLET보다 큼" = desktop) |
| 현재 breakpoint 이름 | context.breakpoint.name |
context.coreBreakpoint.name
(
'xs'
/
'sm'
/
'md'
/
'lg'
/
'xl'
/
'xxl'
)
|
| 현재 viewport 폭 | MediaQuery.of(context).size.width |
Breakpoint(context).width |
커스텀 breakpoint 등록 (ResponsiveBreakpoints.builder(breakpoints: [...])) |
앱 루트에서 전역 설정 | 호출부에서 resolveBreakpoint(context, boundaries: {...})로 필요한 곳만 override — 전역 설정 불필요 |
왜 이관하는가: responsive_framework는 Flutter 전용 패키지라 Web(Jaspr)에 대응이 없습니다. Breakpoint/context.breakpointTier는 Flutter·Web 양쪽에서 동일한 이름·동일한 판정 로직(CoreBreakpoint.toTier()
self-conversion)을 공유하므로, 마이그레이션 후에는 두 플랫폼이 절대 다른 결과를 내지 않습니다.
사용 가이드라인 (Usage Guidelines)#
✅ Do#
픽셀 폭이 필요하면 Breakpoint(context) 인스턴스로 조회
final width = Breakpoint(context).width;
if (width < 900) {
// 3-tier 모델로 표현 안 되는 세밀한 분기
}
BuildContext 확장 getter(context.breakpointTier 등)는 tier 값만 제공합니다. 실제 픽셀 폭이 필요하면 Breakpoint(context) 인스턴스의 .width 를 써야 합니다.
❌ Don't#
desktop tier 하나로 세밀한 반응형 분기를 흉내내지 않기
// ❌ desktop tier 하나에 1024px ~ 2560px 전부를 뭉뚱그림
if (context.isDesktop) {
// ...
}
desktop tier 는 CoreBreakpoint.lg(1024px) 이상을 전부 포함합니다. 그 안에서 더 세분화된 분기가 필요하면 3-tier 대신 context.coreBreakpoint(6단계 Tailwind 스케일: xs/sm/md/lg/xl/xxl) 또는 Breakpoint(context).width 를 직접 비교하세요.
접근성 (Accessibility)#
Breakpoint는 렌더 산출물이 없는 값 접근자입니다. Flutter·Web 어느 쪽도 위젯/컴포넌트가 아니라 DOM 노드도 semantics 노드도 만들지 않으므로, 역할·키보드·포커스·스크린 리더 announcement가 모두
해당 없음입니다. 화면에 나타나는 것은 이 값으로 분기한 소비 코드가 그린 결과뿐이고, 접근성 책임도 그쪽에 있습니다.
소비자가 알아야 할 것#
이 접근자는 CoreBreakpointTier 값 하나를 돌려줄 뿐이므로, 접근성 측면에서 실제로 문제가 되는 지점은 그 값이 언제 다시 평가되는가입니다.
-
Web:
resolveBreakpoint는 호출 시점에 viewport 폭을 한 번 읽는 one-shot 관측이며ResizeObserver나 rebuild 배선이 없습니다. 창 리사이즈뿐 아니라 확대(zoom)로 인한 리플로우에서도 소비 컴포넌트가 스스로 다시 빌드하지 않으면 tier가 갱신되지 않아, 확대한 사용자가 이전 tier의 레이아웃을 계속 보게 됩니다. 리사이즈에 반응해야 한다면 위 ⚠️ 항목대로 소비 측에서resize리스너와setState를 직접 배선하세요. -
SSR: 서버에서는 폭이
0으로 폴백되어CoreBreakpoint.xs→mobiletier로 판정됩니다. 첫 페인트는 항상 mobile 분기이며 클라이언트에서 다시 평가될 때 레이아웃이 바뀔 수 있습니다. -
Flutter:
MediaQuery.sizeOf(context)의존성이 등록되므로 리사이즈 시 위젯이 자동으로 rebuild되고 tier가 따라옵니다 — 별도 배선이 필요 없습니다.
관련 컴포넌트#
-
MediaQuery — 값 대신 조건부로
child/alternateChild를 통째로 교체하고 싶을 때. - StageContainer — breakpoint별 padding을 자동 적용하는 반응형 컨테이너.