Flutter 튜토리얼 16편: 키보드 포커스와 단축키

요약#

핵심 요지#

  • 문제 정의: 데스크톱과 웹 앱에서 키보드 네비게이션과 단축키 지원이 필요하다.
  • 핵심 주장: Flutter의 Focus1 시스템과 Shortcuts2/Actions3로 키보드 인터랙션을 구현한다.
  • 주요 근거: FocusNode4로 포커스를 관리하고, Intent5Action으로 단축키 동작을 정의한다.
  • 실무 기준: FocusTraversalGroup6으로 Tab 순서를 제어하고, SingleActivator로 키 조합을 정의한다.
  • 한계: 플랫폼별 키보드 레이아웃과 단축키 충돌에 주의해야 한다.

문서가 설명하는 범위#

  • Flutter 포커스 시스템과 포커스 트리
  • FocusNode와 FocusScope로 포커스 제어
  • Tab 순서와 포커스 그룹 설정
  • Shortcuts와 Actions로 키보드 단축키 구현

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


참고 자료#


문제 상황#

데스크톱과 웹 애플리케이션에서는 키보드만으로 앱을 조작할 수 있어야 합니다. Tab 키로 요소 간 이동, 단축키로 빠른 실행, Enter로 버튼 활성화 등이 필요합니다.

키보드 접근성 요구사항#

Tab: 다음 포커스 가능한 요소로 이동
Shift+Tab: 이전 요소로 이동
Enter/Space: 포커스된 버튼 활성화
Ctrl+S: 저장
Ctrl+Z: 실행 취소

문제는 다음과 같습니다.

  • 포커스 가능한 요소를 정의해야 한다.
  • Tab 이동 순서를 제어해야 한다.
  • 키보드 단축키를 등록하고 처리해야 한다.
  • 포커스 상태에 따른 시각적 피드백이 필요하다.

해결 방법#

Flutter는 포커스 시스템으로 키보드 입력 대상을 관리하고, Shortcuts/Actions로 단축키를 처리합니다.

챕터 1: 포커스 시스템 기본 개념#

Why#

NOTE

키보드 입력은 화면의 특정 요소로 전달되어야 합니다.
포커스 시스템은 어떤 위젯이 키보드 입력을 받을지 결정합니다.

키보드 입력 → 포커스된 위젯 → 이벤트 처리

What#

NOTE

포커스 시스템은 포커스 트리를 통해 키보드 입력을 라우팅합니다.
주요 개념은 다음과 같습니다.

개념설명
포커스 트리위젯 트리를 미러링하는 포커스 노드 트리
포커스 노드포커스를 받을 수 있는 단일 노드
주 포커스현재 키보드 입력을 받는 노드
포커스 스코프포커스 노드 그룹을 관리하는 특수 노드
포커스 순회Tab 키로 포커스를 이동하는 과정

How#

TIP

기본 Focus 위젯 사용

import 'package:flutter/material.dart';
class FocusableBox extends StatefulWidget {
const FocusableBox({super.key});
@override
State<FocusableBox> createState() => _FocusableBoxState();
}
class _FocusableBoxState extends State<FocusableBox> {
bool _isFocused = false;
@override
Widget build(BuildContext context) {
return Focus(
onFocusChange: (hasFocus) {
setState(() {
_isFocused = hasFocus;
});
},
child: GestureDetector(
onTap: () {
// 탭 시 포커스 요청
Focus.of(context).requestFocus();
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: 200,
height: 100,
decoration: BoxDecoration(
color: _isFocused ? Colors.blue[100] : Colors.grey[200],
border: Border.all(
color: _isFocused ? Colors.blue : Colors.grey,
width: _isFocused ? 2 : 1,
),
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
_isFocused ? '포커스됨!' : '클릭하세요',
style: TextStyle(
fontWeight: _isFocused ? FontWeight.bold : FontWeight.normal,
),
),
),
),
),
);
}
}

Focus 위젯 주요 속성

속성설명
onFocusChange포커스 상태 변경 콜백
onKeyEvent키 이벤트 핸들러
autofocus자동 포커스 여부
canRequestFocus포커스 요청 가능 여부
skipTraversalTab 순회에서 제외 여부

