원문: The Many Offline Options for iOS Apps

 

오프라인 모드는 더 이상 앱에 추가하도록 선택할 수 있는 추가 기능이 아닙니다. 많은 사용자가 기대하는 것입니다. 저는 개발자들이 더 나은 방법으로 해결할 수 있는 문제에 대해서 (본인들이) 선호하는 오프라인 솔루션(Core Data)을 강요하는 것을 종종 보았습니다 .

앱이 오프라인에서 작동하도록 하는 방법에는 여러 가지가 있으며 각각 장단점이 있습니다. 매번 작동하는 궁극의 해결책은 없으므로 신중하게 옵션을 평가하는 것은 사용자에게 달려 있습니다. 때로는 여러 가지 기술의 조합이 최상의 솔루션이 될 수도 있습니다!

앱을 오프라인으로 작동시키는 데 사용할 수 있는 주요 솔루션은 다음과 같습니다.

 

오프라인 모드를 구현하는 방법에는 여러 가지가 있으며 항상 앱에 적합한 도구를 선택하려고 노력해야 합니다. 그 전에 먼저 앱이 오프라인에서 작동하도록 해야 하는 이유는 무엇입니까?

 

오프라인이 왜 중요한가요?

우선 속도가 중요합니다. 사용자의 40%는 로드하는 데 3초 이상 걸리는 페이지를 포기합니다. 사람들은 앱의 반응 속도가 빠르기를 기대합니다. 로딩 스피너가 너무 오래 표시되면 앱을 종료하고 휴대전화에서 다른 많은 관심을 끄는 앱 중 하나를 엽니다 . 그러나 앱이 오프라인에서 작동하면 네트워크 연결이 좋지 않은 경우에도 빠릅니다.
둘째, Business2Community는 미국에서 언제든지 앱 사용의 15%가 오프라인 상태이며 국제 시장에서는 이 수치가 훨씬 더 높을 것으로 추정합니다. 사용자는 비행기에 있거나 연결이 불안정한 카페에 있거나 지하철에 있을 수 있으며 인터넷이 되다 안되다 할 수 있습니다. 앱이 이러한 조건에서 작동하지 않으면 앱 사용을 중지합니다.

 

캐시 vs 데이터베이스

오프라인 솔루션을 구현할 때 첫 번째 단계는 캐시 또는 데이터베이스 중 하나를 결정하는 것입니다. 데이터베이스는 강력할 수 있지만 개발자가 과도하게 사용하는 경향이 있습니다. 캐시는 더 간단하고 많은 애플리케이션에 더 적합합니다.

많은 사람들이 SQLite를 직접 사용하는 것을 선호하지만 가장 일반적인 데이터베이스 솔루션은 Core Data와 Realm 입니다. 모델을 정의하고 네트워크에서 데이터를 로드하여 데이터베이스에 삽입합니다. 그런 다음 뷰 컨트롤러는 데이터베이스에 대한 쿼리를 수신하고 데이터가 변경될 때마다 업데이트합니다.

캐시를 구현할 때 캐시와 네트워크에서 병렬로 데이터를 로드합니다. 캐시된 데이터는 구조화될 필요가 없으며 단순히 네트워크에서 데이터의 직렬화된 버전일 수 있습니다. 이것은 네트워크와 캐시에서 데이터를 로드하는 것이 코드 관점에서 동일하게 보인다는 것을 의미합니다. 캐시와 관련하여 많은 오픈 소스 구현이 있습니다. PINCache 및 NSURLCache 는 두 가지 일반적인 옵션입니다.

 

다음과 같은 경우 데이터베이스가 좋습니다.

  • 사용자를 위한 모든 데이터를 다운로드할 수 있고 너무 많은 디스크 공간을 사용하지 않고도 로컬에 저장할 수 있어야 합니다 .
  • 디바이스에서 사용되는 데이터를 제한하는 로직을 쉽게 작성할 수 있어야 합니다.
  • 수천 개의 레코드에 대해 간단한 로컬 검색을 수행할 수 있어야 합니다.

다음과 같은 경우 캐시가 좋습니다.

  • 사용자에 대한 모든 데이터를 다운로드할 수 없으며 간단한 제거(eviction) 전략이 필요합니다.
  • 네트워크에서 데이터를 다운로드하는 로직을 이미 작성했으며 애플리케이션을 다시 작성하지 않고 오프라인 모드에서 추가하려고 합니다.
  • 더 간단하고 가벼우며 유연한 솔루션을 원합니다.

 

