분류 Reactjs

WatermelonDB를 사용하여 오프라인 우선 React 네이티브 앱 만들기

컨텐츠 정보

  • 조회 455 (작성일 )

본문

React Native는 모바일 앱 목적에 따라 다른 데이터베이스 스토리지 메커니즘을 가지고 있습니다. 

사용자 설정, 앱 설정 및 기타 키-값 쌍 데이터와 같은 간단한 구조는 비동기 저장소 또는 보안 저장소를 사용하여 쉽게 처리 할 수 ​​있습니다.


https://www.sitepoint.com/create-an-offline-first-react-native-app-using-watermelondb/ 


Twitter 클론과 같은 다른 응용 프로그램은 서버에서 데이터를 가져 와서 사용자에게 직접 보여줍니다. 

데이터 캐시를 유지 관리하며 사용자가 문서와 상호 작용해야 하는 경우 API를 직접 호출합니다.


따라서 모든 응용 프로그램에 데이터베이스가 필요한 것은 아닙니다.


처음부터 React Native를 배우고 싶습니까? 이 기사는 프리미엄 라이브러리에서 발췌 한 것입니다. 

SitePoint Premium을 사용하여 기본 사항, 프로젝트, 팁 및 도구 등을 다루는 React Native 서적 전체를 얻으십시오. 월 $ 9에 지금 가입하십시오.


데이터베이스가 필요할 때 


Nozbe (To-do 앱), Expense (추적기) 및 SplitWise (앱 내 구매 용)와 같은 애플리케이션은 오프라인에서 작동해야 합니다. 

이를 위해서는 로컬에 데이터를 저장하고 서버와 동기화 하는 방법이 필요합니다. 이 유형의 응용 프로그램을 오프라인 첫 번째 응용 프로그램이라고합니다. 

시간이 지남에 따라 이러한 앱은 많은 양의 데이터를 수집하므로 해당 데이터를 직접 관리하기가 어려워 지므로 데이터를 효율적으로 관리하려면 데이터베이스가 필요합니다.


React Native의 옵션 


앱을 개발할 때는 요구 사항에 가장 적합한 데이터베이스를 선택하십시오. 두 가지 옵션을 사용할 수 있는 경우 더 나은 문서화 및 문제에 대한 빠른 대응 기능을 갖춘 옵션을 사용하십시오. 

다음은 React Native에서 사용할 수 있는 가장 잘 알려진 옵션입니다.


  • WatermelonDB : 모든 기본 데이터베이스와 함께 사용할 수 있는 오픈 소스 반응 데이터베이스입니다. 기본적으로 React Native에서 기본 데이터베이스로 SQLite를 사용합니다.
  • SQLite (React Native, Expo) : 가장 오래되고 가장 많이 사용되며 전투 테스트 및 잘 알려진 솔루션입니다. 대부분의 플랫폼에서 사용할 수 있으므로 다른 모바일 앱 개발 프레임 워크에서 애플리케이션을 개발 한 경우 이미 익숙 할 것입니다.
  • Realm (React Native) : 오픈 소스 솔루션이지만 다른 많은 기능을 갖춘 엔터프라이즈 버전도 있습니다. 그들은 훌륭한 일을 해왔으며 많은 유명한 회사들이 그것을 사용합니다.
  • FireBase (React Native, Expo) : 모바일 개발 플랫폼을 위한 Google 서비스입니다. 많은 기능을 제공하며 스토리지는 그중 하나일 뿐입니다. 그러나 생태계를 활용하려면 생태계 내에 있어야 합니다.
  • RxDB : 웹용 실시간 데이터베이스. 그것은 GitHub (> 9K 별)에 대한 좋은 문서, 좋은 평가를 가지고 있으며 반응합니다.

전제 조건 


기본 React Native 및 빌드 프로세스에 대한 지식이 있다고 가정합니다. 우리는 react-native-cli를 사용하여 응용 프로그램을 만들 것입니다.


또한 많은 문제가 발생할 수 있으므로 프로젝트를 설정하는 동안 Android 또는 iOS 개발 환경을 설정하는 것이 좋습니다. 디버깅의 첫 번째 단계는 IDE (Android Studio 또는 Xcode)를 열어 로그를 확인하는 것입니다.


참고 : 자세한 내용은 여기에서 종속성 설치에 대한 공식 안내서를 확인하십시오. 공식 가이드 라인은 매우 간결하고 명확하므로 여기서는 다루지 않습니다.


가상 장치 또는 물리적 장치를 설정하려면 다음 가이드를 따르십시오.

물리적 장치를 사용하는 것

가상 장치 사용


참고 : Expo라는 JavaScript 친화적인 툴체인이 있습니다. React Native 커뮤니티도 홍보를 시작했지만 아직 Expo를 사용하는 대규모 프로덕션 지원 응용 프로그램을 보지 못했으며 현재 Realm과 같은 데이터베이스를 사용하는 사용자는 Expo 포트를 사용할 수 없습니다 우리의 경우, WatermelonDB.


앱 요구 사항 


제목, 포스터 이미지, 장르 및 출시 날짜가 포함 된 영화 검색 응용 프로그램을 만듭니다. 각 영화에는 많은 리뷰가 있습니다.


응용 프로그램에는 세 개의 화면이 있습니다.


Home에는 더미 레코드를 생성하는 단추와 새 영화를 추가하는 단추가 표시됩니다. 그 아래에는 데이터베이스에서 영화 제목을 쿼리 하는 데 사용할 수 있는 하나의 검색 입력이 있습니다. 검색 창 아래에 영화 목록이 표시됩니다. 이름을 검색하면 검색된 동영상 만 목록에 표시됩니다.


home screen view영화를 클릭하면 모든 리뷰를 확인할 수 있는 영화 대시 보드가 열립니다. 이 화면에서 동영상을 편집 또는 삭제하거나 새로운 리뷰를 추가 할 수 있습니다.


movie dashboard세 번째 화면은 영화를 만들거나 업데이트하는 데 사용되는 영화 형식입니다.


movie form소스 코드는 GitHub에서 사용할 수 있습니다.


WatermelonDB를 선택하는 이유 (기능) 


오프라인 우선 응용 프로그램을 만들어야 하므로 데이터베이스가 필수입니다.


WatermelonDB의 특징 


WatermelonDB의 일부 기능을 살펴 보겠습니다.


완전히 관찰 가능 


