iOS/Swift

[ Swift ] Identifiable

kimsangjunzzang 2025. 7. 12. 16:18

안녕하세요, 루피입니다.

 

SwiftUI로 개발하다 보면 List나 ForEach를 사용하면서 Identifiable이라는 프로토콜을 마주치는 순간이 꼭 찾아옵니다. 공식 문서를 봐도 그 의미가 선뜻 와닿지 않아 고개를 갸우뚱했던 경험, 다들 한 번쯤 있으실 텐데요.

 

오늘은 Identifiable이 정확히 무엇인지, 왜 중요한지, 그리고 다양한 상황에 맞춰 ID를 어떻게 설계해야 하는지, 정리하는 시간을 갖겠습니다. 바로 시작합니다.


1. Identifiable이란?

protocol Identifiable {
    associatedtype ID : Hashable
    var id: Self.ID { get }
}

먼저 Apple 공식 문서의 정의를 살펴보죠.

안정적인 식별자를 가진 엔티티의 값을 보유하는 인스턴스 타입을 위한 프로토콜

조금 복잡하죠? 의미가 더 잘 와닿도록 한 문장으로 다시 정의해 보겠습니다.

id라는 이름의 '안정적인' 식별자를 갖도록 강제하는 프로토콜

여기서 가장 중요한 핵심은 "안정적인"이라는 단어입니다. Identifiable은 단순히 id라는 프로퍼티를 가지라는 규칙이 아닙니다. 그 id가 마치 우리 각자의 주민등록번호처럼, 다른 정보가 바뀌더라도 절대 변하지 않는 고유한 값이어야 한다는 '약속'입니다.


2. 왜 중요한가요? (SwiftUI와의 관계)

Identifiable이 중요한 가장 큰 이유는 SwiftUI 때문입니다. SwiftUI의 List와 ForEach는 화면에 여러 데이터를 그릴 때, 각 데이터 항목을 구별하고 추적하기 위해 바로 이 안정적인 id를 사용합니다.

 

SwiftUI는 id를 보고 "아, 이 데이터가 바로 이 화면 요소구나!"라고 짝을 맞춥니다. 만약 데이터가 변경되면, id를 통해 정확히 어떤 화면 요소만 새로 그려야 할지 효율적으로 판단하죠. id가 불안정하다면 UI가 깨지거나 엉뚱한 애니메이션이 발생하는 등 예측 불가능한 버그의 원인이 됩니다.


3. ID, 어떻게 설계해야 할까? 

Identifiable 프로토콜은 ID의 생성 방법을 강제하지 않습니다. 어떤 ID를 사용할지는 데이터의 특성과 생명주기에 따라 개발자가 직접 결정해야 합니다. 각 방식의 특징과 사용 예시를 알아보겠습니다.

1) UUID: 앱/기기/시간을 초월한 영구 ID

  • 특징: 전 세계적으로 중복될 가능성이 거의 없는 128비트 고유값입니다.
  • 사용 예시: 서버와 클라이언트 양쪽에서 데이터가 생성될 때, 오프라인에서 생성한 데이터를 나중에 서버에 보낼 때 등 절대 중복되면 안 되는 경우에 사용합니다.
struct Post: Identifiable { 
	let id: UUID = UUID()
    var content: String 
}
  • 장점: 충돌 가능성이 거의 없고, 글로벌 고유성을 보장합니다.
  • 단점: 사람이 읽기 어렵고, 저장 공간을 더 차지합니다.

2) 서버 DB의 고유 키 (Int, String): 서버가 보장하는 영구 ID

  • 특징: 서버 데이터베이스의 기본 키(Primary Key)로 관리되는 값을 id로 사용합니다.
  • 사용 예시: API를 통해 서버에서 내려주는 게시글, 유저, 상품 목록 등에 사용합니다.
struct Product: Identifiable, Decodable {
	let id: Int
    let name: String
}
  • 장점: 서버와 데이터 동기화가 쉽고, 이미 검증된 고유성을 가집니다.
  • 단점: 오프라인 상태이거나 서버가 없는 환경에서는 사용할 수 없습니다.