데이터베이스의 문제

  • (오류 등을) 바로잡기가 매우 어렵기로 악명이 높습니다. 애플리케이션의 이 부분에서 충돌을 처리하는 데 상당한 개발 시간을 할애해야 합니다.
  • 모델을 작성하고 변경 사항이 있을 때마다 마이그레이션해야 합니다.
  • 공간을 확보하기 위해 모델을 삭제하는 것은 현재 화면에서 어떤 모델이 사용 중인지 알 수 없기 때문에 정말 어렵습니다.

 

데이터베이스의 단점과 남용에도 불구하고 일부 응용 프로그램의 경우 훌륭한 옵션입니다. 예를 들어 데이터베이스는 팟캐스트 플레이어에 적합합니다. 적절한 수의 예정된 에피소드를 다운로드할 수 있으며 모델을 삭제할 때(사용자가 에피소드를 완료한 후)를 쉽게 알 수 있습니다. 또 다른 예는 게임을 저장하는 것입니다. 사용자는 저장된 게임이 삭제되는 것을 절대 원하지 않을 것입니다(수동으로 삭제하지 않는 한).

반면에 소셜미디어 애플리케이션은 캐시가 훨씬 더 적합합니다. 사용자를 위해 모든 콘텐츠를 다운로드하는 것은 불가능하며 탐색할 때 데이터베이스가 점점 더 커질 것입니다. 비슷한 맥락에서 뉴스 리더 애플리케이션은 캐시를 사용하는 것이 훨씬 더 좋습니다. 사용자가 새 기사를 탐색할 때 데이터베이스는 성장을 멈추지 않을 것입니다. 캐시는 최근에 본 모든 것을 쉽게 저장하고 오래된 기사를 자동으로 제거합니다. 또한 모델을 마이그레이션하거나 데이터 오류에 대해 걱정할 필요가 없습니다 .

 

정규화된 데이터와 비정규화된 데이터

정규화 또는 비정규화 데이터를 사용하여 캐시에 데이터를 저장하는 두 가지 주요 방법이 있습니다. 대부분의 애플리케이션 데이터는 트리(tree)와 같은 구조로 시각화할 수 있습니다. 여기에는 루트 모델과 여러 하위 모델이 있습니다(더 많은 하위 모델이 있을 수 있음). 이 데이터를 캐싱할 때 전체 트리를 캐시에 하나의 항목으로 간단히 저장할 수 있습니다. 이것은 다음과 같이 보일 것입니다:

비정규화된 트리 캐시

이것은 캐시를 사용하는 가장 간단한 방법입니다. 몇 줄의 코드로 구현할 수 있으며 개체의 고유 ID를 쉽게 선택할 수 있습니다(예: 가져오는 데 사용한 URL로 각 개체를 저장할 수 있음).

그러나 Author 개체가 중첩 개체(nested object)로 데이터베이스에 두 번 삽입되었음을 알 수 있습니다. 즉, id=3인 기사를 업데이트된 Author 개체로 업데이트하면 다음에 id=42인 기사를 읽을 때 업데이트 전의 Author를 얻게 됩니다. 이러한 유형의 일관성이 중요한 경우 데이터를 캐시하기 전에 ‘정규화’하는 것을 고려할 수 있습니다. 이는 트리를 하위 모델로 분리하고 각 모델을 고유한 ID로 캐싱하는 것을 의미합니다. 예:

이것은 데이터베이스가 아님을 알아야 합니다! 각 항목은 여전히 ​​ID로 키가 지정된 구조화되지 않은 데이터의 blob(덩어리)입니다. 즉, 모델의 변경과 제거가 간단해지므로 마이그레이션이 필요하지 않습니다. 그러나 모델을 재구성하고 각 하위 모델에 대해 고유한 ID를 갖는 로직도 작성해야 하므로 이 시스템을 구현하는 것은 간단하지 않습니다.

참고: 위의 점선(포인터)을 저장하는 오버헤드 때문에 이 솔루션은 실제로는 비정규화된 캐시보다 적은 공간을 사용하지 않습니다. 자세한 내용은 ‘추가 팁’ 섹션을 참조하세요.

이전에 LinkedIn 앱에 이 전략을 사용하는 방법에 대해 작성한 적이 있으며 모델 간의 일관성을 유지하는 코드는 오픈 소스 입니다. 이 솔루션을 사용하면 새 페이지에 오프라인 동작을 쉽게 추가할 수 있었고 모델을 마이그레이션하거나 큰 데이터베이스를 정리할 필요가 없었습니다.

 

