고객 연락처를 .vcf로 내보냈는데 공유 시트는 정상적으로 열리지만, 가져오고 나니 전화번호가 하나만 남고 이름에 있던 세미콜론의 위치도 달라졌다면 어떨까요? 이런 문제는 스크린샷만으로 검증할 수 없습니다. 클라우드 Mac에서 먼저 ‘데이터 생성 → 다시 파싱 → 필드별 비교’ 과정을 검증한 다음 iOS에서 공유와 가져오기를 확인하면, 형식 오류와 UI 문제를 분리해서 살펴볼 수 있습니다.
먼저 보존할 데이터를 정의하기
이 글에서는 실제 주소록을 읽지 않고, 앱의 메모리 모델을 vCard로 변환하는 과정을 검증합니다. 중국어, 악센트가 있는 문자, 여러 전화번호, 사용자 지정 레이블을 포함한 가상 연락처를 사용해 개인 정보가 빌드 로그에 섞이지 않도록 합니다.
첫 번째 검증 규약은 간단해도 됩니다. 성과 이름은 원문 그대로 복원되어야 하고, 전화번호는 입력 문자열을 유지해야 합니다. 사용자 지정 레이블과 번호의 연결 관계가 바뀌어서는 안 되며, 빈 필드에 업무 로직의 기본값이 임의로 채워져서도 안 됩니다. 전화번호 정규화는 별도의 로직에서 처리하고, 내보내는 과정에서 앞쪽의 더하기 기호를 무심코 제거하지 않도록 합니다.
파일을 파싱할 수 있다는 것은 문법상 허용된다는 뜻일 뿐입니다. 다시 읽은 필드가 원본과 일치해야 이 변환 경로에서 중요한 데이터가 유실되지 않았다고 볼 수 있습니다.
프로필 사진, 메모, 그룹은 일단 제외합니다. 이 항목들은 용량, 접근 권한, 호환성의 제약이 서로 다르므로, 한 번의 실패에 모든 필드가 얽히게 하기보다 별도의 테스트 데이터로 확장하는 편이 좋습니다.
시스템 직렬화기로 기준 만들기
Xcode 명령줄 도구가 설치된 클라우드 Mac에서 다음 내용을 vcard-check.swift로 저장합니다. Foundation과 Contacts만 사용하며, 주소록 조회 API를 호출하지 않으므로 연락처 접근 권한을 요청할 필요도 없습니다.
import Foundation
import Contacts
struct CheckFailure: Error {
let field: String
}
func require(_ condition: Bool, _ field: String) throws {
if !condition {
throw CheckFailure(field: field)
}
}
func phones(_ contact: CNContact) -> [String: String] {
var result: [String: String] = [:]
for item in contact.phoneNumbers {
result[item.label ?? ""] = item.value.stringValue
}
return result
}
do {
let source = CNMutableContact()
source.familyName = "林;测试"
source.givenName = "Zoë,研发"
source.phoneNumbers = [
CNLabeledValue(
label: "build-primary",
value: CNPhoneNumber(stringValue: "+12025550101")
),
CNLabeledValue(
label: "build-secondary",
value: CNPhoneNumber(stringValue: "+12025550102")
)
]
let data = try CNContactVCardSerialization.data(with: [source])
try data.write(to: URL(fileURLWithPath: "contact.vcf"),
options: .atomic)
let contacts = try CNContactVCardSerialization.contacts(with: data)
try require(contacts.count == 1, "contact-count")
let restored = contacts[0]
try require(restored.familyName == source.familyName, "family-name")
try require(restored.givenName == source.givenName, "given-name")
try require(restored.phoneNumbers.count == 2, "phone-count")
try require(phones(restored) == phones(source), "phone-values")
} catch {
FileHandle.standardError.write(Data("vCard check failed\n".utf8))
exit(1)
}
실행 후에는 ‘성공’이라는 문구를 찾는 대신 프로세스 종료 코드를 확인합니다.
mkdir -p artifacts/vcard
cd artifacts/vcard
xcrun swift ../../vcard-check.swift
이 예제는 스크립트가 저장소 루트에 있다고 가정합니다. 명령의 종료 코드가 0일 때만 파이프라인을 계속 진행하고, 실패하면 이후 배포 단계를 중단합니다. 실제 프로젝트에서는 검증문을 테스트 타깃에 넣어 어떤 필드가 실패했는지 보고서에서 확인할 수 있게 하세요. 연락처 원문을 그대로 출력해서는 안 됩니다.
특수 문자를 개별 테스트 케이스로 만들기
위 예제의 쉼표와 세미콜론은 의도적으로 넣은 것입니다. 이 문자들은 vCard 구조에서 의미를 가지므로, 텍스트를 단순히 이어 붙이면 하나의 이름이 여러 구성 요소로 나뉠 수 있습니다. 시스템 직렬화기로 기준 데이터를 만드세요. 치환 규칙을 직접 작성한 뒤 그 규칙이 맞다는 것을 스스로 입증하는 방식은 피해야 합니다.
입력 데이터는 세 가지를 더 추가합니다. 역슬래시가 들어간 이름, 줄바꿈이 포함된 회사명, 충분히 긴 멀티바이트 문자열입니다. 각 테스트에서는 한 가지 조건만 바꾸고 해당 속성을 다시 읽어 확인합니다. 줄바꿈이 추가 속성이나 레코드로 잘못 처리되지 않도록, 전체 연락처 수가 여전히 하나인지도 검사합니다.
긴 줄은 문자 수만으로 판단할 수 없다
vCard의 물리적 줄에는 줄 접기가 적용될 수 있고, 중국어 문자는 바이트 길이도 다릅니다. ‘Swift 문자 일정 개수마다 줄바꿈 삽입’ 방식으로 줄 접기를 구현하지 마세요. 줄 단위 split으로 자체 파서를 만드는 것은 더더욱 피해야 합니다. 별도의 인코더를 유지 관리한다면, 지원한다고 명시한 vCard 버전의 바이트 단위 규칙을 확인하고 그 출력을 Contacts로 다시 읽어 검증해야 합니다.
예제는 레이블을 키로 딕셔너리를 만들기 때문에 레이블이 모두 고유한 테스트 데이터에만 적합합니다. 실제 연락처에는 같은 레이블이 여러 번 나올 수 있습니다. 테스트 케이스를 확장할 때는 ‘레이블, 번호’ 쌍의 다중집합을 비교하도록 바꾸고, 중복 항목의 개수도 보존하세요.
의미적 일치와 파일 일치를 구분하기
같은 연락처를 다시 직렬화하더라도 속성 순서나 줄 접기 방식 때문에 파일 스냅샷을 그대로 비교하기에는 적합하지 않을 수 있습니다. 파싱한 필드를 우선 비교하고, 파일 해시는 전송 무결성 검사에 사용하세요. 연락처의 의미가 같은지 판단하는 기준으로 사용해서는 안 됩니다.
일괄 내보내기에는 빈 배열, 단일 연락처, 여러 연락처 테스트 케이스를 추가합니다. 파싱 결과의 배열 순서에 의존하지 말고, 테스트 전용의 고유한 이름으로 각 레코드를 식별하세요. 딕셔너리의 값 덮어쓰기로 데이터 유실이 가려지지 않도록 레코드 수, 필드 값, 다중 값 항목 수를 각각 검증합니다.
시스템 프레임워크가 자신이 만든 데이터를 다시 읽는 데 성공했다고 해서 앱 간 상호 운용성까지 완전히 입증되는 것은 아닙니다. 다른 가져오기 도구도 지원해야 한다면, 동일한 가상 연락처 파일 묶음을 보관해 도구별로 검증해야 합니다. 지원하지 않는 레이블을 어떻게 변환할지도 명확한 규약으로 정해야 하며, 아무 설명 없이 무시해서는 안 됩니다.
파이프라인에 연결하고 iOS 검증 마무리하기
공유 기능과 관련된 변경이 있으면 빠른 형식 검사를 실행하도록 연결하고, 시스템과 도구 체인 버전을 기록합니다. 실패 시에는 가상 데이터로 만든 .vcf와 필드 오류 식별자만 보관하세요. 성공한 뒤에는 파이프라인의 보존 정책에 따라 디렉터리를 정리하고, 실제 주소록을 테스트 픽스처로 사용하지 마세요.
iOS에서는 파일 확장자, 공유 항목이 제공하는 데이터 유형, 대상 앱이 실제로 읽은 파일 내용도 확인해야 합니다. 전용 테스트 환경으로 가져온 뒤 전화번호 두 개, 사용자 지정 레이블, 특수 문자를 대조하고 테스트 레코드를 삭제합니다. 시뮬레이터 결과만으로는 대상 기기와 대상 가져오기 앱에서의 확인을 대신할 수 없습니다.
마지막으로 세 가지 결론을 독립적으로 남기세요. 생성한 데이터를 파싱할 수 있는지, 핵심 필드를 다시 읽어 복원할 수 있는지, 공유한 파일을 대상 앱에서 올바르게 가져올 수 있는지입니다. 어느 단계에서 실패했든 해당 계층으로 돌아가 원인을 추적해야 합니다. 특수 문자를 삭제하거나 첫 번째 번호만 남겨 테스트를 겉으로만 통과시키지 마세요.
자주 묻는 질문
이 테스트에 연락처 접근 권한이 필요한가요?
아니요. 메모리에 연락처를 만들고 데이터를 변환할 뿐 시스템 주소록을 조회하거나 저장하지 않습니다.
내보낸 파일을 바이트 단위로 비교하지 않는 이유는 무엇인가요?
속성 순서와 줄 접기 방식은 데이터 손실 없이 바뀔 수 있습니다. 먼저 파일을 파싱하고 필드별 계약에 따라 이름, 번호와 레이블을 비교해야 합니다.
다음 빌드는 클라우드 워크스테이션에서
Mac mini 한 대를 단독으로 사용하며, 가상 머신이 아닙니다. 작업에 맞는 대여 기간을 선택하세요. 실제 사용 가능 여부는 콘솔에 실시간으로 표시됩니다.
클라우드 Mac 대여하기