Flutter 공식 유튜브에 Flutter Technique of the Week라는 재생목록이 있습니다. 플러터에서 제공하는 여러 기술적인 부분들을 알려주는 영상들을 모아둔 것이죠. 이 재생목록의 영상들을 하나씩 보면서, 해당 내용과 관련한 부분들을 묶어서 정리해 보았습니다.

MediaQuery란?

위 영상은 Flutter 공식 유튜브에서 올린 MediaQuery 소개 영상입니다. 간단하게 MediaQuery에 대해 살펴볼 수 있으니, 미리 보는 것을 추천합니다.

Flutter 공식문서의 설명을 보면 아래와 같습니다.

  • MediaQuery : child Widget에게 MediaQueryData를 제공하는 Widget
  • MediaQueryData : 미디어에 대한 정보. 좀 더 상세히 설명하면, 디바이스 관련 정보 및 유저가 설정한 레이아웃 관련 값들(기본 폰트 사이즈 등)을 의미합니다.

사실 Flutter 유튜브 영상도 그렇고, 대다수의 블로그 글이나 책에서는 MediaQuery가 디바이스 관련 정보를 제공한다고 설명하는 경우가 많습니다. MediaQuery가 디바이스 관련 정보를 제공하는 것은 맞습니다. 그러나, 이것이 MediaQuery가 디바이스 관련 정보를 의미하는 클래스라는 뜻은 아닙니다. 위에서 설명한 것처럼, MediaQueryData가 디바이스 및 시스템 레이아웃 관련 정보를 가진 클래스이고, MediaQueryData를 제공하는 Widget이 MediaQuery입니다.
(다만 사실상 MediaQuery의 용도가 MediaQueryData를 얻기 위한 것이기 때문에, 둘의 용도가 동일하다고 봐도 무방합니다.)

MediaQuery의 주요 속성

MediaQuery의 주요 속성은 아래와 같습니다. 위에서 언급한 것처럼 디바이스 관련 정보 및 유저가 설정한 UI 관련 옵션 정보를 가지고 있으며, 사실상 MediaQueryData의 속성값이라고 봐도 무방합니다.

참고로 GetX에서는 아래 속성들에 접근할 수 있는 별도의 메서드를 제공하고 있으니, MediaQuery 대신 GetX에서 제공하는 메서드를 사용하는 것이 좋습니다.
예를 들어, 기존 MediaQuery의 devicePixelRatio 값은 GetX에선 Get.context.devicePixelRatio를 통해 context 없이 접근할 수 있습니다.

size

논리 픽셀 단위의 화면 크기를 나타내는 속성입니다.

  • 타입 : Size
  • 접근 메서드 : MediaQuery.sizeOf(context) 혹은 MediaQuery.of(context).size
  • 용도 : 적응형 레이아웃을 짤 때 사용합니다.
  • 예시 : Flutter에서 context Extension을 통해 반응형 레이아웃을 구현하는 방법
    (해당 글에서 소개하는 3개의 방법 중 3번째 방법인 'Extension BuildContext'를 읽어보시면 됩니다. 개인적으로 반응형 레이아웃을 구현한 사례 중 가장 범용적이고 쉽게 사용할 수 있다고 생각합니다.)
  • 주의점 : 릴리스 모드에서 앱을 켜자마자 size를 호출할 경우, 정상적인 크기가 아닌 Size.zero가 리턴될 수 있습니다.

padding, viewPadding, viewInsets

