모바일 API용 JSON vs Protobuf: 크기, 호환성, 디버깅
모바일 API에서 JSON과 Protobuf의 페이로드 크기, 호환성, 디버깅 트레이드오프를 설명하고 텍스트 또는 바이너리 포맷을 선택하는 실용 규칙을 제시합니다.
네이티브처럼 느껴지는 SwiftUI 폼 유효성 검사: 포커스를 자연스럽게 관리하고, 적절한 시점에 인라인 오류를 보여주며, 서버 메시지를 명확히 표시해 사용자를 불편하게 하지 않습니다.

touched와 submitted를 기준으로 무엇을 보여줄지 결정할 수 있고, 모든 키 입력에 반응할 필요가 없습니다.\n\nswift\nstruct FieldState {\n var value: String = \"\"\n var touched: Bool = false\n var localError: String? = nil\n var serverError: String? = nil\n\n // One source of truth for what the UI displays\n func displayedError(submitted: Bool) -\u003e String? {\n guard touched || submitted else { return nil }\n return localError ?? serverError\n }\n}\n\nstruct FormState {\n var submitted: Bool = false\n var email = FieldState()\n var password = FieldState()\n}\n\n\n몇 가지 작은 규칙이 이를 예측 가능하게 유지합니다:\n\n- 로컬 오류와 서버 오류를 분리하세요. 로컬 규칙(예: "필수", "잘못된 이메일 형식")이 "이미 사용 중인 이메일"과 같은 서버 메시지를 덮어쓰지 않아야 합니다.\n- 사용자가 해당 필드를 다시 편집하면 serverError를 지워 오래된 메시지를 계속 보고 있지 않게 하세요.\n- touched = true는 사용자가 필드를 떠날 때(또는 그들이 상호작용을 시도한 것으로 판단할 때)만 설정하세요. 첫 글자에서 설정하지 마세요.\n\n이 구조를 갖추면 뷰는 value에 자유롭게 바인딩할 수 있습니다. 유효성 검사는 localError를 업데이트하고 API 레이어는 serverError를 설정하되 서로 충돌하지 않습니다.\n\n## 사용자를 안내하는 포커스 처리\n\n좋은 SwiftUI 유효성 검사는 시스템 키보드가 작업을 완료하도록 도와주는 느낌이어야지, 앱이 꾸짖는 느낌이 나면 안 됩니다. 포커스는 이 느낌의 큰 부분입니다.\n\n간단한 패턴은 @FocusState를 사용해 포커스를 단일 진실 소스처럼 다루는 것입니다. 필드별 enum을 정의하고 각 필드를 바인딩한 다음, 사용자가 키보드 버튼을 탭하면 다음으로 이동시킵니다.\n\nswift\nenum Field: Hashable { case email, password, confirm }\n\n@FocusState private var focused: Field?\n\nTextField(\"Email\", text: $email)\n .textContentType(.emailAddress)\n .keyboardType(.emailAddress)\n .textInputAutocapitalization(.never)\n .submitLabel(.next)\n .focused($focused, equals: .email)\n .onSubmit { focused = .password }\n\nSecureField(\"Password\", text: $password)\n .submitLabel(.next)\n .focused($focused, equals: .password)\n .onSubmit { focused = .confirm }\n\n\n이게 네이티브처럼 느껴지게 하는 핵심은 절제입니다. 다음, 완료, 기본 버튼 같은 명확한 사용자 동작에서만 포커스를 이동하세요. 제출 시에는 첫 번째 유효하지 않은 필드로 포커스를 맞추고(필요하면 스크롤도 하세요). 사용자가 입력하는 도중에 포커스를 빼앗지 마세요. 키보드 라벨도 일관되게 유지하세요: 중간 필드에는 Next, 마지막 필드에는 Done.\n\n예를 들어 회원가입에서 사용자가 "Create Account"를 탭하면, 한 번 검증을 실행하고 오류를 보여준 뒤 첫 번째 실패 필드(보통 Email)로 포커스를 맞춥니다. 사용자가 Password 필드에 있고 타이핑 중이라면 입력 도중 Email로 다시 점프하지 마세요. 이 작은 차이가 "다듬어진 iOS 폼"과 "짜증나는 폼"을 가르는 경우가 많습니다.\n\n## 적절한 시점에 나타나는 인라인 오류\n\n인라인 오류는 잔소리가 아니라 조용한 힌트처럼 보여야 합니다. "네이티브"와 "짜증나는" 차이는 메시지를 언제 보여주는지에 크게 좌우됩니다.\n\n### 타이밍 규칙\n\n오류를 입력 시작하자마자 보여주면 방해가 됩니다. 더 좋은 규칙은 사용자가 필드를 마무리할 기회를 공정하게 줄 때까지 기다리는 것입니다.\n\n오류를 드러내기 좋은 순간:\n\n- 필드 포커스가 벗어났을 때\n- 사용자가 제출을 탭했을 때\n- 타이핑 중 잠깐 멈췄을 때(명백한 검사, 예: 이메일 형식 확인에만)\n\n신뢰할 수 있는 접근법은 필드가 touched 되었거나 제출이 시도된 경우에만 메시지를 보여주는 것입니다. 새 폼은 차분하게 유지되지만, 사용자가 상호작용하면 명확한 안내를 받습니다.\n\n### 레이아웃과 스타일\n\n오류가 나타나면서 레이아웃이 튀는 것보다 더 iOS스럽지 못한 것은 없습니다. 메시지를 위한 공간을 미리 확보하거나 표시될 때 애니메이션으로 자연스럽게 나타나 다음 필드를 갑자기 밀어내지 않게 하세요.\n\n오류 텍스트는 짧고 구체적으로, 메시지당 하나의 수정 작업을 제시하세요. "비밀번호는 최소 8자여야 합니다"는 실행 가능한 문구입니다. "잘못된 입력" 같은 문구는 그렇지 않습니다.\n\n스타일은 미묘하고 일관되게 하세요. 필드 아래 작은 폰트(예: footnote), 일관된 오류 색상, 필드의 부드러운 강조가 진한 배경보다 더 잘 읽힙니다. 값이 유효해지면 즉시 메시지를 지우세요.\n\n현실적인 예: 회원가입 폼에서는 사용자가 name@처럼 입력하는 동안 "이메일이 유효하지 않습니다"를 보여주지 마세요. 필드를 떠났을 때나 잠깐 멈췄을 때 보여주고, 주소가 유효해지면 바로 제거하세요.\n\n## 로컬 유효성 흐름: 입력, 필드 이탈, 제출\n\n좋은 로컬 흐름은 세 가지 속도를 가집니다: 입력 중에는 부드러운 힌트, 필드를 벗어날 때는 더 엄격한 검사, 제출 시에는 전체 규칙. 이 리듬이 유효성 검사를 네이티브처럼 느끼게 합니다.\n\n사용자가 입력하는 동안에는 가볍고 조용한 검사를 유지하세요. "완전히 완벽한가?"가 아니라 "명백히 불가능한가?"를 생각하세요. 이메일 필드라면 @가 포함되어 있고 공백이 없는지를 검사하는 정도로 충분할 수 있습니다. 비밀번호는 입력을 시작하면 "8자 이상" 같은 작은 헬퍼를 보여주되, 첫 글자에서 빨간색 오류를 표시하지 마세요.\n\n사용자가 필드를 떠날 때는 더 엄격한 단일 필드 규칙을 실행하고 필요한 경우 인라인 오류를 표시하세요. 여기가 "필수"나 "잘못된 형식"이 표시될 자리입니다. 또한 이때 공백을 정리하고 입력을 정규화(예: 이메일 소문자화)하여 사용자가 제출될 값을 볼 수 있게 하세요.\n\n제출 시에는 모든 것을 다시 검증하세요. 교차 필드 규칙(예: 비밀번호와 비밀번호 확인 일치)도 이때 확인합니다. 실패하면 수정이 필요한 필드로 포커스를 옮기고 그 근처에 하나의 명확한 메시지를 표시하세요.\n\n제출 버튼은 신중히 사용하세요. 사용자가 폼을 채우는 동안 버튼을 활성 상태로 두세요. 탭해도 아무 동작이 없을 경우(예: 이미 제출 중일 때)에만 비활성화하세요. 만약 유효하지 않은 입력 때문에 비활성화한다면 무엇을 고쳐야 하는지 근처에 알려주거나, 제출은 허용하되 첫 번째 문제로 안내하세요.\n\n제출 중에는 명확한 로딩 상태를 보여주세요. 버튼 레이블을 ProgressView로 바꾸고 이중 탭을 방지하며 폼을 계속 표시해 사용자가 무슨 일이 일어나고 있는지 이해하게 하세요. 요청이 1초 이상 걸리면 "계정 생성 중..." 같은 짧은 레이블로 불안을 줄이세요.\n\n## 사용자를 좌절시키지 않는 서버 측 유효성 검사\n\n서버 측 검사는 로컬 검사보다 최종 권위입니다. 로컬 규칙을 모두 통과해도 비밀번호가 너무 흔해서 실패할 수 있고, 이메일이 이미 사용 중일 수 있습니다.\n\n가장 큰 UX 이득은 "입력이 허용되지 않습니다"와 "서버에 연결할 수 없습니다"를 구분하는 것입니다. 요청이 타임아웃되거나 사용자가 오프라인이면 필드를 잘못된 것으로 표시하지 마세요. "연결할 수 없습니다. 다시 시도해 주세요." 같은 차분한 배너나 알림을 표시하고 폼은 그대로 두세요.\n\n서버가 유효성 검사에 실패했다고 하면 사용자의 입력은 그대로 유지하고 정확한 필드를 가리키세요. 폼을 지우거나 비밀번호를 초기화하거나 포커스를 이동시키는 것은 사용자가 시도한 것을 처벌하는 느낌을 줍니다.\n\n간단한 패턴은 구조화된 오류 응답을 필드 오류와 폼 수준 오류로 파싱한 다음, 텍스트 바인딩을 변경하지 않고 UI 상태만 업데이트하는 것입니다.\n\nswift\nstruct ServerValidation: Decodable {\n var fieldErrors: [String: String]\n var formError: String?\n}\n// Map keys like \"email\" or \"password\" to your local field IDs.\n\n\n일반적으로 네이티브처럼 느껴지는 방식:\n\n- 필드 메시지는 필드 바로 아래에 인라인으로 두고, 서버 문구가 명확하면 그대로 사용하세요.\n- 서버 응답 중에는 사용자가 타이핑할 때 포커스를 갑자기 이동시키지 마세요. 제출 후에만 첫 번째 오류 필드로 이동하세요.\n- 서버가 여러 문제를 반환하면 필드당 첫 번째 메시지만 보여 읽기 쉽게 유지하세요.\n- 필드별 상세 정보가 있다면 "문제가 발생했습니다" 같은 모호한 문구로 대체하지 마세요.\n\n예: 사용자가 회원가입 폼을 제출했는데 서버가 "이미 사용 중인 이메일"을 반환하면 입력해둔 이메일은 유지하고 Email 필드 아래에 메시지를 보여주며 그 필드에 포커스를 맞춥니다. 서버가 다운된 경우에는 단일 재시도 메시지를 보여주고 모든 필드는 그대로 둡니다.\n\n## 서버 메시지를 적절한 위치에 표시하는 방법\n\n서버 오류는 임의의 배너에 뜨면 "불공평"하게 느껴집니다. 각 메시지를 가능한 한 문제를 일으킨 필드 가까이에 두세요. 단일 입력과 연결할 수 없을 때만 일반 메시지를 사용하세요.\n\n먼저 서버의 오류 페이로드를 SwiftUI 필드 식별자에 매핑하세요. 백엔드는 email, password, profile.phone 같은 키를 반환할 수 있고, UI는 Field.email, Field.password 같은 enum을 사용할 수 있습니다. 응답 직후에 한 번 매핑하면 나머지 뷰는 일관성을 유지할 수 있습니다.\n\n유연한 모델링 방법은 serverFieldErrors: [Field: [String]]와 serverFormErrors: [String]를 유지하는 것입니다. 보통 하나의 메시지만 표시하더라도 배열로 저장하세요. 인라인 오류를 표시할 때는 가장 도움이 되는 메시지를 우선적으로 선택하세요. 예를 들어, "이미 사용 중인 이메일"은 "잘못된 이메일"보다 더 유용합니다.\n\n필드당 여러 오류가 흔하지만 모두 표시하면 시끄럽습니다. 대부분의 경우 인라인에는 첫 번째 메시지만 표시하고, 정말 필요하면 자세한 정보를 위한 상세 뷰에 나머지를 두세요.\n\n필드와 연결되지 않는 오류(세션 만료, 속도 제한, "나중에 다시 시도하세요")는 제출 버튼 근처에 배치해 사용자가 액션을 취할 때 바로 보이게 하세요. 또한 성공 시 이전 오류를 지워 UI가 "멈춘" 것처럼 보이지 않게 하세요.\n\n마지막으로 사용자가 관련 필드를 변경할 때 서버 오류를 지우세요. 실제로는 email에 대한 onChange 핸들러가 serverFieldErrors[.email]을 제거해 사용자가 바로 "고치고 있다"는 것을 UI가 반영하게 합니다.\n\n## 접근성 및 톤: 네이티브처럼 느껴지게 하는 작은 선택들\n\n좋은 유효성 검사는 논리만이 아니라 읽히는 방식, 들리는 방식, Dynamic Type과 VoiceOver, 다양한 언어에 대한 동작까지 포함합니다.\n\n### 색만으로 의존하지 말고 오류를 읽기 쉽게 만드세요\n\n텍스트가 크게 표시될 수 있음을 가정하세요. Dynamic Type 친화적인 스타일(예: .font(.footnote) 또는 .font(.caption) 같은 고정 크기 사용을 피하는 스타일)을 사용하고 오류 라벨이 줄 바꿈되도록 하세요. 오류가 나타날 때 레이아웃이 너무 튀지 않도록 간격을 일정하게 유지하세요.\n\n빨간색 텍스트만으로 의존하지 마세요. 명확한 아이콘이나 "오류:" 접두어를 추가하세요. 색각에 문제가 있는 사람들에게 도움이 되고 스캔하기 쉬워집니다.\n\n일반적으로 통하는 빠른 체크리스트:\n\n- Dynamic Type에 맞춰 크기가 조절되는 읽기 쉬운 텍스트 스타일을 사용하세요.\n- 오류 메시지는 줄 바꿈을 허용하고 잘리지 않게 하세요.\n- 아이콘이나 "오류:" 같은 라벨을 색과 함께 사용하세요.\n- 라이트/다크 모드 모두에서 대비를 충분히 유지하세요.\n\n### VoiceOver가 올바른 내용을 읽도록 하세요\n\n필드가 유효하지 않을 때 VoiceOver는 레이블, 현재 값, 오류를 함께 읽어야 합니다. 오류가 필드 아래 별도의 Text로 있으면 상황에 따라 건너뛰어지거나 문맥 없이 읽힐 수 있습니다.\n\n도움이 되는 두 가지 패턴:\n\n- 필드와 오류를 하나의 접근성 요소로 결합해 사용자가 필드에 포커스할 때 오류가 함께 발표되게 하세요.\n- 오류 메시지를 포함하는 접근성 힌트나 값(accessibility hint/value)을 설정하세요(예: "비밀번호, 필수, 최소 8자 필요").\n\n톤도 중요합니다. 메시지는 명확하고 현지화하기 쉬운 문구를 사용하세요. 속어, 농담, "이런" 같은 모호한 표현을 피하고 "이메일이 누락되었습니다"나 "비밀번호에 숫자가 포함되어야 합니다"처럼 구체적으로 쓰세요.\n\n## 예: 로컬 및 서버 규칙을 모두 가진 회원가입 폼\n\n세 필드(Email, Password, Confirm Password)가 있는 회원가입 폼을 상상해 보세요. 목표는 사용자가 타이핑하는 동안은 조용히 있다가 앞으로 나아가려 할 때 도움이 되는 폼입니다.\n\n### 포커스 순서(Return 키 동작)\n\nSwiftUI FocusState를 사용하면 Return 키 한 번마다 자연스러운 단계처럼 느껴져야 합니다.\n\n- Email Return: Password로 포커스 이동.\n- Password Return: Confirm Password로 포커스 이동.\n- Confirm Password Return: 키보드 내리고 제출 시도.\n- 제출 실패 시: 수정이 필요한 첫 번째 필드로 포커스 이동.\n\n이 마지막 단계가 중요합니다. 이메일이 유효하지 않다면 포커스는 단지 어딘가의 빨간 메시지로 가지 않고 Email로 돌아가야 합니다.\n\n### 오류가 나타나는 시점\n\nUI를 차분하게 유지하는 간단한 규칙: 필드가 터치되었을 때(사용자가 떠났을 때) 또는 제출 시도 후에만 메시지를 보여주세요.\n\n- Email: 필드를 떠난 후 또는 제출 시 "유효한 이메일을 입력하세요" 표시.\n- Password: 떠난 후 또는 제출 시 규칙(최소 길이 등) 표시.\n- Confirm Password: 떠난 후 또는 제출 시 "비밀번호가 일치하지 않습니다" 표시.\n\n이제 서버 측입니다. 사용자가 제출하고 API가 다음과 같은 응답을 반환한다고 가정하세요:\n\njson\n{\n \"errors\": {\n \"email\": \"That email is already in use.\",\n \"password\": \"Password is too weak. Try 10+ characters.\"\n }\n}\n\n\n사용자가 보는 것: Email 아래에 서버 메시지가 표시되고, Password 아래에도 메시지가 표시됩니다. Confirm Password는 로컬에서 실패하지 않는 한 조용합니다.\n\n다음 행동: 포커스는 첫 번째 서버 오류가 있는 Email로 갑니다. 사용자는 이메일을 변경하고 Return을 눌러 Password로 이동한 뒤 비밀번호를 조정하고 다시 제출합니다. 메시지가 인라인으로 있고 포커스가 의도대로 이동하므로 폼은 협력적으로 느껴집니다.\n\n## 유효성 검사를 "iOS스럽지 않게" 만드는 흔한 함정\n\n폼은 기술적으로는 맞을 수 있지만 여전히 잘못 느껴질 수 있습니다. 대부분의 "iOS답지 않은" 유효성 문제는 타이밍에서 옵니다: 언제 오류를 보여주는지, 언제 포커스를 이동하는지, 서버에 어떻게 반응하는지.\n\n흔한 실수는 너무 일찍 말하기입니다. 첫 글자에서 오류를 보이면 사용자는 입력 중에 꾸중당하는 기분이 듭니다. 필드가 터치되거나 제출을 시도할 때까지 기다리면 대개 문제가 해결됩니다.\n\n비동기 서버 응답도 흐름을 깨뜨릴 수 있습니다. 회원가입 요청이 돌아와서 포커스를 갑자기 다른 필드로 이동시키면 무작위처럼 느껴집니다. 사용자가 마지막으로 있던 곳에 포커스를 유지하고, 그들이 Next를 탭하거나 제출을 처리할 때만 이동하세요.\n\n또 다른 함정은 편집할 때마다 모든 것을 초기화하는 것입니다. 어떤 문자 하나가 변경되면 모든 오류를 지우면 실제 문제를 숨길 수 있습니다(특히 서버 메시지의 경우). 편집 중인 필드의 오류만 지우고 나머지는 실제로 수정될 때까지 유지하세요.\n\n"무언의 실패" 제출 버튼을 피하세요. 제출을 영원히 비활성화해버리고 무엇을 고쳐야 하는지 설명하지 않으면 사용자가 추측하게 만듭니다. 비활성화한다면 구체적인 힌트를 함께 보여주거나 제출을 허용한 뒤 첫 번째 문제로 안내하세요.\n\n느린 요청과 중복 탭도 놓치기 쉽습니다. 진행 상태를 보여주고 이중 제출을 막지 않으면 사용자가 두 번 탭해서 두 개의 응답을 받게 되고 혼란스러운 오류가 발생할 수 있습니다.\n\n간단 체크리스트:\n\n- 오류는 블러(blur)나 제출까지 지연시키세요, 첫 글자에서가 아닙니다.\n- 서버 응답 후에는 사용자가 요청하지 않는 한 포커스를 이동시키지 마세요.\n- 필드별로 오류를 지우되 전체를 한 번에 지우지 마세요.\n- 제출이 막힌 이유를 설명하세요(또는 제출을 허용하고 안내하세요).\n- 로딩을 표시하고 대기 중에는 추가 탭을 무시하세요.\n\n예: 서버가 "이미 사용 중인 이메일"을 반환하면 Email 아래에 메시지를 두고 Password는 건드리지 않으며 사용자가 이메일을 편집할 수 있게 하세요. (예: 백엔드를 AppMaster(appmaster.io)로 만든 경우에도 동일합니다.)\n\n## 빠른 체크리스트와 다음 단계\n\n네이티브처럼 느껴지는 유효성 경험은 대부분 타이밍과 절제에 관한 것입니다. 엄격한 규칙을 유지하면서도 화면을 차분하게 만들 수 있습니다.\n\n출시 전에 확인하세요:\n\n- 적절한 시점에 검증하세요. 명확히 도움이 되지 않는 한 첫 글자에서 오류를 표시하지 마세요.\n- 목적을 가지고 포커스를 이동하세요. 제출 시 첫 번째 유효하지 않은 필드로 점프하고 무엇이 잘못되었는지 분명히 하세요.\n- 문구는 짧고 구체적으로 하세요. 사용자가 다음에 무엇을 해야 할지 알려주세요.\n- 로딩과 재시도를 존중하세요. 전송 중에는 제출 버튼을 비활성화하고 요청이 실패해도 입력값을 유지하세요.\n- 서버 오류는 가능한 경우 필드 피드백으로 다루세요. 서버 코드를 필드에 매핑하고 진정한 전역 이슈에만 상위 메시지를 사용하세요.\n\n그런 다음 실제 사람처럼 테스트하세요. 작은 폰을 한 손에 쥐고 엄지로 폼을 작성해 보세요. 그다음 VoiceOver를 켜서 포커스 순서, 오류 발표, 버튼 레이블이 여전히 이해되는지 확인하세요.\n\n디버깅과 지원을 위해 서버 검증 코드(원시 메시지 아님)를 화면과 필드 이름과 함께 로그하면 도움이 됩니다. 사용자가 "가입이 안 돼요"라고 할 때 email_taken, weak_password, 네트워크 타임아웃 중 어느 것이었는지 빠르게 알 수 있습니다.\n\n일관성을 유지하려면 필드 모델(value, touched, local error, server error), 오류 배치, 포커스 규칙을 표준화하세요. 모든 화면을 수작업으로 코딩하지 않고도 네이티브 iOS 폼을 더 빨리 만들고 싶다면 AppMaster (appmaster.io)는 SwiftUI 앱과 백엔드를 함께 생성해 클라이언트와 서버의 유효성 규칙을 더 쉽게 일치시킬 수 있습니다.