WatermelonDB의 가장 큰 특징은 반응성입니다. 옵저버 블을 사용하여 모든 개체를 관찰 할 수 있으며 데이터가 변경 될 때마다 구성 요소를 자동으로 다시 렌더링 합니다. 

WatermelonDB를 사용하기 위해 추가로 노력할 필요는 없습니다. 우리는 간단한 React 컴포넌트를 감싸고 그것들을 반응 적으로 만들도록 향상 시킵니다. 내 경험상, 그것은 완벽하게 작동하며, 우리는 다른 것에 신경 쓸 필요가 없습니다. 

개체를 변경하고 작업을 완료했습니다! 애플리케이션의 모든 위치에서 유지되고 업데이트 됩니다.


React Native를 위한 SQLite 


최신 브라우저에서는 적시 컴파일이 속도를 향상 시키는 데 사용되지만 휴대 기기에서는 사용할 수 없습니다. 또한 모바일 장치의 하드웨어는 컴퓨터보다 느립니다. 이러한 모든 요인으로 인해 JavaScript 응용 프로그램은 모바일 응용 프로그램에서 느리게 실행됩니다. 이를 극복하기 위해 WatermelonDB는 필요할 때까지 아무것도 가져 오지 않습니다. 빠른 응답을 제공하기 위해 별도의 스레드에서 기본 데이터베이스로 지연 로딩 및 SQLite를 사용합니다.


동기화 기본 요소 및 동기화 어댑터 


WatermelonDB는 로컬 데이터베이스 일 뿐이지 만 동기화 프리미티브 및 동기화 어댑터도 제공합니다. 자체 백엔드 데이터베이스와 함께 사용하기가 매우 쉽습니다. 백엔드의 WatermelonDB 동기화 프로토콜을 준수하고 엔드 포인트를 제공하면 됩니다.


추가 기능은 다음과 같습니다.

  • Flow을 사용하여 정적으로 입력
  • 모든 플랫폼에서 사용 가능


Dev Env 및 WatermelonDB 설정 (v0.0) 


우리는 react-native-cli를 사용하여 응용 프로그램을 만들 것입니다.


참고 : ExpoKit 또는 Ejecting from Expo와 함께 사용할 수 있습니다.


이 부분을 건너 뛰려면 소스 저장소를 복제하고 v0.0 분기를 체크 아웃 하십시오.


새 프로젝트를 시작하십시오.


react-native init MovieDirectory
cd MovieDirectory

종속성을 설치하십시오.


npm i @nozbe/watermelondb @nozbe/with-observables react-navigation react-native-gesture-handler react-native-fullwidth-image native-base rambdax

다음은 설치된 종속성 및 용도 목록입니다.

  • native-base : 앱의 모양과 느낌에 사용될 UI 라이브러리.
  • react 네이티브 전폭 이미지 : 전체 화면 반응 형 이미지를 표시합니다. 때로는 너비, 높이를 계산하고 가로 세로 비율을 유지하기가 어려울 수 있으므로 기존 커뮤니티 솔루션을 사용하는 것이 좋습니다.
  • @ nozbe / watermelondb : 우리가 사용할 데이터베이스.
  • @ nozbe / with-observables : 모델에 사용될 데코레이터 (@)를 포함합니다.
  • react 탐색 : 경로 / 화면 관리에 사용
  • react-native-gesture-handler : 반응 탐색에 대한 종속성.
  • rambdax : 더미 데이터를 생성하는 동안 난수를 생성하는 데 사용됩니다.

package.json을 열고 스크립트를 다음 코드로 바꾸십시오.


"scripts": {
    "start": "node node_modules/react-native/local-cli/cli.js start",
    "start:ios": "react-native run-ios",
    "start:android": "react-native run-android",
    "test": "jest"
}

이것은 각 장치에서 응용 프로그램을 실행하는 데 사용됩니다.


WatermelonDB 설정 


데코레이터를 변환하려면 Babel 플러그인을 추가해야 합니다. 따라서 개발자 의존성으로 설치하십시오 :


npm install -D @babel/plugin-proposal-decorators

프로젝트 루트에 새 파일 .babelrc를 작성하십시오.


// .babelrc
{
  "presets": ["module:metro-react-native-babel-preset"],
  "plugins": [["@babel/plugin-proposal-decorators", { "legacy": true }]]
}

이제 대상 환경에 다음 안내서를 사용하십시오.

Android Studio에서 android 폴더를 열고 프로젝트를 동기화 하십시오. 그렇지 않으면 응용 프로그램을 처음 실행할 때 오류가 발생합니다. iOS를 타겟팅 하는 경우에도 마찬가지입니다.


응용 프로그램을 실행하기 전에 반응 네이티브 제스처 핸들러 패키지, 반응 탐색의 종속성 및 네이티브 네이티브의 종속성 인 react-native-vector-icons를 연결해야 합니다. 기본적으로 응용 프로그램의 이진 크기를 작게 유지하기 위해 React Native에는 기본 기능을 지원하는 모든 코드가 포함되어 있지 않습니다. 따라서 특정 기능을 사용해야 할 때마다 link 명령을 사용하여 기본 종속성을 추가 할 수 있습니다. 의존성을 연결해 봅시다 :


react-native link react-native-gesture-handler
react-native link react-native-vector-icons

응용 프로그램을 실행하십시오.


npm run start:android
# or
npm run start:ios

누락 된 종속성에 대한 오류가 발생하면 npm i를 실행하십시오.


여기까지의 코드는 v0.0 브랜치에서 사용할 수 있습니다.


version 0 

학습서


데이터베이스 응용 프로그램을 만들 때 많은 코드가 백엔드 전용이므로 프런트 엔드에서 많이 볼 수 없습니다. 오래 걸리는 것 같지만 인내심을 가지고 끝까지 튜토리얼을 따르십시오. 후회하지 않을 것입니다!


WatermelonDB 워크 플로우는 세 가지 주요 부분으로 분류 할 수 있습니다.

  • 스키마 : 데이터베이스 테이블 스키마를 정의하는 데 사용됩니다.
  • 모델 : ORM 맵핑 오브젝트. 우리는 응용 프로그램 전체에서 이들과 상호 작용할 것입니다.
  • 액션 : 객체 / 행에서 다양한 CRUD 작업을 수행하는 데 사용됩니다. 데이터베이스 객체를 사용하여 직접 작업을 수행하거나 모델에서 함수를 정의하여 이러한 작업을 수행 할 수 있습니다. 모델에서 정의하는 것이 더 나은 방법이며, 우리는 이것을 사용합니다.

