Flutter 튜토리얼 36편: 플랫폼별 자동 적응

요약#

핵심 요지#

  • 문제 정의: 같은 Flutter 코드가 Android와 iOS에서 다르게 동작해야 할 때, 플랫폼별 코드를 일일이 작성하면 복잡해진다.
  • 핵심 주장: Flutter는 OS 환경 동작(스크롤, 텍스트 편집)을 자동으로 적응하고, UI 컴포넌트는 adaptive()1 생성자로 플랫폼별 위젯을 선택할 수 있다.
  • 주요 근거: iOS 사용자는 edge swipe로 뒤로 가기를, Android 사용자는 back 버튼을 기대한다. 스크롤 물리도 플랫폼마다 다르게 느껴져야 자연스럽다.
  • 실무 기준: 네비게이션, 스크롤, 텍스트 입력은 Flutter가 자동 처리하고, Switch, Slider, AlertDialog 등은 .adaptive() 생성자를 사용한다.

문서가 설명하는 범위#

  • 자동 적응되는 네비게이션 전환 애니메이션
  • 플랫폼별 스크롤 물리와 오버스크롤 동작
  • 텍스트 편집 시 플랫폼별 제스처 차이
  • .adaptive() 생성자를 제공하는 위젯 목록
  • 앱 바와 하단 네비게이션 바 커스터마이징

읽는 시간: 15분 | 난이도: 중급


참고 자료#


문제 상황#

Flutter 앱을 만들었는데 Android에서는 자연스럽지만 iOS에서는 어색합니다. iOS 사용자가 화면 왼쪽 가장자리를 스와이프해도 뒤로 가기가 안 됩니다. 스크롤할 때 iOS 특유의 바운스 효과가 없어서 이상하게 느껴집니다.

// 커스텀 페이지 전환 - 플랫폼 특성 무시
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(),
),
);

문제는 다음과 같습니다.

  • 네비게이션 전환 애니메이션이 플랫폼에 맞지 않다.
  • 스크롤 물리가 iOS/Android 고유의 느낌을 주지 않는다.
  • 텍스트 선택, 복사 등 편집 동작이 익숙하지 않다.
  • Switch, Slider 같은 컨트롤이 플랫폼 스타일과 다르다.

해결 방법#

Flutter는 두 가지 유형의 플랫폼 적응을 구분합니다.

  1. OS 환경 동작: 스크롤, 텍스트 편집처럼 다르면 ‘잘못된’ 느낌을 주는 것 → 자동 적응
  2. OEM SDK 컨벤션: AlertDialog, Switch처럼 플랫폼 관례를 따르는 것 → .adaptive() 생성자로 선택

챕터 1: 네비게이션 전환 애니메이션#

Why#

NOTE

Android와 iOS는 화면 전환 애니메이션이 다릅니다. Android는 startActivity()2 스타일로 아래에서 위로 올라오고, iOS는 오른쪽에서 왼쪽으로 슬라이드합니다.

사용자가 매일 사용하는 앱들과 동일한 전환 효과가 없으면 앱이 어색하게 느껴집니다.

What#

NOTE

Flutter의 Navigator.push()3는 플랫폼에 따라 다른 전환 애니메이션을 적용합니다.

플랫폼기본 전환fullscreenDialog 전환
Android줌(Zoom) 효과아래에서 위로
iOS오른쪽에서 왼쪽 슬라이드아래에서 위로 (Present)

iOS에서는 CupertinoNavigationBar4의 각 요소도 함께 애니메이션됩니다.

How#

TIP

기본 Navigator.push() 사용 - 자동 적응

class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: const HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Center(
child: ElevatedButton(
onPressed: () {
// Flutter가 플랫폼에 맞는 전환 애니메이션 자동 적용
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DetailPage(),
),
);
},
child: const Text('Go to Detail'),
),
),
);
}
}

모달 스타일 전환 (아래에서 위로)

Navigator.push(
context,
MaterialPageRoute(
// fullscreenDialog: true로 설정하면
// 두 플랫폼 모두 아래에서 위로 올라오는 전환 사용
fullscreenDialog: true,
builder: (context) => const SettingsPage(),
),
);

