삽입 순서를 바꾸지 않고 합성 Swift 값의 고유성을 유지하려면 입력 배열을 한 번 훑으면서, 허용된 키를 집합에 기록하고 처음 등장한 값만 출력 배열에 추가하면 됩니다. 이 가이드에서는 Mac에서 실행할 수 있는 간단한 로컬 계산을 만듭니다.

이 작업은 순서가 있는 합성 도형 값 목록을 처리하고, 반복된 값을 제거한 뒤, 남은 항목의 합계를 계산합니다. 모든 과정은 메모리에서 이루어집니다. 이 예제는 외부 입력을 읽지 않으며 계산 결과도 저장하지 않습니다.

컬렉션을 선택하기 전에 “고유”의 의미를 정하세요

Swift 배열은 순서를 유지하며 중복을 허용합니다. 집합에는 고유한 값만 들어가지만, 정의된 순서는 없습니다. 따라서 배열을 집합으로 변환한 다음 다시 배열로 변환해도 처음 등장한 순서는 유지되지 않습니다. 이후 정렬하면 삽입 순서와 다를 수 있는 정렬 순서가 생깁니다. 이러한 컬렉션 동작은 Swift의 컬렉션 유형에 설명되어 있습니다.

이 예제의 각 합성 값에는 도형 이름과 크기가 있습니다. 기본 규칙에서는 두 속성이 모두 일치할 때만 두 항목을 중복으로 간주합니다. 따라서 크기가 12인 원과 크기가 20인 원은 별개의 값으로 남습니다.

규칙에 관한 질문 이 계산의 규칙
입력 순서는 무엇으로 정해지나요? 제공된 배열에서의 위치
어떤 경우 두 값이 중복인가요? 도형과 크기가 모두 같을 때
어느 항목이 남나요? 처음 등장한 항목
나중에 나온 중복 항목이 남은 값의 위치를 바꾸나요? 아니요
입력이 비어 있으면 어떻게 되나요? 빈 배열과 합계 0을 반환합니다
어떤 값이 합계에 반영되나요? 남은 값만 반영됩니다

이 규칙은 계산에서 서로 다른 도형과 크기의 조합이 필요한데 도형 이름만 기준으로 항목을 제거하는 흔한 실수를 방지합니다. 예제에서는 규칙을 바꿨을 때 어떤 결과가 생기는지 확인할 수 있도록 도형만 기준으로 한 결과도 계산합니다.

Mac에서 완전한 로컬 Swift 예제를 빌드하세요

Swift 도구 모음이 설치되어 있어야 합니다. Terminal에서 swift --version을 실행해 명령을 사용할 수 있는지 확인하세요. Swift 공식 명령줄 도구 빌드하기 튜토리얼에는 이 확인 방법과 실행 파일 패키지를 만들고 실행하는 방법이 설명되어 있습니다.

예제를 위한 새 디렉터리를 만들고 실행 파일 패키지를 초기화하세요.

mkdir OrderDemo
cd OrderDemo
swift package init --name OrderDemo --type executable

생성된 Sources 아래의 실행 파일 소스 파일을 아래의 전체 프로그램으로 바꾸세요. 생성된 파일 이름과 디렉터리 구조는 도구 모음에 따라 다를 수 있습니다. 기존 진입점을 교체하고 두 번째 진입점을 추가하지 마세요. 이 프로그램은 @main 진입점을 사용하므로, 필요하면 생성된 main.swift의 이름을 바꾸고 파일 이름을 OrderDemo.swift로 지정하세요. Swift의 명령줄 튜토리얼에서는 main.swift 파일과 @main 진입점 중 무엇을 선택할지 설명합니다.

가져오기나 패키지 종속성은 필요하지 않습니다. 이 프로그램은 표준 라이브러리의 유형과 함수를 사용합니다.

struct Tile: Hashable {
    let shape: String
    let size: Int
}

func uniqueFirst<Value, Key: Hashable>(
    _ values: [Value],
    by key: (Value) -> Key
) -> [Value] {
    var seen: Set<Key> = []
    var result: [Value] = []

    for value in values {
        if seen.insert(key(value)).inserted {
            result.append(value)
        }
    }

    return result
}

