반응형

iOS 개발자가 많이 하는 실수 - 문자열 비교 시 lowercased() 반복 사용 문제

 

1. 흔히 쓰는 패턴

다음과 같은 코드, 한 번쯤은 써 본 적 있을 가능성이 높습니다.

if input.lowercased() == "yes" {
    // ...
}

혹은

if input.uppercased() == "OK" {
    // ...
}

겉으로 보면 문제 없이 동작하고,

코드도 직관적이라 초반에는 이 방식이 “정답”처럼 느껴집니다.

하지만 이 패턴은 다음과 같은 단점을 갖고 있습니다.

  • 매 비교마다 새로운 String 인스턴스 생성
  • 문자 수가 많고, 호출이 반복될수록 불필요한 비용 증가
  • 로케일/언어에 따라 예상치 못한 결과가 발생할 수 있음

2. 왜 lowercased() 남발이 문제인가?

2-1. String은 값 타입 + Copy-on-Write

Swift에서 String은 구조체(Struct) 기반의 값 타입입니다.

lowercased(), uppercased()는 새로운 문자열을 생성합니다.

let lower = input.lowercased()   // 새로운 String 리턴

비교를 위해 매번 이 작업을 하면:

  • text 길이에 비례하는 비용 발생
  • 루프 내에서 수백·수천 번 호출될 경우 체감 가능한 성능 저하 가능

2-2. 반복 호출 시 비용 누적

예:

for word in hugeWordList {
    if word.lowercased() == "target" {
        // ...
    }
}
  • hugeWordList가 크면 클수록
  • 매 반복마다 문자열 변환이 일어나서 CPU와 메모리 소비 증가

2-3. 로케일/언어에 따른 미묘한 이슈

영어만 쓰는 앱이라면 크게 체감되지 않을 수 있지만,

다국어를 처리하거나, 터키어 등의 특수 대소문자 규칙이 있는 언어를 지원하면

단순 lowercased/uppercased 변환이 예상과 다르게 동작할 가능성이 있습니다.


3. 올바른 문자열 비교 패턴

Swift는 문자열 비교 시 다양한 옵션을 지원하는 API를 제공합니다.

3-1. caseInsensitiveCompare(_:) 사용

if input.caseInsensitiveCompare("yes") == .orderedSame {
    // 대소문자 무시하고 문자열이 같은 경우
}

장점:

  • 대소문자만 무시하고 비교
  • 문자열 전체를 새로 생성하지 않고 비교 수행
  • Apple 공식 코드 스타일에서도 자주 등장하는 패턴

3-2. compare(_:options:) + .caseInsensitive

더 일반적인 API입니다.

if input.compare("yes", options: [.caseInsensitive]) == .orderedSame {
    // ...
}

또는 여러 옵션 조합도 가능합니다.

let result = input.compare(
    "yes",
    options: [.caseInsensitive, .diacriticInsensitive]
)

사용 예:

  • 대소문자 무시 + 악센트(é, è 등) 무시 등 복합 조건

3-3. localizedCaseInsensitiveCompare(_:)(사용자 언어 고려)

사용자의 로케일(언어 설정)을 고려한 비교가 필요할 경우:

if input.localizedCaseInsensitiveCompare("예") == .orderedSame {
    // 사용자의 로케일에 맞춰 대소문자/문자 비교 수행
}

다국어 앱에서는 이쪽이 더 적절할 수 있습니다.


3-4. contains / hasPrefix / hasSuffix도 마찬가지

다음과 같은 코드도 종종 보입니다.

if input.lowercased().contains("hello") {
    // ...
}

더 좋은 접근 방식:

if input.range(of: "hello", options: [.caseInsensitive]) != nil {
    // ...
}

또는 prefix/suffix:

if input.hasPrefix("Hello") { }        // 대소문자 구분
if input.lowercased().hasPrefix("he")  // ❌ 비추천

// 대신
if input.range(of: "he", options: [.caseInsensitive, .anchored]) != nil {
    // anchored 옵션은 prefix와 유사하게 동작
}

4. 성능 관점에서의 차이

정리하면:

  • lowercased() / uppercased()
    • 문자열 전체를 새로 생성
    • 다량 호출 시 성능과 메모리 사용량에 영향
  • compare(_:options:), caseInsensitiveCompare(_:), range(of:options:)
    • 문자열을 변환하지 않고 비교/검색
    • 필요할 경우 로케일, 악센트 등까지 제어 가능
    • 더 유연하고, 비용도 상대적으로 적음

실무에서 “문자열 비교/검색이 자주 일어나는 코드” (검색, 필터링, 자동완성 등)에서는

반드시 options 기반 API를 쓰는 것이 좋습니다.


5. 정리

  • lowercased()를 매번 호출해서 비교하는 방식은
    • 직관적이지만 성능/유지보수/다국어 처리 측면에서 비효율적
  • 실무에서는 다음 패턴을 우선 고려:
    • 완전 일치:
    • caseInsensitiveCompare(_:)
    • compare(_:options: [.caseInsensitive])
    • 부분 검색/포함 여부:
    • range(of:options:) + .caseInsensitive
    • 다국어/로케일:
    • localizedCaseInsensitiveCompare(_:)
  • 핵심은 “문자열 전체를 변환하지 말고, 비교 옵션을 사용하라”는 것.
반응형
Posted by 까칠코더
,