Watch out#

WARNING

뒤로 가기 동작도 자동 적응됩니다

  • Android: 시스템 뒤로 가기 버튼이 Navigator.pop() 호출
  • iOS: 화면 왼쪽 가장자리 스와이프로 뒤로 가기

iOS edge swipe를 비활성화하려면 주의가 필요합니다.

// iOS에서 swipe back 비활성화가 필요한 경우
// (예: 결제 화면에서 실수로 뒤로 가기 방지)
MaterialPageRoute(
builder: (context) => const PaymentPage(),
// 전체 화면 Dialog는 기본적으로 swipe back 비활성화
fullscreenDialog: true,
)

결론: Navigator.push()와 MaterialPageRoute를 사용하면 플랫폼별 전환 애니메이션이 자동으로 적용됩니다.


챕터 2: 스크롤 물리 적응#

Why#

NOTE

스크롤은 사용자가 가장 자주 하는 동작입니다. iOS와 Android는 스크롤 물리가 완전히 다르고, 사용자는 이 차이에 매우 민감합니다.

  • iOS: 관성이 크고, 속도가 천천히 올라가며, 느린 속도에서도 미끄러집니다.
  • Android: 정적 마찰이 크고, 빠르게 가속되며, 더 멀리 스크롤됩니다.

What#

NOTE

Flutter는 ScrollPhysics5를 플랫폼에 따라 자동으로 선택합니다.

동작AndroidiOS
오버스크롤Glow 효과 (Material 색상)바운스 (당겨지는 느낌)
모멘텀일정함연속 플링 시 누적
상단 복귀없음상태 바 탭으로 맨 위 이동

ListView, SingleChildScrollView, CustomScrollView 모두 이 동작을 자동으로 따릅니다.

How#

TIP

기본 스크롤 - 자동 적응

class AutoAdaptiveScroll extends StatelessWidget {
const AutoAdaptiveScroll({super.key});
@override
Widget build(BuildContext context) {
return ListView.builder(
// physics 파라미터를 지정하지 않으면
// 플랫폼에 맞는 ScrollPhysics가 자동 적용
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(title: Text('Item $index'));
},
);
}
}

특정 플랫폼 물리 강제 적용

// iOS 스타일 바운스를 Android에서도 사용하고 싶은 경우
ListView.builder(
physics: const BouncingScrollPhysics(),
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(title: Text('Item $index'));
},
)
// Android 스타일 Clamp를 iOS에서도 사용하고 싶은 경우
ListView.builder(
physics: const ClampingScrollPhysics(),
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(title: Text('Item $index'));
},
)

Watch out#

WARNING

iOS 전용 기능: 상태 바 탭으로 맨 위 이동

이 기능은 PrimaryScrollController6에 연결된 스크롤러에서만 동작합니다.

// Scaffold의 body에 직접 넣은 ListView는 자동으로 동작
Scaffold(
body: ListView.builder(...), // 상태 바 탭 지원
)
// 중첩된 ListView나 여러 스크롤러가 있으면 주의
Scaffold(
body: Column(
children: [
Expanded(child: ListView.builder(...)), // primary: true 필요할 수 있음
],
),
)

결론: ListView와 스크롤 위젯은 플랫폼에 맞는 물리 효과를 자동으로 적용합니다.


챕터 3: 텍스트 편집 동작 적응#

Why#

NOTE

텍스트 편집은 플랫폼마다 제스처가 다릅니다. Android에서 길게 누르면 단어가 선택되지만, iOS에서 길게 누르면 커서가 이동합니다.

사용자는 이런 세부 동작에 익숙해져 있어서, 다르게 동작하면 불편함을 느낍니다.

What#

NOTE

Flutter의 TextField7CupertinoTextField8는 플랫폼별 텍스트 편집 동작을 자동으로 적용합니다.

제스처AndroidiOS
탭한 위치에 커서가장 가까운 단어 경계에 커서
길게 누르기단어 선택 + 툴바커서 위치 지정 + 툴바
길게 누른 채 드래그선택 영역 확장커서 이동
더블 탭단어 선택단어 선택
키보드 커서 이동스페이스바 스와이프3D Touch 플로팅 커서