@main
struct OrderDemo {
    static func main() {
        let input = [
            Tile(shape: "circle", size: 12),
            Tile(shape: "square", size: 8),
            Tile(shape: "circle", size: 12),
            Tile(shape: "circle", size: 20),
            Tile(shape: "square", size: 8)
        ]

        let retained = uniqueFirst(input, by: { $0 })
        let total = retained.reduce(0) { $0 + $1.size }
        let skipped = input.count - retained.count

        let expected = [
            Tile(shape: "circle", size: 12),
            Tile(shape: "square", size: 8),
            Tile(shape: "circle", size: 20)
        ]
        precondition(retained == expected)
        precondition(total == 40)
        precondition(skipped == 2)

        let empty: [Tile] = []
        precondition(uniqueFirst(empty, by: { $0 }).isEmpty)
        precondition(
            uniqueFirst([input[0], input[0]], by: { $0 })
                == [input[0]]
        )

        let byShape = uniqueFirst(input, by: { $0.shape })
        precondition(byShape == [input[0], input[1]])
        let shapeTotal = byShape.reduce(0) { $0 + $1.size }
        precondition(shapeTotal == 20)

        for tile in retained {
            print("\(tile.shape): \(tile.size)")
        }
        print("Total: \(total)")
        print("Skipped duplicates: \(skipped)")
        print("Shape-only total: \(shapeTotal)")
    }
}

패키지 디렉터리에서 다음을 실행하세요.

swift run OrderDemo

빌드 메시지가 표시된 뒤의 예상 프로그램 출력은 다음과 같습니다. 이 조사 환경에는 Swift 실행 파일이 없어 프로그램을 직접 실행하지는 않았으며, 가져온 API 문서와 대조해 내용을 확인했습니다.

circle: 12
square: 8
circle: 20
Total: 40
Skipped duplicates: 2
Shape-only total: 20

배열과 집합을 사용하면 순서가 유지되는 이유는 무엇인가요?

출력 순서는 배열만으로 결정됩니다. 반복문은 입력의 각 위치를 차례대로 방문하고, append(_:)은 허용된 값을 결과의 끝에 추가합니다. 집합은 선택된 키가 이미 나타났는지만 확인합니다. 프로그램은 집합을 순회하지 않습니다.

핵심 표현식은 seen.insert(key(value)).inserted입니다. Swift 문서에 따르면 insert(_:)은 새 멤버일 경우 true로 inserted를 반환하고, 같은 멤버가 이미 있으면 false을 반환합니다. 기존 멤버는 변경되지 않습니다. 이 반환값을 사용하면 한 번의 연산으로 원래 값을 추가할지 결정할 수 있습니다. Apple의 집합 삽입 문서를 참조하세요.

제공된 입력에서는 첫 번째 원과 사각형이 허용됩니다. 동일한 두 번째 원은 건너뜁니다. 더 큰 원은 전체 값이 다르므로 허용되고, 반복된 사각형은 건너뜁니다. 따라서 남는 크기는 12 + 8 + 20 = 40입니다.

이 구현은 유용한 불변 조건을 유지합니다. 각 반복이 끝날 때마다 결과에는 seen의 키마다 처음 발견된 값만 발견 순서대로 들어 있습니다. 새 키는 두 컬렉션을 모두 확장하고, 반복된 키는 어느 쪽도 변경하지 않습니다. 이 원리를 이해하면 집합이 멤버를 저장하는 방식과 무관하게 순서가 보장되는 이유를 알 수 있습니다.

사용자 정의 Swift 값은 동등성을 어떻게 표현해야 하나요?

struct Tile: Hashable 선언은 합성된 준수를 요청합니다. 저장된 속성인 String과 Int는 모두 해시 가능하므로 Swift가 필요한 동작을 합성할 수 있습니다. Apple의 Hashable 문서에서 합성 조건을 설명합니다.

{ $0 }을 키로 사용하면 전체 Tile 값이 고유성을 결정합니다. { $0.shape }를 사용하면 도형마다 항목을 하나만 남기는 규칙으로 바뀝니다. 예제에서는 이 규칙에 따라 크기가 20인 뒤쪽 원이 제거되어 합계가 20로 줄어듭니다. 이는 같은 규칙의 두 번째 구현이 아니라, 두 규칙을 의도적으로 비교한 것입니다.

계산이 진행되는 동안 키 선택은 일관되게 유지하세요. 카운터나 값이 바뀌는 외부 변수에서 키를 가져오지 말고 처리 중인 값에서 키를 도출하세요. 그렇지 않으면 같은 입력에도 서로 다른 키가 부여되어 별개의 항목으로 남을 수 있습니다.

동등성과 해싱을 직접 구현한다면 두 곳에서 동일한 핵심 속성을 사용하세요. Apple은 동등한 인스턴스가 동일한 순서로 같은 값을 해셔에 전달해야 한다고 요구합니다. hashValue 자체를 고유성 키로 사용하지 마세요. 해시는 값의 동등성 규칙을 대신할 수 없습니다. 실제 해시 가능 키를 유지하면 집합이 해당 규칙을 적용할 수 있습니다. Hashable 준수 요건을 참조하세요.