서버에서 데이터 다운로드

네트워크와 캐시 또는 데이터베이스에서 데이터를 검색하는 몇 가지 옵션이 있습니다.

가장 간단한 방법은 항상 캐시와 네트워크에서 데이터를 병렬로 검색하는 것입니다. 캐시가 먼저 반환되면 캐시된 데이터로 완성 블록(completion block)을 호출합니다. 그런 다음 네트워크가 종료되면 새 데이터로 완성 블록을 호출합니다. 이를 통해 네트워크 계층에 대부분의 캐싱 로직을 포함할 수 있으며 뷰 컨트롤러가 해야 하는 모든 작업은 데이터를 표시하는 것입니다.

그러나 이 접근 방식은 완성 블록을 두 번 호출하는 경우가 있습니다. 일부 앱의 경우 이것이 문제가 될 수 있으므로 네트워크가 느린 경우 캐시된 데이터만 사용하는 것을 고려할 수 있습니다. 네트워크가 실패하거나 로드하는 데 너무 오래 걸리는 경우에만 캐시된 데이터를 표시합니다. 이 시간 제한을 짧게 설정하는 것이 좋습니다. 오프라인일 때 리퀘스트는 즉시 오류가 발생하는 대신에 1분 후에 시간 초과되는 경우가 많습니다.

또 다른 솔루션은 뷰 컨트롤러와 네트워크 사이에 캐시 또는 데이터베이스를 배치하는 것입니다. 이 ‘반응형(reactive)’ 모델에서는 데이터베이스에서 모델을 즉시 검색하고 변경 사항을 수신합니다. 네트워크 요청이 완료되면 데이터베이스를 편집하고 뷰 컨트롤러는 변경 사항을 자동으로 업데이트합니다. 이 접근 방식은 문서화되어 있습니다 . 간단한 멘탈 모델(사물이 어떻게 작동할지 상상하는 사고 과정을 구조화한 것)이지만 모든 뷰 컨트롤러에서 모델 변경 사항을 리스닝하는것은 때때로 사소한일이 아니며 많은 보일러플레이트 코드로 이어집니다.

마지막으로 Realm Platform 또는 Firebase와 같은 데이터베이스 동기화 솔루션을 사용할 수 있습니다. 이러한 플랫폼을 사용하면 서버에서 데이터베이스 버전을 호스팅하고 변경 사항이 데이터 효율적인 방식으로 클라이언트에 자동으로 동기화됩니다. 이러한 솔루션은 많은 복잡성을 구현하지만 이 복잡성은 블랙박스에 숨겨져 있으며 서버를 구성하는 방법에 대한 사소한 요구 사항이 있을 뿐입니다.

 

데이터 업로드

사용자가 응용 프로그램에서 데이터를 변경할 때 네트워크 연결이 느린 경우 이것이 어떻게 작동해야 할 지 결정하기 어렵습니다. 가장 간단한 방법은 스피너를 표시하고 요청이 완료될 때까지 기다리는 것입니다. 필요한 경우 I를 차단할 수 있으며 배너 또는 경고로 사용자에게 문제가 있음을 알릴 수 있습니다. 그러나 이것은 좋은 사용자 경험이 아닙니다. 나는 여전히 iMessage가 오프라인일 때 메시지를 보낼 수 없다는 것을 믿을 수 없습니다.

이 문제에 대한 일반적인 솔루션은 오프라인 작업을 위한 큐(queue)를 만드는 것입니다. 연결이 느려 요청에 실패하면 디스크 기반(disk-backed) 큐에 추가하고 인터넷에 연결되면 다시 시도하는 것입니다. 이론상으로는 간단해 보이지만 실제로는 다음과 같은 경우를 고려해야 합니다.

  1. 요청을 몇 번이나 재시도해야 합니까?
  2. 요청 순서가 중요합니까?
  3. 여러 번 재시도한 후에도 요청이 끝내 실패하면 사용자에게 어떻게 알려야 합니까?
  4. 요청이 보류중이고 아직 동기화되지 않았음을 사용자에게 알리려면 어떻게 해야 합니까?

