LODY/정리

iOS 개발 필수 기본기 / 로컬 데이터 저장의 선택지

로컬 데이터 저장의 선택지

🟣 무엇을 저장하느냐가 도구를 가른다

앱을 만들다 보면 데이터를 기기에 남겨야 하는 순간이 온다. 다크 모드 설정, 마지막으로 본 화면, 로그인 토큰, 다운로드한 이미지, 오프라인에서도 봐야 할 목록까지 종류가 제각각이다. 처음엔 이 모든 걸 UserDefaults에 넣으려다 문제를 겪었다. 이미지를 통째로 넣었더니 앱이 켜질 때마다 느려졌고, 목록을 넣었더니 일부만 골라 읽는 게 불가능했다.

깨달은 건 저장소 선택이 "무엇을 저장하느냐"에서 갈린다는 점이다. 작은 설정값, 파일, 구조적인 대량 데이터, 민감 정보는 각각 맞는 그릇이 다르다. 이 장은 iOS가 주는 네 가지 저장소를 그 기준으로 정리한다. 도구를 외우는 게 아니라, 데이터를 보고 어디에 둘지 판단하는 감을 잡는 게 목표다.

무엇을저장하나?작은 설정값→ UserDefaults파일·이미지→ FileManager구조적 대량 데이터→ Core Data민감 정보→ Keychain가볍고 단순덩어리질의 필요보안 필요

🟣 UserDefaults: 작고 단순한 설정값

UserDefaults는 키와 값을 짝지어 저장하는 가장 가벼운 그릇이다. 다크 모드 여부, 알림 허용 여부, 마지막으로 선택한 정렬 순서처럼 작고 단순한 설정값을 두기에 알맞다. 쓰기도 읽기도 한 줄이라 부담이 없다.

UserDefaults.standard.set(true, forKey: "isDarkMode")
let isDark = UserDefaults.standard.bool(forKey: "isDarkMode")

편한 만큼 오해도 잦다. UserDefaults는 데이터베이스가 아니라 앱이 켜질 때 통째로 메모리에 올라오는 작은 설정 파일에 가깝다. 그래서 이미지나 긴 목록을 넣으면 앱 실행이 느려지고 메모리를 먹는다. 또한 별도 보호가 없어 로그인 토큰 같은 민감 정보를 넣으면 안 된다. 한마디로 가볍고 안 민감한 값만 여기 둔다.


🟣 FileManager: 파일은 파일답게

이미지, PDF, 다운로드한 문서처럼 덩어리로 된 데이터는 파일로 다루는 게 맞다. FileManager는 앱이 쓸 수 있는 폴더에 파일을 읽고 쓰고 지우는 통로다. 여기서 중요한 건 어느 폴더에 두느냐다. iOS는 앱 폴더를 용도별로 나눠 두고, 그 선택에 따라 백업 여부와 시스템의 정리 대상 여부가 달라진다.

크게 두 곳을 구분하면 된다. 사용자가 만든 데이터나 다시 만들 수 없는 파일은 Documents에 둔다. 이 폴더는 iCloud 백업에 포함된다. 반면 서버에서 다시 받을 수 있는 캐시성 파일은 Caches에 둔다. 이 폴더는 백업되지 않고, 저장 공간이 부족하면 시스템이 지울 수 있다. 이 구분을 놓쳐 캐시 이미지를 Documents에 잔뜩 쌓으면, 지워도 될 파일이 사용자의 백업 용량을 잡아먹는다.

let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
let fileURL = caches.appendingPathComponent("thumb_42.jpg")
try imageData.write(to: fileURL)

여기서 이미지 캐시라는 발상이 뒤에 붙일 서드파티 라이브러리로 이어진다. Kingfisher 같은 이미지 도구가 하는 일도 결국 이 위에 있다. 네트워크로 받은 이미지를 Caches에 저장해 두고 다음엔 파일에서 꺼내는 것이다. 직접 FileManager로 캐시를 한번 만들어 보면, 그런 라이브러리가 안에서 무엇을 대신해 주는지가 보인다.


