리액트 네이티브로 시작하는 앱 개발 #3

  1. 리액트 네이티브로 시작하는 앱 개발 1
  2. 리액트 네이티브로 시작하는 앱 개발 2
  3. 리액트 네이티브로 시작하는 앱 개발 3

리액트 네이티브는 페이스 북이 공개한 iOS와 안드로이드 앱 개발을 위한 라이브러리입니다. 이전 글에서 사용한 예제를 확장하여 Realm과 연동시키고 Realm 데이타베이스를 로컬 캐쉬로 사용하는 예제로 만듭니다.

Realm 환경 설정

Realm을 리액트 네이티브에서 쓰기 위해서 간단한 설정이 필요합니다. 먼저 프로젝트 경로로 이동하신 후 터미널에 다음과 같이 입력합니다.

npm install --save realm

구체적인 추가 설정은 Ream 리액트 네이티브 문서를 참고하세요.

안드로이드 추가 설정

추가된 Realm 의존성을 연결시킵니다.

react-native link realm

android/app/src/main/java/com/awesomeproject/MainActivity.java 파일을 엽니다. 만약 프로젝트 명이 다르면 awesomeproject 대신 다른 경로로 들어갑니다.

다른 import 문들 아래에 아래를 추가합니다.

import io.realm.react.RealmReactPackage;

getPackages() 메서드를 찾아 반환 리스트에 다음을 추가합니다.

iOS 추가 설정

프로젝트 파일을 엽니다.

open ios/AwesomeProject.xcodeproj/

AwesomeProject 경로는 프로젝트 설정에 따라 다를 수 있습니다.

사이드바에 top-level project가 선택됐는지 확인하고, 프로젝트 설정에서 iOS 배포 타겟을 최소한 8.0으로 변경합니다.

사이드바의 Libraries 그룹을 우클릭하고 Add Files to “”를 클릭하고 다이얼로그에서 ../node_modules/realm/RealmJS.xcodeproj를 선택하세요.

앱 타겟 설정의 General 탭을 엽니다. 왼쪽 컬럼에서 Libraries > RealmJS > Products를 확장하고 앱 타겟 설정을 위해 RealmReact.framework을 긁어서 General 탭의 Embedded Binaries 섹션으로 옮깁니다.

리액트 네이티브와 Realm을 같이 쓰기

그 다음으로는 에디터에서 index.android.jsindex.ios.js를 수정합시다. 수정할 파일은 전적으로 어떤 운영체제를 사용하냐에 달려있습니다. 안드로이드를 사용하는 경우엔 전자를 수정하고 아이폰을 가지고 있는 경우 후자를 수정합시다. 저는 index.android.js를 수정하겠습니다.

이런 개발 뉴스를 더 만나보세요

필요한 파일을 열고 다음과 같은 코드를 상단에 넣어줍니다.

const Realm = require('realm');

스키마 정의와 초기화

스키마를 다음과 같이 추가합니다.

const PosterSchema = {
  name: 'Poster',
  properties: {
    thumbnail: 'string',
  }
};

const MovieSchema = {
  name: 'Movie',
  properties: {
    title: 'string',
    year: 'int',
    posters: 'Poster',
  }
};

다른 프로퍼티들은 정의하지 않고 현재 사용하는 title, year, posters.thumbnail만 정의하였습니다.

두개의 스키마를 전달하여 Realm 인스턴스를 생성합시다.

var realm = new Realm({schema:[PosterSchema, MovieSchema]});

캐쉬를 추가한 fetchData

fetchData() {
    if (realm.objects('Movie').length > 0) {
      this.setState({
        dataSource: this.state.dataSource.cloneWithRows(realm.objects('Movie')),
        loaded: true,
      })
      return;
    }
    fetch(REQUEST_URL)
      .then((response) => response.json())
      .then((responseData) => {
        this.setState({
          dataSource: this.state.dataSource.cloneWithRows(responseData.movies),
          loaded: true,
        });
        for (var i in responseData.movies) {
          realm.write(() => {
            let posters = realm.create('Posters', {
              thumbnail: responseData.movies[i].posters.thumbnail,
            });
            let movie = realm.create('Movie', {
              title: responseData.movies[i].title,
              year: responseData.movies[i].year,
              posters: posters,
            });
          });
        }
      })
      .done();
  }

realm 인스턴스에 objects(<객체명>)을 호출하면 객체를 전달받을 수 있습니다. 여기의 개수를 통해 캐슁이 되어 있는지를 판단하고 재사용하도록 합니다.

캐슁되지 않았다면 실제로 fetch(REQUEST_URL)를 통해 서버에서 데이터를 가지고 오고 데이터를 가지고 온 이후 realm.write의 블록에서 캐쉬로 저장을 합니다. realm.write 블록은 쓰기 트랜잭션을 여는 메서드입니다. PosetersMovie 두개의 객체를 만들고 PostersMovie의 필드 posters에 지정합니다.

전체 코드

전체 코드는 아래와 같습니다.

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 */
'use strict';
import React, {
  AppRegistry,
  Component,
  Image,
  ListView,
  StyleSheet,
  Text,
  View
} from 'react-native';

const Realm = require('realm');

var MOCKED_MOVIES_DATA = [
  {title: 'Title', year: '2015', posters: {thumbnail: 'http://i.imgur.com/UePbdph.jpg'}},
];

var REQUEST_URL =
  'https://raw.githubusercontent.com/facebook/react-native/master/docs/MoviesExample.json';

const PostersSchema = {
  name: 'Posters',
  properties: {
    thumbnail: 'string',
  }
};