무엇보다 업로드가 실패하면 데이터를 되돌려야(revert) 합니다. 다음 흐름을 고려하십시오.

  1. 사용자의 폰(phone)이 오프라인일 때 어떤 게시물에 좋아요(like)를 했습니다.
  2. 사용자가 랩탑(laptop)이 온라인일 때 위와 같은 게시물에 ‘좋아요’를 했습니다.
  3. 폰이 인터넷에 연결되고 이 항목에 대한 새 데이터(좋아요 표시됨 여부)를 다운로드합니다.
  4. 다음으로, 폰에 이것을 업로드하는 것은 어떤 이유로 실패합니다.
  5. 폰은 항목을 더 이상 좋아요 되지 않은 상태로 되돌립니다.

 

폰이 어떻게 잘못된 상태로 끝나는지 주목하십시오! 설상가상으로 폰은 자신이 올바른 상태가 아니라는 것을 모르기 때문에 네트워크에서 이를 새로 고치지 못할 수 있습니다. 데이터를 되돌릴 때 단순한 반대 작업을 수행할 수 없습니다. 서버가 옳았다고 마지막으로 말한 것으로 되돌려야 합니다. 이것은 이제 현재 상태와 마지막으로 관찰된 서버 상태를 모두 저장해야 함을 의미합니다.

이러한 문제는 여전히 해결할 수 있습니다. 내가 Superhuman에 있을 때 우리는 이러한 모든 경우를 해결하는 ‘modifier queue’이라는 아키텍처를 구축했습니다. 이에 대한 자세한 내용은 Superhuman 블로그에서 읽을 수 있습니다.

앱의 경우 modifier queue를 구현하는 데까지 갈 필요가 없습니다. 데이터를 업로드하는 것은 어렵습니다. 어떤 사용 사례를 가장 중요하게 생각하고 좋은 경험이 될 것인지에 집중해야 합니다.

 

일관성

캐시 또는 데이터베이스를 로컬에서 편집한 후에는 앱에서 여러 화면을 업데이트해야 할 수 있습니다. 이것은 이 게시물의 범위를 훨씬 벗어나는 심오한 주제이지만 고려해야 할 몇 가지 기술은 다음과 같습니다.

  • 데이터베이스 변경 사항 청취
    대부분의 데이터베이스는 모델의 변경 사항을 리스닝할 수 있는 기능을 제공합니다. 캐시 대신 데이터베이스를 사용하는 경우 이것이 가장 간단한 솔루션일 수 있습니다.
  • 위임(delegation)
    변경 사항이 관계 있는 앱의 몇 가지 뷰 컨트롤러에만 영향을 미치는 경우 대리자(delegator)를 사용하여 변경 사항을 전파할 수 있습니다. 변경 사항이 다른 곳에 나타나지 않는 경우에만 이것을 사용할 수 있지만 이것은 매우 간단한 솔루션이며 제대로 작동한다고 해도 간과해서는 안 됩니다.
  • 이벤트 버스(event bus)
    캐시에 변경 사항을 구현하는 가장 간단한 방법 중 하나는 이벤트 버스 시스템을 사용하여 모든 수신기에 변경 사항을 알리는 것입니다. 특정 ID에 대한 변경 사항을 듣고 변경되면 캐시에서 데이터를 다시 로드하고 보기를 새로 고칩니다. NSNotificationCenter은 기본 솔루션이지만 더 나은 API와 유형 안전 알림을 제공하는 일부 오픈 소스 프로젝트를 살펴보는 것이 좋습니다. SwiftNotificationCenterSwiftEventBus가 두 가지 인기 있는 예입니다.
  • Rocket Data
    Rocket Data는 불변(immutable) 모델의 일관성을 관리하며 특별히 캐시와 잘 작동하도록 설계되었습니다. 단순히 특정 ID의 변경 사항을 리스닝하고 모델이 변경되면 알려줍니다. 이벤트 버스보다 더 강력한 솔루션이며 불변 모델을 사용할 수 있지만 설정하는 데 더 많은 작업이 필요합니다. 자세한 내용은 Rocket Data에 대한 나의 강연 또는 문서를 참조하십시오.

 

기술 결합

최근에 Chess Tactics App에 오프라인 모드를 추가했습니다. 오프라인 사용에 대한 세 가지 사용 사례가 있었는데 각각 요구 사항이 약간 다릅니다.

  • 사용자는 최근에 방문한 적이 있는 경우 앱의 모든 페이지를 오프라인으로 볼 수 있어야 합니다.
  • 사용자는 퍼즐을 명시적으로 저장하여 오프라인에서 볼 수 있어야 합니다. 이것들은 절대 제거해서는 안됩니다.
  • 앱의 주요 기능은 사용자의 기술 수준에 따라 선별된 무작위 퍼즐을 사용자에게 제공합니다. 오프라인에서 작동해야 합니다.