우리의 응용 프로그램을 시작합시다.


DB 스키마 및 WatermelonDB (v0.1) 초기화 


애플리케이션에서 스키마, 모델 및 데이터베이스 객체를 정의합니다. 우리는 응용 프로그램에서 많은 것을 볼 수는 없지만 이것이 가장 중요한 단계입니다. 여기에서 모든 것을 정의한 후 응용 프로그램이 올바르게 작동하는지 확인합니다. 문제가 발생하면 이 단계에서 쉽게 디버깅 할 수 있습니다.


프로젝트 구조 


루트에 새로운 src 폴더를 만듭니다. 이것은 모든 React Native 코드의 루트 폴더입니다. models 폴더는 모든 데이터베이스 관련 파일에 사용됩니다. DAO (Data Access Object) 폴더로 작동합니다. 이것은 일부 유형의 데이터베이스 또는 기타 지속성 메커니즘에 대한 인터페이스에 사용되는 용어입니다. 컴포넌트 폴더에는 모든 React 컴포넌트가 있습니다. 화면 폴더에는 응용 프로그램의 모든 화면이 있습니다.


mkdir src && cd src
mkdir models
mkdir components
mkdir screens
Schema 


models 폴더로 이동하여 schema.js 파일을 새로 작성하고 다음 코드를 사용하십시오.


// schema.js
import { appSchema, tableSchema } from "@nozbe/watermelondb";

export const mySchema = appSchema({
  version: 2,
  tables: [
    tableSchema({
      name: "movies",
      columns: [
        { name: "title", type: "string" },
        { name: "poster_image", type: "string" },
        { name: "genre", type: "string" },
        { name: "description", type: "string" },
        { name: "release_date_at", type: "number" }
      ]
    }),
    tableSchema({
      name: "reviews",
      columns: [
        { name: "body", type: "string" },
        { name: "movie_id", type: "string", isIndexed: true }
      ]
    })
  ]
});

영화 용 테이블과 리뷰 용 테이블을 각각 정의했습니다. 코드 자체는 자명하다. 두 테이블 모두 관련 열이 있습니다.


WatermelonDB의 명명 규칙에 따라 모든 ID는 _id 접미사로 끝나고 날짜 필드는 _at 접미사로 끝납니다.


isIndexed는 열에 색인을 추가하는 데 사용됩니다. 인덱싱을 사용하면 생성 / 업데이트 속도와 데이터베이스 크기를 약간만 희생하면 열을 기준으로 쿼리를 더 빠르게 수행 할 수 있습니다. 우리는 movie_id로 모든 리뷰를 쿼리 할 것이므로 인덱스로 표시해야 합니다. 부울 열에 대해 빈번한 쿼리를 하려면 인덱스도 색인화 해야합니다. 그러나 날짜 (_at) 열은 색인화 하지 않아야 합니다.


Models 


새 파일 models / Movie.js를 만들고이 코드에 붙여 넣습니다.


// models/Movie.js
import { Model } from "@nozbe/watermelondb";
import { field, date, children } from "@nozbe/watermelondb/decorators";

export default class Movie extends Model {
  static table = "movies";

  static associations = {
    reviews: { type: "has_many", foreignKey: "movie_id" }
  };

  @field("title") title;
  @field("poster_image") posterImage;
  @field("genre") genre;
  @field("description") description;

  @date("release_date_at") releaseDateAt;

  @children("reviews") reviews;
}

여기에서는 영화 표의 각 열을 각 변수에 매핑 했습니다. 리뷰를 영화와 매핑 한 방법에 유의하십시오. 우리는 그것을 협회에서 정의했으며 @field 대신 @children을 사용했습니다. 각 리뷰에는 movie_id 외래 키가 있습니다. 이러한 검토 외래 키 값은 영화 모델의 id와 일치하여 리뷰 모델을 영화 모델에 연결합니다.


날짜를 위해서, 우리는 WatermelonDB가 간단한 숫자 대신 Date 객체를 제공 할 수 있도록 @date 데코레이터를 사용해야 합니다.


이제 새 파일 models / Review.js를 작성하십시오. 이것은 영화의 각 리뷰를 매핑하는 데 사용됩니다.


// models/Review.js
import { Model } from "@nozbe/watermelondb";
import { field, relation } from "@nozbe/watermelondb/decorators";

export default class Review extends Model {
  static table = "reviews";

  static associations = {
    movie: { type: "belongs_to", key: "movie_id" }
  };

  @field("body") body;

  @relation("movies", "movie_id") movie;
}

필요한 모델을 모두 만들었습니다. 직접 모델을 사용하여 데이터베이스를 초기화 할 수 있지만 새 모델을 추가하려면 데이터베이스를 초기화 할 위치를 다시 변경해야 합니다. 이를 극복하기 위해 새로운 파일 models / index.js를 만들고 다음 코드를 추가하십시오.


// models/index.js
import Movie from "./Movie";
import Review from "./Review";

export const dbModels = [Movie, Review];

따라서 모델 폴더 만 변경하면 됩니다. 이렇게 하면 DAO 폴더가 보다 체계적으로 구성됩니다.


데이터베이스 초기화 


이제 스키마와 모델을 사용하여 데이터베이스를 초기화하려면 애플리케이션의 루트에 있는 index.js를 여십시오. 아래 코드를 추가하십시오 :


// index.js
import { AppRegistry } from "react-native";
import App from "./App";
import { name as appName } from "./app.json";

import { Database } from "@nozbe/watermelondb";
import SQLiteAdapter from "@nozbe/watermelondb/adapters/sqlite";
import { mySchema } from "./src/models/schema";
import { dbModels } from "./src/models/index.js";

// First, create the adapter to the underlying database:
const adapter = new SQLiteAdapter({
  dbName: "WatermelonDemo",
  schema: mySchema
});

// Then, make a Watermelon database from it!
const database = new Database({
  adapter,
  modelClasses: dbModels
});

AppRegistry.registerComponent(appName, () => App);

기본 데이터베이스에 대한 스키마를 사용하여 어댑터를 작성합니다. 그런 다음이 어댑터와 dbModels를 전달하여 새 데이터베이스 인스턴스를 작성하십시오.


