도움말 - 글감 수집하기 (인용)

도움말 - 부분 리뷰 작성하기

ReactXP - 스카이프(Skype)에서 만든 리액트(React) 기반 멀티 플랫폼 지원 라이브러리 사용 해보기

최근에 마이크로소프트(Microsoft)의 스카이프(Skype) 팀에서 ReactXP라는 라이브러리를 오픈소스화 했습니다. 이 라이브러리는 리액트(React)와 리액트 네이티브(React Native) 기반으로써, 크로스 플랫폼(cross-platform) 개발(iOS, 안드로이드, 윈도우, 웹)을 가능하게 합니다.

이 글에서는 ReactXP에 대해서 알아보고, 실제로 Redux todomvc앱을 ReactXP 구조로 변경 해보면서 이 라이브러리를 사용해 보도록 하겠습니다.

배경

리액트 기반으로 웹 뿐만 아니라 네이티브 개발에도 경험이 있으시다면 한번쯤은 한 소스로 멀티 플랫폼 개발을 할 순 없을까 생각 해봤을 겁니다. UI, 네비게이션(라우트), 특정 플랫폼 api를 제외 한다면, 리액트의 비지니스 로직과 컨테이너 컴포넌트 들은 공통으로 사용 할 수 있는데, 굳이 웹과 네이티브를 각각 개발해야 하는 이유가 있나. 이런 문제를 해결하기 위해 나온 라이브러리가 ReactXP 입니다.

아래는 ReactXP의 블로그 글을 요약 했습니다.

스카이프는 데스크탑, 랩탑, 모바일 폰, 태블릿, 브라우저 뿐만 아니라 TV와 차에까지 서비스를 제공 합니다. 기존의 스카이프 클라이언트 소스의 경우 각각의 네이티브 플랫폼을 사용하여 만들었습니다 (iOS는 Objective-C, 안드로이드는 Java, 웹에서는 HTML과 자바스크립트 등등). 하지만, 새로운 기능을 개발 할 때마다 각각의 플랫폼에 적용 해야하는 문제가 있었습니다. 이런 문제를 해결하는 기존의 라이브러리(Cordova, Xamarin)도 존재 하지만 이것 역시 문제를 완벽하게 해결 한 것은 아니었습니다. 예를들어, Cordova의 경우 웹기반 기술로 만들어 졌기 때문에 네이티브(native) 같지 않은 부자연스러움이 있고, Xamarin은 모바일 개발은 지원하지만 웹은 지원하지 않는 문제가 있습니다. 이런 종합적인 문제들을 해결하기 위해 ReactXP라는 크로스 플랫폼 라이브러리를 만들게 되었습니다. 현재 지원하는 플랫폼은 iOS, 안드로이드, 웹, 윈도우즈를 지원합니다.

한계

하지만, 이 라이브러리 역시 모든 문제를 해결하는 만능 열쇠는 아닙니다. 한계 역시 있습니다. 지금까지 사용 해보고 나서 제가 파악한 몇가지 단점은 아래와 같습니다:

  1. 네이티브 환경에서 메인 모듈은 반드시 "RXApp"으로 레지스터 되어야 한다.
  2. 웹 환경에서는 "app-container" DOM class가 반드시 필요하다.
  3. ReactXP에서 제공하는 컴포넌트를 사용하면 웹 관련 어트리뷰트를 사용 할 수 없다(id, className 등등).
  4. 웹에서 View 컴포넌트를 사용하면 시맨틱 태그(Semantic Tag)를 사용 할 수 없다(무조건 div 태그를 사용).
  5. 아직 문서가 많이 부족하고 예제 소스가 거의 없다.
  6. 리액트를 웹에서만 사용 해보신 분이라면 컴포넌트 prop들이 조금 상이 하기 때문에 처음에는 파악하는데 시간이 걸릴 수 있다.

간단하게 ReactXP에 대해 알아봤습니다. 그럼 redux-todomvc 소스를 ReactXP 기반으로 변경 해보도록 하겠습니다.

주의사항: react-native와 모바일 개발 환경이 구축 되어 있어야 디바이스 또는 에뮬레이터에서 실행 가능합니다.


ios 실행 화면
기존 웹 실행 화면

설치

먼저 기본 react-native init 터미널 명령어를 통해 프로젝트를 구성 합니다:

주의사항: 현재 ReactXP에서는 "RXApp"으로 되어야 하기 때문에 아래와 같이 시작해야 정상적으로 실행이 됩니다.

react-native init RXApp

설치가 완료되면 터미널에서 설치된 폴더로 이동 하고 앱을 실행 해봅니다:

cd RXApp && react-native run-ios

reactxp와 todomvc 관련 패키지를 설치 합니다:

yarn add reactxp react-dom react-redux redux todomvc-app-css

전체 package.json의 모습입니다.

