iOS IME 조합 중 폼 검증을 늦추는 테스트 방법

iOS IME 조합 중 폼 검증을 늦추는 테스트 방법

클라우드 Mac의 iOS 시뮬레이터에서 사용자가 중국어 이름을 입력한다. 병음 입력 중 아직 후보 글자를 선택하지 않았는데 폼에는 “중국어만 입력할 수 있습니다”라는 오류가 뜨고, 후보를 선택한 직후에는 제출 버튼이 잠깐 비활성화된다. 대개 정규식이 잘못된 것이 아니라, 앱이 IME 조합 중인 텍스트를 최종 확정된 값으로 취급해서 생기는 문제다. 이를 재현하고 해결하려면 text 속성만 볼 것이 아니라 필드 상태를 ‘조합 중’과 ‘확정됨’으로 나눠야 한다.

무엇을 검증할지 먼저 정하기

중국어 병음 입력, 일본어 가나 변환, 한국어 자모 조합에는 모두 후보 선택이나 조합 단계가 있다. UITextInput을 따르는 UITextField에서 markedTextRange가 nil이 아니면 현재 표시된 텍스트가 있다. 이때 사용자가 입력 중인 내용은 화면에 보여 주되, 이를 근거로 형식 오류를 표시하거나 제출을 허용하거나 최종 상태에 값을 기록해서는 안 된다.

입력 이벤트가 발생했다고 입력이 끝난 것은 아니다. 조합 중인 텍스트는 편집 과정의 일부이며, 비즈니스 규칙에 따른 검증은 확정된 값에 적용해야 한다.

먼저 필드의 동작 원칙을 정한다. 조합 중에는 기존 오류를 지우고 형식 검증을 멈춘다. 조합이 끝나면 현재 text를 기준으로 다시 검증한다. 제출할 때는 조합이 끝났는지 확인하고 값을 한 번 더 검증한다. 마지막 확인을 생략하면 후보가 확정되지 않은 상태에서도 다른 경로로 제출이 실행될 수 있다. 빈 값을 허용할지, 이름에 한자만 허용할지는 비즈니스 규칙이므로 조합 상태 판단과 분리해야 한다.

필드 이벤트에서 검증 미루기

아래 객체는 검증 함수를 언제 호출할지만 결정한다. 예시 규칙은 값이 비어 있지 않고 한자만 포함하도록 요구한다. 실제 프로젝트에서는 isValid에 해당하는 규칙을 바꿔야 하며, 이름 검증 규칙을 이메일이나 주소 필드에 그대로 적용해서는 안 된다.

import UIKit

@MainActor
final class NameFieldValidator {
    private(set) var message: String?
    private(set) var canSubmit = false

    func editingDidChange(_ field: UITextField) {
        guard field.markedTextRange == nil else {
            message = nil
            canSubmit = false
            return
        }
        evaluate(field.text ?? "")
    }

    func submit(_ field: UITextField) -> Bool {
        guard field.markedTextRange == nil else {
            canSubmit = false
            return false
        }
        evaluate(field.text ?? "")
        return canSubmit
    }

    private func evaluate(_ value: String) {
        canSubmit = !value.isEmpty &&
            value.unicodeScalars.allSatisfy {
                (0x3400...0x9FFF).contains(Int($0.value))
            }
        message = canSubmit ? nil : "请填写中文姓名"
    }
}

뷰 컨트롤러에서 .editingChanged를 editingDidChange(_:)에 연결하고, message와 canSubmit에 따라 안내 문구와 버튼을 갱신한다. 표시된 텍스트의 변화가 이벤트를 발생시키는지는 입력 경로에 따라 다르다. 후보 확정이 반드시 .editingChanged 한 번과 정확히 대응한다고 가정하지 말아야 한다. 편집이 끝날 때와 제출을 처리할 때도 필드 상태를 다시 읽어야 한다. 프로그램에서 text를 변경한 뒤에는 UI를 직접 갱신해야 한다. 값을 대입하는 것은 사용자가 직접 입력하는 것과 같지 않다. 예시의 코드 포인트 범위는 처리 흐름을 보여 주기 위한 것으로, 이름에 쓰이는 모든 문자나 확장 한자, 구분 기호를 포괄하지 않는다.

단위 테스트로 상태 전환 확인하기

앱의 단위 테스트 타깃에서 실제 UITextField를 만들고, 조합 중에는 제출할 수 없으며 확정 후에야 최종 텍스트를 기준으로 판단하는지 테스트한다. UIKit 객체는 메인 스레드에서 사용해야 한다. 이 테스트는 화면 키보드에 의존하지 않는다.

import XCTest
import UIKit

@MainActor
final class NameFieldValidatorTests: XCTestCase {
    func testMarkedTextIsNotSubmitted() {
        let field = UITextField()
        let validator = NameFieldValidator()

        field.setMarkedText("zhong", selectedRange: NSRange(location: 5, length: 0))
        validator.editingDidChange(field)

        XCTAssertNotNil(field.markedTextRange)
        XCTAssertFalse(validator.canSubmit)
        XCTAssertFalse(validator.submit(field))

        field.setMarkedText("中文", selectedRange: NSRange(location: 2, length: 0))
        field.unmarkText()
        validator.editingDidChange(field)

        XCTAssertNil(field.markedTextRange)
        XCTAssertEqual(field.text, "中文")
        XCTAssertTrue(validator.submit(field))
    }
}

