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

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

이전에 이어서 AwesomeProject를 확장하도록 하겠습니다. 이번의 예제는 React Native Tutorial에 나오는 코드를 사용하고 확장할 것입니다.

작업 환경을 설정합시다.

더미 데이터와 간단한 코드로 시작합시다. 편의를 위해 여기의 코드는 안드로이드에서 테스트하는 것으로 가정하겠습니다. 둘의 가장 큰 차이는 사용하는 파일입니다. 안드로이드에서는 index.android.js를 사용하고 iOS에서는 index.ios.js 파일을 사용합니다.

만약 아이폰을 사용하면 index.ios.js을 수정하고 안드로이드를 사용한다면 index.android.js를 수정합니다. 둘다 사용한다면 둘 다 수정해보세요.

안드로이드 환경설정

코드를 최초 실행할 때는 아래와 같이 입력합시다.

react-native run-android

수행이 완료되면 Genymotion을 켜두었으면 Genymotion에서, 실 단말을 연결해두었다면 실 단말로 앱이 수행되게 됩니다.

만약 안드로이드 5.0 이상의 단말에서 화면이 제대로 나오지 않을 수 있습니다. 그런 경우 아래의 커맨드를 이용합시다.

adb reverse tcp:8081 tcp:8081

다시 앱을 실행해보면 정상적으로 표시되는 것을 알 수 있습니다.

iOS의 환경설정

iOS에서는 아래의 커맨드로 수행할 수 있습니다.

open AwesomeProject/ios/AwesomeProject.xcodeproj

혹은 다음의 방법도 있습니다.

react-native run-ios

첫번째 수정

index.android.js 파일을 열어 간단한 내용을 수정해봅시다. React Native!라는 문구를 React Native?로 바꾸겠습니다.

class AwesomeProject extends Component {
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>
          Welcome to React Native?
        </Text>
        <Text style={styles.instructions}>
          To get started, edit index.android.js
        </Text>
        <Text style={styles.instructions}>
          Shake or press menu button for dev menu
        </Text>
      </View>
    );
  }
}

수정된 내용을 보기 위해 (안드로이드)

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

수정된 내용은 앱에 바로 표시가 되지 않습니다. 화면을 갱신하기 위해 폰을 흔들어 봅시다. 메뉴 키가 있는 폰에서는 메뉴키를 눌러도 됩니다.

제일 위에 있는 Reload JS를 누르면 화면이 업데이트 됩니다.

하지만 이렇게 항상 수동으로 갱신하는 일은 귀찮은 일입니다. 다시 화면을 흔들어서 Enable Live Reload를 누릅시다. 이 기능을 활성화 시키면 우리가 수정한 내용으로 항상 업데이트가 이루어집니다.

수정된 내용을 보기 위해 (iOS)

iOS에서는 커맨드 + R을 입력해서 화면을 리프레쉬하는 것을 추천합니다.

목업 데이터 추가하기

상단의 import 구문에 Image를 추가합니다.

import React, {
  AppRegistry,
  Component,
  Image,
  StyleSheet,
  Text,
  View
} from 'react-native';

다음으로 MOCKED_MOVIES_DATA를 추가합시다. 개발 단계를 위해 마련한 목업 데이터입니다.

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

이제 AwesomeProject 클래스의 render 메서드를 수정합니다.

render() {
  var movie = MOCKED_MOVIES_DATA[0];
  return (
    <View style={styles.container}>
      <Text>{movie.title}</Text>
      <Text>{movie.year}</Text>
      <Image source={{uri: movie.posters.thumbnail}} />
    </View>
  );

AwesomeProject 클래스의 전체 모습은 아래와 같습니다.

class AwesomeProject extends Component {
  render() {
    var movie = MOCKED_MOVIES_DATA[0];
    return (
      <View style={styles.container}>
        <Text>{movie.title}</Text>
        <Text>{movie.year}</Text>
        <Image source={{uri: movie.posters.thumbnail}} />
      </View>
    );
  }
}

이후 앱을 확인하면 Title 2015를 볼 수 있습니다.

하지만 이미지는 제대로 표시되지 않습니다. 이미지를 제대로 표시하려면 Image의 크기가 설정되지 않아서입니다. 스타일쉬트 styles를 수정합시다.

var styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  thumbnail: {
    width: 53,
    height: 81,
  },
});