{
"name": "RXApp",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start",
"ios": "react-native run-ios",
"web": "react-scripts start",
"web-watch": "webpack --progress --colors --watch",
"test": "jest"
},
"dependencies": {
"classnames": "^2.2.5",
"react": "16.0.0-alpha.6",
"react-addons-perf": "^15.4.2",
"react-dom": "^15.5.3",
"react-native": "0.43.2",
"react-redux": "^5.0.3",
"reactxp": "^0.42.0-rc.1",
"redux": "^3.5.2",
"todomvc-app-css": "^2.1.0"
},
"devDependencies": {
"babel-jest": "19.0.0",
"babel-preset-react-native": "1.9.1",
"jest": "19.0.2",
"react-scripts": "^0.9.5",
"react-test-renderer": "16.0.0-alpha.6",
"source-map-loader": "^0.2.1",
"webpack": "^2.3.3"
},
"jest": {
"preset": "react-native"
}
}

위의  scripts 처럼 설정 해놓으면, yarn ios ios 에뮬레이터를 yarn web 명령어로 웹에서 실행 할 수 있습니다.

여기에서는 기존 리덕스(redux)관련(reducers, constants, actions) 로직은 그대로 두고, 뷰 컴포넌트와 스타일만 ReactXP 기반으로 수정 하도록 하겠습니다.


containers/App.js

리액트에서 div의 경우 DOM 엘리먼트이기 때문에 네이티브 환경에서 사용 할 수 없습니다. 그렇기 때문에 웹, 네이티브 모두 사용 할 수 있는 reactxp의 View 컴포넌트를 사용 해야 합니다.

기존 App.js내에 <div> 엘리먼트를 reactxp의 View 컴포넌트로 변경 하겠습니다:

const App = ({todos, actions}) => (
<div>
<Header addTodo={actions.addTodo} />
<MainSection todos={todos} actions={actions} />
</div>
)

먼저, View, Styles를 reactxp에서 import 합니다:

import {
View,
Styles,
} from 'reactxp';

divView로 변경 합니다:

const App = ({todos, actions}) => (
<View style={viewStyle}>
<Header addTodo={actions.addTodo} />
<MainSection todos={todos} actions={actions} />
</View>
)

기본적인 레이아웃을 잡아 보도록 하겠습니다. 리액트 네이티브를 사용해본 경험이 있다면 알수 있지만, 기본적으로 리액트 네이티브의 레이아웃은 flex 기반 입니다. 웹 환경에서는 display  속성을 통해 다양한 방식으로 레이아웃을 구성 할 수 있지만, 리액트 네이티브는 기본적으로 flex 기반이기 때문에, 스타일링을 할때도 flex 기반으로 구성해야 합니다. 

const viewStyle = Styles.createViewStyle({
flex: 1,
flexGrow: 1,
justifyContent: 'center',
alignItems: 'center',
});

리액트 네이티브에서는 StyleSheet API의 create 함수를 사용하여 스타일을 정의 하지만, ReactXP에서는 Styles API에서 제공하는 createViewStylecreateTextStyle 함수를 사용해서 스타일을 정의 합니다. 

주의: 리액트 네이티브에서 create 함수를 사용 할 시에 nested 하게 스타일을 정의 할 수 있지만 ReactXP에서는 flat한 객체만 사용 할 수 있습니다:

// React Native StyleSheet example
const styles = StyleSheet.create({
container: {
borderRadius: 4,
borderWidth: 0.5,
borderColor: '#d6d7da',
},
title: {
fontSize: 19,
fontWeight: 'bold',
},
activeTitle: {
color: 'red',
},
}); 

components/Header.js

기존 Header 컴포넌트의 render 부분을 보면 TodoTextInput을 제외한 태그는 모두 html 태그이기 때문에 네이티브에서 사용이 불가능 합니다.

render() {
return (
<header className="header">
<h1>todos</h1>
<TodoTextInput newTodo
onSave={this.handleSave}
placeholder="What needs to be done?" />
</header>
)
}

header 태그를 Viewh1 태그는 Title 컴포넌트로 변경 해서 이 문제를 해결해 보겠습니다:

render() {
return (
<View>
<Title />
<TodoTextInput newTodo
onSave={this.handleSave}
placeholder="What needs to be done?" />
</View>
)
}

웹 소스의 <h1>todos</h1> 이 부분을 Title 컴포넌트로 분리 했습니다. 여기에서는 예제로 웹 화면과 네이티브(iOS)에서 다른 화면 구성을 보여주기 위해서 컴포넌트로 분리를 해봤습니다.

리액트 네이티브에서 컴포넌트 파일을 만들 때, 파일명.플랫폼명.js 규칙으로 만들면 각각의 플랫폼에 맞는 파일을 import 하도록 되어 있습니다. 예를들어,