이 시점에서 애플리케이션이 제대로 작동하는지 확인하는 것이 좋습니다. 따라서 응용 프로그램을 실행하고 확인하십시오.

npm run start:android
# or
npm run start:ios

우리는 UI를 변경하지 않았으므로 모든 것이 제대로 작동하면 화면이 이전과 비슷하게 보입니다.


이 부분까지의 모든 코드는 v0.1 분기 아래에 있습니다.


동작 및 더미 데이터 생성기 (v0.2) 추가 


애플리케이션에 더미 데이터를 추가해 보겠습니다.


Actions 


CRUD 작업을 수행하기 위해 몇 가지 작업을 만들 것입니다. models / Movie.js 및 models / Review.js를 열고 아래와 같이 업데이트하십시오.


// models/Movie.js
import { Model } from "@nozbe/watermelondb";
import { field, date, children } from "@nozbe/watermelondb/decorators";

export default class Movie extends Model {
  static table = "movies";

  static associations = {
    reviews: { type: "has_many", foreignKey: "movie_id" }
  };

  @field("title") title;
  @field("poster_image") posterImage;
  @field("genre") genre;
  @field("description") description;

  @date("release_date_at") releaseDateAt;

  @children("reviews") reviews;

  // add these:

  getMovie() {
    return {
      title: this.title,
      posterImage: this.posterImage,
      genre: this.genre,
      description: this.description,
      releaseDateAt: this.releaseDateAt
    };
  }

  async addReview(body) {
    return this.collections.get("reviews").create(review => {
      review.movie.set(this);
      review.body = body;
    });
  }

  updateMovie = async updatedMovie => {
    await this.update(movie => {
      movie.title = updatedMovie.title;
      movie.genre = updatedMovie.genre;
      movie.posterImage = updatedMovie.posterImage;
      movie.description = updatedMovie.description;
      movie.releaseDateAt = updatedMovie.releaseDateAt;
    });
  };

  async deleteAllReview() {
    await this.reviews.destroyAllPermanently();
  }

  async deleteMovie() {
    await this.deleteAllReview(); // delete all reviews first
    await this.markAsDeleted(); // syncable
    await this.destroyPermanently(); // permanent
  }
}
// models/Review.js
import { Model } from "@nozbe/watermelondb";
import { field, relation } from "@nozbe/watermelondb/decorators";

export default class Review extends Model {
  static table = "reviews";

  static associations = {
    movie: { type: "belongs_to", key: "movie_id" }
  };

  @field("body") body;

  @relation("movies", "movie_id") movie;

  // add these:

  async deleteReview() {
    await this.markAsDeleted(); // syncable
    await this.destroyPermanently(); // permanent
  }
}

업데이트 및 삭제 작업에 정의 된 모든 기능을 사용하겠습니다. 작성하는 동안 모델 오브젝트가 없으므로 데이터베이스 오브젝트를 직접 사용하여 새 행을 작성합니다.


models / generate.js 및 models / randomData.js라는 두 개의 파일을 작성하십시오. generate.js는 더미 레코드를 생성하는 generateRecords 함수를 작성하는 데 사용됩니다. randomData.js에는 더미 레코드를 생성하기 위해 generate.js에서 사용되는 더미 데이터가 있는 다른 배열이 포함되어 있습니다.


// models/generate.js
import { times } from "rambdax";
import {
  movieNames,
  movieGenre,
  moviePoster,
  movieDescription,
  reviewBodies
} from "./randomData";

const flatMap = (fn, arr) => arr.map(fn).reduce((a, b) => a.concat(b), []);

const fuzzCount = count => {
  // Makes the number randomly a little larger or smaller for fake data to seem more realistic
  const maxFuzz = 4;
  const fuzz = Math.round((Math.random() - 0.5) * maxFuzz * 2);
  return count + fuzz;
};

const makeMovie = (db, i) => {
  return db.collections.get("movies").prepareCreate(movie => {
    movie.title = movieNames[i % movieNames.length] + " " + (i + 1) || movie.id;
    movie.genre = movieGenre[i % movieGenre.length];
    movie.posterImage = moviePoster[i % moviePoster.length];
    movie.description = movieDescription;
    movie.releaseDateAt = new Date().getTime();
  });
};

const makeReview = (db, movie, i) => {
  return db.collections.get("reviews").prepareCreate(review => {
    review.body =
      reviewBodies[i % reviewBodies.length] || `review#${review.id}`;
    review.movie.set(movie);
  });
};

const makeReviews = (db, movie, count) =>
  times(i => makeReview(db, movie, i), count);

// Generates dummy random records. Accepts db object, no. of movies, and no. of reviews for each movie to generate.
const generate = async (db, movieCount, reviewsPerPost) => {
  await db.action(() => db.unsafeResetDatabase());
  const movies = times(i => makeMovie(db, i), movieCount);

  const reviews = flatMap(
    movie => makeReviews(db, movie, fuzzCount(reviewsPerPost)),
    movies
  );

  const allRecords = [...movies, ...reviews];
  await db.batch(...allRecords);
  return allRecords.length;
};

// Generates 100 movies with up to 10 reviews
export async function generateRecords(database) {
  return generate(database, 100, 10);
}
// models/randomData.js
export const movieNames = [
  "The Shawshank Redemption",
  "The Godfather",
  "The Dark Knight",
  "12 Angry Men"
];

export const movieGenre = [
  "Action",
  "Comedy",
  "Romantic",
  "Thriller",
  "Fantasy"
];

export const moviePoster = [
  "https://m.media-amazon.com/images/M/MV5BMDFkYTc0MGEtZmNhMC00ZDIzLWFmNTEtODM1ZmRlYWMwMWFmXkEyXkFqcGdeQXVyMTMxODk2OTU@._V1_UX182_CR0,0,182,268_AL__QL50.jpg",
  "https://m.media-amazon.com/images/M/MV5BM2MyNjYxNmUtYTAwNi00MTYxLWJmNWYtYzZlODY3ZTk3OTFlXkEyXkFqcGdeQXVyNzkwMjQ5NzM@._V1_UY268_CR3,0,182,268_AL__QL50.jpg",
  "https://m.media-amazon.com/images/M/MV5BMTMxNTMwODM0NF5BMl5BanBnXkFtZTcwODAyMTk2Mw@@._V1_UX182_CR0,0,182,268_AL__QL50.jpg",
  "https://m.media-amazon.com/images/M/MV5BMWU4N2FjNzYtNTVkNC00NzQ0LTg0MjAtYTJlMjFhNGUxZDFmXkEyXkFqcGdeQXVyNjc1NTYyMjg@._V1_UX182_CR0,0,182,268_AL__QL50.jpg"
];