하나의 아키텍처를 선택하고 이러한 모든 사용 사례를 적용하는 대신 세 가지 다른 오프라인 기술을 선택하기로 결정했습니다!

임의의 페이지를 보기 위해 요청에 대한 URL을 입력 데이터로 하는 키-값 저장소 캐시를 추가했습니다. 내 요청에 하위 모델이 많지 않고 하위 모델 일관성이 내 애플리케이션에 중요하지 않기 때문에 비정규화된 캐시를 선택했습니다. 이미 전체 앱을 코딩했지만 이를 추가하는 것은 간단하고 몇 시간 밖에 걸리지 않았습니다.

퍼즐을 저장하기 위해 구조화된 데이터베이스를 사용했습니다. 나는 퍼즐들이 제거되는 것을 결코 원하지 않았고 개별 퍼즐이 몇 KB에 불과하기 때문에 사용자가 과도한 데이터를 저장할 일이 없습니다. 구조화된 데이터베이스는 또한 미래에 이러한 퍼즐을 정렬하거나 필터링할 수 있는 유연성을 허용합니다.

사용자에게 무작위 퍼즐을 제시하기 위해 데이터베이스도 사용했지만 구조화된 데이터베이스를 사용하는 대신 단순히 NSData 블롭을 사용하여 JSON을 저장했습니다. 백그라운드에서 앱은 약 200개의 퍼즐을 다운로드하여 저장합니다. 사용자가 오프라인 상태인 경우 앱은 무작위로 하나(캐시로는 어려운 작업)를 선택하여 사용자에게 제시한 후 삭제합니다. 데이터베이스를 사용하면 크기를 쉽게 제어할 수 있으므로 내 요구 사항에 맞습니다. 별도의 필드를 사용하는 대신 데이터를 blob으로 저장하면 코드를 단순화하고 추가 파싱 로직을 피할 수 있습니다. 알려진 것과 달리, 데이터를 블롭으로 저장하는 것은 블롭이 작은 한 데이터베이스에서 매우 빠릅니다 .

오프라인에서 무작위 퍼즐을 선택할 때 내 뷰 컨트롤러는 데이터베이스 모델을 직접 사용하지 않았습니다. 대신 데이터 blob을 뷰 컨트롤러에 전달하기 전에 메모리 모델로 파싱했습니다. 이렇게 하면 내 뷰 컨트롤러가 디스크 스토리지 로직과 완전히 분리되었고 언제든지 이 모델을 안전하게 삭제할 수 있었습니다.

데이터 업로드를 위해 사용자 평가 변경 사항을 업로드하는 간단한 대기열을 구현했습니다. 앱은 읽기가 많기 때문에 다른 POST 요청을 구현하는 것은 가치가 없다고 판단했습니다. 앱은 단순히 ‘온라인 상태일 때 평가가 업데이트됩니다’라는 메시지를 표시합니다.

여기서 요점은 모든 기능이 동일한 오프라인 솔루션을 사용하도록 강제할 필요가 없다는 것입니다. 다양한 기술을 결합하여 더 나은 사용자 경험을 제공하고 개발 시간을 단축할 수 있습니다.

 

결론

다른 것은 아니지만 앱을 오프라인에서 작동시키기 위한 다양한 솔루션이 있음을 기억하시기 바랍니다. Core Data 구현이 익숙하기 때문에 즉시 Core Data 구현에 대해 머리를 맞대고 시작하지 마십시오. 최상의 솔루션을 선택하기 전에 해결하려는 문제에 대해 열심히 생각하십시오.

많은 앱의 경우 캐시가 더 간단한 솔루션이기 때문에 더 나은 선택입니다. 제거를 위한 쉬운 방법을 제공하고 모델 마이그레이션이 필요하지 않으며 항상 충돌하지 않습니다.

선택한 기술에 관계없이 오프라인 전략의 일부로 변경 사항을 업로드하고 앱의 일관성을 유지하는 방법도 고려해야 합니다. 마지막으로, 하나의 기술에만 자신을 가두어서는 안 됩니다. 종종 서로 다른 기술의 조합이 최상의 솔루션이 될 것입니다.


문의 | 코멘트 또는 yoonbumtae@gmail.com  donaricano-btn

카테고리: Swift

답글 남기기

이메일 주소를 발행하지 않을 것입니다. 필수 항목은 *(으)로 표시합니다