텍스트 선택 툴바도 플랫폼에 맞는 디자인이 표시됩니다.

How#

TIP

기본 TextField 사용 - 자동 적응

class AdaptiveTextField extends StatelessWidget {
const AdaptiveTextField({super.key});
@override
Widget build(BuildContext context) {
return const TextField(
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Enter text',
),
// 텍스트 편집 제스처는 플랫폼에 맞게 자동 적응
);
}
}

맞춤법 검사도 자동 적응

TextField(
// 맞춤법 검사 활성화 - 플랫폼별 맞춤법 검사 사용
spellCheckConfiguration: SpellCheckConfiguration(),
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Type with spell check',
),
)

결론: TextField는 탭, 길게 누르기, 드래그 등 모든 텍스트 편집 제스처를 플랫폼에 맞게 자동 적응합니다.


챕터 4: 타이포그래피와 아이콘 적응#

Why#

NOTE

Android는 Roboto 폰트를, iOS는 San Francisco 폰트를 사용합니다. 아이콘도 플랫폼마다 디자인 스타일이 다릅니다.

폰트와 아이콘이 플랫폼에 맞지 않으면 앱이 이질적으로 느껴집니다.

What#

NOTE

폰트 자동 적응

패키지AndroidiOS
MaterialRobotoSan Francisco
Cupertino(폴백 폰트)San Francisco

아이콘 자동 적응

Material 패키지의 일부 아이콘은 플랫폼에 따라 다른 디자인을 표시합니다.

아이콘AndroidiOS
뒤로 가기화살표 + 막대단순 화살표(<)
더보기세로 점 3개가로 점 3개

How#

TIP

플랫폼 적응 아이콘 사용

AppBar(
leading: IconButton(
// 플랫폼에 맞는 뒤로 가기 아이콘 자동 선택
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
),
actions: [
IconButton(
// 플랫폼에 맞는 더보기 아이콘 자동 선택
icon: const Icon(Icons.more_vert),
onPressed: () {},
),
],
)

Icons.adaptive 사용하기

// 플랫폼 적응 아이콘 명시적 사용
Icon(Icons.adaptive.arrow_back)
Icon(Icons.adaptive.share)
Icon(Icons.adaptive.more)

Watch out#

WARNING

San Francisco 폰트 라이선스 제한

San Francisco 폰트는 iOS, macOS, tvOS에서만 사용 가능합니다. Android에서 Cupertino 테마를 사용하면 폴백 폰트가 적용됩니다.

// Android에서 Cupertino 테마 사용 시 주의
CupertinoApp(
theme: const CupertinoThemeData(
// Android에서는 San Francisco 대신 다른 폰트 사용됨
),
)

결론: Material 패키지는 폰트와 일부 아이콘을 플랫폼에 맞게 자동으로 선택합니다.


챕터 5: .adaptive() 생성자 위젯#

Why#

NOTE

Switch, Slider, AlertDialog 같은 컨트롤은 OS와 밀접하게 통합됩니다. 사용자는 이런 컨트롤을 보고 즉시 어떻게 사용하는지 인식합니다.

플랫폼 컨벤션을 따르면 사용자가 더 빠르게 적응할 수 있습니다.

What#

NOTE

Flutter는 .adaptive() 생성자를 제공하는 위젯이 있습니다. iOS에서는 Cupertino 버전으로, Android에서는 Material 버전으로 렌더링됩니다.

Material 위젯Cupertino 위젯적응 생성자
SwitchCupertinoSwitchSwitch.adaptive()
SliderCupertinoSliderSlider.adaptive()
CircularProgressIndicatorCupertinoActivityIndicatorCircularProgressIndicator.adaptive()
RefreshProgressIndicatorCupertinoActivityIndicatorRefreshIndicator.adaptive()
CheckboxCupertinoCheckboxCheckbox.adaptive()
RadioCupertinoRadioRadio.adaptive()
AlertDialogCupertinoAlertDialogAlertDialog.adaptive()