사실 셋은 비슷한 듯 다른 값입니다. 각각을 여기서 상세하게 다루고 싶지만, 그러기엔 양이 너무 많은 관계로 여기선 셋의 차이 위주로 짚겠습니다.

  • padding : 시스템 UI(상단의 상태 바, 하단의 홈 버튼 등)에 의해 가려지지 않고, 앱의 콘텐츠가 안전하게 표시될 수 있는 padding 영역
    • 용도 : 상태 바, 홈 인디케이터, 노치 등의 고정된 시스템 UI 요소에 가려지지 않도록 UI를 배치할 때 사용합니다.
      (다만 이 값을 이용해 UI를 그릴 경우, 키보드가 올라올 때 해당 UI가 가려질 수 있습니다. 키보드에도 가려지지 않는 UI를 그리려면 아래의 viewPadding이나 viewInsets를 사용해야 합니다.)
    • 접근 메서드 : MediaQuery.paddingOf(context) 혹은 MediaQuery.of(context).padding
  • viewPadding : 시스템 UI(화면 키보드, 소프트 네비게이션 바 등)에 의해 가려질 수 있는 화면의 실제 물리적 여백
    • 키보드가 열리면 padding은 변화하지만, viewPadding은 변하지 않습니다.
    • 용도 : 시스템 UI에 의해 가려지지 않는 영역에 UI를 그릴 때 사용합니다.
    • 접근 메서드 : MediaQuery.viewPaddingOf(context) 혹은 MediaQuery.of(context).viewPadding
  • viewInsets : 시스템 UI가 화면을 가리거나 덮는 영역. 주로 키보드나 소프트 내비게이션 바가 차지하는 영역을 계산할 때 사용합니다.
    • 용도 : 입력창이 있는 화면에서 viewInsets 값을 통해 키보드가 표시되었는지 여부를 감지할 때 유용합니다. 혹은 키보드가 열릴 때 한정으로, 화면 하단에 여백을 추가하여 텍스트 필드나 버튼이 키보드에 의해 가려지지 않도록 처리할 때 유용합니다.
    • 접근 메서드 : MediaQuery.viewInsetsOf(context) 혹은 MediaQuery.of(context).viewInsets

orientation

현재 화면의 방향을 나타내는 속성입니다.

  • 타입 : Orientation
    • Orientation.portrait : 세로 방향
    • Orientation.landscape : 가로 방향
  • 접근 메서드 : MediaQuery.orientationOf(context) 혹은 MediaQuery.of(context).orientation
  • 용도 : 화면 방향별 맞춤 UI를 제공할 때 사용합니다.

devicePixelRatio

디바이스의 물리적 픽셀과 논리적 픽셀의 비율을 나타내는 값입니다. 간단히 말해, 디바이스가 얼마나 많은 물리적 픽셀을 사용해 하나의 논리적 픽셀을 렌더링하는지를 의미합니다.

  • 타입 : double
  • 접근 메서드 : MediaQuery.devicePixelRatioOf(context) 혹은 MediaQuery.of(context).devicePixelRatio
  • 용도 : 디바이스별로 devicePixelRatio에 맞는 레이아웃을 제공하거나, 이미지 등의 시각적 에셋을 devicePixelRatio별로 제공하는 데에 사용합니다.
  • 예시 코드 : devicePixelRatio 값에 맞는 이미지 에셋을 제공하는 방법
    아래처럼 각 배율별 폴더를 만들고, 그 안에 해당하는 이미지를 넣으면 된다.
flutter:
  assets:
    - assets/images/my_image.png
    - assets/images/2.0x/my_image.png
    - assets/images/3.0x/my_image.png

displayFeatures

디바이스 화면의 특정 기능이 있는 영역들에 대한 정보를 제공하는 속성입니다. 여기서 말하는 특정 기능은 접히는 화면, 노치(notch), 카메라 컷아웃, 힌지와 같은 디스플레이 영역을 의미합니다.

  • 타입 : List<DisplayFeature>
  • 접근 메서드 : MediaQuery.displayFeaturesOf(context) 혹은 MediaQuery.of(context).displayFeatures
  • 용도
    • 폴더블폰에 대한 맞춤형 UI 제공 : 폴더블 기기에서 화면을 접거나 펼쳤을 때 각 상황에 맞는 UI 제공
    • 두 개 이상의 화면을 사용하는 장치에서 화면별 분할 레이아웃 제공
    • 노치 및 카메라 컷아웃 영역과 UI가 겹치는 상황 방지하기
  • 안드로이드폰에서만 사용 가능한 속성입니다. (2024.10 기준. 추후 변경될 여지가 있습니다.)

플랫폼이 사용하는 입력 방식을 나타내는 속성입니다. 키보드+마우스 입력 방식을 사용하는지, 터치 입력 방식을 사용하는지를 나타냅니다.

  • 타입 : NavigationMode
    • NavigationMode.traditional : 키보드+마우스 입력 방식
    • NavigationMode.directional : 터치 기반 입력 방식
  • 접근 메서드 : MediaQuery.navigationModeOf(context) 혹은 MediaQuery.of(context).navigationMode
  • 용도 : 키보드+마우스 입력 방식과 터치 기반 입력 방식을 둘 다 지원해야 하는 앱에서 각 입력 방식에 따른 이벤트를 처리할 때 사용할 수 있다.