import Title from './Title';

위처럼 했을 때, ios 환경에서는 Title.ios.js가 있다면 이것을 없다면 Title.js를 import 하도록 되어 있습니다.

ReactXP에서는 web과 windows를 지원하기 때문에 웹은 파일명.js, 윈도우즈의 경우 파일명.windows.js로 파일명을 만들게 되면 각 플랫폼에 맞는 파일을 import해서 사용하게 됩니다. 

여기서는 간단하게 Title 컴포넌트를 통해 각각의 플랫폼(web, ios)에서 어떻게 다르게 표현되는지 보도록 하겠습니다. 

iOS - Title/index.ios.js

import React from 'react';
import {
Text,
Styles,
} from 'reactxp';
const Title = () => (
<Text style={h1Style}>Todo Native</Text>
);
export default Title;
const h1Style = Styles.createTextStyle({
width: '100%',
fontSize: 50,
fontWeight: '100',
textAlign: 'center',
color: 'cyan',
});

Web - Title/index.js

import React from 'react';
import {
Text,
Styles,
} from 'reactxp';
const Title = () => (
<h1>Todo Web</h1>
);
export default Title;

만약 이렇게 따로 컴포넌트를 각각의 파일로 구성 한다면 웹에서는 기존의 html 태그를 사용 할 수 있습니다.

이렇게 파일로 분리해서 각각의 플랫폼에 대응 하도록 구성 할 수도 있지만, ReactXP의 Platform API의 getType 함수를 사용해서 각각의 플랫폼에 따른 분기처리도 가능 합니다.

Platform.getType(); // ios, android, web, windows

예제:

import {
Platform,
Styles,
} from 'reactxp';
const styles = Styles.createTextStyle({
color: (Platform.getType() === 'web') ? 'red' : 'blue',
});

components/TodoTextInput.js

render() {
return (
<input className={
classnames({
edit: this.props.editing,
'new-todo': this.props.newTodo
})}
type="text"
placeholder={this.props.placeholder}
autoFocus="true"
value={this.state.text}
onBlur={this.handleBlur}
onChange={this.handleChange}
onKeyDown={this.handleSubmit} />
)
}

input 태그는 html 태그이기 때문에 네이티브 환경에서는 사용 할 수 없습니다. 이것을 reactxp의 TextInput 컴포넌트로 대체 하고, onChange prop을 onChangeTextonKeyDownonKeyPress로 변경 합니다.

render() {
return (
<TextInput className={
classnames({
edit: this.props.editing,
'new-todo': this.props.newTodo
})}
style={inputStyle}
placeholder={this.props.placeholder}
autoFocus="true"
value={this.state.text}
onBlur={this.handleBlur}
onChangeText={this.handleChange}
onKeyPress={this.handleSubmit}
allowFontScaling
autoCorrect={false}
/>
)
}

TextInput 컴포넌트가 input 태그와 다른 점은 inputonChange 함수는 event를 받지만 TextInputonChangeText는 text 값을 받기 때문에 아래의 소스를

  handleChange = e => {
this.setState({ text: e.target.value })
}

이렇게 바뀌어야 합니다:

handleChange = text => {
this.setState({ text })
}

나머지,checkbox는 Button으로 대체하고, Footer도 HTML 태그를 reactxp와 맞는 컴포넌트로 변경 하면 됩니다. 

기존 웹기반으로 만들어진 소스를 비지니스 로직은 그대로 두고, HTML 태그와 대응 하는 reactxp 컴포넌트로 변경 하는 것만으로 웹과 네이티브 환경 모두에서 동작하는 앱을 만들어 봤습니다.

ReactXP의 기본 개념과 사용법에 대해서 간단하게 알아 봤습니다. 리액트 네이티브를 사용한 경험이 있다면 많이 어렵진 않겠지만, 리액트를 웹에서만 사용해봤다면 조금 헷갈릴 수 있는 부분이 있어 보입니다.

결론

리액트와 리액트 네이티브 모두 사용해본 경험이 있다면 웬만한 비지니스 로직 부분이 동일 한데 원 소스(one source)로 웹과 네이티브를 지원 할 순 없나라고 생각해보셨을 겁니다. 그런 생각을 해본적이 있다면, 눈여겨 볼만한 라이브러리라고 생각합니다. 하지만, 많은 장점이 있지만 이 라이브러리를 지금 바로 프로덕션에 적용 하기엔 아직은 무리가 있다고 생각합니다(위에서 다룬 한계 부분 때문에). 아직 발전해야 할 부분이 많이 있지만, 스카이프 팀에서 계속 ReactXP를 발전 시켜 나가기로 했고(한달에 한번 릴리즈를 목표), 마이크로소프트의 다른팀에서도 사용중이라고 하니 지켜볼만 하다고 생각합니다.


리뷰