const MovieSchema = {
  name: 'Movie',
  properties: {
    title: 'string',
    year: 'int',
    posters: 'Posters',
  }
};

var realm = new Realm({schema:[PostersSchema, MovieSchema]});

class AwesomeProject extends Component {
  constructor(props) {
    super(props);
    this.state = {
      dataSource: new ListView.DataSource({
        rowHasChanged: (row1, row2) => row1 !== row2,
      }),
      loaded: false,
    };
  }

  componentDidMount() {
    this.fetchData();
  }

  fetchData() {
    if (realm.objects('Movie').length > 0) {
      this.setState({
        dataSource: this.state.dataSource.cloneWithRows(realm.objects('Movie')),
        loaded: true,
      })
      return;
    }
    fetch(REQUEST_URL)
      .then((response) => response.json())
      .then((responseData) => {
        this.setState({
          dataSource: this.state.dataSource.cloneWithRows(responseData.movies),
          loaded: true,
        });
        for (var i in responseData.movies) {
          realm.write(() => {
            let posters = realm.create('Posters', {
              thumbnail: responseData.movies[i].posters.thumbnail,
            });
            let movie = realm.create('Movie', {
              title: responseData.movies[i].title,
              year: responseData.movies[i].year,
              posters: posters,
            });
          });
        }
      })
      .done();
  }

  render() {
    if (!this.state.loaded) {
      return this.renderLoadingView();
    }

    return (
      <ListView
        dataSource={this.state.dataSource}
        renderRow={this.renderMovie}
        style={styles.listView}
      />
    );
  }

  renderLoadingView() {
    return (
      <View style={styles.container}>
        <Text>
          Loading movies...
        </Text>
      </View>
    );
  }

  renderMovie(movie) {
    return (
      <View style={styles.container}>
        <Image
          source={{uri: movie.posters.thumbnail}}
          style={styles.thumbnail}
        />
        <View style={styles.rightContainer}>
          <Text style={styles.title}>{movie.title}</Text>
          <Text style={styles.year}>{movie.year}</Text>
        </View>
      </View>
    );
  }
}

var styles = StyleSheet.create({
  container: {
    flex: 1,
    flexDirection: 'row',
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  thumbnail: {
    width: 53,
    height: 81,
  },
  rightContainer: {
    flex: 1,
  },
  title: {
    fontSize: 20,
    marginBottom: 8,
    textAlign: 'center',
  },
  year: {
    textAlign: 'center',
  },
  listView: {
    paddingTop: 20,
    backgroundColor: '#F5FCFF',
  },
});

AppRegistry.registerComponent('AwesomeProject', () => AwesomeProject);

튜토리얼을 마치며

여태까지 리액트 네이티브로 안드로이드와 iOS 앱을 동시에 개발하는 방법을 함께 알아봤습니다. 많은 코드를 작성하지 않고도 여러 운영체제에서 잘 작동하는 앱을 만들 수 있다는 점이 리액트 네이티브의 장점인 것 같습니다. 더 알아보고 싶은 분들을 위해 추가로 보면 좋을 자료를 공유하며 이번 튜토리얼을 마치겠습니다.

함께 쓰면 좋은 제품과 오픈소스 프로젝트

react-native-rmp

Realm 모바일 데이터베이스는 리액트 네이티브에서 사용할 수 있는 빠르고 간편한 객체형 데이터베이스입니다. 안드로이드와 iOS에서는 이미 널리 사용되고 있으며, 언제나 살아있는 객체로 최신의 데이터를 유지하면서 앱 모델 레이어를 정말 빠르게 작성하도록 도와줍니다. 리액트 네이티브 문서에서 단계별로 따라하면서 적용할 수 있습니다.

react-native-rmp2

Realm 모바일 플랫폼은 더 나은 리액티브 앱 개발을 위해 백엔드 솔루션을 제공합니다. 특별히 관리하지 않아도 데이터가 실시간 동기화되므로 네트워크 연동을 위한 별도의 코딩을 하지 않아도 됩니다. 현재는 안드로이드와 iOS만을 지원하지만, 곧 리액트 네이티브를 지원한다고 하니 Realm 모바일 데이터베이스와 함께 연동하시면 시너지를 누릴 수 있을 겁니다.

영문이기는 하지만 FinanceReactNative에서 증권 정보를 보여주기 위해 서드파티 API를 활용하는 방법을 확인할 수 있습니다. 차트와 데이터를 다루는 법도 포함된 오픈소스 프로젝트로 해당 분야에 관심있는 분께 많은 도움이 될 겁니다.

또한 Build a Coffee Finder App에서 Yelp API를 활용해 가까운 카페를 찾는 튜토리얼과 전체 코드를 살펴보는 것도 도움이 될 것 같습니다.

Using Stripe API in React Native with fetch에서는 Stripe API 사용법에 대해 알려주고 있습니다. 위 세 튜토리얼을 통해 서드파티 API 활용 능력을 향상해 보세요.

이전 시리즈로

이전 글을 보고 싶은 분은 아래 링크를 이용하세요.

다음: Realm Mobile Platform으로 실시간 협업 기능과 확장이 가능한 리액티브 앱을 만들어 보세요.

General link arrow white

컨텐츠에 대하여

이 컨텐츠는 저자의 허가 하에 이곳에서 공유합니다.


Leonardo YongUk Kim

Leonardo YongUk Kim is a software developer with extensive experience in mobile and embedded projects, including: several WIPI modules (Korean mobile platform based on Nucleus RTOS), iOS projects, a scene graph engine for Android, an Android tablet, a client utility for black boxes, and some mini games using Cocos2d-x.

4 design patterns for a RESTless mobile integration »

close