How#

TIP

Switch.adaptive() 사용

class AdaptiveSwitchDemo extends StatefulWidget {
const AdaptiveSwitchDemo({super.key});
@override
State<AdaptiveSwitchDemo> createState() => _AdaptiveSwitchDemoState();
}
class _AdaptiveSwitchDemoState extends State<AdaptiveSwitchDemo> {
bool _value = false;
@override
Widget build(BuildContext context) {
return Switch.adaptive(
// iOS에서는 CupertinoSwitch로,
// Android에서는 Material Switch로 렌더링
value: _value,
onChanged: (value) => setState(() => _value = value),
);
}
}

AlertDialog.adaptive() 사용

void _showAdaptiveDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog.adaptive(
// iOS에서는 CupertinoAlertDialog 스타일,
// Android에서는 Material AlertDialog 스타일
title: const Text('Confirm'),
content: const Text('Are you sure you want to proceed?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
// 확인 동작
},
child: const Text('OK'),
),
],
),
);
}

CircularProgressIndicator.adaptive() 사용

class LoadingIndicator extends StatelessWidget {
const LoadingIndicator({super.key});
@override
Widget build(BuildContext context) {
return const Center(
// iOS: 스피너 스타일
// Android: 원형 진행 표시기
child: CircularProgressIndicator.adaptive(),
);
}
}

결론: .adaptive() 생성자를 사용하면 플랫폼에 맞는 네이티브 스타일 컨트롤이 자동으로 선택됩니다.


챕터 6: 앱 바와 네비게이션 바 커스터마이징#

Why#

NOTE

앱 바는 자동 적응되지 않습니다. 하지만 iOS 사용자를 위해 Apple Human Interface Guidelines 스타일에 맞게 커스터마이징할 수 있습니다.

iOS에서 Material 앱 바가 어색하게 느껴질 수 있습니다.

What#

NOTE

Top App Bar 비교

속성Material 3iOS HIG
높이64dp44pt
그림자스크롤 시 약간거의 없음
타이틀 스타일작음크게 (Large Title)

Bottom Navigation Bar 비교

속성Material 3iOS Tab Bar
레이블선택 시 표시항상 표시
선택 표시상단 인디케이터색상 변경

How#

TIP

iOS 스타일 앱 바 적용

import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class AdaptiveAppBar extends StatelessWidget {
const AdaptiveAppBar({super.key});
@override
Widget build(BuildContext context) {
// iOS 텍스트 테마 매핑
final cupertinoTextTheme = TextTheme(
headlineMedium: const CupertinoThemeData()
.textTheme
.navLargeTitleTextStyle
.copyWith(letterSpacing: -1.5),
titleLarge: const CupertinoThemeData()
.textTheme
.navTitleTextStyle,
);
return MaterialApp(
theme: ThemeData(
// iOS에서는 Cupertino 스타일 텍스트 테마 사용
textTheme: Platform.isIOS ? cupertinoTextTheme : null,
),
home: Scaffold(
appBar: AppBar(
title: const Text('Adaptive App'),
// iOS 스타일 적용
surfaceTintColor: Platform.isIOS ? Colors.transparent : null,
shadowColor: Platform.isIOS
? CupertinoColors.darkBackgroundGray
: null,
scrolledUnderElevation: Platform.isIOS ? 0.1 : null,
toolbarHeight: Platform.isIOS ? 44 : null,
),
body: const Center(child: Text('Content')),
),
);
}
}

플랫폼별 하단 네비게이션 바