export const movieDescription =
  "Lorem ipsum dolor sit amet enim. Etiam ullamcorper. Suspendisse a pellentesque dui, non felis. Maecenas malesuada elit lectus felis, malesuada ultricies. Curabitur et ligula. Ut molestie a, ultricies porta urna. Vestibulum commodo volutpat a, convallis ac, laoreet enim. Phasellus fermentum in, dolor. Lorem ipsum dolor sit amet enim. Etiam ullamcorper. Suspendisse a pellentesque dui, non felis. Maecenas malesuada elit lectus felis, malesuada ultricies. Curabitur et ligula. Ut molestie a, ultricies porta urna. Vestibulum commodo volutpat a, convallis ac, laoreet enim. Phasellus fermentum in, dolor. Pellentesque facilisis. Nulla imperdiet sit amet magna. Vestibulum dapibus, mauris nec malesuada fames ac turpis velit, rhoncus eu, luctus et interdum adipiscing wisi. Aliquam erat ac ipsum. Integer aliquam purus. Quisque lorem tortor fringilla sed, vestibulum id, eleifend justo vel bibendum sapien massa ac turpis faucibus orci luctus non, consectetuer lobortis quis, varius in, purus. Integer ultrices posuere cubilia Curae, Nulla ipsum dolor lacus, suscipit adipiscing. Cum sociis natoque penatibus et ultrices volutpat.";

export const reviewBodies = [
  "First!!!!",
  "Cool!",
  "Why dont you just…",
  "Maybe useless, but the article is extremely interesting and easy to read. One can definitely try to read it.",
  "Seriously one of the coolest projects going on right now",
  "I think the easiest way is just to write a back end that emits .NET IR since infra is already there.",
  "Open source?",
  "This article is obviously wrong",
  "Just Stupid",
  "The general public won't care",
  "This is my bear case for Google.",
  "All true, but as a potential advertiser you don't really get to use all that targeting when placing ads",
  "I wonder what work environment exists, that would cause a worker to hide their mistakes and endanger the crew, instead of reporting it. And how many more mistakes go unreported? I hope Russia addresses the root issue, and not just fires the person responsible."
];

이제 더미 데이터를 생성하기 위해 generateRecords 함수를 호출해야 합니다.


React 탐색을 사용하여 경로를 만듭니다. 루트에서 index.js를 열고 다음 코드를 사용하십시오.


// index.js
import { AppRegistry } from "react-native";
import { name as appName } from "./app.json";

import { Database } from "@nozbe/watermelondb";
import SQLiteAdapter from "@nozbe/watermelondb/adapters/sqlite";
import { mySchema } from "./src/models/schema";
import { dbModels } from "./src/models/index.js";

// Added new import
import { createNavigation } from "./src/screens/Navigation";

// First, create the adapter to the underlying database:
const adapter = new SQLiteAdapter({
  dbName: "WatermelonDemo",
  schema: mySchema
});

// Then, make a Watermelon database from it!
const database = new Database({
  adapter,
  modelClasses: dbModels
});

// Change these:
const Navigation = createNavigation({ database });

AppRegistry.registerComponent(appName, () => Navigation);

우리는 createNavigation 함수를 사용하고 있지만 지금은 가지고 있지 않으므로 만들어 봅시다. src / screens / Navigation.js를 작성하고 다음 코드를 사용하십시오.


// screens/Navigation.js
import React from "react";
import { createStackNavigator, createAppContainer } from "react-navigation";

import Root from "./Root";

export const createNavigation = props =>
  createAppContainer(
    createStackNavigator(
      {
        Root: {
          // We have to use a little wrapper because React Navigation doesn't pass simple props (and withObservables needs that)
          screen: ({ navigation }) => {
            const { database } = props;
            return <Root database={database} navigation={navigation} />;
          },
          navigationOptions: { title: "Movies" }
        }
      },
      {
        initialRouteName: "Root",
        initialRouteParams: props
      }
    )
  );

Root를 첫 번째 화면으로 사용하므로 screens / Root.js를 만들고 다음 코드를 사용하겠습니다.


// screens/Root.js
import React, { Component } from "react";
import { generateRecords } from "../models/generate";
import { Alert } from "react-native";
import { Container, Content, Button, Text } from "native-base";

import MovieList from "../components/MovieList";

export default class Root extends Component {
  state = {
    isGenerating: false
  };

  generate = async () => {
    this.setState({ isGenerating: true });
    const count = await generateRecords(this.props.database);
    Alert.alert(`Generated ${count} records!`);
    this.setState({ isGenerating: false });
  };

  render() {
    const { isGenerating } = this.state;
    const { database, navigation } = this.props;

    return (
      <Container>
        <Content>
          <Button
            bordered
            full
            onPress={this.generate}
            style={{ marginTop: 5 }}
          >
            <Text>Generate Dummy records</Text>
          </Button>

          {!isGenerating && (
            <MovieList database={database} search="" navigation={navigation} />
          )}
        </Content>
      </Container>
    );
  }
}

MovieList를 사용하여 생성 된 영화의 목록을 표시했습니다. 만들어 봅시다. 아래와 같이 src / components / MovieList.js 파일을 새로 만듭니다 :


// components/MovieList.js
import React from "react";

import { Q } from "@nozbe/watermelondb";
import withObservables from "@nozbe/with-observables";
import { List, ListItem, Body, Text } from "native-base";

const MovieList = ({ movies }) => (
  <List>
    {movies.map(movie => (
      <ListItem key={movie.id}>
        <Body>
          <Text>{movie.title}</Text>
        </Body>
      </ListItem>
    ))}
  </List>
);

// withObservables is HOC(Higher Order Component) to make any React component reactive.
const enhance = withObservables(["search"], ({ database, search }) => ({
  movies: database.collections
    .get("movies")
    .query(Q.where("title", Q.like(`%${Q.sanitizeLikeString(search)}%`)))
}));

export default enhance(MovieList);

MovieList는 영화 목록을 렌더링하는 간단한 React 구성 요소이지만, Observables로 호출하는 향상을 관찰하십시오. withObservables는 수박 DB에서 반응 성분을 반응 적으로 만드는 HOC (고차 성분)입니다. 

