[ Swift ] Identifiable
안녕하세요, 루피입니다.
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인지 알지 못합니다. 따라서 개발자 간의 소통, 즉 '약속'이 중요합니다.
소통 실패 시나리오
- 만든 사람: User의 id를 앱 실행 중에만 유효한 임시 ID로 만들고, 이 사실을 문서화하지 않았습니다.
- 쓰는 사람: User를 가져와 '즐겨찾기' 기능을 만들면서, id가 영구적일 것이라 추측하고 기기에 저장했습니다.
- 결과: 앱을 재시작하자 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
오늘도 화이팅입니다!