class AdaptiveBottomNav extends StatefulWidget {
const AdaptiveBottomNav({super.key});
@override
State<AdaptiveBottomNav> createState() => _AdaptiveBottomNavState();
}
class _AdaptiveBottomNavState extends State<AdaptiveBottomNav> {
int _currentIndex = 0;
final Map<String, Icon> _navigationItems = {
'Home': Platform.isIOS
? const Icon(CupertinoIcons.house_fill)
: const Icon(Icons.home),
'Search': Icon(Icons.adaptive.share),
};
@override
Widget build(BuildContext context) {
return Scaffold(
body: const Center(child: Text('Content')),
bottomNavigationBar: Platform.isIOS
? CupertinoTabBar(
currentIndex: _currentIndex,
onTap: (index) => setState(() => _currentIndex = index),
items: _navigationItems.entries
.map((entry) => BottomNavigationBarItem(
icon: entry.value,
label: entry.key,
))
.toList(),
)
: NavigationBar(
selectedIndex: _currentIndex,
onDestinationSelected: (index) =>
setState(() => _currentIndex = index),
destinations: _navigationItems.entries
.map((entry) => NavigationDestination(
icon: entry.value,
label: entry.key,
))
.toList(),
),
);
}
}

Watch out#

WARNING

앱 전체 일관성 유지

앱 바만 iOS 스타일로 바꾸면 다른 부분과 어울리지 않을 수 있습니다.

// 앱 전체적으로 일관된 스타일을 유지하세요
// 앱 바만 iOS 스타일이고 나머지가 Material이면 어색함

앱 바 스타일을 변경하려면 다른 UI 요소도 함께 조정하는 것을 고려하세요.

결론: 앱 바와 네비게이션 바는 Platform.isIOS 조건으로 플랫폼별 스타일을 적용할 수 있습니다.


챕터 7: 햅틱 피드백 적응#

Why#

NOTE

터치 피드백도 플랫폼마다 다릅니다. Android에서 텍스트를 길게 눌러 선택하면 진동이 울리지만, iOS에서는 그렇지 않습니다.

What#

NOTE

Flutter의 Material/Cupertino 패키지는 적절한 햅틱 피드백을 자동으로 트리거합니다.

상황AndroidiOS
텍스트 길게 눌러 선택진동없음
피커 스크롤없음약한 충격

How#

TIP

커스텀 햅틱 피드백

import 'package:flutter/services.dart';
class HapticFeedbackDemo extends StatelessWidget {
const HapticFeedbackDemo({super.key});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
// 플랫폼에 맞는 햅틱 피드백 트리거
HapticFeedback.mediumImpact();
},
child: const Text('Tap for haptic'),
);
}
}

결론: 햅틱 피드백은 Material/Cupertino 위젯에서 플랫폼에 맞게 자동으로 트리거됩니다.


한계#

  • 자동 적응은 Android와 iOS에만 적용됩니다. 웹, 데스크톱에서는 별도 처리가 필요합니다.
  • .adaptive() 생성자는 모든 위젯에 제공되지 않습니다. 필요하면 Platform.isIOS 조건문을 사용하세요.
  • 앱 전체의 디자인 일관성을 유지하면서 플랫폼 적응을 적용하는 것은 팀 간 합의가 필요합니다.

Footnotes#

  1. adaptive(적응형): 플랫폼에 따라 자동으로 다른 스타일이나 동작을 적용하는 것을 말한다.

  2. startActivity(): Android에서 새 Activity를 시작하는 메서드로, Flutter Navigator.push()의 Android 전환 애니메이션 기반이다.

  3. Navigator.push(): Flutter에서 새 화면을 스택에 추가하고 해당 화면으로 이동하는 메서드다.

  4. CupertinoNavigationBar: iOS 스타일 네비게이션 바를 구현하는 Flutter 위젯이다.

  5. ScrollPhysics: 스크롤 동작의 물리 효과(마찰, 바운스 등)를 정의하는 클래스다.

  6. PrimaryScrollController: Scaffold 내 기본 스크롤러를 관리하며, iOS 상태 바 탭 동작과 연결된다.

  7. TextField: Material Design 스타일의 텍스트 입력 위젯이다.

  8. CupertinoTextField: iOS 스타일의 텍스트 입력 위젯이다.

공유

이 글이 도움이 되었다면 다른 사람과 공유해주세요!

Flutter 튜토리얼 36편: 플랫폼별 자동 적응
https://moodturnpost.net/posts/flutter/flutter-platform-adaptations/
작성자
Moodturn
게시일
2026-01-08
Moodturn

목차