응용 프로그램의 어느 곳에서나 영화의 가치를 변경하면 변경 사항을 반영하기 위해 영화가 다시 렌더링 됩니다. 두 번째 인수 ({database, search})는 구성 요소 소품으로 구성됩니다. 

검색은 Root.js에서 전달되고 데이터베이스는 Navigation.js에서 전달됩니다. 첫 번째 인수 [ "search"]는 관찰 재시작을 트리거 하는 소품 목록입니다. 따라서 검색이 변경되면 관찰 가능한 객체가 다시 계산되어 다시 관찰됩니다. 이 함수에서는 데이터베이스 객체를 사용하여 제목이 검색과 같은 영화 모음을 가져옵니다. 

% 및 _와 같은 특수 문자는 자동으로 이스케이프 되지 않으므로 항상 위생적인 ​​사용자 입력을 사용하는 것이 좋습니다.


Android Studio 또는 Xcode를 열어 프로젝트를 동기화 한 다음 애플리케이션을 실행하십시오. GENERATE DUMMY RECORDS 버튼을 클릭하십시오. 더미 데이터가 생성되고 목록이 표시됩니다.


npm run start:android
# or
npm run start:ios

이 코드는 v0.2 브랜치에서 사용할 수 있습니다.


version 0.2 

모든 CRUD 작업 추가 (v1) 


영화 및 리뷰를 작성 / 업데이트 / 삭제하는 기능을 추가하겠습니다. 새 버튼을 추가하여 새 영화를 추가하고 TextInput을 만들어 검색 키워드를 쿼리에 전달합니다. 따라서 Root.js를 열고 다음과 같이 내용을 변경하십시오.


// screens/Root.js
import React, { Component } from "react";
import { generateRecords } from "../models/generate";
import { Alert } from "react-native";
import {
  View,
  Container,
  Content,
  Button,
  Text,
  Form,
  Item,
  Input,
  Label,
  Body
} from "native-base";

import MovieList from "../components/MovieList";
import styles from "../components/styles";

export default class Root extends Component {
  state = {
    isGenerating: false,
    search: "",
    isSearchFocused: false
  };

  generate = async () => {
    this.setState({ isGenerating: true });
    const count = await generateRecords(this.props.database);
    Alert.alert(`Generated ${count} records!`);
    this.setState({ isGenerating: false });
  };

  // add these:

  addNewMovie = () => {
    this.props.navigation.navigate("NewMovie");
  };

  handleTextChanges = v => this.setState({ search: v });

  handleOnFocus = () => this.setState({ isSearchFocused: true });

  handleOnBlur = () => this.setState({ isSearchFocused: false });

  render() {
    const { search, isGenerating, isSearchFocused } = this.state;
    const { database, navigation } = this.props;

    return (
      <Container style={styles.container}>
        <Content>
          {!isSearchFocused && (
            <View style={styles.marginContainer}>
              <Button
                bordered
                full
                onPress={this.generate}
                style={{ marginTop: 5 }}
              >
                <Text>Generate Dummy records</Text>
              </Button>

              {/* add these: */}
              <Button
                bordered
                full
                onPress={this.addNewMovie}
                style={{ marginTop: 5 }}
              >
                <Text>Add new movie</Text>
              </Button>
              <Body />
            </View>
          )}

          {/* add these: */}
          <Form>
            <Item floatingLabel>
              <Label>Search...</Label>
              <Input
                onFocus={this.handleOnFocus}
                onBlur={this.handleOnBlur}
                onChangeText={this.handleTextChanges}
              />
            </Item>
          </Form>
          {!isGenerating && (
            <MovieList
              database={database}
              search={search}
              navigation={navigation}
            />
          )}
        </Content>
      </Container>
    );
  }
}

새로운 화면 인 MovieForm.js를 만들고 동일한 구성 요소를 사용하여 영화를 편집합니다. 우리가 handleSubmit 메소드를 호출하고 handleAddNewMovie 또는 handleUpdateMovie를 호출하는지 확인하십시오. handleUpdateMovie는 Movie 모델에서 앞서 정의한 액션을 호출합니다. 그게 다야 이것은 그것을 유지하고 다른 곳에서도 업데이트 할 것입니다. MovieForm.js에 다음 코드를 사용하십시오.


// screens/MovieForm.js
import React, { Component } from "react";
import {
  View,
  Button,
  Container,
  Content,
  Form,
  Item,
  Input,
  Label,
  Textarea,
  Picker,
  Body,
  Text,
  DatePicker
} from "native-base";
import { movieGenre } from "../models/randomData";

class MovieForm extends Component {
  constructor(props) {
    super(props);
    if (props.movie) {
      this.state = { ...props.movie.getMovie() };
    } else {
      this.state = {};
    }
  }

  render() {
    return (
      <Container>
        <Content>
          <Form>
            <Item floatingLabel>
              <Label>Title</Label>
              <Input
                onChangeText={title => this.setState({ title })}
                value={this.state.title}
              />
            </Item>
            <View style={{ paddingLeft: 15 }}>
              <Item picker>
                <Picker
                  mode="dropdown"
                  style={{ width: undefined, paddingLeft: 15 }}
                  placeholder="Genre"
                  placeholderStyle={{ color: "#bfc6ea" }}
                  placeholderIconColor="#007aff"
                  selectedValue={this.state.genre}
                  onValueChange={genre => this.setState({ genre })}
                >
                  {movieGenre.map((genre, i) => (
                    <Picker.Item key={i} label={genre} value={genre} />
                  ))}
                </Picker>
              </Item>
            </View>

            <Item floatingLabel>
              <Label>Poster Image</Label>
              <Input
                onChangeText={posterImage => this.setState({ posterImage })}
                value={this.state.posterImage}
              />
            </Item>

            <View style={{ paddingLeft: 15, marginTop: 15 }}>
              <Text style={{ color: "gray" }}>Release Date</Text>
              <DatePicker
                locale={"en"}
                animationType={"fade"}
                androidMode={"default"}
                placeHolderText="Change Date"
                defaultDate={new Date()}
                onDateChange={releaseDateAt => this.setState({ releaseDateAt })}
              />
              <Text>
                {this.state.releaseDateAt &&
                  this.state.releaseDateAt.toString().substr(4, 12)}
              </Text>

              <Text style={{ color: "gray", marginTop: 15 }}>Description</Text>
              <Textarea
                rowSpan={5}
                bordered
                placeholder="Description..."
                onChangeText={description => this.setState({ description })}
                value={this.state.description}
              />
            </View>

            {!this.props.movie && (
              <View style={{ paddingLeft: 15, marginTop: 15 }}>
                <Text style={{ color: "gray" }}>Review</Text>
                <Textarea
                  rowSpan={5}
                  bordered
                  placeholder="Review..."
                  onChangeText={review => this.setState({ review })}
                  value={this.state.review}
                />
              </View>
            )}
            <Body>
              <Button onPress={this.handleSubmit}>
                <Text>{this.props.movie ? "Update " : "Add "} Movie</Text>
              </Button>
            </Body>
          </Form>
        </Content>
      </Container>
    );
  }

