클라우드 Mac에서 iOS 목록 차이 업데이트 검증하기

클라우드 Mac에서 iOS 목록 차이 업데이트 검증하기

클라우드 Mac에서 iOS 목록 차이 업데이트 검증하기

목록에서 삭제, 삽입, 재정렬이 동시에 일어날 때 재현하기 가장 어려운 오류는 데이터를 잘못 계산해서가 아니라 이전 위치를 새 위치로 착각해서 발생하는 경우가 많습니다. OSXFlow 클라우드 Mac에서 시뮬레이터를 실행하지 않고 순수 Swift 검증 스크립트부터 돌려 보세요. 차이를 적용하고 되돌렸을 때 원래 상태가 복원되는지 확인한 뒤, 검증된 구조 변경을 화면 업데이트 계층에 연결하면 됩니다.

차이가 나타내는 두 상태부터 정의하기

업데이트 전의 안정적인 ID 배열을 이전 스냅샷, 업데이트 후의 배열을 새 스냅샷이라고 하겠습니다. 호출 방향은 new.difference(from: old)이며, 그 반대가 아닙니다. 삭제 오프셋은 이전 스냅샷 기준이고 삽입 오프셋은 새 스냅샷 기준입니다. 이 둘을 작업할 때마다 바뀌는 하나의 좌표계로 해석해서는 안 됩니다.

예를 들어 이전 목록이 [a,b,c]이고 새 목록이 [c,a,d]라고 해 보겠습니다. 새 위치에 먼저 삽입한 다음 이전 위치에서 삭제하면 방금 삽입한 요소를 지울 수 있습니다. 표준 라이브러리로 결과를 재구성할 때는 각 오프셋을 직접 보정하지 말고 old.applying(diff)를 사용하세요.

먼저 세 가지 원칙을 정해야 합니다. 하나의 스냅샷 안에서 ID는 고유해야 하고, 내용이 바뀌어도 ID는 바뀌지 않아야 하며, 백그라운드에서 차이를 계산하는 동안 원본 스냅샷을 제자리에서 수정해서는 안 됩니다. ID가 중복된다고 표준 라이브러리가 반드시 오류를 내는 것은 아니지만, 화면에서 각 행을 식별하는 기준이 모호해집니다.

차이는 아무 목록에나 적용할 수 있는 패치가 아닙니다. 특정한 두 스냅샷에 속하며, 적용에 성공했다고 해서 원본 데이터의 버전까지 올바르다는 뜻은 아닙니다.

실행 가능한 스크립트로 왕복 복원 검증하기

원격 터미널에서 xcrun --find swift로 컴파일러를 찾을 수 있는지 확인하세요. 아래 코드를 /tmp/list-diff-check.swift에 저장한 뒤 xcrun swift /tmp/list-diff-check.swift를 실행합니다. 이 예제는 표준 라이브러리만 사용하므로 프로젝트를 만들거나 의존성을 다운로드하거나 서명을 설정할 필요가 없습니다.

func hasUniqueIDs(_ values: [String]) -> Bool {
    Set(values).count == values.count
}

func verify(_ old: [String], _ new: [String]) {
    precondition(hasUniqueIDs(old))
    precondition(hasUniqueIDs(new))

    let raw = new.difference(from: old)
    let moved = raw.inferringMoves()

    precondition(old.applying(raw) == new)
    precondition(old.applying(moved) == new)
    precondition(new.applying(moved.inverse()) == old)
}

verify([], [])
verify([], ["a"])
verify(["a"], [])
verify(["a", "b", "c"], ["c", "a", "d"])
verify(["a", "b", "c"], ["c", "b", "a"])
precondition(!hasUniqueIDs(["a", "a"]))

정상적으로 실행되면 스크립트는 아무것도 출력하지 않고 프로세스 종료 코드는 0이 됩니다. 사전 조건 중 하나라도 실패하면 실행이 중단되어 파이프라인이 실패로 처리됩니다. 이 검사들을 제거하는 -Ounchecked 컴파일 모드는 사용하지 마세요.

여기서는 원본 차이, 이동을 추론한 차이, 역변환을 함께 검증합니다. 각각 “목표 상태에 도달할 수 있는가”, “이동 연결 관계가 의미를 보존하는가”, “원래 상태로 돌아갈 수 있는가”를 확인하는 검사입니다. 요소 개수만 확인해서는 위치가 어긋난 문제를 찾을 수 없으므로 배열 전체를 비교해야 합니다.

몇 가지 익숙한 사례 대신 작은 집합 전수 탐색하기

고정된 테스트 사례는 오류를 설명하기에 좋지만, “첫 요소를 삭제하면서 마지막 요소를 중간으로 옮기는” 식의 조합은 놓치기 쉽습니다. 서로 다른 ID 네 개로 만들 수 있는 중복 없는 순서 있는 부분집합은 총 65개이며, 이들 사이의 모든 변환은 4225가지입니다. 이 정도 규모라면 난수 시드 없이도 전수 탐색할 수 있습니다.

아래 코드를 같은 파일 끝에 추가한 뒤 다시 실행하세요.