textScaler

가독성을 높이기 위해 애플리케이션 내 텍스트 크기를 사용자의 접근성 설정에 따라 유연하게 조정할 수 있도록 도와주는 속성입니다.

  • 타입 : TextScaler
  • 접근 메서드 : MediaQuery.textScalerOf(context) 혹은 MediaQuery.of(context).textScaler
  • 용도 : 사용자 접근성 설정에 맞춘 더 유연한 텍스트 스타일링을 지원할 때 사용합니다. 예를 들어, 휴대폰 폰트 크기를 굉장히 크게 설정해서 사용하는 유저에 맞춰서 앱 내의 폰트 크기도 크게 키우기 위해 이 속성을 사용할 수 있습니다.
  • 예시 코드
Text(
	'Hello, World!',
	style: Theme.of(context).textTheme.bodyText1?.copyWith(
		fontSize: MediaQuery.textScalerOf(context).scale(16),
	),
);
  • 기존 textScaleFactor를 대체하는 속성입니다. (textScaleFactor는 deprecated됨)
    textScaleFactor는 앱 전체의 텍스트 크기를 한꺼번에 변경하는 반면, textScaler는 각 텍스트별로 크기를 조정할 수 있기 때문에 더 유용합니다.

boldText

사용자의 접근성 설정 중 "글자 굵게(안드로이드)/볼드체 텍스트(iOS)" 옵션이 활성화되어 있는지를 나타내는 속성입니다.

  • 타입 : bool
  • 접근 메서드 : MediaQuery.boldTextOf(context) 혹은 MediaQuery.of(context).boldText
  • 용도 : 사용자의 "글자 굵게(안드로이드)/볼드체 텍스트(iOS)" 설정을 확인해서, 설정에 맞는 글꼴을 제공하는 데에 사용합니다.

alwaysUse24HourFormat

시각 관련 값을 24시 단위로 포맷팅할지 12시간 단위로 포맷팅할지를 결정하는 속성입니다.

  • 타입 : bool
  • 접근 메서드 : MediaQuery.alwaysUse24HourFormatOf(context) 혹은 MediaQuery.of(context).alwaysUse24HourFormat
  • 용도 : 주로 시각 관련 Widget의 시각을 24시간 단위로 표시하거나, 12시간 단위로 표시하고자 할 때 사용합니다. (관련 링크 : showTimePicker에서 24시간 단위로 시각을 표시하는 방법)
  • 안드로이드에선 사용자 설정 중 "24시간 형식 사용"이 켜져 있는 경우 true가 됩니다. 이 값은 앱에서 따로 설정한 custom locale에도 적용됩니다.
  • iOS에선 사용자 설정 중 "24시간제"가 켜져 있거나, 시스템 locale이 24시 단위 포맷팅을 사용하는 경우 true가 됩니다.

disableAnimations

플랫폼에서 애니메이션을 비활성화하거나 단순화할 것을 요청하는지 여부를 나타내는 속성입니다. 사용자의 접근성 설정 중 "애니메이션 삭제(안드로이드)/동작 줄이기(iOS)"가 활성화되어 있으면 true를 리턴합니다.

  • 타입 : bool
  • 접근 메서드 : MediaQuery.disableAnimationsOf(context) 혹은 MediaQuery.of(context).disableAnimations
  • 용도 : 만약 이 값이 true라면 앱의 애니메이션을 비활성화하거나, 최소화함으로써 더 나은 사용자 접근성을 제공할 수 있다.

gestureSettings

사용자의 터치 및 제스처와 관련된 설정 정보를 제공하는 속성입니다.

  • 타입 : GestureSettings
  • 접근 메서드 : MediaQuery.gestureSettingsOf(context) 혹은 MediaQuery.of(context).gestureSettings
  • 용도 : 사용자가 플랫폼(안드로이드/iOS 디바이스)에 대해 설정한 터치 민감도나 제스처 크기 설정을 반영하여, 앱에서 맞춤형 터치 인식을 구현하거나 제스처 감지를 조정할 수 있습니다.
    • 단일 터치/멀티 터치 감도 조정
    • 접근성 지원 : 특정 사용자들을 위해서, 더 쉽게 터치를 할 수 있도록 지원

highContrast

