[Trouble Shooting] realm 파일을 프로젝트 내부로 가져와서 Initial Data로 설정하기

 

Realm은 iOS 앱을 만들 때, 데이터베이스로 CoreData에 비해 쉽게 사용할 수 있는 라이브러리이다. 안드로이드를 비롯해 여러 플랫폼에서 지원하기에 호환성이 좋아서 많은 서비스에서 Realm을 사용한다. 그런데 realm에 복잡한 데이터가 앱을 설치한 처음부터 있었으면 하는 개발자도 분명 있을 것이다. 하지만 이에 관련된 자료가 없는 건지, 내가 못 찾는 건지 찾을 수가 없었다. 그래서 방법을 고민하다가 내가 찾은 방법을 공유해보려고 한다. (+ 에러 상황과 해결 방안까지)

 

 

 

0. Realm 데이터 준비하기

 단순한 데이터를 Initial Data로 추가할 때에는 사실 Realm 데이터를 준비할 필요 없이, 코드로 저장을 하면 된다. 그러나 앱에서 저장하는 기능이 많으면 많을수록, 다양한 기능을 사용하면 할수록 Realm 데이터 역시 복잡해지고, 하드코딩하기엔 힘들고 귀찮아진다. 이때 본인의 앱 기능들을 활용해서 만든 Realm 데이터를 활용해서 Initial Data를 만들 수 있다.

 

 

  1. 시뮬레이터에서 데이터 가져오기

//realm이 사용되는 곳에서
print(realm.configuration.fileURL)

 우선 위의 코드를 작성하면 

 콘솔 창에 realm파일이 저장된 URL이 나온다.

 

그 후 file부터 Documents까지 마우스 드레그를 한 후 마우스 오른쪽 버튼을 눌러 위의 사진과 같이 'URL 열기'를 누른다.

그럼 파일들이 보이는데, 여기서 default.realm이 지금 시뮬레이터에 저장된 realm 데이터이다. 후에 이 default.realm 파일을 사용하면 된다.

 

 

 

  2. 실기기에서 데이터 가져오기

 

 실기기에서 데이터를 가져오려면, 실기기가 연결된 상태에서 혹은 실기기에 빌드를 시킨 상태에서

 

 

Window - Devices and Simulators에 들어간다.

 

그후 연결된 Device를 선택한 후

 

데이터를 가져올 앱을 선택하고 Download Container를 누른다. 그리고 원하는 위치에 저장하자.

저장된 파일을 마우스 오른쪽 버튼을 눌러 클릭한 후 '패키지 내용 보기'를 누르자.

 

그럼 Documents 폴더 안에 realm 파일이 있는 것을 볼 수 있다. 이 파일을 사용할 예정이다.

 

 

1. Bundle에 추가하기

 

 

 

아까 찾은 데이터 파일을 Xcode 프로젝트로 끌어오자. 그리고 꼭 Add to targets를 클릭하고 Finish를 해주자.(그래야 번들로 들어가게 된다.)

 

2. Bundle.main으로 데이터 읽어오기

 

 우선 코드로 데이터를 Read 하기 전에 앞서 추가한 파일의 이름을 구별하기 쉽게 Initial로 바꾸었다!

 

 혹시 Bundle에 추가가 되어 있지 않는지 확인하기 위해서 Targets - Build Phases - Copy Bundle Resources로 이동해서 데이터 realm 파일이 있는지 확인해 보자. (있어야 한다!)

 

그 후에 코드로 Bundle에 있는 데이터를 읽어오면 된다.

 

    func fetchInitialData() {
    	let realm = try! Realm()
        let initialRealmURL = Bundle.main.url(forResource: "initial", withExtension: "realm")
        
        let config = Realm.Configuration(fileURL: initialRealmURL, readOnly: false)

        do {
            let initialRealm = try Realm(configuration: config)
            try realm.write {
            // yourRealmModel 사용자의 realm 데이터 모델
                for object in initialRealm.objects(yourRealmModel.self) {
                // Realm을 여러개 사용할 때는 .add를 사용하면 런타임 에러가 발생한다!
                    realm.create(yourRealmModel.self, value: object, update: .modified)
                }
            }
        } catch let error as NSError {
            print("Error: \(error.localizedDescription)")
        }
    }

 

위의 코드는 기존에 추가한 Realm 파일을 관리하는 객체와 local의 기본 realm 객체를 둘 다 불러서 사용한다.

initialRealm으로 기존 데이터를 불러와서, 로컬의 기본 realm 객체에 데이터를 써주는 방식으로 작동하는 코드이다. (해결하고 나니 생각보다 간단하다!)

 

위 메서드를 앱에 따라서, 상황에 따라서 적절한 곳에서 호출해 주면 초기 데이터를 사용할 수 있다.

 

 

3. 에러 발생

 

위의 방법을 이용해 시뮬레이터로 실행하면 작동이 잘된다.

그러나 실기기로 빌드를 해보면 

콘솔 창에서 이런 문구를 보여주며 초기 데이터가 실행되지 않는 것을 볼 수 있다.

(만약 local에 생성되는 기본 realm을 쓰지 않고, 위에서 따로 설정한 initialRealm만 사용한다면 realm 파일에 접근할 수 없기 때문에 런타임 에러가 발생해서 앱이 꺼지게 된다.)

 

 

이유는??

 

 앱의 보안 및 데이터 무결성 유지를 위해서 번들에 접근하는 것은 허용되지 않는다고 한다. 즉 번들은 앱의 private 영역으로 간주되며 앱 외부에서 직접적인 접근 및 수정이 허용되지 않는 것이다.  

 

 그렇기에 앱 번들에 있는 Realm 파일은 읽기 전용이며, 앱 실행 중에는 수정할 수 없는 것이다.

 

 

 

4. 해결 방법

 

 그럼 접근도 안되는데 어떻게 해결할 수 있을까? 바로 realm 파일을 documentDirectory나 다른 저장 경로로 복사해서, 복사한 파일을 사용해 realm 인스턴스를 사용하면 된다.

 

   func copyInitialRealm() {
   	let fileManager = FileManager.default
        let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
        let fileURL = documentDirectory.appendingPathComponent("InitialData.realm")
        
        if !fileManager.fileExists(atPath: fileURL.path) {
            let bundleURL = Bundle.main.url(forResource: "initial", withExtension: "realm")!
            
            do {
                try fileManager.copyItem(at: bundleURL, to: fileURL)
            } catch {
                print("Error copy file: \(error)")
            }
        }
    }

 

 우선 documentDirectory에 이니셜 데이터를 카피한 realm 파일이 있는지 없는지 확인하고, 만약 없다면 파일을 복사해 주는 메서드를 만들어준다.

 

    func fetchInitialData() {
       	let fileManager = FileManager.default
        let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
        let fileURL = documentDirectory.appendingPathComponent("InitialData.realm")

        do {
            let initialRealm = try Realm(fileURL: fileURL)
            try realm.write {
                for object in initialRealm.objects(yourRealmModel.self) {
                    realm.create(yourRealmModel.self, value: object, update: .modified)
                }
            }
        } catch let error as NSError {
            print("Error: \(error.localizedDescription)")
        }
    }

 

 그리고 복사한 파일을 찾아서 가져와서 local realm에 write 해주면 된다.

 

물론 적절한 시기에 이 메서드들을 호출해 주면 된다!