반응형

SwiftUI Study – FocusState를 안전하게 설계하는 방법

 

1. 왜 중요한가 (문제 배경)

SwiftUI의 @FocusState는 키보드 포커스를 선언적으로 제어할 수 있게 해주는 강력한 도구이지만,

설계를 잘못하면 다음과 같은 문제가 쉽게 발생합니다.

  • 포커스가 특정 TextField에 안 잡힘 / 튐 / 사라짐
  • 화면 이동 후 포커스가 엉뚱한 필드에 들어감
  • 로컬 FocusState를 여러 곳에서 선언해 포커스 흐름이 꼬임
  • Bool 기반 FocusState를 여러 필드에 공유하여 예측 불가능한 동작 발생

이 문제들은 대부분 “포커스의 단일 소유자”를 두지 않았기 때문에 발생합니다.

 

2. 잘못된 패턴 예시

❌ 예시 1: Bool 하나로 여러 TextField 포커스를 제어

struct WrongFocusView: View {
    @State private var name = ""
    @State private var email = ""

    @FocusState private var isFocused: Bool   // ❌ 어떤 필드를 위한 것인지 구분 불가

    var body: some View {
        VStack {
            TextField("이름", text: $name)
                .focused($isFocused)

            TextField("이메일", text: $email)
                .focused($isFocused)

            Button("포커스 주기") {
                isFocused = true   // ❌ 어떤 필드가 포커스될지 예측 어려움
            }
        }
    }
}

문제점

  • 특정 필드에 포커스를 지정할 방법이 없음
  • UI 상황에 따라 다른 필드가 선택되는 예측 불가한 현상 발생

❌ 예시 2: 컴포넌트 내부에서 로컬 FocusState 선언

struct NameField: View {
    @Binding var text: String
    @FocusState private var focused: Bool   // ❌ 로컬에 종속된 포커스

    var body: some View {
        TextField("이름", text: $text)
            .focused($focused)
            .onAppear {
                focused = true   // 화면 전체 흐름과 무관하게 강제 포커스
            }
    }
}

문제점

  • 특정 화면 로직과 관계없이 컴포넌트가 스스로 포커스를 강제로 잡음
  • 상위 화면에서 포커스 흐름을 제어하기 어려움

 

3. 올바른 패턴 예시

✅ 예시 1: enum 기반 FocusState로 명확하게 제어

struct CorrectFocusView: View {
    enum Field: Hashable {
        case name
        case email
    }

    @State private var name = ""
    @State private var email = ""

    @FocusState private var focusedField: Field?

    var body: some View {
        VStack {
            TextField("이름", text: $name)
                .focused($focusedField, equals: .name)

            TextField("이메일", text: $email)
                .focused($focusedField, equals: .email)

            Button("이메일로 이동") {
                focusedField = .email
            }
        }
        .onAppear { focusedField = .name }
    }
}

장점

  • 포커스 대상이 명확하고 예측 가능
  • 화면 이동, 시트, Navigation과 조합해도 흐름이 깨지지 않음

✅ 예시 2: 화면 단위에서 FocusState를 관리하고, 자식 뷰에는 Binding으로 전달

struct ParentView: View {
    enum Field: Hashable { case name, email }

    @State private var name = ""
    @State private var email = ""

    @FocusState private var focusedField: Field?

    var body: some View {
        VStack {
            NameField(
                text: $name,
                isFocused: Binding(
                    get: { focusedField == .name },
                    set: { $0 ? focusedField = .name : focusedField = nil }
                )
            )

            EmailField(
                text: $email,
                isFocused: Binding(
                    get: { focusedField == .email },
                    set: { $0 ? focusedField = .email : focusedField = nil }
                )
            )
        }
        .onAppear { focusedField = .name }
    }
}

struct NameField: View {
    @Binding var text: String
    @Binding var isFocused: Bool

    var body: some View {
        TextField("이름", text: $text)
            .focused($isFocused)
    }
}

장점

  • 포커스 주체(부모)가 명확해져 흐름이 안정됨
  • 자식은 단순히 포커스를 표현만 할 뿐 제어하지 않음

 

4. 실전 적용 팁

✔ 팁 1 – FocusState는 화면 단위 하나만 두는 것이 가장 안전

포커스의 소유권을 단일화하면 포커스 튐/소실 문제 대부분이 해결됩니다.

✔ 팁 2 – 어떤 필드에 포커스를 줄지 명확하게 표현할 수 있는 enum 기반 설계를 사용

Bool 기반 FocusState는 예측할 수 없는 동작을 만들기 쉽습니다.

✔ 팁 3 – 자식 컴포넌트에서 onAppear로 강제 포커스를 잡지 말 것

포커스 흐름은 부모에서 통제해야 합니다.

✔ 팁 4 – NavigationStack, sheet, fullScreenCover와 함께 사용할 때 더 빛남

다양한 화면 전환 상황에서도 포커스가 일관적으로 유지됩니다.

 

5. 정리

  • FocusState는 “누가 포커스를 제어하는가”를 명확히 해야 안정적으로 동작합니다.
  • Bool 기반 포커스는 절대 여러 필드에 공유하지 말 것.
  • enum 기반 FocusState가 가장 예측 가능하고 확장 가능하며 실무 친화적입니다.
  • 자식 뷰는 포커스를 표현만 하고, 제어는 부모가 담당하도록 분리하는 것이 가장 안전합니다.
반응형
Posted by 까칠코더
,