ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Swift 공식문서 톺아보기 (4) - Collection Types
    Swift 2025. 11. 3. 02:47

    안녕하세요 :) 🧀

    이번 포스팅에서도 공식문서의 Collection Types 섹션을 정독하면서,

    그동안 헷갈렸던 부분이나 새롭게 알게 된 내용을 정리해보려 합니다.


    우선 Swift에는 세 가지 기본 컬렉션 타입이 있습니다.

    모두 타입 안정성을 보장하며, 잘못된 타입의 데이터를 담을 수 없습니다.

    • Array: 순서가 있는 집합
    • Set: 순서가 없고 중복되지 않는 값의 집합
    • Dictionary: 키-값 쌍으로 이루어진 순서 없는 집합

    Array와 Dictionary는 평소에도 많이 사용했는데,

    Set은 활용해 본 기억이 별로 없어서 Set에 대해서는 아래에서 더 자세히 정리해 볼 예정입니다.

     

    그럼 하나씩 살펴보겠습니다.

    🎯 컬렉션의 변경 유무

    컬렉션(Array, Set, Dictionary)을 선언하는 방법에 따라 아래와 같이 변경 유무가 달라집니다.

    • 변수(var)에 담으면 mutable
    • 상수(let)에 담으면 immutable

    즉, var는 컬렉션에 추가, 삭제, 수정이 가능하지만 let은 완전히 고정되어 불가능합니다.

    var fruits = ["Apple", "Banana"]
    fruits.append("Orange")
    fruits[0] = "Grape"
    
    print(fruits) // ["Grape", "Banana", "Orange"]
    
    let colors = ["Red", "Blue"]
    colors.append("Green") 
    // ❌ Cannot use mutating member on immutable value: 'colors' is a 'let' constant

     

    🎯 기본값으로 배열 생성

    Array는 같은 값으로 채워진 일정 크기의 배열을 쉽게 만들 수 있는 생성자를 제공한다.

    init(repeating repeatedValue: Element, count: Int)
    • repeating: 배열의 각 요소에 복사될 기본값
    • count: 그 값을 반복할 횟수

    이 생성자를 사용할 때는 아래와 같은 주의점이 있다.

    var numbers = Array(repeating: 0.0, count: 3)
    // [0.0, 0.0, 0.0]
    
    class Counter { var value = 0 }
    var counters = Array(repeating: Counter(), count: 3)
    counters[0].value = 20
    print(counters[1].value)
    // 20

     

    각 요소에 복사될 값이 값 타입인지 참조 타입인지에 따라 달라지는데,

    Double 같은 값 타입은 값이 복사되므로 안전하지만,

    class 같은 참조 타입은 하나의 인스턴스 참조를 여러 번 반복하므로 복사되는 모든 요소가 같은 객체를 공유하게 된다.

    (이 차이를 모르면 간혹 모든 요소가 동시에 바뀌는 버그를 일으킬 수 있음!)

    🎯 배열의 범위(Range) 수정

    범위 연산자를 사용하면 여러 요소를 한 번에 교체 가능

    var shoppingList = ["Eggs", "Milk", "Flour", "Bacon"]
    shoppingList[0...2] = ["Banana", "Apple"]
    // ["Banana", "Apple", "Bacon"]

     

    교체되는 값의 개수가 달라도 자동으로 크기를 조정한다.

    🎯 Sets

    Set 타입은 다른 타입과 비교해서 그동안 덜 사용했던 것 같아서 자세하게 알아보겠습니다.

    • Set은 순서가 없는 값들의 모음입니다.
    • 동일한 값은 한 번만 존재하며, 중복 요소를 자동으로 제거합니다.
    • Set에 저장하려면 값이 반드시 Hashable 해야 합니다.
    • (String, Int, Double, Bool 등은 기본적으로 Hashable 합니다.)

    🎯 Array vs Set

    비교항목 Array Set
    순서 있음 (인덱스로 접근 가능) 없음
    중복 허용 가능 불가능
    접근 방식 인덱스 기반 해시 기반
    검색 속도 O(n) 평균 O(1)
    주요 용도 순서가 중요하거나 중복 허용 시 유일한 값 관리, 포함 관계 검사

    🎯 Hash Values for Set Types

    Set에 저장하려면 해당 타입이 Hashable 해야 합니다.

    즉, 각 요소가 자신을 식별할 정수형 해시값(Int)을 제공할 수 있어야 합니다.

     

    Swift의 주요 기본 타입(String, Int, Double, Bool)은 이미 Hashable을 채택하고 있으므로 바로 Set에 저장할 수 있습니다.

    또한 연관 값이 없는 enum도 기본적으로 Hashable입니다.

    enum Direction { case north, south, east, west }
    let directions: Set<Direction> = [.north, .south]

     

    커스텀 타입을 Set에 저장하기 위해서는 Hashable을 채택해야 합니다.

    커스텀 타입의 모든 저장 프로퍼티가 Hashable일 경우, 자동으로 구현이 제공됩니다.

    struct Person: Hashable {
      let id: Int
      let name: String
    }
    
    let people: Set<Person> = [
      Person(id: 1, name: "Alice"),
      Person(id: 1, name: "Alice") // 중복 제거됨
    ]

     

    하지만 직접 구현해야 하는 경우에는 아래와 같이 hash 관련 메서드를 선언해주어야 합니다.

    struct UserName: Equatable {
      let name: String
    }
    
    struct Person: Hashable {
      var id: Int
      var name: UserName
      
      static func == (lhs: Person, rhs: Person) -> Bool {
        return lhs.id == rhs.id && lhs.name == rhs.name
      }
      
      func hash(into hasher: inout Hasher) {
        hasher.combine(id)
      }
    }
    
    let people: Set<Person> = [
      Person(id: 1, name: UserName(name: "Alice")),
      Person(id: 1, name: UserName(name: "Alice")) // 중복 제거됨
    ]

     

    실무에서 리스트에서 선택된 아이템들을 기존에는 배열에 담았던 것 같은데

    앞으로는 중복을 방지하고 선택 상태를 간결하게 관리할 수 있도록 

    @State var selectedItems: Set<UUID> 같은 형식으로 사용해 봐야겠습니다!

    🎯 Set 타입의 선언문

    Array와 달리 Set은 축약형 문법이 존재하지 않습니다.

    Array는 [Element]로 축약이 가능하지만, Set은 항상 제네릭 형태로 명시해야 합니다.

    • [Element]는 순서를 보장하는 Array의 의미로 이미 확립되어 있음
    • Set은 순서를 가지지 않는 컬렉션이므로, 같은 형태의 축약 문법을 부여하면 혼동이 생김
    • 따라서 Set<Element> 형태를 유지합니다.

    🎯 배열 리터럴로 Set 생성하기

    위에서 Set은 [Element]와 같이 축약해서 타입을 명시할 수 없다고 했습니다.

    하지만 배열 리터럴([])로 초기화는 가능합니다.

    (단, 타입 명시는 필수)

    var favoriteGenres: Set<String> = ["Rock", "Classical", "Hip hop"]

     

    이렇게 하면 리터럴 자체는 배열 리터럴이지만, 타입이 Set이므로 Set 초기화 문법이 됩니다.

    타입 추론 덕분에 <String>을 생략할 수도 있습니다.

    🎯 Set의 순회

    Set의 모든 요소는 for-in 반목문으로 순회할 수 있습니다. 단 Set은 순서가 없는 컬렉션 이므로,

    순서를 보장하려면 sorted() 메서드를 활용해야 합니다.

    기본 순회

    for genre in favoriteGenres {
      print(genre)
    }
    • Sequece 프로토콜을 채택하고 있어 각 요소에 한 번씩 접근할 수 있습니다.
    • 순회 순서는 정해져 있지 않습니다.
    • 즉, 실행할 때마다 순서가 바뀔 수 있습니다.

    정렬된 순회: sorted()

    for genre in favoriteGenres.sorted() {
      print(genre)
    }
    • sorted()는 Set의 모든 요소를 정렬된 배열로 변환합니다.
    • 정렬 기준은 기본적으로 오름차순입니다.
    • 커스텀 정렬 기준을 지정할 수도 있습니다.
    for genre in favoriteGenres.sorted(by: >) {
      print(genre)
    }

    🎯 Hash 기반 구조와 성능

    Set 위처럼 기본적으로는 순서가 보장되지 않는다고 했습니다.

    내부적으로 Hash Table로 구현되어 각 요소의 해시값을 기반으로 저장 위치가 결정되기 때문인데,

    대신 삽입, 삭제, 탐색이 모두 평균 O(1)로 빠릅니다.

    순서를 희생하여 성능을 극대화한 컬렉션이라고 볼 수 있습니다.

    🎯 Set의 집합 연산 수행

    Hash Table을 기반으로 구현된 Set은 요소의 포함 여부를 검사하거나

    두 Set을 비교하는 연산이 평균적으로 빠르다고 했습니다.

    따라서 집합 연산 역시 효율적으로 수행할 수 있습니다.

    • 교집합(intersection): 두 Set의 모든 고유 요소를 하나로 결합
    • 대칭차(symmertricDifference): 두 Set 중 한쪽에만 존재하는 요소를 반환
    • 합집합(union): 두 Set의 모든 고유 요소를 하나로 결합
    • 차집합(subtracting): 한 Set에는 있지만 다른 Set에는 없는 요소만 반환

    이러한 연산은 수학적 집합과 동일한 의미를 가지며, 아래와 같이 메서드 기반 API로 제공됩니다.

    let oddDigits: Set = [1, 3, 5, 7, 9]
    let evenDigits: Set = [0, 2, 4, 6, 8]
    let primeDigits: Set = [2, 3, 5, 7]
    
    oddDigits.union(evenDigits)
    oddDigits.intersection(primeDigits)
    oddDigits.subtracting(primeDigits)
    oddDigits.symmetricDifference(primeDigits)

     

    각 연산들은 새로운 Set을 반환하므로, 원본 집합은 변경되지 않습니다.

    🎯 Set의 포함 관계와 동등

    두 Set이 서로 어떤 관계(포함, 상위, 동등, 베타)를 갖는지 다양한 연산자로 쉽게 판별할 수 있습니다.

    ==, 동등성

    let houseAnimals: Set = ["🐶", "🐱"]
    let farmAnimals: Set = ["🐮", "🐔", "🐷", "🐶", "🐱"]
    
    houseAnimals == farmAnimals
    • 두 Set이 모든 동일한 요소를 가질 때 true
    • 순서와 중복 여부는 무시됩니다.

    isSubset(of:), 부분집합

    houseAnimals.isSubset(of: farmAnimals) // true
    • setA의 모든 요소가 setB에도 존재하면 true
    • 수학적으로 “A ⊆ B” 관계를 표현합니다.

    isSuperset(of:), 상위집합

    farmAnimals.isSuperset(of: houseAnimals) // true
    • setA가 setB의 모든 요소를 포함할 때 true
    • 수학적으로 “A ⊇ B” 관계를 표현합니다.

    isStrictSubset(of:),  isStrictSuperset(of:)

    • Strict는 “부분집합이지만 완전히 같지는 않다”는 의미입니다.
    • 예를 들어, 두 Set이 완전히 동일하면 false가 됩니다.

    isDisjoint(with:), 공통 원소 없음

    farmAnimals.isDisjoint(with: houseAnimals) // false
    • 두 Set이 서로 겹치는 요소가 하나도 없을 때 true
    • 수학적으로 “A ∩ B = ∅” 관계를 표현합니다.

    🎯 딕셔너리의 축약 문법

    딕셔너리는 전체 표기로 Dictionary<Key, Value>라고 쓰며, 이를 축약하여 [Key: Value] 형태로 표현할 수 있습니다.

    두 문법은 완전히 동일하지만, Swift에서는 축약형이 더 간결하고 읽기 쉬워 사실상 표준 문법으로 사용합니다.

    var airports: Dictionary<String, String> = Dictionary<String, String>()
    var airports: [String: String] = [:]

     

    여기서 Key는 반드시 Hashable 프로토콜을 따라야 합니다.

    딕셔너리 내부적으로 Hash Table을 사용하기 때문에 키의 해시값을 이용해 저장 위치를 결정합니다.

    따라서 해시를 계산할 수 없는 타입은 키로 사용할 수 없습니다.

    🎯 딕셔너리의 항목 삭제

    var airports = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]
    airports["YYZ"] = nil
    

     

    삭제하고자 하는 키에 nil을 할당하면 해당 Key-Value 쌍이 삭제됩니다.

    var airports = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]
    if let removedValue = airports.removeValue(forKey: "YYZ") {
      print(removedValue)
    }
    
    • removeValue()로 해당 키를 삭제하면 삭제된 값을 반환합니다.
    • 존재하지 않으면 nil 반환
    • 반환 타입 역시 Optional입니다.

    🎯 딕셔너리의 순회

    딕셔너리는 for-in 루프로 순회할 수 있으며, 각 항목은 (key, value) 형태의 튜플로 반환됩니다.

    또한 keys와 values 프로퍼티를 통해 키와 값만 따로 순회할 수도 있습니다.

    기본순회

    var airports = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]
    for (code, name) in airports {
      print("\\(code): \\(name)")
    }
    

     

    이때 배열과는 다르게 순서는 보장되지 않습니다.

    키 또는 값만 순회하기

    for code in airports.keys {
      print("\\(code)")
    }
    
    for name in airports.values {
      print("\\(name)")
    }

    순서를 보장하며 순회하기

    for (code, name) in airports.sorted(by: { $0.key > $1.key }) {
      print("\\(code)")
    }
    • sorted()는 한 쌍의 배열을 반환합니다.
    • 정렬 기준을 명시적으로 지정할 수 있습니다.
    • 단, 이는 연산 비용이 발생하므로, 대규모 딕셔너리에는 주의가 필요합니다.
Designed by Tistory.