Watch out#

WARNING

FocusNodebuild() 메서드 안에서 생성하면 안 됩니다.
매번 새로운 노드가 생성되어 포커스가 유실됩니다.

// ❌ 잘못된 방법
@override
Widget build(BuildContext context) {
return Focus(
focusNode: FocusNode(), // 매번 새 노드 생성!
child: Container(),
);
}
// ✅ 올바른 방법
class _MyWidgetState extends State<MyWidget> {
late FocusNode _focusNode;
@override
void initState() {
super.initState();
_focusNode = FocusNode(debugLabel: 'MyFocusNode');
}
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Focus(
focusNode: _focusNode,
child: Container(),
);
}
}

결론: Focus 위젯으로 포커스 가능한 요소를 만들고, onFocusChange로 상태 변화를 감지합니다.


챕터 2: FocusNode로 포커스 제어#

Why#

NOTE

프로그래밍 방식으로 포커스를 제어해야 할 때가 있습니다.
검증 실패 시 해당 필드로 포커스 이동, 대화상자 열릴 때 자동 포커스 등이 필요합니다.

이벤트 발생 → focusNode.requestFocus() → 포커스 이동

What#

NOTE

FocusNode는 포커스 상태를 관리하는 장수명 객체입니다.
requestFocus(), unfocus(), hasFocus 등의 메서드와 속성을 제공합니다.

How#

TIP

FocusNode 활용

class FocusControlDemo extends StatefulWidget {
const FocusControlDemo({super.key});
@override
State<FocusControlDemo> createState() => _FocusControlDemoState();
}
class _FocusControlDemoState extends State<FocusControlDemo> {
late FocusNode _redFocus;
late FocusNode _greenFocus;
late FocusNode _blueFocus;
@override
void initState() {
super.initState();
_redFocus = FocusNode(debugLabel: 'Red');
_greenFocus = FocusNode(debugLabel: 'Green');
_blueFocus = FocusNode(debugLabel: 'Blue');
}
@override
void dispose() {
_redFocus.dispose();
_greenFocus.dispose();
_blueFocus.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildFocusableBox(_redFocus, Colors.red),
_buildFocusableBox(_greenFocus, Colors.green),
_buildFocusableBox(_blueFocus, Colors.blue),
],
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () => _redFocus.requestFocus(),
child: const Text('빨강 포커스'),
),
ElevatedButton(
onPressed: () => _greenFocus.requestFocus(),
child: const Text('초록 포커스'),
),
ElevatedButton(
onPressed: () => _blueFocus.requestFocus(),
child: const Text('파랑 포커스'),
),
],
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
// 현재 포커스 해제
FocusManager.instance.primaryFocus?.unfocus();
},
child: const Text('포커스 해제'),
),
],
);
}
Widget _buildFocusableBox(FocusNode focusNode, Color color) {
return Focus(
focusNode: focusNode,
child: Builder(
builder: (context) {
final isFocused = Focus.of(context).hasFocus;
return Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: isFocused ? color : color.withOpacity(0.3),
border: Border.all(
color: isFocused ? Colors.black : Colors.grey,
width: isFocused ? 3 : 1,
),
borderRadius: BorderRadius.circular(8),
),
);
},
),
);
}
}

FocusNode 주요 API

API설명
requestFocus()이 노드로 포커스 요청
unfocus()포커스 해제
hasFocus포커스 보유 여부
hasPrimaryFocus주 포커스 보유 여부
addListener()포커스 변경 리스너 등록
debugLabel디버깅용 레이블

Watch out#

WARNING

unfocus() 호출 시 포커스가 완전히 사라지지 않습니다.
다른 노드로 포커스가 이동합니다.

// unfocus의 disposition 옵션
focusNode.unfocus(
// 부모 스코프로 포커스 이동 (기본값)
disposition: UnfocusDisposition.scope,
);
focusNode.unfocus(
// 이전에 포커스되었던 자식으로 이동
disposition: UnfocusDisposition.previouslyFocusedChild,
);