사용자의 접근성 설정 중 고대비 옵션이 켜져있는지를 나타내는 속성입니다.

  • 타입 : bool
  • 접근 메서드 : MediaQuery.highContrastOf(context) 혹은 MediaQuery.of(context).highContrast
  • 용도 : 사용자가 고대비 옵션으로 폰을 사용하는 경우, 앱에서도 똑같이 고대비 옵션을 제공하기 위해 사용할 수 있습니다.
  • iOS폰에서만 사용 가능한 속성입니다. 정확히는, iOS 13 이상의 환경에서만 사용 가능합니다.
    (2024.10 기준. 추후 변경될 여지가 있습니다.)

invertColors

사용자의 접근성 설정 중 '색상 반전'이 켜져있는지를 나타내는 속성입니다.

  • 타입 : bool
  • 접근 메서드 : MediaQuery.invertColorsOf(context) 혹은 MediaQuery.of(context).invertColors
  • 용도 : 사용자가 '색상 반전' 옵션으로 폰을 사용하는 경우, 앱에서도 똑같이 색상을 반전시킨 UI를 제공하기 위해 사용할 수 있습니다.
  • iOS폰에서만 사용 가능한 속성입니다. (2024.10 기준. 추후 변경될 여지가 있습니다.)

accessibleNavigation

사용자가 TalkBack 혹은 VoiceOver와 같은 접근성 서비스를 사용하고 있는지 여부를 나타내는 속성입니다.

  • 타입 : bool
  • 접근 메서드 : MediaQuery.accessibleNavigationOf(context) 혹은 MediaQuery.of(context).accessibleNavigation
  • 용도 : 접근성 서비스를 사용하는 유저들을 위한 맞춤 기능을 제공할 때 사용합니다.
    • 예를 들어, 사용자가 접근성 서비스를 사용 중인 경우 특정 기능이나 로직을 사용하기 어려운 경우가 있습니다. 이럴 때 별도의 처리를 위해 accessibleNavigation를 통해 접근성 서비스를 사용 중인지 확인하고, 이에 알맞는 로직을 짤 수 있습니다.

onOffSwitchLabels

사용자가 iOS의 스위치 내부에 라벨을 표시하도록 설정했는지 여부를 나타내는 속성입니다. iOS에서 '설정 > 손쉬운 사용 > 디스플레이 및 텍스트 크기 > 켬/끔 레이블'에 해당하는 값입니다.

  • 타입 : bool
  • 접근 메서드 : MediaQuery.onOffSwitchLabelsOf(context) 혹은 MediaQuery.of(context).onOffSwitchLabels
  • 용도 : 사실 이거... 용도를 잘 모르겠습니다. 만약 커스텀 스위치를 따로 구현해서 쓰는 경우라면, iOS에서 해당 설정을 켠 유저들을 위해 커스텀 스위치에도 동일한 라벨을 표시해주기 위해 사용할 수 있지 않을까 싶네요.
  • 만약 이 값이 true라면 스위치 내부에 'O'나 '|'와 같은 표시가 나옵니다.
  • iOS에서만 사용 가능한 속성입니다.

platformBrightness

플랫폼의 현재 테마입니다. 정확히는, 현재 플랫폼이 라이트모드(라이트 테마)를 사용하는지, 다크모드(다크 테마)를 사용하는지를 나타내는 속성입니다.

  • 타입 : Brightness
    • Brightness.dark : 다크모드
    • Brightness.light : 라이트모드
  • 접근 메서드 : MediaQuery.platformBrightnessOf(context) 혹은 MediaQuery.of(context).platformBrightness
  • 용도 : 현재 플랫폼의 테마에 맞는 앱 테마를 제공하기 위해 사용합니다. 예를 들어, 현재 플랫폼이 다크모드를 사용한다면, 앱에서 다크모드에 맞는 테마를 제공하고자 할 때, platformBrightness 값을 통해 현재 플랫폼의 테마를 확인할 수 있습니다.
  • 특정 플랫폼에선 이 기능을 지원하지 않을 수도 있습니다. 이 경우 Brightness.light를 리턴합니다.

supportsShowingSystemContextMenu

