React Native에서의 오프라인 동기화 및 데이터 저장

들어가기 전에

스마트폰 사용자는 언제 어디서나 인터넷에 연결이 되어있습니다. 그러나 실제로는 Wi-Fi 또는 이동 통신망에 항상 연결되어 있지 않을 수 있습니다. 이때 모바일 앱이 오프라인에서도 문제 없이 작동해야 사용자 경험이 중단되지 않습니다. 이를 위해 오프라인 동기화와 데이터 저장이 중요한 역할을 하게 됩니다.

오프라인 상황에서도 앱의 주요 기능이 손상되지 않도록 로컬 데이터베이스에 정보를 저장하고 관리하는 것은 사용자 경험을 크게 향상시킬 수 있는 핵심 요소입니다. 사용자가 다시 온라인 상태가 되면, 로컬에서 저장된 데이터를 서버와 동기화해야 합니다. 이 과정은 사용자에게 원활한 경험을 제공하며, 데이터의 무결성을 보장합니다.


로컬 데이터베이스: Realm

Realm의 특징 및 사용 이유

Realm은 모바일 앱 개발을 위한 객체 중심의 데이터베이스입니다. Realm의 주요 특징은 성능, 간편함, 실시간 동기화 등이 있습니다.

  • 성능: Realm은 고성능 쿼리 및 쓰기를 제공하며, 기존의 SQLite보다 더 빠른 처리 속도를 자랑합니다.
  • 간편함: 객체 중심의 설계로 인해, 개발자는 간단한 코드로 복잡한 데이터 관계를 관리할 수 있습니다.
  • 실시간 동기화: Realm은 실시간으로 데이터를 동기화할 수 있는 기능을 제공, 앱과 서버 간의 데이터 일관성 유지가 수월합니다.

기타 로컬 데이터베이스 비교 (SQLite 등)

SQLite는 가장 널리 사용되는 임베디드 SQL 데이터베이스 중 하나입니다. Realm과 비교할 때 SQLite는 더 많은 플랫폼 지원과 커뮤니티 지원을 제공합니다. 그러나 Realm은 객체 중심의 설계와 더 나은 성능을 제공하므로 특정 사용 사례에서 더 효과적일 수 있습니다.

종합적으로 볼 때, Realm은 모바일 앱에서 오프라인 동기화와 데이터 저장을 용이하게 만들며, 특히 React Native와 같은 크로스 플랫폼 개발 환경에서 강력한 선택지가 될 수 있습니다.


Realm 설치 및 설정

설치 방법

Realm을 사용하려면 먼저 필요한 패키지를 설치해야 합니다. 다음과 같은 명령어로 설치할 수 있습니다.

npm install realm

초기 설정과 데이터베이스 스키마 정의

Realm을 사용하기 전에 데이터베이스 스키마를 정의해야 합니다. 스키마는 데이터베이스에 저장될 객체의 구조를 정의합니다.

const PersonSchema = {
  name: 'Person',
  properties: {
    name: 'string',
    age: 'int',
  }
};

const realm = new Realm({schema: [PersonSchema]});

데이터 생성, 조회, 수정, 삭제 (CRUD) 작업

Realm은 CRUD 작업을 수행하기 위한 간단한 API를 제공합니다.

  • 생성: 객체를 생성하려면 realm.write() 내에서 realm.create()를 호출합니다.
  • 조회: realm.objects()를 사용하여 객체를 조회합니다.
  • 수정: realm.write() 내에서 객체 속성을 직접 변경합니다.
  • 삭제: realm.delete()를 사용하여 객체를 삭제합니다.

예제 코드를 통한 CRUD 설명

// 생성
realm.write(() => {
  realm.create('Person', {name: 'John', age: 29});
});

// 조회
let persons = realm.objects('Person');
console.log(persons[0].name); // 'John'

// 수정
realm.write(() => {
  persons[0].name = 'Mike';
});

// 삭제
realm.write(() => {
  realm.delete(persons[0]);
});

비동기 작업 처리

Realm은 비동기 작업을 지원하며, 이를 활용하면 앱의 반응성을 향상시킬 수 있습니다.

const person = await realm.write(async () => {
  return realm.create('Person', {name: 'Sara', age: 24});
});