  handleSubmit = () => {
    if (this.props.movie) {
      this.handleUpdateMovie();
    } else {
      this.handleAddNewMovie();
    }
  };

  handleAddNewMovie = async () => {
    const { database } = this.props;
    const movies = database.collections.get("movies");
    const newMovie = await movies.create(movie => {
      movie.title = this.state.title;
      movie.genre = this.state.genre;
      movie.posterImage = this.state.posterImage;
      movie.description = this.state.description;
      movie.releaseDateAt = this.state.releaseDateAt.getTime();
    });
    this.props.navigation.goBack();
  };

  handleUpdateMovie = async () => {
    const { movie } = this.props;
    await movie.updateMovie({
      title: this.state.title,
      genre: this.state.genre,
      posterImage: this.state.posterImage,
      description: this.state.description,
      releaseDateAt: this.state.releaseDateAt.getTime()
    });
    this.props.navigation.goBack();
  };
}

export default MovieForm;

Stateless 구성 요소에서 렌더링을 제어 할 수 있도록 MovieList.js를 나눕니다. 다음과 같이 업데이트하십시오.


// components/MovieList.js
import React from "react";

import { Q } from "@nozbe/watermelondb";
import withObservables from "@nozbe/with-observables";

import RawMovieItem from "./RawMovieItem";
import { List } from "native-base";

// add these:
const MovieItem = withObservables(["movie"], ({ movie }) => ({
  movie: movie.observe()
}))(RawMovieItem);

const MovieList = ({ movies, navigation }) => (
  <List>
    {movies.map(movie => (
      // change these:
      <MovieItem
        key={movie.id}
        movie={movie}
        countObservable={movie.reviews.observeCount()}
        onPress={() => navigation.navigate("Movie", { movie })}
      />
    ))}
  </List>
);

const enhance = withObservables(["search"], ({ database, search }) => ({
  movies: database.collections
    .get("movies")
    .query(Q.where("title", Q.like(`%${Q.sanitizeLikeString(search)}%`)))
}));

export default enhance(MovieList);

여기에서는 RawMovieItem을 사용했습니다. 렌더링 방법을 작성하겠습니다. RawMovieItem을 withObservables로 래핑 한 방법에 주목하십시오. 반응 적으로 만드는 데 사용됩니다. 사용하지 않으면 데이터베이스가 업데이트 될 때 수동으로 업데이트 해야 합니다.


참고 : 간단한 React 컴포넌트를 생성 한 후 관찰하는 것은 WatermelonDB의 요점입니다.


새로운 파일 components / RawMovieItem.js를 만들고 다음 코드를 사용하십시오.


// components/RawMovieItem.js
import React from "react";
import withObservables from "@nozbe/with-observables";
import {
  ListItem,
  Thumbnail,
  Text,
  Left,
  Body,
  Right,
  Button,
  Icon
} from "native-base";

// We observe and render the counter in a separate component so that we don't have to wait for the database until we can render the component. You can also prefetch all data before displaying the list
const RawCounter = ({ count }) => count;
const Counter = withObservables(["observable"], ({ observable }) => ({
  count: observable
}))(RawCounter);

const CustomListItem = ({ movie, onPress, countObservable }) => (
  <ListItem thumbnail onPress={onPress}>
    <Left>
      <Thumbnail square source={{ uri: movie.posterImage }} />
    </Left>
    <Body>
      <Text>{movie.title}</Text>
      <Text note numberOfLines={1}>
        Total Reviews: <Counter observable={countObservable} />
      </Text>
    </Body>
    <Right>
      <Button transparent onPress={onPress}>
        <Icon name="arrow-forward" />
      </Button>
    </Right>
  </ListItem>
);

export default CustomListItem;

영화의 모든 정보를 보고 편집 할 수 있어야 합니다. 따라서 새로운 화면 Movie.js를 만들고 모든 리뷰를 가져 와서 반응하게 하려면 components / ReviewList.js라는 두 가지 새로운 구성 요소를 만듭니다. 및 components / RawReviewItem.js.


존중되는 파일에 다음 코드를 사용하십시오.


// screens/Movie.js
import React, { Component } from "react";
import {
  View,
  Card,
  CardItem,
  Text,
  Button,
  Icon,
  Left,
  Body,
  Textarea,
  H1,
  H2,
  Container,
  Content
} from "native-base";
import withObservables from "@nozbe/with-observables";
import styles from "../components/styles";
import FullWidthImage from "react-native-fullwidth-image";
import ReviewList from "../components/ReviewList";

class Movie extends Component {
  state = {
    review: ""
  };

  render() {
    const { movie, reviews } = this.props;
    return (
      <Container style={styles.container}>
        <Content>
          <Card style={{ flex: 0 }}>
            <FullWidthImage source={{ uri: movie.posterImage }} ratio={1} />
            <CardItem />
            <CardItem>
              <Left>
                <Body>
                  <H2>{movie.title}</H2>
                  <Text note textStyle={{ textTransform: "capitalize" }}>
                    {movie.genre}
                  </Text>
                  <Text note>
                    {movie.releaseDateAt.toString().substr(4, 12)}
                  </Text>
                </Body>
              </Left>
            </CardItem>
            <CardItem>
              <Body>
                <Text>{movie.description}</Text>
              </Body>
            </CardItem>
            <CardItem>
              <Left>
                <Button
                  transparent
                  onPress={this.handleDelete}
                  textStyle={{ color: "#87838B" }}
                >
                  <Icon name="md-trash" />
                  <Text>Delete Movie</Text>
                </Button>
                <Button
                  transparent
                  onPress={this.handleEdit}
                  textStyle={{ color: "#87838B" }}
                >
                  <Icon name="md-create" />
                  <Text>Edit Movie</Text>
                </Button>
              </Left>
            </CardItem>

            <View style={styles.newReviewSection}>
              <H1>Add new review</H1>
              <Textarea
                rowSpan={5}
                bordered
                placeholder="Review..."
                onChangeText={review => this.setState({ review })}
                value={this.state.review}
              />
              <Body style={{ marginTop: 10 }}>
                <Button bordered onPress={this.handleAddNewReview}>
                  <Text>Add review</Text>
                </Button>
              </Body>
            </View>

            <ReviewList reviews={reviews} />
          </Card>
        </Content>
      </Container>
    );
  }

