Swift에서 문자열 비교
Swift의 String은 유니코드 확장 문자소(Extended Grapheme Cluster) 를 단위로 동작합니다. 즉, 사람이 인식하는 “문자” 기준으로 비교·탐색·슬라이싱이 설계되어 있습니다. 이 문서는 실무에서 마주치는 문자열 비교 요구사항을 정확성(국제화) 과 성능(최적화) 관점에서 정리하고, 올바른 API 선택과 예제를 제공합니다.
1. 핵심 요약
| 목적 | 권장 API/기법 | 비고 |
| 동등성(같은 글자?) | == | 유니코드 문자소 동등성. 정규화 차이(NFC/NFD)도 동일로 판단 |
| 정렬/사전순(논로컬) | a < b 또는 a.compare(b) == .orderedAscending | 코드 포인트 기반에 가깝고, 사용자 표시용 정렬로는 부적절 |
| 사용자 표시용 정렬/검색 | localizedStandardCompare, localizedCaseInsensitiveContains | Finder 유사, 로캘 민감(공백/기호 처리 포함) |
| 접두/접미 | hasPrefix, hasSuffix | 대소문자/발음 무시는 range(of:options:) + .anchored/.backwards |
| 부분 포함 | contains(간단), range(of:options:)(옵션 필요) | 옵션: .caseInsensitive, .diacriticInsensitive, .widthInsensitive 등 |
| 다수 후보 매칭 | Set<String>.contains | 선형 == 반복보다 평균 O(1) |
| 바이트 동일성(프로토콜 키 등) | data(using:) 후 바이트 비교 또는 utf8.elementsEqual | 문자소 동등성이 아닌 바이트 일치가 필요할 때 |
| 성능 파이프라인 | lazy + 고수준 API | 대량 처리에서 중간 문자열 최소화 |
사람에게 보여주는 정렬/검색은 반드시 현지화 API 를 쓰세요. 단순 lowercased() 후 비교는 다국어에서 버그의 원인입니다(예: 터키어 i).
2. 동등 비교(Equality): ==
Swift의 == 는 유니코드 정규화가 달라도 같은 문자면 true 입니다.
let nfc = "café" // U+00E9 (é)
let nfd = "cafe\u{301}" // 'e' + 결합 악센트 U+0301
print(nfc == nfd) // true
이 특성 덕분에 사용자 입력의 조합형/분해형 차이를 자동으로 흡수합니다. 반면 바이트 일치가 필요하다면 아래를 사용합니다.
// 바이트 동일성 (네트워크/해시 키/디스크 포맷 등)
let b1 = nfc.utf8.elementsEqual(nfd.utf8) // false
let d1 = nfc.data(using: .utf8)! == nfd.data(using: .utf8)! // false
3. 정렬/비교 순서
3.1 논로컬 정렬(간단, 규칙적)
let a = ["ä", "a", "b"]
print(a.sorted()) // ["a","ä","b"] (플랫폼/버전에 따라 다소 차이 가능)
- sorted() 는 로캘 규칙을 고려하지 않습니다. 사용자 표시용 목록 정렬에는 적합하지 않습니다.
3.2 현지화 정렬(사용자 친화)
import Foundation
let cities = ["Zürich","Århus","Örebro","Aachen"]
let sortedLocalized = cities.sorted {
$0.localizedStandardCompare($1) == .orderedAscending
}
print(sortedLocalized) // 로캘에 맞춘 기대 결과
- localizedStandardCompare 는 Finder 스타일 비교(숫자 묶음, 기호/악센트 처리 등)를 수행합니다.
- 보다 엄격한 제어가 필요하면 compare(_:options:range:locale:) 를 사용합니다.
let result = "straße".compare("Strasse",
options: [.caseInsensitive, .diacriticInsensitive],
range: nil,
locale: Locale(identifier: "de_DE"))
4. 접두/접미/부분 문자열
4.1 기본
let s = "Swift Programming"
s.hasPrefix("Swift") // true
s.hasSuffix("ming") // true
s.contains("gram") // true
4.2 옵션이 필요할 때: range(of:options:)
let text = "Café au lait"
let ok1 = text.range(of: "CAFE", options: [.caseInsensitive, .diacriticInsensitive]) != nil
// 접두/접미(앵커) + 대소문자/악센트 무시
let prefixOK = text.range(of: "café", options: [.caseInsensitive, .diacriticInsensitive, .anchored]) != nil
let suffixOK = text.range(of: "LAIT", options: [.caseInsensitive, .diacriticInsensitive, .backwards, .anchored]) != nil
안티패턴: s.prefix(n) == "…", s.lowercased().hasPrefix("…")
권장: hasPrefix/hasSuffix 또는 range(of:options:) 로 필요한 옵션만 부여
5. 사용자 검색(국제화)
UI 검색은 단순 contains 가 아닌 현지화 + 대소문자/악센트 무시가 보통의 기대입니다.
import Foundation
func userContains(haystack: String, needle: String, locale: Locale = .current) -> Bool {
haystack.range(of: needle,
options: [.caseInsensitive, .diacriticInsensitive],
range: nil,
locale: locale) != nil
}
// Finder 유사 동작
"File (2) copy".localizedStandardContains("file 2") // true
6. 안전/보안 관점
- 파일 확장자 검사: 대소문자/로캘 차이를 고려합니다.swift extension String { var isImagePath: Bool { let lower = self.lowercased() return lower.hasSuffix(".png") || lower.hasSuffix(".jpg") || lower.hasSuffix(".jpeg") || lower.hasSuffix(".gif") || lower.hasSuffix(".webp") } }
- CSV/스프레드시트 수식 주입 방지: =,+,-,@ 로 시작하는 셀을 차단swift func isSafeCSVCell(_ s: String) -> Bool { guard let c = s.first else { return true } return !"=+-@".contains(c) }
7. 이모지/합성 문자 고려
합성 이모지는 여러 스칼라의 조합입니다. 문자 수(Character.count) 와 바이트 수가 크게 다를 수 있습니다.
let flag = "🇰🇷" // 두 문자처럼 보이지만 2개의 지역표시 기호 조합
print(flag.count) // 1 (문자소 기준)
print(flag.unicodeScalars.count) // 2
접두/접미/슬라이싱은 항상 문자 경계를 유지하는 고수준 API를 쓰세요.
8. 성능 팁
8.1 다수 후보 매칭은 Set로 승격
let allowed: Set<String> = ["GET","POST","PUT","DELETE","PATCH","HEAD","OPTIONS"]
func isAllowed(_ m: String) -> Bool { allowed.contains(m) } // 평균 O(1)
8.2 대량 필터는 조기 종료 가능한 API
let rows = Array(repeating: "USER-abcdef-12345", count: 100_000)
let prefixed = rows.filter { $0.hasPrefix("USER-") } // 빠름
8.3 불필요한 전역 변환 금지
// 🚫 매번 lowercased()는 비용 + 버그(터키어 i)
s.lowercased().contains("abc")
// ✅ 필요한 비교에서만 옵션
s.range(of: "abc", options: .caseInsensitive) != nil
8.4 바이트 수준 비교(정말 필요할 때만)
// API 키, 해시 키 등
func bytesEqual(_ a: String, _ b: String) -> Bool {
a.utf8.elementsEqual(b.utf8)
}
9. Swift Regex와 정확 매치
Swift 5.7+의 정규식은 문자 경계/유니코드 규칙을 따릅니다.
if let _ = try? /^(?i)caf(e|\u{00E9})$/.wholeMatch(in: "Café") {
// 대소문자 무시 + é 변형 허용
}
정규식은 강력하지만 오용하면 비용이 큽니다. 정확히 필요한 경우에만 사용하세요(검증/파싱 등).
10. 샘플 코드
import Foundation
let hay = Array(repeating: "USER-abcdef-12345", count: 200_000)
func measure(_ name: String, _ body: () -> Void) {
let t1 = CFAbsoluteTimeGetCurrent(); body(); let t2 = CFAbsoluteTimeGetCurrent()
print(name, ":", t2 - t1, "sec")
}
measure("hasPrefix") { _ = hay.filter { $0.hasPrefix("USER-") } }
measure("prefix+== (anti)") { _ = hay.filter { $0.prefix(5) == "USER-" } }
let words = (0..<100_000).map { _ in "café" }
measure("contains (case-insensitive)") {
_ = words.filter { $0.range(of: "CAFE", options: [.caseInsensitive, .diacriticInsensitive]) != nil }
}
실제 수치는 Release 빌드 + 실기기에서 측정하세요. 디버그 빌드는 최적화가 꺼져 있어 의미가 없습니다.
11. 흔한 안티패턴과 교정
1) 다수 후보를 == 로 선형 스캔 → Set.contains
2) 접두/접미 검사에 prefix/suffix 잘라서 비교 → hasPrefix/hasSuffix 또는 range(of:options:) + 앵커
3) 전역 lowercased()/uppercased() → 필요 지점에만 옵션 적용 (.caseInsensitive, .diacriticInsensitive)
4) 사용자 검색에 단순 contains → localizedStandardContains 또는 range(of:options:locale:)
5) 바이트 동일성과 문자 동등성 혼동 → 요구사항을 명확히 구분(키/프로토콜/암호학 vs UI)
12. 참조 링크
- String — 기본 문서: https://developer.apple.com/documentation/swift/string
- hasPrefix(_:) / hasSuffix(_:): https://developer.apple.com/documentation/swift/string/1541080-hasprefix , https://developer.apple.com/documentation/swift/string/1541047-hassuffix
- range(of:options:range:locale:): https://developer.apple.com/documentation/swift/string/2893688-range
- localizedStandardCompare(_:): https://developer.apple.com/documentation/foundation/nsstring/1416483-localizedstandardcompare
- localizedCaseInsensitiveContains(_:): https://developer.apple.com/documentation/foundation/nsstring/1416395-localizedcaseinsensitivecontains
- The Swift Programming Language — Strings and Characters: https://docs.swift.org/swift-book/documentation/the-swift-programming-language/stringsandcharacters/
13. 결론
- 동등성은 == (문자소 기준), 정렬/검색은 현지화 API 를 사용하세요.
- 접두/접미는 hasPrefix/hasSuffix, 옵션이 필요하면 range(of:options:) + 앵커.
- 다수 후보는 Set으로 승격, 바이트 동일성은 utf8/Data 로.
- 성능은 조기 종료 가능한 API 와 불필요한 변환을 없애고, 실제로 Release 빌드에서 테스트 해야합니다.
'Dev Study > Swift' 카테고리의 다른 글
| TCA Study - Swift 개발자가 TCA를 알아야 하는 이유 (0) | 2025.12.16 |
|---|---|
| Swift에서 정렬: sort vs sorted (1) | 2025.11.11 |
| Swift에서 옵셔널 기본값: 중첩 if let vs a ?? b (0) | 2025.11.11 |
| Swift에서 배열 초기화: 반복 append vs Array(repeating:count:) (0) | 2025.11.11 |
| Swift에서 ARC 최적화: weak vs unowned (0) | 2025.11.11 |
| Swift에서 @inlinable / @inline(__always) (0) | 2025.11.11 |
| Swift에서 구조체(Value Type) 기반 설계 (0) | 2025.11.11 |
| Swift에서 switch문의 pattern matching (0) | 2025.11.11 |