데이터 생성, 조회, 수정, 삭제 (CRUD) 작업

생성 (Create)

// Create a new realm instance
const realm = new Realm();

// Define a person object
const PersonSchema = {
  name: 'Person',
  properties: {
    _id: 'int',
    name: 'string',
    age: 'int',
  },
};

realm.write(() => {
  // Create a new person object
  const newPerson = realm.create('Person', {
    _id: 1,
    name: 'Alice',
    age: 30,
  });

  console.log(`Created new person: ${newPerson.name} who is ${newPerson.age}`);
});

위 코드는 새로운 Person 객체를 생성합니다. realm.create 함수는 스키마 이름과 객체를 인수로 받습니다.

조회 (Read)

// Query the realm for all objects of the "Person" type
const persons = realm.objects('Person');
console.log(`Total number of persons: ${persons.length}`);

// Filter for persons older than 25
const olderThan25 = persons.filtered('age > 25');
console.log(`Number of persons older than 25: ${olderThan25.length}`);

realm.objects 함수는 주어진 스키마의 모든 객체를 반환합니다. filtered 함수를 사용하여 원하는 조건에 맞는 객체를 필터링할 수 있습니다.

수정 (Update)

realm.write(() => {
  // Get the first person
  const firstPerson = persons[0];

  // Update the age
  firstPerson.age = 35;

  console.log(`Updated the age of ${firstPerson.name} to ${firstPerson.age}`);
});

객체를 수정하려면 먼저 객체를 조회한 후 해당 객체의 속성을 변경해야 합니다. 모든 변경은 realm.write 내부에서 이루어져야 합니다.

삭제 (Delete)

realm.write(() => {
  // Get the first person
  const firstPerson = persons[0];

  // Delete the first person
  realm.delete(firstPerson);

  console.log(`Deleted ${firstPerson.name}`);
});

객체를 삭제하려면 realm.delete 함수를 사용합니다. 이 함수는 삭제할 객체를 인수로 받습니다.

비동기 작업 처리

// Async function to fetch age from an API
async function fetchNewAge() {
  const response = await fetch('https://api.example.com/age');
  const age = await response.json();
  return age;
}

realm.write(async () => {
  // Create a new person object
  const newPerson = realm.create('Person', {
    _id: 2,
    name: 'Bob',
  });

  // Fetch new age and update
  newPerson.age = await fetchNewAge();

  console.log(`Created new person: ${newPerson.name} who is ${newPerson.age}`);
});

realm.write는 비동기 함수를 처리할 수 있습니다. 위 예시에서는 외부 API에서 새로운 나이를 가져와서 객체의 나이를 업데이트하는 과정을 비동기적으로 처리합니다. 이러한 방식을 통해 네트워크 요청과 같은 비동기 작업을 쉽게 통합할 수 있습니다.

다른 예제를 하나 더 살펴보겠습니다.

다음 예제는 상품과 주문의 관계를 나타내는 데이터베이스를 다룹니다.

스키마 정의

const ProductSchema = {
  name: 'Product',
  properties: {
    _id: 'int',
    name: 'string',
    price: 'double',
  },
};

const OrderSchema = {
  name: 'Order',
  properties: {
    _id: 'int',
    products: 'Product[]', // Array of products
    totalAmount: 'double',
  },
};

const realm = new Realm({ schema: [ProductSchema, OrderSchema] });

여기에서 Product 스키마와 Order 스키마를 정의하고 있다. Order 스키마는 Product의 배열을 포함하며, totalAmount로 주문 총액을 계산합니다.

상품 생성

realm.write(() => {
  const newProduct = realm.create('Product', {
    _id: 1,
    name: 'Laptop',
    price: 1000.0,
  });

  console.log(`Created new product: ${newProduct.name}`);
});

주문 생성과 상품 추가

realm.write(() => {
  const order = realm.create('Order', {
    _id: 1,
    products: [],
    totalAmount: 0,
  });

  const product = realm.objects('Product').filtered('_id == 1')[0];

  order.products.push(product); // Add product to the order
  order.totalAmount += product.price; // Update the total amount

  console.log(`Added product to order. Total amount: ${order.totalAmount}`);
});