포커스 트리에 다른 스코프가 없으면 루트 스코프로 이동하여 Tab 순회가 깨질 수 있습니다.

결론: FocusNode로 포커스를 프로그래밍 방식으로 제어하고, 반드시 dispose()에서 해제합니다.


챕터 3: 키 이벤트 처리#

Why#

NOTE

특정 키 입력에 반응하는 커스텀 동작이 필요합니다.
방향키로 선택 이동, Escape로 대화상자 닫기 등을 구현해야 합니다.

키 입력 → onKeyEvent → 처리 또는 전파

What#

NOTE

Focus 위젯의 onKeyEvent 콜백으로 키 이벤트를 처리합니다.
KeyEventResult.handled를 반환하면 이벤트가 더 이상 전파되지 않습니다.

How#

TIP

키 이벤트 처리 기본

class KeyEventDemo extends StatefulWidget {
const KeyEventDemo({super.key});
@override
State<KeyEventDemo> createState() => _KeyEventDemoState();
}
class _KeyEventDemoState extends State<KeyEventDemo> {
String _lastKey = '키를 누르세요';
int _selectedIndex = 0;
final List<String> _items = ['사과', '바나나', '체리', '딸기', '포도'];
@override
Widget build(BuildContext context) {
return Focus(
autofocus: true,
onKeyEvent: (node, event) {
// KeyDownEvent만 처리 (반복 입력 방지)
if (event is! KeyDownEvent) {
return KeyEventResult.ignored;
}
setState(() {
_lastKey = event.logicalKey.keyLabel;
});
// 방향키 처리
if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
setState(() {
_selectedIndex = (_selectedIndex - 1).clamp(0, _items.length - 1);
});
return KeyEventResult.handled;
}
if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
setState(() {
_selectedIndex = (_selectedIndex + 1).clamp(0, _items.length - 1);
});
return KeyEventResult.handled;
}
// Enter 키로 선택 확인
if (event.logicalKey == LogicalKeyboardKey.enter) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${_items[_selectedIndex]} 선택됨!')),
);
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
child: Column(
children: [
Text('마지막 키: $_lastKey'),
const SizedBox(height: 16),
...List.generate(_items.length, (index) {
return Container(
padding: const EdgeInsets.all(8),
margin: const EdgeInsets.symmetric(vertical: 4),
decoration: BoxDecoration(
color: index == _selectedIndex ? Colors.blue[100] : null,
border: Border.all(
color: index == _selectedIndex ? Colors.blue : Colors.grey,
),
borderRadius: BorderRadius.circular(4),
),
child: Text(_items[index]),
);
}),
const SizedBox(height: 16),
const Text('↑↓ 이동, Enter 선택'),
],
),
);
}
}

KeyEventResult 옵션

설명
handled이벤트 처리됨, 전파 중단
ignored이벤트 무시, 부모로 전파
skipRemainingHandlers나머지 핸들러 건너뜀

Watch out#

WARNING

키 이벤트는 주 포커스에서 시작하여 루트까지 전파됩니다.
handled를 반환하면 네이티브 컨트롤도 이벤트를 받지 못합니다.

onKeyEvent: (node, event) {
// TextField 내부에서 이 핸들러가 호출되면
// handled 반환 시 TextField에 입력이 안 됨
// 특정 조합만 처리하고 나머지는 무시
if (HardwareKeyboard.instance.isControlPressed &&
event.logicalKey == LogicalKeyboardKey.keyS) {
_save();
return KeyEventResult.handled;
}
// 일반 입력은 TextField로 전달되도록 ignored 반환
return KeyEventResult.ignored;
}

결론: onKeyEvent로 키 입력을 처리하고, KeyEventResult로 이벤트 전파를 제어합니다.


챕터 4: 포커스 순회와 그룹화#

Why#

NOTE

Tab 키로 논리적인 순서로 포커스가 이동해야 합니다.
관련 요소들을 그룹화하여 그룹 내에서 먼저 순회하도록 할 수 있습니다.

그룹 A (버튼1 → 버튼2) → 그룹 B (입력1 → 입력2) → 그룹 C

What#