대신 순서가 있는 컬렉션을 언제 사용해야 하나요?

단일 계산에서는 배열과 로컬 집합을 함께 사용하면 동작을 명확하게 표현할 수 있습니다. 애플리케이션에서 고유 멤버를 계속 추가, 제거, 재정렬하고 조회한다면, 전용 순서 컬렉션을 사용해 이런 작업을 더 쉽게 유지 관리할 수 있습니다.

선택지 적합한 조건 관련 제약
배열과 집합 선택한 키를 사용할 수도 있는 합성 Swift 값의 단일 순회 함수가 첫 등장 항목을 유지하는 동작을 정의합니다
Swift Collections OrderedSet 해시 가능한 고유 값의 순서 컬렉션을 지속적으로 사용 별도의 Swift Collections 패키지가 필요합니다
Foundation NSOrderedSet API가 이미 Foundation의 순서 있는 객체 컬렉션과 함께 작동 정적 순서 컬렉션을 나타내는 클래스이며, 변경 가능한 연산에는 NSMutableOrderedSet을 사용합니다

공식 Swift Collections 문서는 OrderedSet을 고유 요소의 순서 컬렉션으로 설명하며, OrderedCollections를 통해 가져옵니다. append(_:) 및 insert(_:at:) 같은 위치 기반 연산을 제공합니다. 이는 패키지 API이므로 import OrderedCollections만 추가한다고 해서 종속성이 없는 프로젝트의 구성이 완료되지는 않습니다. OrderedSet을 참조하세요.

Apple의 Foundation 문서는 NSOrderedSet을 고유 객체의 정적 순서 컬렉션으로 설명하고, NSMutableOrderedSet을 변경 가능한 대응 유형으로 소개합니다. 위의 네이티브 Swift 값 계산에서는 객체 컬렉션을 도입하지 않고 제네릭 함수로 선택한 동등성 키를 직접 표현할 수 있습니다. NSOrderedSet을 참조하세요.

계산 결과를 데스크톱 인터페이스에 전달하면 무엇이 달라지나요?

완전한 예제는 동기 방식이며 함수 내부에서 임시 컬렉션을 관리합니다. 작은 합성 목록이라면 작업을 도입하지 않고도 명확한 처리 순서를 얻을 수 있습니다.

macOS 인터페이스에서는 UI 상태와 업데이트를 적절히 격리하세요. Apple은 MainActor를 실행기가 메인 디스패치 큐와 동등한 전역 액터로 설명합니다. Swift 동시성 가이드에서는 인터페이스 상태에 접근하는 함수와 유형에 @MainActor를 적용하는 방법을 설명합니다. MainActor 및 동시성을 참조하세요.

동시성을 사용할 때는 순서도 명확히 정의해야 합니다. Swift는 작업 그룹의 자식 작업이 완료되는 순서를 보장하지 않습니다. 따라서 작업이 완료될 때마다 결과를 추가하면 원래 입력 순서와 다를 수 있는 완료 순서가 유지됩니다. 공식 동시성 가이드에서 이 제약을 설명합니다.

향후 버전에서 서로 독립적인 합성 계산을 병렬로 수행한다면, 각 입력 위치를 결과와 함께 전달하고, 수집한 뒤 입력 위치 순서로 복원한 다음 중복 제거 과정을 적용하세요. 이는 문서에 설명된 완료 순서 제약에서 도출한 설계입니다. 추가 작업이 조정을 정당화할 때까지 현재의 작은 예제는 동기 방식으로 유지하세요.

함수를 재사용하기 전에 규칙을 확인하세요

프로그램에는 정확한 유지 순서, 빈 입력, 동일한 값의 반복, 도형만 기준으로 하는 대체 규칙을 확인하는 검사가 포함되어 있습니다. 이 검사는 계산의 의미를 바꿀 가능성이 가장 높은 결정을 점검합니다.

응용할 때는 세 가지를 확인하세요. 입력에 이미 의미 있는 순서가 있어야 하고, 키가 의도한 동등성 규칙을 표현해야 하며, 첫 등장 항목을 유지하는 것이 적절해야 합니다. 이 함수는 나중에 일치하는 항목을 의도적으로 건너뜁니다. 해당 항목의 크기를 합치거나 남겨 둔 값을 교체하지 않습니다. 건너뛴 개수를 표시해 그 영향을 알 수 있습니다.

제공된 합성 도형 목록을 처리하면 처음 등장한 순서의 값 세 개와 40의 합계가 나옵니다. 배열은 순서를, 집합은 멤버 여부를, 선택한 키는 “고유”의 의미를 각각 담당하도록 유지하세요.

출처