주문 수정: 상품 제거

realm.write(() => {
  const order = realm.objects('Order').filtered('_id == 1')[0];
  const productToRemove = order.products[0];

  order.totalAmount -= productToRemove.price; // Update the total amount
  order.products.splice(0, 1); // Remove the first product from the order

  console.log(`Removed product from order. Total amount: ${order.totalAmount}`);
});

주문 조회: 상품 목록

const order = realm.objects('Order').filtered('_id == 1')[0];
console.log(`Order total amount: ${order.totalAmount}`);
order.products.forEach(product => {
  console.log(`Product: ${product.name}, Price: ${product.price}`);
});

이렇게 상품과 주문 간의 관계를 나타내는 더 복잡한 예제에서도 Realm을 사용하여 간편하게 CRUD 작업을 수행할 수 있습니다.


오프라인에서의 동작 전략

네트워크 연결 감지

먼저, 네트워크 연결 상태를 실시간으로 감지해야 합니다. React Native에서는 NetInfo 라이브러리를 사용하여 이를 수행할 수 있습니다.

import NetInfo from "@react-native-community/netinfo";

NetInfo.addEventListener(state => {
  console.log("Connection type:", state.type);
  console.log("Is connected?", state.isConnected);
});

2. 로컬 데이터와 서버 데이터 동기화 전략

네트워크가 끊겼을 때 변경된 데이터를 로컬에 저장하고, 연결이 복원되면 서버와 동기화해야 합니다. 다음은 동기화 전략의 기본 구조입니다.

  • 로컬에서 변경 감지: 사용자가 데이터를 변경할 때마다 로컬 데이터베이스에 변경 내용을 저장합니다.
  • 네트워크 연결 복원 감지: 네트워크 연결이 복원되면 변경된 내용을 서버에 전송합니다.
  • 서버와 동기화: 서버와 로컬 데이터베이스를 동기화합니다.
예제: 로컬 데이터 저장 및 동기화
// 변경 내용을 로컬에 저장
const saveToLocal = (changes) => {
  realm.write(() => {
    // Save changes to local database
  });
};

// 네트워크 연결 복원 시 서버와 동기화
NetInfo.addEventListener(state => {
  if (state.isConnected) {
    const changes = getChangesFromLocal();
    syncWithServer(changes);
  }
});

// 서버와 동기화 함수
const syncWithServer = (changes) => {
  // Send changes to the server
  // Update local database with server response if needed
};

이런 전략을 통해 사용자는 네트워크 연결 상태와 무관하게 앱을 사용할 수 있으며, 연결이 복원되면 자동으로 서버와 동기화되어 데이터 일관성을 유지할 수 있습니다.


데이터 동기화 구현

데이터 동기화는 로컬과 원격 데이터베이스 간의 일관성을 유지하기 위한 중요한 작업입니다. 오프라인 환경에서 작업 후 네트워크 연결이 복원되면 변경 사항을 원격 데이터베이스와 동기화해야 합니다. 이 과정에서는 동기화 로직 설계와 충돌 해결 전략이 필요합니다.

동기화 로직 설계

데이터 동기화 로직은 다음과 같은 단계로 구성될 수 있습니다.

  • 로컬 변경 감지: 앱이 오프라인 상태에서 변경된 데이터를 로컬에 저장합니다.
  • 서버와의 동기화: 네트워크가 연결되면 로컬 변경 사항을 서버와 동기화합니다.

동기화 과정의 예제 코드는 다음과 같습니다.

// 변경 내용 로컬에 저장
function saveChangesToLocal(changes) {
  // 로컬 데이터베이스에 변경 사항 저장
}

// 네트워크 연결 시 서버와 동기화
function synchronizeWithServer() {
  const changes = getLocalChanges();
  sendChangesToServer(changes).then(response => {
    // 서버 응답 처리
  });
}

충돌 해결 전략