NOTE

FocusTraversalGroup은 포커스 순회 그룹을 정의합니다.
FocusTraversalPolicy로 순회 순서를 커스터마이징할 수 있습니다.

How#

TIP

기본 포커스 순회 그룹

class FocusTraversalDemo extends StatelessWidget {
const FocusTraversalDemo({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
// 그룹 1: 네비게이션
FocusTraversalGroup(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(onPressed: () {}, child: const Text('홈')),
ElevatedButton(onPressed: () {}, child: const Text('검색')),
ElevatedButton(onPressed: () {}, child: const Text('설정')),
],
),
),
const Divider(),
// 그룹 2: 폼 입력
FocusTraversalGroup(
child: Column(
children: [
const TextField(decoration: InputDecoration(labelText: '이름')),
const SizedBox(height: 8),
const TextField(decoration: InputDecoration(labelText: '이메일')),
const SizedBox(height: 8),
ElevatedButton(onPressed: () {}, child: const Text('제출')),
],
),
),
],
);
}
}

커스텀 순회 순서

class CustomOrderDemo extends StatelessWidget {
const CustomOrderDemo({super.key});
@override
Widget build(BuildContext context) {
return FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// 숫자가 작은 순서대로 순회: 2 → 1 → 3
FocusTraversalOrder(
order: const NumericFocusOrder(2),
child: ElevatedButton(
onPressed: () {},
child: const Text('TWO'),
),
),
FocusTraversalOrder(
order: const NumericFocusOrder(1),
child: ElevatedButton(
onPressed: () {},
child: const Text('ONE'),
),
),
FocusTraversalOrder(
order: const NumericFocusOrder(3),
child: ElevatedButton(
onPressed: () {},
child: const Text('THREE'),
),
),
],
),
);
}
}

FocusTraversalPolicy 종류

정책설명
ReadingOrderTraversalPolicy읽기 순서 (기본값)
OrderedTraversalPolicy명시적 순서 지정
WidgetOrderTraversalPolicy위젯 트리 순서

Watch out#

WARNING

skipTraversaltrue로 설정하면 Tab 순회에서 제외되지만, requestFocus()로는 여전히 포커스할 수 있습니다.

Focus(
skipTraversal: true, // Tab으로 이동 불가
child: MyWidget(),
)
// 하지만 프로그래밍 방식으로는 포커스 가능
focusNode.requestFocus();
// 완전히 포커스 불가능하게 하려면
Focus(
canRequestFocus: false, // 어떤 방식으로도 포커스 불가
child: MyWidget(),
)

결론: FocusTraversalGroup으로 포커스 그룹을 만들고, OrderedTraversalPolicy로 순서를 커스터마이징합니다.


챕터 5: Shortcuts와 Actions 기본#

Why#

NOTE

Ctrl+S로 저장, Ctrl+Z로 실행 취소 같은 단축키가 필요합니다.
ShortcutsActions는 키 조합과 동작을 분리하여 관리합니다.

키 조합 (Shortcuts) → Intent → Action (Actions) → 실행

What#

NOTE

Shortcuts 위젯은 키 조합을 Intent로 매핑합니다.
Actions 위젯은 IntentAction으로 매핑하여 실행합니다.

How#

TIP

Intent와 Action 정의

// 1. Intent 정의 - 사용자의 의도를 표현
class IncrementIntent extends Intent {
const IncrementIntent();
}
class DecrementIntent extends Intent {
const DecrementIntent();
}
class ResetIntent extends Intent {
const ResetIntent();
}
// 2. Action 정의 - 실제 동작 구현
class IncrementAction extends Action<IncrementIntent> {
IncrementAction(this.onIncrement);
final VoidCallback onIncrement;
@override
void invoke(covariant IncrementIntent intent) {
onIncrement();
}
}
class DecrementAction extends Action<DecrementIntent> {
DecrementAction(this.onDecrement);
final VoidCallback onDecrement;
@override
void invoke(covariant DecrementIntent intent) {
onDecrement();
}
}

Shortcuts와 Actions 연결