StyleSheet.create 메서드를 이용해서 스타일쉬트 styles를 만들었고 이 안에 containerthumbnail 두개의 항목이 있습니다.

flex: 1로 설정되었기 때문에 container의 사이즈는 유연하게 결정되며 주변 여백에 따라 그것을 채우도록 설정되어 있습니다.

flex를 포함하여 여러 내용을 알기 위해서는 플렉스박스(Flexbox) 모델을 참고해야하는데 여기에서는 간단히 설명하겠습니다. justifyContent는 메인 축 alignItems는 크로스 축을 기준으로 어떻게 정렬할지를 결정합니다. backgroundColor는 배경색상입니다. widthheight는 각기 가로와 세로폭입니다.

스타일쉬트를 수정하였으면 stylesthumbnail 항목, 즉 styles.thumbnailImage에서 쓰도록 설정합시다.

<Image
  source={{uri: movie.posters.thumbnail}}
  style={styles.thumbnail} />

제대로 설정했다면 AwesomeProject는 아래와 같은 모양이 됩니다.

class AwesomeProject extends Component {
  render() {
    var movie = MOCKED_MOVIES_DATA[0];
    return (
      <View style={styles.container}>
        <Text>{movie.title}</Text>
        <Text>{movie.year}</Text>
        <Image
          source={{uri: movie.posters.thumbnail}}
          style={styles.thumbnail} />
      </View>
    );
  }
}

이제 리로딩된 결과를 확인해봅시다.

스타일 수정하기

이제 스타일링을 조금 고쳐봅시다. 먼저 가로 방향으로 배치하기 위해 containerflexDirectionrow로 지정하겠습니다. 이는 나열되는 항목들을 가로로 배치하겠다는 의미입니다.

container: {
  flex: 1,
  flexDirection: 'row',
  justifyContent: 'center',
  alignItems: 'center',
  backgroundColor: '#F5FCFF',
},

만약에 세로로 표현하겠다면 flexDirection의 값을 column으로 지정합니다. 기본 값이 column이기 때문에 별도로 지정할 필요는 없습니다.

View 항목을 아래와 같이 수정합시다. Image를 앞으로 배치하고 두개의 Text는 새로운 View로 감싸도록 합시다.

<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>

두개의 TextView로 감싸고 별도의 스타일 styles.rightContainer를 지정한 걸 볼 수 있습니다. 이렇게 감싸둔 것은 계층적으로 레이아웃을 구성하기 위함입니다. 상위 요소 styles.container에서 row방향으로 flexDirection을 지정하여 가로 방향으로 레이아웃이 구성되지만 새로운 View에서 지정한 styles.rightContainer에서는 자식들을 세로 방향으로 레이아웃할 수 있습니다.

오른쪽 컨테이너 도입

styles.rightContainer을 위한 스타일쉬트도 구성해봅시다.

rightContainer: {
  flex: 1,
},

스타일쉬트를 바꾸면 다음과 같이 보입니다.

왼편으로 많이 쏠려 있습니다. 타이틀과 연도를 담은 Text를 중앙으로 정렬합시다.

title: {
  fontSize: 20,
  marginBottom: 8,
  textAlign: 'center',
},
year: {
  textAlign: 'center',
},

데이타 가져오기

실제 데이터를 가져오기 위해 변수 REQUEST_URL를 정의합니다.

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

AwesomeProject 클래스 내에 생성자를 만듭시다.

constructor(props) {
  super(props);
  this.state = {
    movies: null,
  };
}

this.state를 통해 moviesnull로 초기화합니다.

아래의 메서드들도 클래스 내에 추가합니다.

componentDidMount() {
  this.fetchData();
}