디바이스가 시스템 컨텍스트 메뉴(복사, 붙여넣기 등)를 표시할 수 있는지를 나타내는 속성입니다. 이 속성은 특히 모바일과 데스크톱 간의 차이를 처리하는 데 유용합니다.

  • 타입 : bool
  • 접근 메서드 : MediaQuery.supportsShowingSystemContextMenuOf(context) 혹은 MediaQuery.of(context).supportsShowingSystemContextMenu
  • 용도 : 특정 상황에서 시스템 컨텍스트 메뉴가 지원되지 않는 경우, 앱에서 자체적으로 메뉴 UI를 보여주거나, 관련 기능을 사용할 수 없도록 안내할 때 사용합니다.

systemGestureInsets

시스템 제스처가 차지하는 영역에 대한 여백을 나타내는 속성입니다. 이 여백은 보통 시스템 제스처(예: 스와이프 제스처)가 차지하는 화면의 경계 부분을 의미합니다.

  • 타입 : EdgeInsets
  • 접근 메서드 : MediaQuery.systemGestureInsetsOf(context) 혹은 MediaQuery.of(context).systemGestureInsets
  • 용도 : Android Q 이후 버전의 안드로이드 폰에서 슬라이더를 사용할 때, 슬라이더로 스와이프 제스처가 전달되지 않는 경우가 있습니다. 더 정확히는, 슬라이더 위에서 스와이프를 할 때, 슬라이더가 움직이는 대신 페이지 네비게이션이 작동해서 이전 페이지나 다음 페이지로 이동하는 경우가 있습니다. 이 문제를 해결하기 위해 사용합니다.
    (해당 내용에 대한 더 자세한 설명은 링크를 참고해주세요.)

MediaQuery.of VS MediaQuery.propertyOf

이제부턴, 'Flutter Technique of the Week' 영상에 나온 이야기를 해 보겠습니다.
Flutter에서는 MediaQuery를 사용할 때 MediaQuery.propertyOf(context)를 사용할 것을 권장하고 있습니다. 왜 그럴까요? 무슨 차이가 있는지 자세히 알아봅시다.

MediaQuery.of(context)가 비효율적인 이유

: MediaQuery의 하위 속성 중 하나라도 변경된다면 MediaQuery 자체가 rebuild되기 때문에 지나친 rebuild로 인한 리소스 낭비가 발생할 수 있습니다. (심지어 현재 클래스에서 사용하지 않는 속성값이 변경되더라도 rebuild가 발생합니다.)
(참고 : 정확히는, MediaQuery의 속성 중 하나인 MediaQueryData의 필드 값 중 하나라도 변하면 rebuild가 발생합니다.)

위에서 살펴본 것처럼, MediaQuery의 속성은 상당히 많습니다. 20개가 넘어가죠. 이 중 실제로 우리가 사용하는 값은 많아봐야 1~2개입니다. 그렇기에, 우리가 사용하는 이 1~2개의 값이 변할 때만 rebuild가 일어나면 되는 거죠. 하지만, MediaQuery.of(context)는 현재 코드에서 사용하지 않는 하위 속성이 변경되더라도 rebuild가 일어납니다.

예시를 통해 좀 더 자세히 알아보죠.

  • 가정 : 입력창을 가지고 있는 화면을 하나 상상해 봅시다.
    그리고 입력창의 크기를 조정하기 위해, MediaQuery에서 size 관련 값만 사용하고 있다고 가정해 봅시다. 코드상에선 당연히 MediaQuery.of(context).size로 사용할 겁니다.
  • 상황 : 사용자가 입력창을 클릭하면
    -> 입력창에 포커스가 가면서 키보드가 올라오고, MediaQuery의 padding 관련 값이 계속 변함
    -> padding값이 계속 변하면서 지속적으로 MediaQuery가 rebuild됨
  • 결론 : 위 가정에서 MediaQuery.of(context).padding을 사용하지 않았음에도 불구하고, 해당 값이 변할 때마다 MediaQuery가 rebuild되는 비효율적인 상황이 발생합니다.

MediaQuery.propertyOf(context)는 다르다!

: 코드에서 사용하는 'property'에 해당하는 값이 변할 때만 MediaQuery를 rebuild하기 때문에, MediaQuery의 rebuild 횟수를 크게 줄일 수 있습니다.

결론 : MediaQuery를 사용한다면 MediaQuery.of(context) 대신 MediaQuery.propertyOf(context)를 사용하자!

profile
Flutter 메인의 풀스택 개발자 / 한양대 컴퓨터소프트웨어학과, HUHS의 화석

1개의 댓글

comment-user-thumbnail
2024년 12월 15일

수몽 나 윤광

답글 달기