class ShortcutsDemo extends StatefulWidget {
const ShortcutsDemo({super.key});
@override
State<ShortcutsDemo> createState() => _ShortcutsDemoState();
}
class _ShortcutsDemoState extends State<ShortcutsDemo> {
int _counter = 0;
void _increment() => setState(() => _counter++);
void _decrement() => setState(() => _counter--);
void _reset() => setState(() => _counter = 0);
@override
Widget build(BuildContext context) {
return Shortcuts(
shortcuts: const <ShortcutActivator, Intent>{
// 키 조합 → Intent 매핑
SingleActivator(LogicalKeyboardKey.arrowUp): IncrementIntent(),
SingleActivator(LogicalKeyboardKey.arrowDown): DecrementIntent(),
SingleActivator(LogicalKeyboardKey.keyR, control: true): ResetIntent(),
},
child: Actions(
actions: <Type, Action<Intent>>{
// Intent → Action 매핑
IncrementIntent: IncrementAction(_increment),
DecrementIntent: DecrementAction(_decrement),
ResetIntent: CallbackAction<ResetIntent>(
onInvoke: (intent) => _reset(),
),
},
child: Focus(
autofocus: true,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'$_counter',
style: const TextStyle(fontSize: 48),
),
const SizedBox(height: 24),
const Text('↑: 증가, ↓: 감소, Ctrl+R: 초기화'),
],
),
),
),
);
}
}

단축키 정의 방식

방식사용 시점
SingleActivator단일 키 + 수정자 (권장)
LogicalKeySet여러 키 조합
CharacterActivator문자 기반 입력

Watch out#

WARNING

ShortcutsActions는 포커스된 위젯의 조상에 있어야 합니다.
포커스가 다른 곳에 있으면 단축키가 작동하지 않습니다.

// ❌ Focus가 Actions 바깥에 있어서 작동 안 함
Column(
children: [
Focus(autofocus: true, child: TextField()),
Actions(
actions: {...},
child: Shortcuts(
shortcuts: {...},
child: Container(),
),
),
],
)
// ✅ Focus가 Actions 안에 있어서 작동함
Actions(
actions: {...},
child: Shortcuts(
shortcuts: {...},
child: Focus(
autofocus: true,
child: Column(
children: [TextField(), Container()],
),
),
),
)

결론: Intent로 의도를, Action으로 동작을 분리하고, ShortcutsActions로 연결합니다.


챕터 6: CallbackShortcuts와 고급 기능#

Why#

NOTE

간단한 단축키는 Intent와 Action을 정의하는 것이 번거로울 수 있습니다.
CallbackShortcuts는 직접 콜백을 연결하여 코드를 간소화합니다.

키 조합 → 콜백 함수 (직접 실행)

What#

NOTE

CallbackShortcuts는 키 조합에 콜백을 직접 매핑합니다.
FocusableActionDetector는 포커스, 마우스, 단축키를 한 번에 처리합니다.

How#

TIP

CallbackShortcuts 사용

class SimpleShortcutsDemo extends StatefulWidget {
const SimpleShortcutsDemo({super.key});
@override
State<SimpleShortcutsDemo> createState() => _SimpleShortcutsDemoState();
}
class _SimpleShortcutsDemoState extends State<SimpleShortcutsDemo> {
int _count = 0;
@override
Widget build(BuildContext context) {
return CallbackShortcuts(
bindings: <ShortcutActivator, VoidCallback>{
const SingleActivator(LogicalKeyboardKey.arrowUp): () {
setState(() => _count++);
},
const SingleActivator(LogicalKeyboardKey.arrowDown): () {
setState(() => _count--);
},
const SingleActivator(LogicalKeyboardKey.escape): () {
setState(() => _count = 0);
},
},
child: Focus(
autofocus: true,
child: Center(
child: Text('Count: $_count', style: const TextStyle(fontSize: 32)),
),
),
);
}
}

FocusableActionDetector 사용

