Checkbox
하나 이상 선택하는 체크박스. 불확정(indeterminate) 상태 지원
목차
하나 이상의 항목을 선택할 수 있는 체크박스 컴포넌트입니다. 불확정(indeterminate) 상태를 지원합니다.
기본 사용법 (Basic Usage)#
값은 Flutter 의 tristate bool? 입니다 — true / false / null(불확정).
onChanged 는 다음 값을 받습니다.
// 기본 체크박스
Checkbox(
checked: false,
onChanged: handleChanged,
label: '이용약관에 동의합니다',
)
// 체크된 상태
Checkbox(
checked: true,
onChanged: handleChanged,
label: '알림 받기',
)
// 불확정 상태
Checkbox(
checked: null,
onChanged: handleChanged,
tristate: true,
label: '전체 선택',
)
// 기본 체크박스
Checkbox(
checked: false,
onChanged: handleChanged,
label: '이용약관에 동의합니다',
)
// 체크된 상태
Checkbox(
checked: true,
onChanged: handleChanged,
label: '알림 받기',
)
// 불확정 상태
Checkbox(
checked: null,
onChanged: handleChanged,
label: '전체 선택',
)
Live Preview#
코드 보기
class CheckboxDefaultExample extends StatefulComponent {
const CheckboxDefaultExample({super.key});
@override
State<CheckboxDefaultExample> createState() => _CheckboxDefaultExampleState();
}
class _CheckboxDefaultExampleState extends State<CheckboxDefaultExample> {
bool? _checked = false;
@override
Component build(BuildContext context) {
return Checkbox(
checked: _checked,
label: 'Accept terms',
onChanged: (value) => setState(() => _checked = value),
);
}
}
class CheckboxDefaultExample extends StatefulWidget {
const CheckboxDefaultExample({super.key});
@override
State<CheckboxDefaultExample> createState() => _CheckboxDefaultExampleState();
}
class _CheckboxDefaultExampleState extends State<CheckboxDefaultExample> {
bool? _checked = false;
@override
Widget build(BuildContext context) {
return Checkbox(
checked: _checked,
label: 'Accept terms',
onChanged: (value) => setState(() => _checked = value),
);
}
}
구조 (Anatomy)#
코드가 정본입니다. 부분 이름은 위젯 슬롯과 같고, Figma ❖ Checkbox
레이어와도 맞습니다.
| 부분 | 역할 | 슬롯 |
|---|---|---|
prefix | 인디케이터 왼쪽 | prefix |
suffix | 라벨 오른쪽 | suffix |
trailing | 행 끝 | trailing |
사용 시기 (When to Use)#
이 컴포넌트를 사용하세요:
- 여러 옵션 중 하나 이상을 선택할 때 (다중 선택)
- 약관 동의, 옵션 활성화 등 boolean 값을 입력받을 때
- "전체 선택" 같은 부분 선택 상태(indeterminate)를 표현할 때
대신 다른 컴포넌트를 사용하세요:
- SwitchField: 체크·불확정 상태 없이 라벨·설명을 가진 bool 설정 행이 필요할 때
- Toggle: 체크·불확정 표시 없이 on/off 스위치 표현이 필요할 때
- RadioGroup: 여러 옵션 중 하나만 선택해야 할 때
- Select: 옵션 목록을 펼쳐 두는 대신 드롭다운에서 값 하나를 선택할 때
Props / Parameters#
W 는 Flutter의 Widget, Web의 Jaspr Component 입니다. W?
는 선택 슬롯입니다.
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
checked |
bool? |
명시¹ | 체크 값 — true / false / null(불확정) |
onChanged |
void Function(bool?)? |
명시¹ | 다음 값을 받는 콜백. 표시 전용이면 null |
label | String? | null | 라벨 텍스트 |
labelMaxLines |
int? |
null |
라벨이 넘칠 때까지 허용하는 줄 수. null 은 현행대로 줄 수를 제한하지 않습니다 |
labelOverflow |
CoreTextOverflow? |
null |
labelMaxLines
에 도달한 라벨의 넘침 처리 (
clip
/
fade
/
ellipsis
/
visible
)
|
description |
String? |
null |
라벨 아래 보조 텍스트 |
tristate |
bool |
false |
탭이 true → false → null → true 로 순환 |
enabled |
bool? |
null |
활성화 여부 (null = onChanged != null) |
size |
CoreComponentSize |
md |
크기 토큰 (xs / sm / md / lg / xl / xl2) |
prefix | W? | null | 인디케이터 왼쪽 슬롯 |
suffix | W? | null | 라벨을 대신하는 슬롯 |
trailing | W? | null | 행 끝 슬롯 |
name | String? | null | 폼 필드 이름 |
checkboxStyle |
CoreCheckboxStyle? |
null |
박스 chrome / 상태 레이어 / 중첩 슬롯 묶음 (아래 Style 표 참고) |
focusNode |
FocusNode? |
null |
Flutter 전용 — Web은 브라우저 기본 포커스 |
statesController |
WidgetStatesController? |
null |
Flutter 전용 — Web은 CSS pseudo-class |
controller |
CheckboxController? |
null |
Flutter 전용 — 외부에서 상태를 제어할 때 |
¹ 공통 호출에서는 checked 와 onChanged 를 모두 명시합니다. Flutter는 두 인자를 요구하고 Web은 생략 시 false
/ null 을 쓰는 기존 호환 완화를 유지합니다. 이는 새 동작 옵션이 아니라 Web의 soft requiredness 예외입니다.
값은 tristate
bool?하나입니다 —true/false/null(불확정). 크기 외의 시각 축은 없어variant인자도 없습니다. hover / focus / pressed 는 prop 이 아닙니다 — 각 플랫폼이 감지하고 chrome 은checkboxStyle의 상태별 필드(stateLayer*,clickableStyle.focusOutlineStyle)에서 읽습니다.
빠른 오버라이드 (Chain)#
이미 만든 Checkbox 인스턴스에 스타일을 빠르게 덧붙이고 싶다면 withStyle 체인을 쓸 수 있습니다. 생성자의 style 슬롯 인자와 동일하게 동작하지만, 이미 구성된 위젯 위에서 바로 이어 쓸 수 있습니다.
class CheckboxChainExample extends StatefulWidget {
const CheckboxChainExample({super.key});
@override
State<CheckboxChainExample> createState() => _CheckboxChainExampleState();
}
class _CheckboxChainExampleState extends State<CheckboxChainExample> {
bool? _checked = false;
@override
Widget build(BuildContext context) {
return Checkbox(
checked: _checked,
label: 'Accept terms',
onChanged: (value) => setState(() => _checked = value),
).withStyle(
const CoreCheckboxStyle(
boxBorderColor: CoreColor.token(CoreColors.primary),
boxBorderWidth: CoreStrokeWidth.stroke2,
boxBorderRadius: CoreBorderRadius.all(CoreRadius.radius8),
boxSize: CoreSize.size24,
indicatorLabelGapStyle: CoreGapStyle(size: CoreSpace.space12),
),
);
}
}
class CheckboxChainExample extends StatefulComponent {
const CheckboxChainExample({super.key});
@override
State<CheckboxChainExample> createState() => _CheckboxChainExampleState();
}
class _CheckboxChainExampleState extends State<CheckboxChainExample> {
bool? _checked = false;
@override
Component build(BuildContext context) {
return Checkbox(
checked: _checked,
label: 'Accept terms',
onChanged: (value) => setState(() => _checked = value),
).withStyle(
const CoreCheckboxStyle(
boxBorderColor: CoreColor.token(CoreColors.primary),
boxBorderWidth: CoreStrokeWidth.stroke2,
boxBorderRadius: CoreBorderRadius.all(CoreRadius.radius8),
boxSize: CoreSize.size24,
indicatorLabelGapStyle: CoreGapStyle(size: CoreSpace.space12),
),
);
}
}
스타일 시스템 (Style System)#
Checkbox 의 box chrome / 상태 레이어 / 슬롯 미세 조정은 단일 checkboxStyle
(CoreCheckboxStyle) 으로 흐릅니다. 시맨틱 enum (size) 은 위젯 파라미터.
보더(stroke1_5, 활성 경계)와 모서리(radius4)는 사이즈와 무관한 flat 기본값이고,
박스·글리프 길이·gap·라벨·설명 fontSize(설명은 11 / 12 / 13 / 14 / 14 / 14 로
라벨 한 단 아래) 만 defaultsBySize 사다리를 탑니다.
체크 표시는 평범한 LucideIcons.check 아이콘이라 별도 예외 없이
Lucide 획 규칙을 따르고, 체크박스 전 크기에서 하한 1.25로
그려집니다. markThickness 는 일부 선택 가로줄의 두께만 정하며 기본값이 그 규칙의 하한과 같아서,
모든 크기에서 체크와 가로줄이 같은 두께로 읽힙니다.
Checkbox(
size: CoreComponentSize.md,
checked: true,
onChanged: handleChange,
label: '동의함',
checkboxStyle: CoreCheckboxStyle(
boxBorderColor: CoreColor.token(CoreColors.outline),
boxActiveColor: CoreColor.token(CoreColors.primary),
boxBorderRadius: CoreBorderRadius.all(CoreRadius.radius4),
// hover 8% / pressed 16% halo — 박스 +8, radius 8
stateLayer: CoreStateLayer.standard,
stateLayerSpread: CoreSpace.space4,
indicatorLabelGapStyle: CoreGapStyle(size: CoreSpace.space8),
checkmarkIconStyle: CoreIconStyle(
size: CoreSize.size16,
color: CoreColor.token(CoreColors.onPrimary),
),
labelStyle: CoreTextStyle(fontWeight: CoreFontWeight.semiBold),
),
)
CoreCheckboxStyle 필드#
| 필드 | 타입 | 설명 |
|---|---|---|
boxBackgroundColor |
CoreColor? |
Resting (unchecked) box fill colour override.
null
defers to [defaultBoxBackgroundColor].
|
boxActiveColor |
CoreColor? |
Box fill colour when checked or indeterminate — the border takes the same colour, so the stroke blends into the fill.
null
defers to [defaultBoxActiveColor].
|
boxBorderColor |
CoreColor? |
Resting (unchecked) box stroke colour override. null defers to [defaultBoxBorderColor]. |
boxBorderWidth |
double? |
Box stroke width override (logical px, pre-scaling).
null
defers to [defaultBoxBorderWidth].
|
boxBorderRadius |
CoreBorderRadius? |
Box corner override.
null
defers to [defaultBoxBorderRadius]. The focus ring's corner derives from this one (plus its own offset), so the ring stays concentric with a rounder box.
|
boxSize |
double? |
Box size (width = height) override (logical px).
null
defers to [defaultsBySize] for the widget's size.
|
checkmarkSize |
double? |
Checkmark glyph icon size override (logical px).
null
falls back to [defaultsBySize] for the widget's size + any nested
checkmarkIconStyle.size
override.
|
indeterminateLength |
double? |
Indeterminate dash
length
override (logical px) — the dash is a horizontal pill [markThickness] tall.
null
falls back to [defaultsBySize] for the widget's size.
|
indeterminateBorderRadius |
CoreBorderRadius? |
Corner of the indeterminate dash. null → [defaultIndeterminateBorderRadius] (a pill). |
markThickness |
double? |
Thickness of the indeterminate dash — one constant across sizes. The checkmark is an icon and follows the icon stroke rule, not this value.
null
defers to [defaultMarkThickness].
|
disabledBackgroundColor |
CoreColor? |
Box fill when disabled. null → [defaultDisabledBackgroundColor]. |
disabledBorderColor |
CoreColor? |
Box stroke when disabled. null → [defaultDisabledBorderColor]. |
disabledForegroundColor |
CoreColor? |
Label and description foreground when disabled.
null
→ [defaultDisabledForegroundColor].
|
disabledCheckmarkColor |
CoreColor? |
Checkmark / dash colour when disabled. null → [defaultDisabledCheckmarkColor]. |
stateLayer |
CoreStateLayer? |
Ramp the halo is painted at while hovered or pressed.
null
→ [defaultStateLayer]. The focus rung is unused — focus draws the ring.
|
stateLayerColor |
CoreColor? |
Halo ink. null → [defaultStateLayerColor]. |
stateLayerSpread |
double? |
How far the halo reaches beyond the box on each side (logical px, pre-scaling).
null
→ [defaultStateLayerSpread].
|
stateLayerBorderRadius |
CoreBorderRadius? |
Halo corner. null → [defaultStateLayerBorderRadius]. |
indicatorLabelGapStyle |
CoreGapStyle? |
Nested [CoreGapStyle] slot for the gap between the checkbox indicator (box) and the label / prefix / suffix slot — forwarded straight to the
Gap
widget that separates the slots.
null
defers to [defaultsBySize] for the widget's size, then to [defaultIndicatorLabelGapStyle].
|
transitionDuration |
Duration? |
State-change transition duration override (box colour fade + glyph slot scale / opacity + halo fade).
null
defers to [defaultTransitionDuration].
|
checkmarkIconStyle |
CoreIconStyle? |
Checkmark icon style override (size / colour). Merged on top of
CoreIconStyle(size: checkmarkSize)
and [defaultCheckmarkIconStyle], so a colour-only override keeps the size-ladder glyph and a size-only override keeps the
onPrimary
ink.
|
clickableStyle |
CoreClickableStyle? |
Nested [CoreClickableStyle] slot for the composed
Clickable
(press scale / durations / disabled opacity). Merged on top of [defaultClickableStyle] and raw-forwarded; the Clickable's own resolver fills the remaining defaults. Its
focusOutlineStyle
half is inert here — see [focusOutlineStyle].
|
focusOutlineStyle |
CoreFocusOutlineStyle? |
Nested [CoreFocusOutlineStyle] slot for the ring the checkbox draws around its box while focused. Merged on top of [defaultFocusOutlineStyle]; the corner comes from the box. |
labelStyle |
CoreTextStyle? |
Label text style override. |
descriptionStyle |
CoreTextStyle? |
Description text style override — the supporting line under the label.
null
defers to [defaultsBySize] for the widget's size (which carries only the
fontSize
step), then to [defaultDescriptionStyle] for the role and colour.
|
labelDescriptionGapStyle |
CoreGapStyle? |
Nested [CoreGapStyle] slot for the vertical gap between the label and the description below it.
null
defers to [defaultLabelDescriptionGapStyle].
|
minHeight |
double? |
Minimum height of the pressable row — box, label and description together — before scaling. The row keeps its cross-axis alignment in the extra height and stays the press area as a whole, so a theme can give every checkbox a touch target (for example
CoreA11ySize.touchTarget
). Falls back to [defaultMinHeight].
|
Resolve chain#
design system default (flat default* + defaultsBySize[size])
→ CoreCheckboxTheme.style
→ 부모 컴포넌트 슬롯 오버라이드
→ widget.checkboxStyle
변형 (Variants)#
값#
// 체크됨
Checkbox(
checked: true,
label: '선택됨',
onChanged: handleChanged,
)
// 불확정
Checkbox(
checked: null,
label: '일부 선택',
onChanged: handleChanged,
)
// 비활성화 — 박스·마크·라벨 모두 disabled 토큰 계열
Checkbox(
checked: false,
enabled: false,
label: '비활성',
onChanged: handleChanged,
)
상태 (hover / focus / pressed)#
prop 이 아니라 감지입니다. hover 8% · pressed 16% 의 on-surface halo 가
박스 바깥 +8 (radius 8) 에 깔리고, 키보드 포커스는 박스를 감싸는
링(+4, focus-ring) 으로 표시됩니다 — 행 전체가 아닙니다. 박스와 링 사이 2px 간격은
투명이라 체크박스가 놓인 배경이 보입니다.
동작 스펙 (Behavior)#
상태 전환#
- 2단계 (
tristate: false):true↔false토글 -
3단계 (
tristate: true):true→false→null(불확정) →true순환 -
checked는 호출부의 값입니다. 활성화는 다음 값으로onChanged를 호출할 뿐 자체 선택 값을 바꾸지 않으므로 부모가 갱신해야 합니다.enabled: false는 활성화·포커스 진입을 막고,enabled를 생략하면 콜백 유무로 결정합니다.
사용 가이드라인 (Usage Guidelines)#
✅ Do#
체크박스에 항상 라벨을 제공하세요.
Checkbox(
checked: false,
onChanged: handleChanged,
label: '마케팅 이메일 수신에 동의합니다',
)
라벨 영역도 클릭 가능하여 조작이 쉬워집니다.
❌ Don't#
라벨 없이 체크박스만 두지 마세요.
Checkbox(
checked: false,
onChanged: handleChanged,
)
체크박스의 목적을 알 수 없고, 작은 영역만 클릭 가능합니다.
✅ Do#
긍정형 라벨을 사용하세요.
Checkbox(label: '알림 받기', checked: false, onChanged: handleChanged)
체크 = 활성화가 직관적입니다.
❌ Don't#
부정형 라벨을 사용하지 마세요.
Checkbox(label: '알림 받지 않기', checked: false, onChanged: handleChanged)
체크하면 "받지 않기"가 되어 이중 부정으로 혼란스럽습니다.
접근성 (Accessibility)#
역할#
-
Flutter —
Semantics(checked: ..., mixed: ...)로 체크 상태를 내보냅니다. 합성된Clickable에semanticsButton: false를 넘겨 일반 "버튼" 역할은 꺼져 있고,Clickable이 붙이는Semantics(enabled:)는 그대로 남습니다. -
Web — 인터랙션 루트에
role="checkbox"·aria-checked="true" | "false" | "mixed"·tabindex가 붙고, 비활성이면aria-disabled="true"가 더해집니다.
name 을 주면 폼 제출용 히든 <input type="checkbox"> 가 함께 나가지만 sr-only
· tabindex="-1" · aria-hidden="true" 라 접근성 트리에는 들어가지 않습니다 — 상태는 위의 role="checkbox"
노드가 나릅니다.
키보드#
| 키 | Flutter | Web |
|---|---|---|
Space | 토글 | 토글 |
Enter | 토글 | 토글 |
Tab | 다음 포커스 가능 요소로 | 다음 포커스 가능 요소로 |
Shift+Tab | 이전 포커스 가능 요소로 | 이전 포커스 가능 요소로 |
활성화 키는 양쪽 다 합성된 Clickable 이 처리합니다 — 네이티브 체크박스와 달리 Space 뿐 아니라 Enter
로도 토글됩니다. tristate: true 면 키 활성화도 포인터와 같은 3-상태 순환(체크 → 해제 → 부분 선택)을 탑니다.
비활성 체크박스는 양 플랫폼 모두 Tab 순회에서 빠집니다 — Flutter 는 FocusableActionDetector(enabled: false), Web 은
tabindex="-1".
스크린 리더#
접근 가능한 이름은 role="checkbox" 노드의 내용에서 옵니다 — label(또는 suffix
· prefix)에 넣은 텍스트가 그대로 이름이 됩니다. 셋 다 비우면 이름이 비고, 리더는 역할과 상태만 읽습니다(아래 알려진 제약 참고).
부분 선택(CoreCheckboxState.indeterminate)은 양쪽 다 별도로 통보됩니다 — Flutter 는 Semantics(mixed: true), Web 은
aria-checked="mixed". 해제로 반올림되지 않습니다.
체크가 바뀐 사실을 따로 알리는 live region 은 없습니다 — 상태는 체크박스 노드 자체에 실려 있습니다.
포커스 관리#
- 진입 — 이동 없음. 오버레이가 아니라 인라인 컨트롤이라 마운트 시 포커스를 옮기지 않습니다.
- 이탈 — 해당 없음.
-
트랩 — 없음.
Tab은 체크박스를 지나 그대로 빠져나갑니다. 단 Web 은 한 체크박스에서 두 번 멈춥니다(아래 알려진 제약). -
포커스 표시 — 합성된
Clickable의 포커스 링이 양쪽에서 키보드 포커스일 때만 나타납니다(focusOutline기본값true).
터치 타깃 (눌림 영역 최소 높이)#
라벨·설명을 포함한 행 전체가 하나의 눌림 영역입니다. 기본값은 바닥이 없어(minHeight = 0) 행이 내용 높이 그대로이며, 누르는 영역의 하한은 공통 24 를 따릅니다. 터치 화면에서 48 이 필요한 앱은 테마로 바닥을 켭니다 — 행이
minHeight × scaling 이상이 되고 내용은 가운데에 놓이며, 늘어난 높이까지 눌립니다.
ThemeData(
coreComponentTheme: CoreComponentTheme(
checkbox: CoreCheckboxTheme(
style: CoreCheckboxStyle(minHeight: CoreA11ySize.touchTarget),
),
),
)
Web 은 같은 값을 행에 min-height 로 냅니다.
알려진 제약#
-
Web 은
role="checkbox"를 중첩된 두 요소에 동시에 붙입니다. 컴포넌트가 계산한 속성 묶음(role·aria-checked·tabindex)이 바깥 인터랙션 루트와 안쪽 사각형<div>양쪽에 그대로 실려서, 스크린 리더는 체크박스 안에 체크박스가 하나 더 있는 것으로 읽고Tab은 하나의 체크박스에서 두 번 멈춥니다. 그중 안쪽 사각형에 포커스가 있을 때는Space·Enter가 아무 일도 하지 않습니다 — 키보드 활성화는 인터랙션 루트 자신이 포커스일 때만 동작하도록 이벤트 대상이 게이트되어 있고, 포인터 클릭만 루트까지 버블링됩니다.id를 넘겼다면 같은 이유로 DOM 에 두 번 나타납니다. 두 요소 모두 컴포넌트 내부에서 만들어져 호출부가 끊을 수 있는 지점이 없습니다. -
라벨 없이 쓰면 이름이 비고, Web 에는 이름을 채울 다른 경로가 없습니다. 이름은
label이나suffix의 내용에서만 오는데, WebCheckbox는attributes·classes·css를 생성자에서 받지 않습니다 —copyWith가 인자로 받아들이기는 하지만 새 인스턴스로 넘기지 않습니다. 표의 전체 선택 열처럼 시각 라벨을 둘 수 없는 자리에서는 Flutter 만 호출부의Semantics(label: ...)로 이름을 줄 수 있습니다. -
Web 에서
CoreDuration스케일 밖transitionDuration은 '동작 줄이기' 설정에 반응하지 않습니다. 스케일 위의 값은var(--coui-duration-*)로 나가 media query 가 덮지만 — 그 media query 는 전환 tier(0초과 ~CoreReducedMotion.transitionCeilingMs이하)의 변수만 0 으로 덮고 표시 tier 는 값을 유지합니다 — 그 밖의 값은 인라인{ms}ms리터럴로 나가 덮이지 않습니다. Flutter 는 어떤 값이든 억제합니다.CoreDuration의 alias(instant·fast…)나msN스케일 값을 쓰면 양쪽이 같아집니다.
스크린 리더 / ARIA#
체크박스 하나는 접근성 트리에서 노드 하나, 탭 순서에서 정지 하나입니다.
-
Web —
role="checkbox"·aria-checked(true/false/ 부분 선택이면mixed) · 비활성일 때aria-disabled="true"·tabindex, 그리고 호출자가 준id는 인터랙션 루트 한 곳에만 실립니다. 안쪽의 네모 상자는 순수한 시각 요소라 아무 정체성도 갖지 않습니다 — 같은id가 문서에 둘 생기거나 체크박스 안에 체크박스가 읽히지 않습니다. -
Flutter —
Semantics(checked:, mixed:)가 상태를 말하고, 일반 버튼 역할은 걷어내 "버튼" 이 아니라 "체크박스" 로 읽힙니다. 비활성이면 비활성으로 발표됩니다. -
접근 가능한 이름은 양 플랫폼 모두 루트의 내용 —
label(과description) 텍스트 — 에서 옵니다. 라벨 없이 쓰면 이름이 없으므로, 그때는prefix/suffix로 텍스트를 넣거나 호출자가 이름을 붙여야 합니다. -
폼 제출용 숨은 입력 — Web 은
name을 줄 때만<input type="checkbox">를 함께 그립니다. 이 입력은aria-hidden="true"·tabindex="-1"이라 리더에도 탭 순서에도 나타나지 않고, 폼 값만 나릅니다.
크로스 플랫폼 차이점 (Platform Differences)#
| 항목 | Flutter | Web |
|---|---|---|
| 체크 의미 | Semantics(checked:, mixed:) |
Clickable 루트의 checkbox 역할·aria-checked |
| 폼 값 | FormValueSupplier 및 Flutter 컨트롤러 |
FormValueSupplier 와 이름이 있을 때 숨은 native input |
| 포커스·상태 인프라 | FocusNode, WidgetStatesController |
DOM 포커스 · pseudo-class |
| 최소 히트 영역 | TouchTarget 래퍼 | 합성된 Clickable의 touch-target 마커 |
| 틀 굵기 1.5 | 1.5 그대로 | 1배 화면은 브라우저가 1.5 CSS px 를 기기 픽셀로 내려 1px 로 그릴 수 있음 (2배 이상은 3 기기 픽셀) |