클라우드 Mac에서 iOS 목록 정렬 규칙 검증하기

클라우드 Mac에서 iOS 목록 정렬 규칙 검증하기

클라우드 Mac에서 iOS 목록 정렬 규칙 검증하기

파일 목록에 “보고서2”와 “보고서10”이 있는데, 새로고침하자 이름이 같은 두 항목의 위치까지 뒤바뀐다면 문제는 화면 갱신이 아닐 수도 있습니다. 비교 함수가 항목 사이의 순서를 완전히 정의하지 않았을 가능성도 있습니다. 클라우드 Mac에서 iOS 테스트를 실행할 때는 정렬 로직을 뷰에서 분리하고, 작은 표본으로 규칙부터 검증하는 편이 화면 녹화를 보며 추측하는 것보다 문제를 재현하기 쉽습니다.

먼저 정렬 계약을 명확히 정의하기

이 글에서는 파일 목록에 다음 규칙을 적용합니다. 이름을 오름차순으로 정렬하되 숫자는 자연 순서로 비교하고, 대소문자와 악센트 차이는 무시합니다. 이름 비교 결과가 같으면 영구적으로 유지되는 비즈니스 식별자의 오름차순으로 순서를 결정합니다. 비교에 사용할 로케일도 고정해 테스트 결과가 기기 설정에 따라 달라지지 않도록 합니다.

이 규칙이 모든 제품의 기본 답은 아닙니다. 사용자용 연락처 목록은 현재 언어에 맞는 정렬이 필요할 수 있고, 여러 기기에서 공유하는 파일 목록은 일관된 순서가 더 중요할 수 있습니다. 요구사항을 먼저 정하고 구현해야 합니다. “보기에 자연스럽다”는 느낌을 검수 기준으로 삼아서는 안 됩니다.

안정적인 식별자는 새로고침 전후에도 변하지 않아야 합니다. 배열 인덱스, 수신 시각, 객체를 만들 때마다 새로 생성하는 UUID로는 이름이 같은 항목의 순서를 결정할 수 없습니다. 데이터 유입 단계에서 중복 식별자도 거부해야 합니다. 그렇지 않으면 최종 정렬 키로도 항목을 구분할 수 없습니다.

안정 정렬은 입력에서 비교 값이 같은 항목들의 상대적 순서를 유지할 뿐입니다. 서버가 매번 다른 순서로 반환하는 문제까지 해결해 주지는 않습니다.

비교 함수를 순수 함수로 분리하기

다음 파일을 앱 타깃에 추가합니다. 예제에서는 en_US_POSIX를 고정해서 사용합니다. 이는 고정된 비교 정책을 선택했다는 뜻이지, 모든 언어의 이름 표시에 적합하다는 뜻은 아닙니다.

import Foundation

struct ListEntry {
    let id: String
    let name: String
}

func entryLess(_ lhs: ListEntry, _ rhs: ListEntry) -> Bool {
    let order = lhs.name.compare(
        rhs.name,
        options: [.numeric, .caseInsensitive, .diacriticInsensitive],
        range: nil,
        locale: Locale(identifier: "en_US_POSIX")
    )
    if order != .orderedSame {
        return order == .orderedAscending
    }
    return lhs.id < rhs.id
}

비교 도중에 네트워크 상태를 읽거나, 카운터를 변경하거나, 바뀔 수 있는 사용자 설정을 조회하지 마세요. 한 번의 정렬에서도 같은 객체를 여러 차례 비교하므로 결과는 항상 일관되어야 합니다. 비교 함수를 “작거나 같다”로 구현해서도 안 됩니다. 객체가 자기 자신보다 앞에 온다고 판단해서는 안 되기 때문입니다.

이름 비교 결과가 동등하다고 해서 같은 비즈니스 객체인 것은 아닙니다. 현재 정책에서는 “Resume”와 “résumé”가 같은 그룹으로 묶일 수 있지만, 이를 이유로 레코드를 합쳐서는 안 됩니다. 정렬, 검색, 중복 제거는 서로 다른 계약입니다.

작은 표본으로 핵심 동작 고정하기

단위 테스트 파일에서 앱 모듈을 임포트한 뒤 테스트 클래스를 추가합니다. 위에서 정의한 타입에 접근할 수 있는 테스트 타깃에 코드를 넣으세요. ListSortingTests의 표본에 실제로 사용하는 파일명을 추가해 확장하면 됩니다.

import XCTest

final class ListSortingTests: XCTestCase {
    let items = [
        ListEntry(id: "d", name: "Report10"),
        ListEntry(id: "b", name: "report2"),
        ListEntry(id: "a", name: "REPORT2"),
        ListEntry(id: "c", name: "Report1")
    ]

    func testNaturalOrderAndTies() {
        XCTAssertEqual(
            items.sorted(by: entryLess).map(\.id),
            ["c", "a", "b", "d"]
        )
    }

    func testComparatorProperties() {
        for a in items {
            XCTAssertFalse(entryLess(a, a))
            for b in items {
                if entryLess(a, b) {
                    XCTAssertFalse(entryLess(b, a))
                }
                for c in items {
                    if entryLess(a, b) && entryLess(b, c) {
                        XCTAssertTrue(entryLess(a, c))
                    }
                }
            }
        }
    }

