"하루에 세 번이나 Foundation에 대한 뻔한 짐작이 정반대로 틀렸어. 매번 확인하는 데 필요한 건 열 줄짜리 스크립트뿐이었고."
Swift 문자열은 뜻을 비교하고, 파일은 바이트를 비교해
Swift의 String ==은 유니코드 정규 동등성으로 비교해. 한글을 완성형으로 쓴 것(NFC, UTF-8로 6바이트)이랑 자모로 풀어 쓴 것(NFD, 18바이트)은 같은 문자열이야. 사람이 입력한 글자를 비교할 땐 아주 좋아. 근데 그 문자열이 파일 이름이나 해시 입력, 파일 시스템이 보게 될 키로 쓰이는 순간 위험해져. macOS는 디렉터리 목록에서 풀어 쓴 이름을 주고, 텍스트 필드는 완성형을 줄 수 있어. 둘을 각각 해시하면 노트 하나에 사이드카 폴더가 두 개 생기고, 기록이 반씩 나뉘어 들어가. 그런데도 에러는 하나도 안 나. 가족 편집기 하나가 Rust로 된 파일 키 함수를 Swift로 옮기다가 이걸 알게 됐어. "이 둘은 달라야 한다"는 자기 테스트가 실패했거든. Swift에선 둘이 같다고 나오니까. 명시적으로 정규화하고(precomposedStringWithCanonicalMapping이 NFC야), 바이트가 중요할 땐 utf8 바이트로 비교해.
같은 순간을 적는 올바른 표기가 두 가지
웹 클라이언트는 캡처 시각을 2026-09-10T12:17:29+09:00으로 찍었어. 네이티브 클라이언트는 2026-09-10T10:45:13.146Z로 찍었고. 둘 다 올바른 ISO 8601이야. 근데 목록이 문자열 그대로 정렬했더니, 현지 시각 19시 45분 캡처가 12시 17분 캡처 밑으로 가라앉았어. 캡처 앱이 존재하는 이유나 다름없는 최신 항목이 아예 안 들어온 것처럼 보였지. 비교 전엔 Date로 파싱해. 그리고 소수점 초를 켠 ISO8601DateFormatter는 소수점 없는 표기를 거부하고 그 반대도 마찬가지니까, 둘 다 시도해. 망가진 타임스탬프가 최신 항목인 척 못 하게, 파싱 안 되는 건 일부러 맨 밑으로 보내.
비슷한 함정 하나 더. 위치 측정값의 시각은 UTC로 찍힌 순간이고, 캡처 자체의 시각은 사람이 실제로 보는 벽시계 기준 오프셋을 달고 있어. 측정값으로 만든 라벨엔 "13:25"라고 떴는데, 같은 화면 다른 곳엔 같은 시각이 "22:25"로 나왔어. 필드가 둘이면 역할도 둘이야. 화면엔 사람이 뜻하는 쪽을 그려.
안 보이는 문자, 비었는데 있는 데이터
- 받아쓰기가 U+FFFC를 남길 수 있어. iOS에서 SwiftUI
TextField(axis: .vertical)에 받아쓰기로 입력했더니, 바인딩된 문자열 맨 앞에 OBJECT REPLACEMENT CHARACTER가 남았어. 받는 쪽 언어 모델은 그걸 첨부 이미지 표시로 읽고, 있지도 않은 첨부 파일을 찾아 헤맸어. 기기 밖으로 나가는 텍스트에선 U+FFF9부터 U+FFFC까지 지워. - "쓸 수 있다"랑 "들어 있다"는 달라. PNG를 약속만 한 페이스트보드 항목은
data(forType:)에 nil이 아니라 0바이트로 대답해. "nil이면 불완전"으로 짠 가드는 약속만 한 항목에선 절대 안 걸려. 비어 있는 게 실패라면 비었는지를 확인해.