SwiftUI 포커스 시스템 & 키보드 내부
Source: Dev.to
SwiftUI 포커스는 간단해 보입니다:
@FocusState var isFocused: Bool
하지만 실제로는 그렇지 않을 때가 있습니다.
그때 겪게 되는 문제들은 다음과 같습니다:
- 포커스가 무작위로 사라짐
- 키보드가 예기치 않게 사라짐
- 포커스가 필드 사이를 뛰어넘음
- 폼 내비게이션이 깨짐
ScrollView와 키보드가 서로 충돌- 접근성 포커스가 다르게 동작
- 내비게이션 후 포커스가 복구되지 않음
이 글에서는 SwiftUI 포커스가 내부적으로 어떻게 동작하는지, 키보드, 내비게이션, 스크롤 뷰, 접근성과 어떻게 상호작용하는지, 그리고 실제 앱에서 올바르게 사용하는 방법을 설명합니다.
🧠 정신 모델: 포커스는 상태 + 라우팅
SwiftUI 포커스는 단순히 불리언이 아닙니다. 내부적으로는:
- 포커스 그래프
- 상태에 의해 구동
- 뷰 계층 구조를 통해 해결
- 키보드 + 접근성과 조정
포커스를 입력을 위한 네비게이션으로 생각하세요.
🧩 1. 포커스는 뷰 기반이 아니라 값 기반
가장 중요한 규칙.
잘못된 사고 모델: “이 TextField가 포커스를 소유한다.”
올바른 사고 모델: “포커스는 필드를 가리키는 상태이다.”
그래서 이렇게 동작한다:
enum Field {
case email
case password
}
@FocusState private var focusedField: Field?
SwiftUI는 포커스를 다음과 같이 해결한다:
- 포커스된 값을 매칭한다
- 뷰 트리를 순회한다
- 첫 번째 호환 가능한 포커스 대상을 찾는다
🧱 2. SwiftUI가 포커스 트리를 구축하는 방법
렌더링 시점에 SwiftUI는:
- 포커스 가능한 뷰를 스캔합니다
- 포커스 트리를 구축합니다
- 각각에 포커스 아이덴티티를 할당합니다
- 활성 포커스 상태를 해결합니다
포커스 가능한 뷰에는 다음이 포함됩니다:
TextFieldSecureFieldTextEditor- 사용자 정의 포커스 가능한 컨트롤
- 접근성 요소
뷰가 사라지면 → 해당 포커스 노드가 제거됩니다.
🔄 3. 왜 포커스가 “무작위로” 사라지는가
포커스가 사라지는 경우:
- 포커스된 뷰가 계층 구조를 떠날 때
- 뷰의 식별자가 변경될 때
- 포커스 상태 값이 변경될 때
- 네비게이션이 뷰를 제거할 때
- 부모가 재생성될 때
ScrollView가 콘텐츠를 다시 레이아웃할 때- 키보드 해제가 트리거될 때
무작위가 아니라 — 식별자 + 생명주기 때문입니다.
🧠 4. 포커스 vs. 뷰 아이덴티티 (중요 연결)
This breaks focus:
TextField("Email", text: $email)
.id(UUID()) // ❌
Why?
- identity changes → 아이덴티티가 변경됨
- focus node destroyed → 포커스 노드가 파괴됨
- focus state points to nothing → 포커스 상태가 아무것도 가리키지 않음
- keyboard dismisses → 키보드가 사라짐
Rule: 📌 Focus requires stable view identity. → 포커스는 안정적인 뷰 아이덴티티가 필요합니다.
⌨️ 5. 키보드는 부수 효과이며, 원천이 아니다
SwiftUI 포커스가 키보드를 제어합니다 — 반대는 아닙니다.
Flow:
Focus change
↓
Responder change
↓
Keyboard presentation
그래서 포커스를 업데이트하지 않고 키보드를 수동으로 해제하면 버그가 발생합니다.
Correct dismissal:
focusedField = nil
Avoid:
- 강제로
resignFirstResponder호출 - UIKit 해킹
- 포커스 업데이트 없이 제스처 기반 해제
📜 6. ScrollView + Keyboard 내부 동작
키보드가 나타날 때 SwiftUI는:
- 안전 영역 inset을 조정합니다
- 포커스된 필드가 보이도록 시도합니다
- 자동으로 스크롤할 수 있습니다
- 레이아웃이 복잡하면 실패할 수 있습니다
일반적인 문제:
- 중첩된
ScrollViews GeometryReader사용- 커스텀 레이아웃
- 동적인 높이 변화
베스트 프랙티스:
- 폼을 단순하게 유지합니다
- 폼에서
GeometryReader사용을 피합니다 - 적절할 때
.scrollDismissesKeyboard(.interactively)를 사용합니다
🧭 7. 프로그래밍 방식 포커스 (올바른 방법)
올바른 패턴:
focusedField = .email
지연된 포커스 (네비게이션 / 애니메이션) 경우:
Task {
try await Task.sleep(for: .milliseconds(100))
focusedField = .email
}
왜 지연이 필요한가?
- 포커스 트리가 존재해야 함
- 뷰가 렌더링되어야 함
- 네비게이션이 완료되어야 함
🧪 8. Focus & Navigation
탐색할 때:
- 포커스가 자동으로 전환되지 않습니다
- 새 뷰는 포커스가 없는 상태로 시작합니다
- 이전 포커스는 사라집니다
포커스 복원을 원한다면:
- 포커스 상태를 외부에 저장합니다
onAppear에서 복원합니다
.onAppear {
focusedField = savedFocus
}
♿ 9. Focus vs. Accessibility Focus
이들은 서로 다른 시스템입니다.
- Input focus → keyboard → 입력 포커스 → 키보드
- Accessibility focus → VoiceOver → 접근성 포커스 → VoiceOver
SwiftUI가 이를 조정하지만,
- 두 포커스가 달라질 수 있습니다.
- 접근성이 독립적으로 포커스를 이동시킬 수 있습니다.
- 접근성 포커스가 항상 키보드를 트리거하는 것은 아닙니다.
두 포커스가 동일하다고 가정하지 마세요.
🧠 10. 사용자 정의 포커스 가능한 뷰
You can make custom controls focusable:
.focusable()
.focused($focusedField, equals: .custom)
다음에 사용하세요:
- 사용자 정의 입력
- 게임 같은 UI
- tvOS / visionOS
- 고급 키보드 내비게이션
⚠️ 11. 가장 큰 포커스 안티‑패턴
Avoid:
- 인라인
UUID아이디 - 폼 행 재생성
- UIKit 응답자 혼합
- 포커스 업데이트 없이 키보드 해제
- ViewModel 대신 뷰에 포커스 로직 넣기
- 포커스 전환 중 무거운 레이아웃 변경
These cause ~90 % of focus bugs.
🧠 Focus System Cheat Sheet
- ✔ Focus is state → 포커스는 상태이다
- ✔ Identity must be stable → 아이덴티티는 안정적이어야 한다
- ✔ Keyboard follows focus → 키보드는 포커스를 따라간다
- ✔ Navigation destroys focus → 네비게이션은 포커스를 파괴한다
- ✔ Delay focus until views exist → 뷰가 존재할 때까지 포커스를 지연한다
- ✔ Accessibility focus is separate → 접근성 포커스는 별개이다
- ✔ Forms require layout stability → 폼은 레이아웃 안정성을 요구한다
🚀 최종 생각
SwiftUI 포커스는 약하지 않습니다 — 정확합니다. 다음을 이해하면:
- 포커스를 상태로
- 포커스‑트리 해석
- 키보드, 내비게이션 및 접근성과의 관계
일반적인 골칫거리 없이도 신뢰할 수 있는 프로덕션‑레디 폼을 만들 수 있습니다.
정체성 + 라이프사이클
- 키보드는 부수 효과로
Forms, editors, and input‑heavy screens become predictable and rock solid.