컴포넌트가 마운트되면 (componentDidMount) fetchData를 수행합니다. AwesomeProject가 연결되어 화면에 표시될 때 데이터 로딩을 요청하는 것입니다.

fetchData() {
  fetch(REQUEST_URL)
    .then((response) => response.json())
    .then((responseData) => {
      this.setState({
        movies: responseData.movies,
      });
    })
    .done();
}

실제 데이터를 가져오는 메서드 fetchData입니다. 이전에 정의한 REQUEST_URL URL을 이용해서 데이터를 가져옵니다.

fetch가 끝나면 .then으로 연결된 프로미스(promise) 체인이 수행됩니다. .done은 그 프로미스 체인이 끝났다는 것을 의미합니다. 데이터를 가져오면 먼저 response.json()를 통해 json 데이터를 파싱하고 그 결과를 this.setState를 통해 컴퍼넌트의 상태를 설정합니다.

리액트 네이티브는 this.setState가 상태를 바꿀 때 마다 렌더링을 새로 합니다. 그렇기 때문에 fetch가 수행이되고 json 파싱을 거쳐 상태 설정이 완료되면 다시 그려지게 되는 것입니다.

render를 아래와 같이 수정합시다.

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

  var movie = this.state.movies[0];
  return this.renderMovie(movie);
}

데이터가 없는 경우 (아직 fetchData가 끝나지 않은 경우) renderLoadingView가 호출되며 데이터를 가져오면 첫번째 데이터(this.state.movies[0])만 renderMovie의 인자로 전달하여 화면에 표시합니다.

데이터가 없을 때 표시되는 renderLoadingView는 아래와 같습니다.

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>
  );
}

데이터가 있는 경우 파라미터 movie로 전달 받아 출력합니다.

제대로 수행이 되면 아래와 같이 항목 하나의 정보를 네트워크에서 받아 표시합니다.

리스트 뷰

여러개의 항목을 출력하기 위해서는 리스트 뷰를 이용합니다. 먼저 ListViewimport합니다.

import React, {
  AppRegistry,
  Component,
  Image,
  ListView,
  StyleSheet,
  Text,
  View
} from 'react-native';

이제 render 메서드가 ListView를 쓰도록 수정합니다.

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

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

개별 라인은 renderRow에 지정한 this.renderMovie 메서드에 의해 처리됩니다. 이제 this.state.loaded에 의해 로딩 중인지를 판단합니다.

dataSourcethis.state.dataSource를 사용합니다. 해당 상태를 초기화하고 갱신하는 코드가 필요합니다. 생성자에 초기화하는 코드를 먼저 추가합시다.

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

fetchData 메서드도 해당 상태를 갱신하도록 수정합니다.

fetchData() {
  fetch(REQUEST_URL)
    .then((response) => response.json())
    .then((responseData) => {
      this.setState({
        dataSource: this.state.dataSource.cloneWithRows(responseData.movies),
        loaded: true,
      });
    })
    .done();
}

json 데이터를 이용해 this.state.dataSource를 업데이트합니다.

마지막으로 스타일쉬트를 업데이트합시다.

listView: {
  paddingTop: 20,
  backgroundColor: '#F5FCFF',
},

코드를 다 수정하면 다음과 같은 화면이 뜹니다.

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

react-native-rmp

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

react-native-rmp2

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

React native navbar는 리액트 네이티브에서 편하게 네비게이션 바를 변경할 수 있도록 돕는 라이브러리입니다. 사용자 인터페이스를 개선하고 싶은 분들께 추천합니다.

모바일 앱에서는 사이드 메뉴도 자주 사용되는데요. React native side menu에서 간단하게 사이드메뉴를 만들어주는 라이브러리를 다운받을 수 있습니다.

더 많은 리액트 네이티브 라이브러리가 필요하다면 React.parts를 방문해 보세요.

다음으로

지금까지 네트워크와 리스트를 다루어 보았습니다. 다음 시간에는 Realm을 결합하여 데이터를 저장하고 불러와 보겠습니다. 아래 링크에서 보실 수 있습니다.

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

다음: 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