🟣 Core Data: 골라 읽고 정렬해야 할 때

목록이 수백, 수천 건이 되고 그중 일부를 조건으로 골라 읽거나 정렬해야 하면 UserDefaults나 파일로는 감당이 안 된다. "읽지 않은 항목만", "최근 순으로", "이 카테고리만" 같은 요구가 생기는 순간이 데이터베이스가 필요한 지점이다. iOS에서 그 자리를 맡는 대표 도구가 Core Data다.

Core Data를 단순한 저장소로만 보면 오해다. 데이터를 파일에 넣고 빼는 도구라기보다, 객체의 생애를 관리하는 계층에 가깝다. 실제 저장은 대개 그 아래의 SQLite가 맡고, Core Data는 그 위에서 객체를 메모리에 올리고, 바뀐 것을 추적하고, 한 번에 저장하고, 조건으로 질의하는 일을 한다. 그래서 학습 곡선이 있고, 잘못 쓰면 오히려 복잡해진다.

let request: NSFetchRequest<Article> = Article.fetchRequest()
request.predicate = NSPredicate(format: "isRead == NO")
request.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: false)]
let unread = try context.fetch(request)

그래서 선택 기준이 분명하다. 데이터가 단순하고 양이 적으면 굳이 Core Data를 꺼낼 필요가 없다. 반대로 관계가 있는 데이터를 조건으로 골라 읽고 정렬해야 하면, 파일에 직접 하려다 결국 조잡한 데이터베이스를 다시 만들게 된다. 요즘은 같은 자리에 더 가벼운 SwiftData나 서드파티 Realm을 쓰기도 하는데, 고르는 질문은 같다. "조건으로 질의할 일이 있는가."

🚀 토큰은 어디에 두나: Keychain

앞의 셋과 결이 다른 데이터가 하나 있다. 로그인 토큰, 비밀번호 같은 민감 정보다. 이건 UserDefaults에도 파일에도 두면 안 된다. 둘 다 별도 암호화 없이 기기 안에 그대로 남기 때문이다. 이런 값은 Keychain에 둔다. iOS가 운영체제 차원에서 암호화해 보관하는 보안 저장소다.

Keychain은 API가 번거롭기로 유명해서 얇은 래퍼를 만들어 쓰거나 검증된 라이브러리를 붙이는 경우가 많다. 다만 무엇을 쓰든 원칙은 하나다. 민감 정보는 일반 저장소가 아니라 Keychain으로 간다. 이 구분을 지키는지가 앱의 기본 보안 수준을 가른다.


🟣 정리

  • 저장소 선택은 "무엇을 저장하느냐"에서 갈린다. 작은 설정값, 파일, 구조적 대량 데이터, 민감 정보는 각각 맞는 그릇이 다르다.
  • UserDefaults는 가볍고 안 민감한 설정값 전용이다. 앱 시작 때 통째로 올라오므로 이미지나 큰 목록을 넣으면 실행이 느려진다.
  • FileManager는 덩어리 파일을 다룬다. 다시 만들 수 없는 데이터는 Documents, 다시 받을 수 있는 캐시는 Caches에 둔다. 이 구분이 백업과 정리 동작을 가른다.
  • Core Data는 조건 질의와 정렬이 필요한 대량 데이터의 자리다. 단순한 저장소가 아니라 객체 생애를 관리하는 계층이며, 실제 저장은 아래의 SQLite가 맡는다.
  • 토큰 같은 민감 정보는 반드시 Keychain에 둔다. 면접에서 "로그인 토큰을 어디에 저장했나"는 자주 나오고, UserDefaults라고 답하면 감점 요인이 된다.

데이터를 어디에 둘지까지 정리했으면, 이제 만든 앱을 실제 기기에 올리는 마지막 관문이 남았다. 다음 장은 명령줄로 ipa를 만들어 설치하는 배포의 원리와, 그 앞단의 패키지 관리를 다룬다.