여러 사용자가 동시에 같은 데이터를 변경하거나, 사용자가 오프라인 상태에서 데이터를 변경한 후 다시 온라인 상태가 되었을 때 데이터 충돌이 발생할 수 있습니다. 이런 충돌을 해결하기 위한 전략이 필요하며, 일반적인 방법은 아래와 같습니다.

  • 클라이언트 우선: 클라이언트의 변경 사항이 서버의 데이터를 덮어쓰는 방식입니다. 이 방식은 사용자 경험을 우선시하지만, 데이터 일관성 문제가 발생할 수 있습니다.
  • 서버 우선: 서버의 데이터가 항상 우선적으로 적용되는 방식입니다. 이 방식은 데이터 일관성을 유지하되 사용자 경험을 저하시킬 수 있습니다.
  • 자동 병합: 변경된 필드가 겹치지 않을 경우 자동으로 병합하는 방식입니다.
  • 수동 충돌 해결: 충돌이 감지되면 사용자에게 어떤 데이터를 유지할지 선택하게 하는 방식입니다.
function resolveConflict(clientData, serverData) {
  // 충돌 해결 로직 (예: 자동 병합, 수동 충돌 해결 등)
}

데이터 동기화 구현은 앱의 복잡도와 사용자 경험, 데이터 일관성 등을 고려해야 하므로 신중한 설계와 구현이 필요합니다.


테스팅: 로컬 데이터베이스와 동기화 코드 테스트

로컬 데이터베이스와 동기화 코드의 테스트는 앱의 정확성과 신뢰성을 확보하는 데 중요한 단계입니다. 아래에서는 테스팅 라이브러리와 방법, 테스트 케이스 작성 예시를 소개하겠습니다.

테스팅 라이브러리와 방법

JavaScript에서는 Jest, Mocha, Jasmine 등의 테스팅 라이브러리를 사용할 수 있습니다. 예제에서는 Jest를 사용하겠습니다.

  1. Jest 설치: 테스트 환경을 설정하기 위해 Jest를 설치합니다.
   npm install --save-dev jest
  1. 테스트 스크립트 설정: package.json에 테스트 스크립트를 추가합니다.
   "scripts": {
     "test": "jest"
   }

테스트 케이스 작성 예시

동기화 코드의 테스트 케이스를 작성하는 예시입니다.

  1. 로컬 변경 감지 테스트: 로컬 변경이 올바르게 감지되는지 테스트합니다.
   test('should detect local changes', () => {
     const changes = detectLocalChanges();
     expect(changes).toEqual(expectedChanges);
   });
  1. 서버와의 동기화 테스트: 서버와의 동기화가 성공적으로 이루어지는지 테스트합니다.
   test('should synchronize with server', async () => {
     const response = await synchronizeWithServer();
     expect(response.status).toBe(200);
   });
  1. 충돌 해결 전략 테스트: 충돌 해결 로직이 올바르게 작동하는지 테스트합니다.
   test('should resolve conflicts', () => {
     const resolvedData = resolveConflict(clientData, serverData);
     expect(resolvedData).toEqual(expectedData);
   });

테스팅은 데이터 동기화의 복잡한 로직을 안정적으로 작동시키는 데 필수적인 단계입니다. 코드의 변경이나 확장 시에도 테스팅을 통해 기능이 올바르게 작동하는지 확인하면서 개발할 수 있습니다. 이는 유지보수성을 향상시키고, 오류를 줄여, 사용자 경험을 향상시키는 중요한 역할을 합니다.


성능 최적화 및 보안 고려사항

모바일 앱에서 로컬 데이터베이스를 사용하고, 데이터 동기화를 처리하는 과정에서 성능과 보안은 매우 중요한 부분입니다. 이 섹션에서는 쿼리 성능 최적화와 사용자 데이터 보안에 대한 주요 고려사항을 설명하겠습니다.

쿼리 성능 최적화

로컬 데이터베이스에서의 쿼리 성능은 사용자 경험에 큰 영향을 미칩니다. 반응성이 높고 빠른 앱을 제공하기 위해 쿼리 성능을 최적화해야 합니다.

  1. 인덱싱(Indexing): 자주 조회되는 열(column)에 인덱스를 생성하여 조회 성능을 향상시킬 수 있습니다.
  2. 비동기 처리: 데이터베이스 작업을 비동기로 처리하여 UI 스레드의 차단을 최소화합니다.
  3. 캐싱: 자주 사용되는 데이터나 쿼리 결과를 캐시하여 재활용하면 응답 시간을 단축시킬 수 있습니다.
  4. 불필요한 쿼리 최소화: 불필요한 쿼리를 최소화하거나, 효율적인 쿼리문을 작성함으로써 데이터베이스의 부하를 줄입니다.

