TextField#
사용자로부터 텍스트를 입력받는 컴포넌트입니다. 다양한 타입과 상태를 지원합니다.
Live Preview#
class TextFieldDefaultExample extends StatefulComponent {
const TextFieldDefaultExample({super.key});
@override
State<TextFieldDefaultExample> createState() =>
_TextFieldDefaultExampleState();
}
class _TextFieldDefaultExampleState extends State<TextFieldDefaultExample> {
@override
Component build(BuildContext context) {
return TextField(
placeholder: Text('Enter text...'),
);
}
}
class TextFieldDefaultExample extends StatefulWidget {
const TextFieldDefaultExample({super.key});
@override
State<TextFieldDefaultExample> createState() =>
_TextFieldDefaultExampleState();
}
class _TextFieldDefaultExampleState extends State<TextFieldDefaultExample> {
@override
Widget build(BuildContext context) {
return TextField(
placeholder: const Text('Enter text...'),
);
}
}
This field is required.
class TextFieldErrorExample extends StatefulComponent {
const TextFieldErrorExample({super.key});
@override
State<TextFieldErrorExample> createState() => _TextFieldErrorExampleState();
}
class _TextFieldErrorExampleState extends State<TextFieldErrorExample> {
@override
Component build(BuildContext context) {
return TextField(
placeholder: Text('Enter text...'),
errorText: 'This field is required.',
);
}
}
class TextFieldErrorExample extends StatefulWidget {
const TextFieldErrorExample({super.key});
@override
State<TextFieldErrorExample> createState() => _TextFieldErrorExampleState();
}
class _TextFieldErrorExampleState extends State<TextFieldErrorExample> {
@override
Widget build(BuildContext context) {
return TextField(
placeholder: const Text('Enter text...'),
errorText: 'This field is required.',
);
}
}
class TextFieldChainExample extends StatefulComponent {
const TextFieldChainExample({super.key});
@override
State<TextFieldChainExample> createState() => _TextFieldChainExampleState();
}
class _TextFieldChainExampleState extends State<TextFieldChainExample> {
@override
Component build(BuildContext context) {
return div(
[
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
TextField(placeholder: Text('Enter text...')).radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(borderWidth)까지 한 번에.
TextField(placeholder: Text('Enter text...')).withStyle(
const CoreTextFieldStyle(
borderColor: CoreColor.token(CoreColors.tertiary),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
borderWidth: CoreStrokeWidth.stroke2,
),
),
],
classes: 'flex flex-col items-start',
);
}
}
class TextFieldChainExample extends StatefulWidget {
const TextFieldChainExample({super.key});
@override
State<TextFieldChainExample> createState() => _TextFieldChainExampleState();
}
class _TextFieldChainExampleState extends State<TextFieldChainExample> {
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
TextField(placeholder: const Text('Enter text...')).radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(borderWidth)까지 한 번에.
TextField(placeholder: const Text('Enter text...')).withStyle(
const CoreTextFieldStyle(
borderColor: CoreColor.token(CoreColors.tertiary),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
borderWidth: CoreStrokeWidth.stroke2,
),
),
],
);
}
}
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 사용자로부터 짧은 텍스트를 입력받을 때 (이름, 이메일, 비밀번호 등)
- 검색 필드가 필요할 때
- 숫자, 전화번호 등 특정 형식의 입력이 필요할 때
대신 다른 컴포넌트를 사용하세요:
TextArea: 여러 줄의 긴 텍스트를 입력받을 때Select: 미리 정해진 옵션 중 선택할 때Autocomplete: 입력하면서 추천 목록에서 선택할 때DatePicker: 날짜를 입력받을 때
기본 사용법 (Basic Usage)#
// 기본 입력
TextField(
onChanged: handleNameChange,
placeholder: Text('이름을 입력하세요'),
)
// 비밀번호 입력
TextField(
obscureText: true,
placeholder: Text('비밀번호'),
)
// 라벨 및 에러
TextField(
label: '이메일',
onChanged: handleEmailChange,
errorText: '올바른 이메일을 입력하세요',
)
// 기본 입력
TextField(
onChanged: handleNameChange,
placeholder: Text('이름을 입력하세요'),
)
// 비밀번호 입력
TextField(
obscureText: true,
placeholder: Text('비밀번호'),
)
// 라벨 + 에러
TextField(
label: '이메일',
onChanged: handleEmailChange,
errorText: '올바른 이메일을 입력하세요',
)
Props / Parameters#
콘텐츠 / 상태#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
placeholder |
Widget? / Component? |
null |
플레이스홀더 |
initialValue |
String? |
null |
초기 값 (비제어) |
value | String? | null | 제어 값 |
onChanged |
ValueChanged<String>? |
null |
값 변경 콜백 |
onSubmitted |
ValueChanged<String>? |
null |
제출 콜백 |
enabled | bool | true | 활성화 여부 |
readOnly | bool | false | 읽기 전용 |
autofocus | bool | false | 자동 포커스 |
selectAllOnFocus |
bool |
false |
포커스 시 전체 선택 |
size |
CoreComponentSize |
md |
크기 토큰 (위젯 파라미터) |
textFieldStyle |
CoreTextFieldStyle? |
null |
chrome / dimensional / cursor / nested slot 단일 진입점 |
라벨 / 검증#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
label | String? | null | 라벨 텍스트 |
description |
String? |
null |
설명 텍스트 |
errorText |
String? |
null |
에러 메시지 (설정 시 자동 error variant) |
required | bool | false | 필수 입력 표시 |
name |
String? |
null |
폼 제출용 필드 이름 |
hintText |
String? |
null |
네이티브 힌트 텍스트 |
title |
String? |
null |
HTML title (검증 실패 안내) |
pattern | String? | null | 검증 정규식 |
minLength / maxLength |
int? |
null |
최소·최대 글자 수 |
min / max |
num? |
null |
숫자 입력 범위 |
텍스트 동작#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
type |
CoreInputType |
CoreInputType.defaultType |
HTML-semantic 입력 타입 |
obscureText |
bool |
false |
비밀번호 마스킹 |
maxLines |
int? |
1 (Flutter) / null (Web) |
최대 줄 수 |
minLines | int? | null | 최소 줄 수 |
expands |
bool |
false |
부모 높이만큼 확장 |
autocorrect |
bool? |
null (→ true) |
자동 교정 |
keyboardType |
TextInputType? / String? |
null |
키보드 타입 (Web → inputmode) |
textInputAction |
TextInputAction? / String? |
null |
키보드 액션 키 (Web → enterkeyhint) |
textCapitalization |
TextCapitalization / String? |
none / null |
자동 대문자화 |
textDirection |
TextDirection? / String? |
null |
읽기 방향 |
autofillHints |
Iterable<String> / Iterable<String>? |
const [] / null |
자동완성 힌트 (Web → autocomplete) |
inputFormatters |
List<TextInputFormatter>? / List<CoreInputFormatter>? |
null |
입력 포매터 |
submitFormatters | 동일 타입 | const [] | 제출 시점 포매터 |
showCursor | bool? | null | 커서 표시 |
슬롯 / 어포던스 / 콜백#
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
prefix / suffix |
Widget? / Component? |
null |
입력 영역 좌/우 슬롯. 아이콘도 글자(단위·카운트·타이머)도 받는다 — 글자는 필드의 사이즈별 base 타이포를 affixColor 로 입는다 |
features |
List<CoreInputFeature> |
const [] |
clear / password-toggle 어포던스 |
clearButtonSemanticLabel |
String? |
null |
clear 버튼 접근성 이름 |
onTap |
VoidCallback? |
null |
탭 콜백 |
onEditingComplete |
VoidCallback? |
null |
편집 완료 콜백 |
onTapOutside / onTapUpOutside |
TapRegionCallback? / void Function()? |
null |
외부 탭 콜백 |
focusNode |
FocusNode? / CoreFocusNode? |
null |
포커스 노드 |
statesController |
WidgetStatesController? / CoreStatesController? |
null |
상태 컨트롤러 |
Flutter 전용 확장#
Web 은 이 capability 들을 브라우저 native <input> · DOM focus/scroll ·
네이티브 컨텍스트 메뉴/확대경으로 제공합니다.
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
controller |
TextEditingController? |
null |
텍스트 컨트롤러 |
obscuringCharacter |
String |
'•' |
마스킹 문자 |
cursorOpacityAnimates |
bool |
true |
커서 깜빡임 애니메이션 |
enableSuggestions |
bool |
true |
입력 제안 |
enableInteractiveSelection |
bool |
!readOnly || !obscureText |
선택 제스처 허용 |
enableIMEPersonalizedLearning |
bool |
true |
IME 개인화 학습 |
stylusHandwritingEnabled |
bool |
EditableText.defaultStylusHandwritingEnabled |
스타일러스 필기 입력 |
maxLengthEnforcement |
MaxLengthEnforcement? |
null |
길이 제한 적용 방식 |
keyboardAppearance |
Brightness? |
null |
키보드 밝기 |
scrollController |
ScrollController? |
null |
스크롤 컨트롤러 |
scrollPhysics |
ScrollPhysics? |
null |
스크롤 물리 |
selectionControls |
TextSelectionControls? |
null |
선택 핸들 UI |
selectionHeightStyle / selectionWidthStyle |
BoxHeightStyle / BoxWidthStyle |
tight |
선택 영역 박스 |
dragStartBehavior |
DragStartBehavior |
start |
드래그 시작 기준 |
clipBehavior |
Clip |
hardEdge |
클리핑 |
restorationId |
String? |
null |
상태 복원 ID |
contentInsertionConfiguration |
ContentInsertionConfiguration? |
null |
리치 콘텐츠 삽입 |
contextMenuBuilder |
EditableTextContextMenuBuilder? |
null |
컨텍스트 메뉴 |
magnifierConfiguration |
TextMagnifierConfiguration? |
null |
확대경 |
spellCheckConfiguration |
SpellCheckConfiguration? |
null |
스펠 체크 |
undoController |
UndoHistoryController? |
null |
undo 히스토리 |
strutStyle |
StrutStyle? |
null |
strut 메트릭 |
skipInputFeatureFocusTraversal |
bool |
true |
어포던스 버튼 포커스 순회 제외 |
groupId |
Object |
EditableText |
tap-region 그룹 ID |
빠른 오버라이드 (Chain)#
이미 만든 TextField 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
.radius16처럼 Core 토큰 상수 이름과 똑같은 이름의 getter도 있습니다 — withStyle을 한 번 더 줄인 sugar로, 이름이 곧 값이라(radius16 ==
CoreRadius.radius16) 어느 컴포넌트에서 써도 뜻이 갈리지 않습니다.
class TextFieldChainExample extends StatefulWidget {
const TextFieldChainExample({super.key});
@override
State<TextFieldChainExample> createState() => _TextFieldChainExampleState();
}
class _TextFieldChainExampleState extends State<TextFieldChainExample> {
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
TextField(placeholder: const Text('Enter text...')).radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(borderWidth)까지 한 번에.
TextField(placeholder: const Text('Enter text...')).withStyle(
const CoreTextFieldStyle(
borderColor: CoreColor.token(CoreColors.tertiary),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
borderWidth: CoreStrokeWidth.stroke2,
),
),
],
);
}
}
class TextFieldChainExample extends StatefulComponent {
const TextFieldChainExample({super.key});
@override
State<TextFieldChainExample> createState() => _TextFieldChainExampleState();
}
class _TextFieldChainExampleState extends State<TextFieldChainExample> {
@override
Component build(BuildContext context) {
return div(
[
// 토큰-정확 getter combo — 이름이 곧 값 (getter 이름 = Core 토큰 상수 1:1).
TextField(placeholder: Text('Enter text...')).radius16.primary,
const Gap.space12(),
// withStyle full-control — getter 가 없는 필드(borderWidth)까지 한 번에.
TextField(placeholder: Text('Enter text...')).withStyle(
const CoreTextFieldStyle(
borderColor: CoreColor.token(CoreColors.tertiary),
borderRadius: CoreBorderRadius.all(CoreRadius.radius24),
borderWidth: CoreStrokeWidth.stroke2,
),
),
],
classes: 'flex flex-col items-start',
);
}
}
스타일 시스템 — textFieldStyle#
TextField 의 chrome / dimensional / cursor / 슬롯 텍스트 미세 조정은 단일
textFieldStyle 필드(CoreTextFieldStyle) 하나로 흐릅니다.
TextField(
size: CoreComponentSize.md, // ← 크기 토큰 위젯 파라미터
onChanged: handleEmailChange,
textFieldStyle: CoreTextFieldStyle( // ← chrome / 슬롯 단일 진입점
borderRadius: CoreBorderRadius.all(CoreRadius.radius8),
padding: CoreEdgeInsets.symmetric(
horizontal: CoreSpace.space16,
vertical: CoreSpace.space12,
),
focusBorderColor: CoreColor.token(CoreColors.primary),
cursorColor: CoreColor.token(CoreColors.primary),
valueStyle: CoreTextStyle.token(CoreTextStyles.bodyMedium),
placeholderStyle: CoreTextStyle.token(
CoreTextStyles.bodyMedium,
color: CoreColor.token(CoreColors.onSurfaceVariant),
),
leadingIconStyle: CoreIconStyle(size: CoreSize.size16),
),
prefix: Icon(LucideIcons.mail),
label: '이메일',
)
CoreTextFieldStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
backgroundColor |
CoreColor? |
Background fill colour override. |
borderColor |
CoreColor? |
Border stroke colour override (idle state). |
focusBorderColor |
CoreColor? |
Border stroke colour when the field is focused. |
errorBorderColor |
CoreColor? |
Border stroke colour when the field is in error state. |
borderWidth |
double? |
Border stroke width override (logical px, pre-scaling). |
borderRadius |
CoreBorderRadius? |
Border radius override (pre-scaling). |
height |
double? |
Fixed height override (logical px). |
minHeight |
double? |
Minimum height constraint (logical px). No
default*
, deliberately: the field's resting height is the per-size
height
in [defaultsBySize], and these two are caps a caller adds ON TOP of it (a multiline / auto-growing field). Web emits
min-height
only inside
if (merged.minHeight != null)
, so a default would put a floor on every text field at every size, and the size table's
height
would stop being what decides the box.
|
maxHeight |
double? |
Maximum height constraint (logical px). No
default*
— same null test as [minHeight] (Web's
if (merged.maxHeight != null)
); a value here caps every field.
|
padding |
CoreEdgeInsets? |
Padding override (logical px). |
labelPadding |
CoreEdgeInsets? |
Padding applied to the label element. null defers to [defaultLabelPadding]. |
descriptionPadding |
CoreEdgeInsets? |
Padding applied to the description / error helper text element.
null
defers to [defaultDescriptionPadding].
|
rowSpacing |
double? |
Native
Row.spacing
between the prefix / input / suffix row children (logical px).
null
defers to [defaultRowSpacing].
|
scrollPadding |
CoreEdgeInsets? |
Scroll padding around the input when scrolled into view.
null
defers to [defaultScrollPadding].
|
iconStyle |
CoreIconStyle? |
Nested [CoreIconStyle] slot shared as the per-size base for the leading / trailing icon affixes. Raw forwarded to the affix
Icon
resolvers —
Icon
's own resolver folds in scaling.
null
defers to
CoreTextFieldStyle.defaultsBySize[size]!.iconStyle!
.
|
fileLabelFontWeight |
int? |
Font weight applied to the file-upload
<input type="file">
label segment.
null
defers to [defaultFileLabelFontWeight].
|
transitionDuration |
Duration? |
Chrome transition duration override. null defers to [defaultTransitionDuration]. |
textAlign |
CoreTextAlign? |
Horizontal text alignment override. null defers to [defaultTextAlign]. |
textAlignVertical |
CoreVerticalAlign? |
Vertical alignment of the text within the input.
null
defers to [defaultTextAlignVertical].
|
crossAxisAlignment |
CoreVerticalAlign? |
Vertical alignment override for the prefix / input / suffix row.
null
defers to [defaultCrossAxisAlignment].
|
cursorColor | CoreColor? | Cursor colour override. |
cursorWidth |
double? |
Cursor width override (logical px). |
cursorRadius |
CoreBorderRadius? |
Cursor corner radius override. |
labelDisabledColor |
CoreColor? |
Disabled-state label text colour override. null → [defaultLabelDisabledColor]. |
descriptionColor |
CoreColor? |
Description / helper text colour override. null → [defaultDescriptionColor]. |
affixColor |
CoreColor? |
Prefix / suffix affix glyph colour override. null → [defaultAffixColor]. |
errorColor |
CoreColor? |
Error helper-text colour override. null → [defaultErrorColor]. |
selectionColor |
CoreColor? |
Text-selection tint colour override. null → [defaultSelectionColor]. |
backgroundCursorColor |
CoreColor? |
Background-cursor (composition/IME) colour override.
null
→ [defaultBackgroundCursorColor].
|
focusOutlineStyle |
CoreFocusOutlineStyle? |
Focus-outline style override. Nested [CoreFocusOutlineStyle] covers ring colour / radius / width / offset / offset-background.
null
(or any unset field on the slot) defers to [defaultFocusOutlineStyle]. A
borderColor
stated here outranks [focusBorderColor] and the variant's focus-ring tone; a
borderRadius
stated here outranks the field's own [borderRadius].
|
baseTextStyle |
CoreTextStyle? |
Base text style applied to every text slot (label / placeholder / value / description / error) before each slot's own style override merges on top.
null
defers to
CoreTextFieldStyle.defaultsBySize[size]!.baseTextStyle!
(size-aware typography token —
bodySmall
for xs/sm/md,
bodyMedium
for lg/xl).
|
labelStyle |
CoreTextStyle? |
Label text style override. |
placeholderStyle |
CoreTextStyle? |
Placeholder text style override. |
valueStyle |
CoreTextStyle? |
Input value text style override. |
descriptionStyle |
CoreTextStyle? |
Description / helper text style override. |
errorStyle |
CoreTextStyle? |
Error text style override. |
leadingIconStyle |
CoreIconStyle? |
Leading icon style override. No
default*
, and one would be actively wrong: the base for both affix icons already lives in Core as
defaultsBySize[size].iconStyle
, and these two are per-affix overrides layered on it. Both resolvers pick the icon size by precedence —
text merged.leadingIconStyle?.size ?? merged.trailingIconStyle?.size ?? merged.iconStyle?.size ?? defaultsBySize[size].iconStyle.size
— so a
static const
sitting at the head of that chain would win over the shared [iconStyle] slot AND over the size table, pinning one icon size across every size tier. Being unset is what makes the two lower rungs reachable.
|
trailingIconStyle |
CoreIconStyle? |
Trailing icon style override. No
default*
— second rung of the precedence chain documented on [leadingIconStyle]; a value here would shadow the shared [iconStyle] slot and the size table.
|
featureButtonStyle |
CoreButtonStyle? |
Nested [CoreButtonStyle] slot for the input-feature affordance buttons (clear / password toggle) — raw-forwarded to
Button(variant:.plain, buttonStyle: …)
on both platforms. Recursively merged onto [defaultFeatureButtonStyle] (child-component-composition).
|
Resolve chain#
variant 는 위젯 파라미터가 아닙니다 — errorText 유무로 CoreTextFieldVariant
가 결정되고(defaultVariant / error), 그 키로 variantStyles
가 조회됩니다.
design system default for variant (defaultsByVariant[variant] / defaultsBySize[size])
→ CoreTextFieldTheme.style // 프로젝트 공통
→ CoreTextFieldTheme.variantStyles[variant] // 에러 상태에서 파생된 variant 별
→ 부모 컴포넌트 슬롯 오버라이드
→ widget.textFieldStyle // 인스턴스별
nested slot styles 는 재귀 머지 — valueStyle.fontWeight 만 override 해도
valueStyle.fontSize / valueStyle.color 는 이전 레이어 값 유지.
변형 (Variants)#
variant는 errorText 파라미터로 자동 결정됩니다:
errorText가 null이면 → 기본 스타일errorText가 설정되면 → 에러 스타일 (빨간 보더)
// 기본 상태
TextField(
placeholder: Text('이름을 입력하세요'),
)
// 에러 상태 (errorText 설정 시 자동)
TextField(
errorText: '필수 입력 항목입니다',
placeholder: Text('이름을 입력하세요'),
)
// 비활성화
TextField(
enabled: false,
placeholder: Text('수정 불가'),
)
// 읽기 전용
TextField(
readOnly: true,
initialValue: '읽기 전용 값',
)
동작 스펙 (Behavior)#
포커스 관리#
- 클릭 또는 Tab 키로 포커스 획득 시 테두리 색상 변경
autofocus: true로 페이지 로드 시 자동 포커스
상태 전환#
-
default→focused(포커스 획득) →filled(값 입력) →default(포커스 해제) error:errorText설정 시 테두리와 에러 메시지 표시disabled: 모든 인터랙션 비활성화, 흐리게 표시readOnly: 포커스는 가능하나 편집 불가
사용 가이드라인 (Usage Guidelines)#
✅ Do#
항상 라벨과 함께 사용하세요.
TextField(
label: '이메일 주소',
placeholder: Text('user@example.com'),
)
라벨은 입력 필드의 용도를 명확히 하고, 접근성을 향상시킵니다.
❌ Don't#
placeholder만으로 라벨을 대체하지 마세요.
TextField(
placeholder: Text('이메일 주소'),
)
입력을 시작하면 placeholder가 사라져 필드 용도를 알 수 없게 됩니다.
✅ Do#
에러 메시지는 구체적으로 작성하세요.
TextField(
errorText: '이메일 형식이 올바르지 않습니다 (예: user@example.com)',
)
사용자가 무엇을 수정해야 하는지 바로 알 수 있습니다.
❌ Don't#
모호한 에러 메시지를 사용하지 마세요.
TextField(
errorText: '입력이 올바르지 않습니다',
)
사용자가 무엇을 고쳐야 하는지 알 수 없습니다.
접근성 (Accessibility)#
키보드 인터랙션#
| 키 | 동작 |
|---|---|
Tab | 다음 입력 필드로 이동 |
Shift+Tab | 이전 입력 필드로 이동 |
Enter | 폼 제출 (단일 라인) 또는 줄바꿈 (다중 라인) |
Escape | 포커스 해제 |
스크린 리더#
-
Flutter: 편집 표면의 시맨틱 노드가 활성 상태(
enabled)와SemanticsAction.focus핸들러를 진술합니다. 웹 엔진은 접근성이 켜지면 포인터 이벤트가 아니라 이 focus 액션으로 편집 세션을 열므로, 그 경로가 이 배선으로 성립합니다. 도달 범위는 여기까지입니다.- 노드가 내놓는 액션은
focus하나이고tap은 없습니다. 따라서 모바일 보조기술의 활성화 제스처가 보내는SemanticsAction.tap은 커버되지 않습니다. label:·errorText:는 시각 표시일 뿐 이 노드에 닿지 않습니다 — 노드의label은 빈 문자열입니다.
- 노드가 내놓는 액션은
- Web:
<input>요소의 네이티브 접근성.label속성으로 접근성 라벨 자동 연결
터치 타겟#
- 최소 터치 타겟 높이는 24 (
CoreTouchTarget.minimum) 이며, 실제 필드는 그보다 크게 그립니다. - 충분한 내부 패딩으로 터치 영역 확보