테스트 타깃에 테스트 대상 타입을 포함한 뒤, 클라우드 Mac에서 Xcode의 Test 동작으로 실행한다. CI에 연결할 때는 먼저 Xcode에서 scheme을 공유하고 xcodebuild -showdestinations -scheme <方案名> -project <工程名>.xcodeproj로 사용 가능한 시뮬레이터를 찾는다. 선택한 platform=iOS Simulator,id=<设备ID>를 xcodebuild test에 전달한다. 기기 ID는 현재 머신에서 읽어야 하며, 다른 머신의 식별자를 스크립트에 고정해서는 안 된다.

이 테스트가 검증하는 것은 필드 상태에 대한 검증기의 반응이지, 시스템 키보드가 이벤트를 발생시키는 순서가 아니다. markedTextRange 단언이 실패하면 먼저 테스트 필드가 표시된 텍스트를 지원하는지 확인하고, 호출이 메인 스레드에서 실행되는지 점검한다. 테스트를 통과시키려고 조합 상태 단언을 삭제해서는 안 된다.

시뮬레이터에서 실제 입력 경로 확인하기

단위 테스트가 통과하면 클라우드 Mac의 그래픽 인터페이스에서 대상 iOS 시뮬레이터를 연다. 시스템 설정에서 대상 입력기를 활성화하고 현재 키보드가 실제로 해당 입력기로 전환됐는지 확인한다. 입력 내용을 물리 키보드의 텍스트로 바로 전달하는 설정을 끈 다음, 이름 필드를 눌러 다음을 확인한다.

  1. 병음을 입력하되 후보는 선택하지 않고 오류 안내와 제출 버튼을 확인한다. 어느 쪽도 확정되지 않은 텍스트를 유효한 이름으로 취급해서는 안 된다.
  2. 한자 후보를 선택한 뒤 안내 문구, 버튼, 필드 값이 일치하는지 확인한다. 이어서 한 글자를 삭제하고 확정된 값에 맞춰 검증 결과가 갱신되는지 확인한다.
  3. 조합 중에 키보드의 Return 키나 다른 제출 경로로 제출을 시도한다. 버튼이 비활성화돼 보이는 데 그치지 않고 제출 처리 단계에서도 거부하는지 확인한다.
  4. 대상 시스템 버전과 입력기를 바꿔 같은 검사를 반복한다. 자동화 스크립트가 완성된 한자를 붙여 넣기만 한다면 가장 중요한 조합 과정을 건너뛰게 된다.

“후보를 확정했는데 재검증되지 않는” 문제를 추적할 때는 .editingChanged, 편집 종료, 제출 처리 시점의 상태를 임시로 기록할 수 있다. 디버그 출력은 로컬 문제 해결에만 사용하고, 사용자가 입력한 내용을 영구 로그에 남겨서는 안 된다.

경계는 올바른 계층에 두기

실제 폼에서는 자동 완성, 붙여넣기, 음성 입력, 프로그램을 통한 값 채우기도 일어난다. 이런 경로는 표시된 텍스트를 만들지 않더라도 필드 값을 바꿀 수 있으므로, 최종 제출 단계에서 독립적으로 다시 검증해야 한다. 반대로 markedTextRange가 nil이 아니라는 이유로 편집 중인 텍스트를 버려서는 안 된다. 잘못된 판단을 일으킬 수 있는 비즈니스 규칙 검증만 멈추고, 입력 내용은 정상적으로 표시해야 한다.

검수할 때는 세 가지를 함께 확인한다. 후보가 나타나기 전에 잘못된 오류가 표시되는지, 확정 후 결과가 제때 갱신되는지, 버튼을 거치지 않는 제출도 검사를 통과해야만 처리되는지 살펴본다. 이 세 항목을 나눠 테스트해야 클라우드 Mac에서 재현된 입력 문제가 “한자를 입력할 수 있다”는 스크린샷 한 장에 가려지지 않는다.

자주 묻는 질문

IME 조합 중에는 왜 필드 검증을 미뤄야 하나요?

markedTextRange가 존재하면 입력 결과가 아직 바뀔 수 있습니다. 조합이 끝난 값에 대해 검증하고 제출할 때 다시 확인하세요.

XCTest만으로 실제 키보드 동작을 검증할 수 있나요?

아니요. XCTest는 검증 규칙을 확인합니다. 후보 선택과 이벤트 순서는 대상 iOS 버전의 시뮬레이터에서 별도로 점검해야 합니다.

전용 물리 노드에서 시작하기

다음 작업에 맞춰 클라우드 Mac 구성하기

모델의 메모리 용량이나 빌드 작업량에 맞춰 Mac mini를 선택하세요. 세 가지 모델을 일·주·월·분기 단위로 대여할 수 있으며, 노드의 실시간 이용 가능 여부는 콘솔에서 확인할 수 있습니다.

클라우드 Mac 대여하기