class CustomButton extends StatefulWidget {
const CustomButton({
super.key,
required this.label,
required this.onPressed,
});
final String label;
final VoidCallback onPressed;
@override
State<CustomButton> createState() => _CustomButtonState();
}
class _CustomButtonState extends State<CustomButton> {
bool _isFocused = false;
bool _isHovered = false;
@override
Widget build(BuildContext context) {
return FocusableActionDetector(
onFocusChange: (focused) => setState(() => _isFocused = focused),
onShowFocusHighlight: (show) => setState(() => _isFocused = show),
onShowHoverHighlight: (show) => setState(() => _isHovered = show),
actions: <Type, Action<Intent>>{
ActivateIntent: CallbackAction<ActivateIntent>(
onInvoke: (intent) => widget.onPressed(),
),
},
shortcuts: const <ShortcutActivator, Intent>{
SingleActivator(LogicalKeyboardKey.enter): ActivateIntent(),
SingleActivator(LogicalKeyboardKey.space): ActivateIntent(),
},
child: GestureDetector(
onTap: widget.onPressed,
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(
color: _isHovered ? Colors.blue[100] : Colors.grey[100],
border: Border.all(
color: _isFocused ? Colors.blue : Colors.grey,
width: _isFocused ? 2 : 1,
),
borderRadius: BorderRadius.circular(8),
),
child: Text(
widget.label,
style: TextStyle(
fontWeight: _isFocused ? FontWeight.bold : FontWeight.normal,
),
),
),
),
);
}
}

LoggingActionDispatcher로 디버깅

class LoggingActionDispatcher extends ActionDispatcher {
@override
Object? invokeAction(
covariant Action<Intent> action,
covariant Intent intent, [
BuildContext? context,
]) {
print('Action: $action, Intent: $intent');
return super.invokeAction(action, intent, context);
}
}
// 사용
Actions(
dispatcher: LoggingActionDispatcher(),
actions: {...},
child: child,
)

Watch out#

WARNING

플랫폼별 단축키 충돌에 주의해야 합니다.
macOS는 Cmd를, Windows/Linux는 Ctrl을 사용합니다.

// 플랫폼별 수정자 처리
SingleActivator(
LogicalKeyboardKey.keyS,
// macOS에서는 Cmd, 그 외에서는 Ctrl
meta: Platform.isMacOS,
control: !Platform.isMacOS,
)
// 또는 ControlOrMeta 사용
shortcuts: const <ShortcutActivator, Intent>{
SingleActivator(
LogicalKeyboardKey.keyS,
control: true, // Windows/Linux
meta: true, // macOS에서 Cmd로 처리됨
): SaveIntent(),
}

결론: 간단한 단축키는 CallbackShortcuts로, 복합 위젯은 FocusableActionDetector로 구현합니다.


한계#

키보드 포커스와 단축키 시스템에는 몇 가지 한계가 있습니다.

  • 플랫폼 차이: 키보드 레이아웃과 단축키 관례가 플랫폼마다 다릅니다.
  • 네이티브 단축키 충돌: 시스템 단축키와 충돌할 수 있습니다.
  • 접근성: 스크린 리더 사용자를 위한 추가 고려가 필요합니다.
  • 모바일 제한: 물리적 키보드가 없는 환경에서는 적용되지 않습니다.

Footnotes#

  1. Focus(포커스): 키보드 입력을 받을 위젯을 지정하는 시스템이다. 포커스된 위젯만 키 이벤트를 받는다.

  2. Shortcuts(단축키): 키 조합을 Intent로 매핑하는 위젯이다.

  3. Actions(액션): Intent를 Action으로 매핑하여 실행하는 위젯이다.

  4. FocusNode(포커스 노드): 포커스 상태를 관리하는 장수명 객체다. requestFocus()로 포커스를 요청한다.

  5. Intent(인텐트): 사용자의 의도를 추상화한 클래스다. Action과 연결되어 실제 동작을 수행한다.

  6. FocusTraversalGroup(포커스 순회 그룹): Tab 키로 포커스가 이동하는 그룹을 정의하는 위젯이다.

공유

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

Flutter 튜토리얼 16편: 키보드 포커스와 단축키
https://moodturnpost.net/posts/flutter/flutter-keyboard-shortcuts/
작성자
Moodturn
게시일
2026-01-08
Moodturn

목차