func snapshots(
    prefix: [String],
    remaining: [String]
) -> [[String]] {
    var result = [prefix]
    for index in remaining.indices {
        var rest = remaining
        let next = rest.remove(at: index)
        result += snapshots(
            prefix: prefix + [next],
            remaining: rest
        )
    }
    return result
}

let cases = snapshots(
    prefix: [],
    remaining: ["a", "b", "c", "d"]
)
precondition(cases.count == 65)

for old in cases {
    for new in cases {
        verify(old, new)
    }
}

이 검사는 빈 목록, 삽입만 있는 경우, 삭제만 있는 경우, 순서 전체 뒤집기, 여러 편집이 섞인 경우를 다루지만, 모든 길이에서 올바르게 동작한다는 증명은 아닙니다. 실제로 연동할 때는 운영 환경에서 발생한 문제를 최소 입력 쌍으로 줄여 고정 테스트 사례에 추가하세요. 큰 목록은 실행 시간과 메모리 사용량을 별도로 측정해야 하며, 여기서 얻은 실행 결과를 성능 보장으로 받아들여서는 안 됩니다.

이동 연결 관계는 화면 이동 명령이 아니다

inferringMoves()는 이동으로 식별할 수 있는 삭제와 삽입에 연결 관계를 추가할 뿐, 이를 새로운 열거형 케이스로 바꾸지는 않습니다. applying(_:)에 전달할 때는 차이 전체를 유지해야 하며, 연결 관계가 있다는 이유로 어느 한쪽을 건너뛰어서는 안 됩니다.

화면에서 자체 배치 업데이트 API를 사용한다면 별도 어댑터를 구현하고, 해당 API가 요구하는 원본 위치, 대상 위치, 데이터 소스 업데이트 시점을 확인해야 합니다. 표준 라이브러리 차이가 검증을 통과했다는 것은 배열 변환이 올바르다는 뜻일 뿐, 임의의 화면 호출 순서까지 올바르다는 뜻은 아닙니다.

내용 갱신은 별도 경로로 처리하기

ID가 a인 행에서 제목만 바뀌었다고 해 보겠습니다. ID 배열은 그대로이므로 차이는 비어 있습니다. 그래도 이전 모델과 새 모델의 화면 표시 필드를 비교해 갱신할 ID를 모아야 합니다. 갱신을 유도하려고 임시로 새 ID를 만들지 마세요. 내용 수정 한 번이 삭제와 삽입으로 바뀌면서 선택 상태나 펼침 상태가 사라집니다.

버전 검사로 오래된 차이 차단하기

백그라운드 계산을 시작할 때 이전 스냅샷과 단조 증가하는 버전 번호를 함께 캡처하세요. 결과가 UI 격리 영역으로 돌아오면 현재 버전이 캡처한 버전과 여전히 같은지 먼저 확인한 뒤 모델과 화면을 업데이트합니다. 버전 확인과 변경 사항 반영 사이에는 비동기 대기가 끼어들면 안 됩니다.

버전이 이미 바뀌었다면 결과를 버리고 현재 스냅샷을 기준으로 다시 계산하세요. “표준 라이브러리가 nil이 아닌 결과를 반환했다”는 사실을 버전 검증으로 간주해서는 안 됩니다. 중복 값이 있거나 위치가 우연히 맞아떨어지면 오래된 차이로도 배열이 만들어질 수 있습니다.

배포 전에는 마지막으로 네 가지를 확인하세요. 중복 ID를 명시적으로 거부하는지, 구조 변경과 내용 갱신을 분리했는지, 오래된 버전의 결과가 반영되지 않는지, 화면 어댑터가 삭제·이동·삽입이 섞여 발생하는 사례를 적어도 하나 다루는지 확인해야 합니다. 입력 스냅샷, 버전 번호, 실패한 단언문을 보관하면 문제를 추적할 수 있으며, 행 안의 민감한 내용까지 기록할 필요는 없습니다.

자주 묻는 질문

inferringMoves() 호출 뒤 연결된 삭제와 삽입을 생략해도 되나요?

안 됩니다. 이동은 여전히 서로 연결된 삭제와 삽입으로 표현됩니다. applying(_:)에는 전체 차이를 전달하고, 화면 이동 연산으로 변환할 때는 별도 어댑터의 인덱스 규칙을 검증해야 합니다.

안정적인 ID만 비교하면 제목 변경도 감지하나요?

감지하지 못합니다. ID 차이는 목록 구조만 나타냅니다. 내용 변경은 별도로 비교해 새로 고침을 요청하고, 구조 업데이트 후 최신 모델을 읽어야 합니다.

나만의 Mac으로 워크플로를 확장하세요

기본 macOS 환경에서 빌드, 테스트, 실험을 진행하세요. 사양과 이용 기간을 비교한 뒤 적합한 물리 노드를 선택할 수 있습니다.

클라우드 Mac 대여하기 Mac 요금제 보기

다음 빌드는 클라우드 Mac에서

가상 머신이 아닌 전용 물리 머신입니다. 프로젝트 일정에 맞춰 사양과 이용 기간을 선택하세요.