3) 프로세스 생명주기 동안의 고유값: 앱 실행 중에만 유효한 임시 ID

  • 특징: 앱이 실행되는 동안에만 고유성이 유지되는 값입니다.
  • 사용 예시: 화면에 잠시 떴다 사라지는 알림, 저장하지 않는 임시 작업 목록 등에 사용합니다.
    struct TempAlert: Identifiable {
        private static var nextID = 0
        let id: Int
    
        init() {
            self.id = TempAlert.nextID
            TempAlert.nextID += 1
        }
    }
    }
  • 장점: 구현이 간단하고 메모리가 효율적입니다.
  • 단점: 앱을 재시작하면 id가 초기화되며, 여러 스레드에서 동시에 접근 시 id가 중복될 수 있는 치명적인 문제(Data Race)가 있습니다.
  • ✅ 실무 레벨의 안전한 구현 (Actor 사용): 위와 같은 스레드 문제를 해결하기 위해, 실제 프로젝트에서는 Actor를 사용해 안전한 '번호표 발급기'를 만드는 것이 좋습니다.
actor IDGenerator {
    static let shared = IDGenerator()
    private var counter = 0
    private init() {}
    func next() -> Int {
        counter += 1
        return counter
    }
}
// 사용법: let newID = await IDGenerator.shared.next()

4) 객체 생명주기 동안의 고유값: 클래스 인스턴스 자체의 ID

  • 특징: 클래스 인스턴스의 메모리 주소를 기반으로 고유성을 보장하는 ObjectIdentifier를 사용합니다.
  • 사용 예시: 데이터 내용은 같더라도, 메모리에 생성된 각기 다른 객체 인스턴스를 구분해야 할 때 사용합니다.
class Person: Identifiable {
    var name: String
    // Identifiable의 기본 구현이기도 합니다.
    var id: ObjectIdentifier { ObjectIdentifier(self) }

    init(name: String) { self.name = name }
}
  • 장점: 별도의 id 프로퍼티 관리 없이 인스턴스를 쉽게 구분할 수 있습니다.
  • 단점: 객체가 메모리에서 해제되었다가 다시 생성되면 id가 달라지므로 영구적인 식별에는 부적합합니다.

5) 컬렉션 내에서만 고유: 배열의 인덱스 (⚠️ Anti-Pattern)

  • 특징: ForEach(0..<items.count) 처럼 배열의 순서(index)를 id로 사용하는 방식입니다.
  • 사용 예시권장하지 않습니다.
  • 단점: 배열의 항목이 삭제, 추가, 재정렬되면 인덱스가 전부 밀리거나 바뀌어버립니다. 즉, id가 안정적이지 않으므로 SwiftUI 뷰에서 심각한 오류를 유발할 수 있습니다.

4. 가장 중요한 약속: "만드는 쪽과 쓰는 쪽의 책임"

공식 문서의 이 문장은 Identifiable의 철학을 보여줍니다.

ID의 특성을 명확히 하는 것은 프로토콜을 채택하는 쪽과 사용하는 쪽 모두의 책임입니다.

컴파일러는 id가 UUID인지, 임시 Int인지 알지 못합니다. 따라서 개발자 간의 소통, 즉 '약속'이 중요합니다.

소통 실패 시나리오

  1. 만든 사람: User의 id를 앱 실행 중에만 유효한 임시 ID로 만들고, 이 사실을 문서화하지 않았습니다.
  2. 쓰는 사람: User를 가져와 '즐겨찾기' 기능을 만들면서, id가 영구적일 것이라 추측하고 기기에 저장했습니다.
  3. 결과: 앱을 재시작하자 id가 모두 바뀌었고, 즐겨찾기 기능은 완전히 망가졌습니다.

이처럼 id의 특성을 주석 등으로 명확히 알리고, 사용 전 그 특성을 확인하는 것은 버그 없는 앱을 만들기 위한 필수적인 약속입니다.


Ref

https://developer.apple.com/documentation/swift/identifiable

 

Identifiable | Apple Developer Documentation

A class of types whose instances hold the value of an entity with stable identity.

developer.apple.com


오늘도 화이팅입니다!