사용자 데이터 보안

데이터 보안은 사용자의 정보를 안전하게 보호하는 데 중요한 역할을 합니다.

  1. 데이터 암호화: 중요한 사용자 데이터는 로컬 데이터베이스에 저장할 때 암호화하여 저장합니다.
  2. 액세스 제어: 앱 내에서 데이터베이스에 접근할 수 있는 부분을 제한하며, 필요한 권한만 부여합니다.
  3. 인증 및 인가: 동기화 과정에서 서버와의 통신은 인증 및 인가 과정을 통해 이루어져야 하며, 이를 통해 부정 접근을 차단합니다.
  4. 데이터 노출 최소화: 필요한 데이터만 노출하고, 불필요한 정보는 숨기는 원칙을 적용합니다.

쿼리의 성능 최적화와 보안은 앱의 전반적인 품질과 사용자 경험을 크게 향상시킬 수 있습니다. 따라서 설계와 개발 과정에서 이러한 부분을 철저히 고려해야 하며, 지속적인 테스팅과 모니터링을 통해 이를 유지하고 개선하는 작업이 필요합니다.


실전 예제: 오프라인 지원 모바일 앱 개발

이 실전 예제에서는 오프라인 지원을 위한 모바일 앱을 개발하는 과정을 단계별로 안내합니다. 예제 프로젝트는 사용자가 메모를 작성하고, 오프라인에서도 조회 및 수정이 가능한 메모 앱을 만들어 보겠습니다.

프로젝트 요구사항 및 설계

요구사항:

  1. 메모 작성, 수정, 삭제 기능
  2. 오프라인에서 메모 작성 및 조회 가능
  3. 온라인 상태에서 로컬 데이터와 서버 데이터 동기화

설계:

  1. 로컬 데이터베이스로 Realm 사용
  2. RESTful API를 통한 서버와의 데이터 동기화
  3. React Native로 UI 구현

단계별 구현 가이드 및 코드 설명

1. Realm 설치 및 설정

먼저, Realm을 설치하고 초기 설정을 합니다.

npm install realm

데이터베이스 스키마를 정의하고 초기화합니다.

const MemoSchema = {
  name: 'Memo',
  primaryKey: 'id',
  properties: {
    id: 'string',
    title: 'string',
    content: 'string',
    date: 'date',
  },
};

const realm = new Realm({schema: [MemoSchema]});
2. CRUD 작업
– 메모 생성 (Create)

새 메모를 작성하고 로컬 데이터베이스에 저장합니다.

const createMemo = (title, content) => {
  realm.write(() => {
    realm.create('Memo', {
      id: generateId(),
      title,
      content,
      date: new Date(),
    });
  });
};
– 메모 조회 (Read)

저장된 메모를 로컬 데이터베이스에서 조회합니다.

const readMemos = () => {
  return realm.objects('Memo').sorted('date', true);
};
– 메모 수정 (Update)

선택한 메모를 수정합니다.

const updateMemo = (id, title, content) => {
  realm.write(() => {
    realm.create('Memo', {id, title, content}, 'modified');
  });
};
– 메모 삭제 (Delete)

선택한 메모를 삭제합니다.

const deleteMemo = (id) => {
  realm.write(() => {
    const memo = realm.objectForPrimaryKey('Memo', id);
    realm.delete(memo);
  });
};
3. 오프라인에서의 동작 및 동기화 전략

네트워크 연결 여부를 확인하고, 온라인 상태일 때만 서버와 데이터 동기화를 수행합니다.

const syncWithServer = () => {
  if (isOnline()) {
    // 서버와의 동기화 로직
  }
};

이 예제 프로젝트는 오프라인에서도 메모의 CRUD 작업을 지원하며, 온라인 상태에서는 로컬 데이터와 서버 데이터를 동기화하는 기능을 제공합니다. Realm을 활용한 로컬 데이터베이스 관리와 React Native를 통한 UI 구현을 통해 사용자 친화적인 앱을 만들 수 있습니다.

Leave a Comment