  handleAddNewReview = () => {
    let { movie } = this.props;
    movie.addReview(this.state.review);
    this.setState({ review: "" });
  };

  handleEdit = () => {
    let { movie } = this.props;
    this.props.navigation.navigate("EditMovie", { movie });
  };

  handleDelete = () => {
    let { movie } = this.props;
    movie.deleteMovie();
    this.props.navigation.goBack();
  };
}

const enhance = withObservables(["movie"], ({ movie }) => ({
  movie: movie.observe(),
  reviews: movie.reviews.observe()
}));

export default enhance(Movie);

ReviewList.js는 영화 리뷰 목록을 표시하는 반응 형 구성 요소입니다. RawReviewItem 구성 요소를 향상 시키고 이를 반응성으로 만듭니다.


// components/ReviewList.js
import React from "react";

import withObservables from "@nozbe/with-observables";
import { List, View, H1 } from "native-base";
import RawReviewItem from "./RawReviewItem";
import styles from "./styles";

const ReviewItem = withObservables(["review"], ({ review }) => ({
  review: review.observe()
}))(RawReviewItem);

const ReviewList = ({ reviews }) => {
  if (reviews.length > 0) {
    return (
      <View style={styles.allReviewsSection}>
        <H1>Reviews</H1>
        <List>
          {reviews.map(review => (
            <ReviewItem review={review} key={review.id} />
          ))}
        </List>
      </View>
    );
  } else {
    return null;
  }
};

export default ReviewList;

RawReviewItem.js는 단일 리뷰를 렌더링 하는 데 사용되는 간단한 React 구성 요소입니다.


// components/RawReviewItem.js
import React from "react";
import { ListItem, Text, Left, Right, Button, Icon } from "native-base";

// We observe and render the counter in a separate component so that we don't have to wait for the database until we can render the component. You can also prefetch all data before displaying the list.
const RawReviewItem = ({ review }) => {
  handleDeleteReview = () => {
    review.deleteReview();
  };

  return (
    <ListItem>
      <Left>
        <Text>{review.body}</Text>
      </Left>
      <Right>
        <Button transparent onPress={this.handleDeleteReview}>
          <Icon name="md-trash" />
        </Button>
      </Right>
    </ListItem>
  );
};

export default RawReviewItem;

마지막으로 두 개의 새 화면을 라우팅 하려면 Navigation.js를 다음 코드로 업데이트 해야 합니다.


// screens/Navigation.js
import React from "react";
import { createStackNavigator, createAppContainer } from "react-navigation";

import Root from "./Root";
import Movie from "./Movie";
import MovieForm from "./MovieForm";

export const createNavigation = props =>
  createAppContainer(
    createStackNavigator(
      {
        Root: {
          // We have to use a little wrapper because React Navigation doesn't pass simple props (and withObservables needs that)
          screen: ({ navigation }) => {
            const { database } = props;
            return <Root database={database} navigation={navigation} />;
          },
          navigationOptions: { title: "Movies" }
        },
        Movie: {
          screen: ({ navigation }) => (
            <Movie
              movie={navigation.state.params.movie}
              navigation={navigation}
            />
          ),
          navigationOptions: ({ navigation }) => ({
            title: navigation.state.params.movie.title
          })
        },
        NewMovie: {
          screen: ({ navigation }) => {
            const { database } = props;
            return <MovieForm database={database} navigation={navigation} />;
          },
          navigationOptions: { title: "New Movie" }
        },
        EditMovie: {
          screen: ({ navigation }) => {
            return (
              <MovieForm
                movie={navigation.state.params.movie}
                navigation={navigation}
              />
            );
          },
          navigationOptions: ({ navigation }) => ({
            title: `Edit "${navigation.state.params.movie.title}"`
          })
        }
      },
      {
        initialRouteName: "Root",
        initialRouteParams: props
      }
    )
  );

모든 구성 요소는 패딩 및 여백에 스타일을 사용합니다. 따라서 components / styles.js라는 파일을 작성하고 다음 코드를 사용하십시오.

// components/styles.js
import { StyleSheet } from "react-native";

export default StyleSheet.create({
  container: { flex: 1, paddingHorizontal: 10, marginVertical: 10 },
  marginContainer: { marginVertical: 10, flex: 1 },
  newReviewSection: {
    marginTop: 10,
    paddingHorizontal: 15
  },
  allReviewsSection: {
    marginTop: 30,
    paddingHorizontal: 15
  }
});

응용 프로그램을 실행하십시오.


npm run start:android
# or
npm run start:ios

최종 코드는 마스터 브랜치에서 사용할 수 있습니다.


연습


방금 배운 것을 연습하기 위해 수행해야 할 다음 단계는 다음과 같습니다. 원하는 순서대로 자유롭게 접근하십시오.

  • 새 영화가 나오도록 쿼리를 정렬하십시오.
  • 리뷰를 업데이트하는 기능을 추가하십시오.
  • 인 화면에서 장르와 날짜 필터를 추가하십시오.


유용한 링크 

스택 오버플로 : React Native, iOS 및 Android 사용시 데이터 저장 옵션

WatermelonDB : React 및 React Native 앱용 데이터베이스

WatermelonDB : 고성능 React Native



결론 


이 튜토리얼이 React Native에서 데이터베이스를 시작하는 데 도움이 되었기를 바랍니다. 우리는 응용 프로그램에서 데이터베이스의 필요성을 다루었습니다. 사용 가능한 데이터베이스 옵션; 응용 프로그램에 대한 데이터베이스 선택; WatermelonDB에서 모델, 스키마, 동작 및 반응 형 구성 요소를 사용하는 방법을 보여주는 예제 응용 프로그램.


GitHub / MovieDirectory에서 응용 프로그램 코드 저장소를 확인하십시오.