Breakpoint | CoUI
LogoCoUI

Breakpoint

현재 글로벌 viewport의 breakpoint/tier를 조회하는 non-widget 접근자

Breakpoint#

Breakpoint는 "지금 화면이 mobile/tablet/desktop 중 무엇인가"를 답하는 non-widget 값 접근자입니다. 렌더 산출물이 없습니다 — MediaQueryVisibility처럼 조건부로 child를 그리는 컴포넌트가 아니라, BuildContext 확장 getter(context.breakpointTier 등)로 값만 반환합니다. 값 분기(if (context.isMobile) ... else ...)는 직접 작성합니다.

Live Preview#

Web
width=0 tier=mobile coreBreakpoint=xs
Flutter
Loading Flutter...
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 유무, NavigationBarNavigationBar(rail:) 전환 등)
  • 컴포넌트 내부에서 이미 존재하는 CoreBreakpoint(Tailwind 6단계 스케일) 소비처와 상호운용할 때
  • responsive_frameworkcontext.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 와 동일 — BreakpointCoreBreakpointContract 를 구현하는 인스턴스 형태.
Breakpoint(context).tier CoreBreakpointTier context.breakpointTier와 동일.

resolveBreakpointboundaries override#

내부 resolver resolveBreakpoint(context, {boundaries:})가 모든 접근자 뒤에서 호출됩니다. boundariestier 경계를 재정의하는 유일한 진입점입니다 — 필요하면 직접 호출해 부분(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)
mobilexs0
tabletmd768
desktoplg1024

CoreBreakpoint 값 (6단계, Tailwind 표준)#

이름최소 너비 (px)
xs0
sm640
md768
lg1024
xl1280
xxl1536

마이그레이션 가이드 — 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.xsmobile tier로 판정됩니다. 첫 페인트는 항상 mobile 분기이며 클라이언트에서 다시 평가될 때 레이아웃이 바뀔 수 있습니다.
  • Flutter: MediaQuery.sizeOf(context) 의존성이 등록되므로 리사이즈 시 위젯이 자동으로 rebuild되고 tier가 따라옵니다 — 별도 배선이 필요 없습니다.

관련 컴포넌트#

  • MediaQuery — 값 대신 조건부로 child/alternateChild를 통째로 교체하고 싶을 때.
  • StageContainer — breakpoint별 padding을 자동 적용하는 반응형 컨테이너.