    func testInputOrderDoesNotChangeOutput() {
        let expected = items.sorted(by: entryLess).map(\.id)
        XCTAssertEqual(
            Array(items.reversed()).sorted(by: entryLess).map(\.id),
            expected
        )
        let rotated = Array(items.dropFirst()) + [items[0]]
        XCTAssertEqual(
            rotated.sorted(by: entryLess).map(\.id),
            expected
        )
    }
}

첫 번째 테스트는 숫자의 순서와 이름이 같은 항목의 순서를 검증합니다. 두 번째 테스트는 비반사성, 비대칭성, 추이성을 확인해 여러 규칙을 임시로 조합하면서 생긴 모순을 찾아냅니다. 세 번째 테스트는 입력 순서를 바꿔 결과가 수신 순서에 의존하지 않는지 검증합니다.

삼중 반복문은 대표 표본 십여 개를 검사하는 데 적합합니다. 운영 환경의 전체 목록에 그대로 적용하지는 마세요. 대규모 데이터셋의 정렬 소요 시간은 따로 측정해야 하며, 성질 검사를 성능 벤치마크로 오해해서는 안 됩니다.

Unicode와 누락 값의 경계 사례 보완하기

이름이 빈 문자열인 항목, 선행 0이 있는 이름, 중국어 이름, 결합 악센트, 이모지 시퀀스도 추가합니다. 미리 조합된 문자와 “문자에 결합 악센트를 붙인 형태”는 화면에서 똑같이 보일 수 있습니다. 문자열 길이로 표시 순서를 판단해서는 안 됩니다.

“文件02”와 “文件2”의 숫자 비교 결과를 임의로 가정하지 마세요. 제품에서 두 이름을 같은 그룹으로 취급할지 먼저 정하고, 선택한 런타임에서 확인해야 합니다. 선행 0을 구분해야 한다면 이름이 동등하게 비교된 뒤 적용할 명시적인 보조 이름 규칙을 추가할 수 있습니다. 이때도 최종적으로는 비즈니스 식별자로 순서를 결정해야 합니다.

누락된 이름은 모델 변환 단계에서 일관되게 처리해야 합니다. 예를 들어 빈 문자열로 바꾸거나, “누락된 항목은 뒤에 배치한다”는 분기를 정의할 수 있습니다. 비교 함수 안에서 임의로 기본값을 채워 넣지 마세요. 내림차순도 각 키의 비교 방향을 다시 정의해야 합니다. 오름차순 비교 결과를 단순히 반전하면 동등한 객체까지 “작다”고 판단하게 됩니다.

클라우드에서 실행하고 실패 근거 기록하기

클라우드 Mac의 테스트 작업에서는 Xcode, 시뮬레이터 런타임, 비교 로케일을 고정하고 기존 단위 테스트 실행 경로로 이 테스트들을 실행합니다. 커밋 식별자, 툴체인 버전, 실패한 입력, 실제 출력의 식별자 순서를 보관하세요. 실제 고객의 파일명은 기록할 필요가 없습니다.

먼저 순수 함수를 검증한 다음 화면을 검증합니다. 연속으로 두 번 새로고침해 이름이 같은 항목의 위치가 바뀌지 않는지 확인하세요. 항목 하나를 삭제한 뒤 다시 삽입하고, 나머지 항목의 상대적 순서가 규칙에 맞는지도 확인합니다. 화면 계층에서도 같은 비교 함수를 재사용해야 하며, 별도의 정렬 로직을 작성해서는 안 됩니다.

로케일을 고정한다고 해서 시스템의 문자열 정렬 규칙이 영원히 바뀌지 않는 것은 아닙니다. 런타임을 업그레이드한 뒤에는 대표적인 언어 표본을 다시 검증하세요. 차이가 시스템 규칙에서 비롯되었다면 변경을 받아들일지, 사용자 정의 정렬 키를 사용할지 결정해야 합니다. 모든 스냅샷을 곧바로 갱신하는 것으로 끝내서는 안 됩니다.

최종 산출물에는 정렬 계약, 대표 표본, 비교 함수의 성질 검사, 새로고침 검수 결과가 포함되어야 합니다. 그래야 다음에 순서가 바뀌었을 때 데이터, 규칙, 런타임 중 무엇이 달라졌는지 판단할 수 있으며, 다시 스크린샷을 보며 추측하는 데서 시작하지 않아도 됩니다.

자주 묻는 질문

안정 정렬만으로 동명 항목의 순서를 유지할 수 있나요?

아니요. 안정 정렬은 동일하게 비교되는 항목의 입력 순서를 유지합니다. 서버 응답 순서가 달라질 수 있다면 영구적이고 고유한 업무 식별자를 마지막 정렬 키로 사용해야 합니다.

비교 로케일을 고정하면 정렬 스냅샷을 계속 재사용해도 되나요?

시스템의 문자열 비교 규칙은 런타임 업데이트로 달라질 수 있습니다. 로케일 고정만으로 충분하지 않으므로 업데이트 시 대표 샘플과 비교 함수의 성질을 다시 검증하세요.

다음 빌드를 위한 클라우드 Mac 예약하기

원하는 모델과 도시, 대여 기간을 선택하고 전용 물리 노드에서 개발 도구를 실행하세요. 이용 가능 여부는 콘솔에서 실시간으로 확인할 수 있습니다.

읽은 내용을 다음 빌드로 이어가세요

먼저 모델과 대여 기간을 확인한 뒤 프로젝트를 준비하세요.