최근 React Native로 개발하는 프로젝트의 UI개발을 맡아 진행하였습니다.

컴포넌트나 스타일 등 웹에서 작업하던 것과 달라 초기에 많은 시행착오를 겪었는데요
저와 같이 처음 React Native를 경험할 분들에게 기본적인 가이드가 있으면 좋을 것 같다고 생각해서 이 글을 작성하게 되었습니다.

* React Native는 리액트 기반으로 동작되기 때문에 리액트 기본 지식과 ES6 기본 개념을 숙지하고 있어야 하며 UI 작업을 위해 CSS Flexbox에 대한 이해가 필요합니다. 이 글에서도 리액트의 기본 동작, Flexbox의 동작 등의 설명은 생략하였으니 참고해주세요. 

React Native UI 개발 시작하기

React Native(이하 RN)는 리액트를 기반으로 ‘모바일 웹 앱’이나 ‘하이브리드 앱’이 아닌 ‘네이티브 앱’을 제작할 수 있는 오픈소스 프레임워크입니다.
RN에서의 UI개발은 리액트 환경에서 JSX와 CSS를 작성하는 방식과 유사하지만 네이티브 개발이기 때문에 HTML 태그와 CSS를 사용할 수 없습니다.
때문에 마크업 작성에 능숙하고 리액트를 다룰줄 아는 UI개발자라도 RN을 처음 접한다면 혼란에 빠지기 쉽습니다.
이 글에서는 RN UI개발이 리액트를 사용해 웹에서 작업하던 방식과 비교해 어떤 특징과 차이점이 있는지 알아보려 합니다.

1. style

RN의 스타일은 대게 웹에서의 CSS동작과 일치합니다.
다만 웹에서 id, class 등의 선택자를 이용해 스타일을 적용하는 방식이 아닌 StyleSheet.create 메소드를 이용해 스타일 object를 작성하여 컴포넌트에 style props를 전달합니다. 아래 예시 코드와 이미지를 참고해 RN 스타일 방식이 CSS와 어떻게 다른지 한 번 알아보도록 하겠습니다.

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

const styles = StyleSheet.create({
  bigBlue: {
    color: 'blue',
    fontWeight: 'bold',
    fontSize: 30,
  },
  red: {
    color: 'red',
  },
});

export default class LotsOfStyles extends Component {
  render() {
    return (
      <View>
        <Text style={styles.red}>just red</Text>
        <Text style={styles.bigBlue}>just bigBlue</Text>
        <Text style={[styles.bigBlue, styles.red]}>bigBlue, then red</Text>
        <Text style={[styles.red, styles.bigBlue]}>red, then bigBlue</Text>
      </View>
    );
  }
}

1.1 기본 문법

RN의 스타일은 자바스크립트를 이용하여 object로 작성하기 때문에 문법 규칙도 object를 작성할 때와 동일합니다.
CSS 문법 역시 key, value 값을 가지는 object 형태인데 모양이 비슷하기 때문에 RN 작업에 익숙해지기 전이라면 손에 익어버린 CSS 문법은 오히려 독이 될 수 있습니다.
조금만 신경써서 작업한다면 금방 익숙해지지만 초기 작업시에는 문법 오류로 인한 에러 화면을 자주 만나볼 수 있을텐데요. 문법 오류는 알아차리기 쉽지만 네이티브 특성상 빌드를 해야하고 디버깅이 웹에서만큼 자유롭지는 않아 잦은 문법 오류는 생산성 저하로 이어질 수 있습니다.
RN 작업을 한다면 버려야하는 CSS 작성 습관을 알아보고 적응해봅시다.
RN에 익숙해지면 CSS 작업을 할 때 다시 혼란이 찾아옵니다.

id, 클래스 등의 선택자 사용

RN에서 스타일의 구별은 id나 클래스 선택자가 아닌 object의 namespace를 이용합니다.
클래스 선택자에 익숙해져 .을 습관적으로 붙이지 않았는지 :이 누락되지는 않았는지 확인합시다.

CSS

.red { ... }

RN

red { ... }

; 사용

각 스타일 속성의 구분은 ;가 아닌 ,로 합니다.
스타일 object 뒤에 다른 스타일 object의 구분 역시 ,를 사용합니다.

CSS

.bigBlue {
    color: blue;
    font-weight: bold;
    font-size: 30px;
}
.red {
    color: red;
}

RN

bigBlue: {
    color: 'blue',
    fontWeight: 'bold',
    fontSize: 30,
},
red: { 
    color: 'red',
}

스타일 속성명의 하이픈(‘-’)을 사용

눈치채셨겠지만 스타일 속성명(property)의 단어 구분은 하이픈(‘-‘)이 아닌 카멜케이스를 이용합니다.
속성값(value)의 경우 하이픈을 사용하며 이 경우 문자열이기 때문에 따옴표를 사용해야합니다.

CSS

.content {
    justify-content: space-between;
    background-color: #eee;
}

RN

content: {
    justifyContent: 'space-between',
    backgroundColor: '#eee',
},
‘px’, ‘em’ 등의 단위 사용

RN에서는 fontSize: 30, margin: 10와같이 ‘px’, ‘em’ 등의 단위를 생략합니다. 적용되는 단위는 iOS에서는 논리 픽셀, 안드로이드 환경에서는 iOS의 논리 pt와 유사한 DIP(DP)입니다. 

관련 링크

1.2 RN 스타일과 CSS의 차이점

RN의 스타일은 CSS의 모든 속성과 값을 지원하지 않습니다. 또 ios와 android 중 하나의 os에서만 동작하는 스타일도 있기 때문에 내가 사용하려는 스타일이 RN에서 지원하고 있는지 확인해야합니다. 반대로 CSS에서는 존재하지 않지만 리액트 네이티브에서는 존재하는 속성이 있으며 알아두면 유용하게 사용할 수 있습니다.

축약형이 존재하지 않는다.

RN에서는 축약형을 사용하지 않습니다.
margin이나 padding의 경우 축약형 대신 상하, 좌우 값을 한 번에 적용할 수 있는 속성이 존재합니다. (marginVerticalpaddingHorizontal 등 )

CSS

.item {
    flex: 1 1 auto;
    margin: 0 4px 0 6px; 
}

RN

item: {
    flexGrow: 1,
    flexShrink: 1,
    flexBasis: 'auto',
    marginVertical: 0,
    marginBottom: 4,
    marginLeft: 6,
},

스타일 우선 순위가 CSS와 다르다.

RN은 스타일을 props로 전달하기 때문에 inline 방식이나 internal, external에 의미가 없고 선택자, 구체성에 따른 우선 순위도 고려하지 않습니다.
우선 순위에 영향을 주는 딱 한 가지는 컴포넌트에 스타일을 전달할 때 나중에 전달하는 스타일이 항상 우선순위가 높다는 것입니다.
먼저 CSS 코드를 살펴봅시다.

CSS/HTML

.bigBlue {
  color: blue;
  font-weight: bold;
  font-size: 30px;
}

.red {
  color: red;
}
<div class="red bigBlue">red, not blue</div>

html에서 클래스 순서(class="red bigBlue")에 관계없이 color값은 CSS의 우선 순위 규칙에 따라 스타일이 나중에 선언된 red값이 적용되는 것을 볼 수 있습니다.

RN

bigBlue: {
    color: 'blue',
    fontWeight: 'bold',
    fontSize: 30,
},
red: { 
    color: 'red',
}
render() {
return (
    // 여러개의 스타일을 전달할 때 배열을 이용한다.
    <Text style={[styles.red, styles.bigBlue]}>red, then bigBlue</Text>
);
}

스타일 선언 순서와 상관 없이 blue값이 적용된 것을 볼 수 있습니다.(먼저 red값이 전달되고 후에 blue값이 갱신)

의사 클래스(가상 클래스), 가상요소, 형제 선택자, 자식 선택자 등을 사용할 수 없다.

RN의 스타일은 :first-child:nth-child:focus 등의 의사 클래스, 자식선택자(>), 형제 선택자(+)와 가상요소( ::before::after ), attribute를 이용한 속성 선택자 등 CSS에서 편리하게 사용할 수 있는 기능을 제공하지 않습니다. 해당 역할들이 필요하다면 스크립트 처리를 해야하기 때문에 작업시 꽤 불편할 수 있습니다.

위의 가격표를 보면 최상단과 최하단을 제외하고 item 사이에 border가 있고 짝수번째 행에 background-color가 적용되어있습니다.
CSS에서는 nth-child와 형제 선택자를 사용하면 간단하지만 RN에서 사용할 수 없습니다. 그럼 위의 UI를 구현하기 위해 스크립트를 적용해봅시다.

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

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  item: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    height: 50,
    paddingHorizontal: 20,
    borderTopWidth: 1,
    borderColor: '#000',
  }
});

const data = [
  {
    name: 'item1',
    price: '$100'
  },
  {
    name: 'item2',
    price: '$120'
  },
  {
    name: 'item3',
    price: '$130'
  },
  {
    name: 'item4',
    price: '$200'
  },
  {
    name: 'item5',
    price: '$500'
  }
]
export default class AppView extends Component {
  render() {
    return (
      <SafeAreaView style={styles.container}>
        <View>
          {
            data.map((item, index) => (
              <View 
                style={[
                  styles.item,
                  (index === 0) && { borderTopWidth: 0 }, // CSS: first-child
                  (index % 2 === 1) && { backgroundColor: '#eee' } // CSS: nth-child(even)
              ]}>
                <Text>{item.name}</Text>
                <Text>{item.price}</Text>
              </View>
            ))
          }
        </View>
      </SafeAreaView>
    );
  }
}

map함수와 index를 이용해 첫 번째와 짝수번째 item에 스타일을 적용하였습니다. 위 방법은 map을 사용하기 때문에 데이터 배열이 필요하기 때문에 데이터를 사용하지 않는 디자인적인 요소라면 의미없는 배열을 사용해야합니다. 
또한 item들이 서로 관련되어 있지 않아 map함수와 index의 활용이 어렵다면 사용하기 힘든 단점이 있습니다.

스크립트의 장점을 활용할 수 있다.

스타일 역시 자바스크립트로 작성되기 때문에 편리한 부분도 존재합니다.
자주 사용하는 값이나 관련있는 수치들을 변수로 사용하면 코드 관리가 용이합니다.
또 스타일 코드 내에 분기 로직을 삽입할 수 있습니다. 안드로이드와 ios 양 쪽 모두 개발을 진행할 경우 스타일 분기가 필요할 때가 있는데 스타일 내부에 os분기를 사용하면 주석을 남기지 않더라도 분기 내용을 파악할 수 있어 협업이나 유지보수에 도움이 됩니다.

import {Platform, StyleSheet} from 'react-native';

const styles = StyleSheet.create({
  container: {
    flex: 1,
    ...Platform.select({
      ios: {
        backgroundColor: 'red',
      },
      android: {
        backgroundColor: 'blue',
      },
    }),
  },
});

위 예시는 Platform 모듈을 이용해 os에 따라 스타일을 삽입하는 방법입니다. https://reactnative.dev/docs/platform-specific-code#platform-module

2. RN의 flex

앞에서 언급했던 것처럼 CSS에서 존재하는 속성이나 값이 리액트 네이티브에서 존재하지 않을 수 있습니다. 가장 대표적인 점이 display 속성의 값이 flex와 none 두 가지 밖에 없다는 것입니다.
display 속성의 기본 값은 flex이며 컴포넌트의 렌더링 유무는 display: none보다는 props나 state값으로 결정될 때가 많기 때문에 스타일에 display 속성을 선언할 일은 거의 없습니다.

messageImage_1582795524443.jpg

대부분의 UI는 flex로 표현이 가능하지만 display 속성이 flex밖에 없다는 것은 조금 아쉬운 부분입니다. 그래서 flex로 구현이 불가능한 UI는 표현할 수 없습니다.

예를들어 위의 UI는 웹에서는 float을 쓰면 간단하게 구현가능하지만 flex로는 구현할 수 없습니다.
따라서 float을 지원하지 않는 RN에서는 해당 UI를 구현할 수 없습니다.

그렇다면 RN의 flex는 CSS의 flex와 동일할까요?
매우 유사하지만 몇 가지 차이점이 있으며 웹에서와 같은 스타일을 적용하여도 다른 결과가 노출될 수 있습니다.

2.1 main axis

CSS의 flex와 RN의 flex는 main axis가 반대입니다.
즉 flexDirection의 기본 값이 RN에서는 column입니다.
flex 컨테이너와 아이템은 main axis(주축)과 cross axis(교차축)에 따라 정렬되기 때문에 flex에 대한 개념이 제대로 잡혀있지 않다면 justifyContent, alignItems 등 여러 속성을 사용할 때 헷갈릴 수 있기 때문에 주의가 필요합니다.

import React, { Component } from 'react';
import { StyleSheet, View, SafeAreaView } from 'react-native';


const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'space-between',
  },
  item: {
    height: 50,
    backgroundColor: 'steelblue',
  }
});

export default class AppView extends Component {
  render() {
    return (
      <SafeAreaView style={styles.container}>
        <View style={styles.item} />
        <View style={styles.item} />
        <View style={styles.item} />
      </SafeAreaView>
    );
  }
}

2.2 지원하지 않는 flex 관련 속성 

앞에서 언급했듯이 RN은 CSS의 모든 속성들을 지원하는 것은 아니며 flex 관련 속성 역시 마찬가지입니다.
flex: 양수 값은 사용할 수 있지만 flex: 1 1 auto 등의 flex 축약형이나 flex-flow 를 지원하지 않습니다.
또한 order 역시 지원하지 않아 이를 이용한 flex item의 정렬은 불가능합니다.
아래는 RN에서 지원하는 flex 관련 속성입니다.

속성명(property)값(value)기본값
flexnumber
flexDirection‘row’ | ‘row-reverse’ | ‘column’ | ‘column-reverse’‘column’
flexGrownumber0
flexShrinknumber1
flexBasisnumber | string‘auto’
flexWrap‘wrap’‘nowrap’
justifyContent‘flex-start’ | ‘flex-end’ | ‘center’ | ‘space-between’ | ‘space-around’ | ‘space-evenly’‘flex-start’
alignItems‘flex-start’ | ‘flex-end’ | ‘center’ | ‘stretch’ | ‘baseline’‘stretch’
alignContent‘flex-start’ | ‘flex-end’ | ‘center’ | ‘stretch’ | ‘space-between’ | ‘space-around’‘flex-start’
alignSelf‘auto’ | ‘flex-start’ | ‘flex-end’ | ‘center’ | ‘stretch’ | ‘baseline’‘stretch’

3. component

html에서 div, span, img 등의 태그를 사용하는 것 처럼 RN도 UI를 구현을 위한 기본 컴포넌트를 제공합니다.
html의 태그들과 비슷한 역할을 하는 컴포넌트도 있지만 새로운 기능을 가지고 있는 컴포넌트도 있으며 기본 컴포넌트를 편의에 따라 수정해서 사용하기도 합니다.

3.1 HTML 태그와 비슷한 역할을 하는 컴포넌트

View
View 컴포넌트는 UI를 구축하기 위한 가장 기본적인 구성요소로서 웹에서는 div와 사용성이 유사합니다. 중첩이 가능하며 레이아웃을 구축하기 위해서 가장 많이 사용합니다. 일부 터치 핸들링 및 접근성 제어가 가능합니다.

import React, { Component } from 'react';
import { StyleSheet, View, SafeAreaView } from 'react-native';


const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  header: {
    height: 60,
    backgroundColor: '#e93e42',
  },
  content: {
    flex: 1,
    backgroundColor: '#f5a942',
  },
  footer: {
    height: 50,
    backgroundColor: '#4fbc7a',
  },
});

export default class AppView extends Component {
  render() {
    return (
      <SafeAreaView style={styles.container}>
        <View style={styles.header}></View>
        <View style={styles.content}></View>
        <View style={styles.footer}></View>
      </SafeAreaView>
    );
  }
}

Text

Text는 텍스트를 표현하기 위한 컴포넌트로 리액트 네이티브에서 기본적으로 제공하는 컴포넌트에서는 Text와 TextInput을 사용해야만 텍스트를 삽입할 수 있기 때문에 필수적으로 사용되는 컴포넌트입니다.
Text 내부에 사용할 수 있는 컴포넌트는 매우 제한적이며 내부 요소의 스타일 제약을 받을 수 있습니다.

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


const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  icon: {
    width: 10,
    height: 10,
  },
  icon2: {
    width: 10,
    height: 10,
    marginRight: 10,
  },
});

export default class AppView extends Component {
  render() {
    return (
      <SafeAreaView style={styles.container}>
        <Text>
          리뷰를 등록하고 <Image style={styles.icon} source={require('~/assets/images/icon_coin.png')} />100을 받으세요!
        </Text>
        <Text>
          리뷰를 등록하고 <Image style={styles.icon2} source={require('~/assets/images/icon_coin.png')} />100을 받으세요!
        </Text>
      </SafeAreaView>
    );
  }
}

위의 예시를 보면 Text 내부에 Image 컴포넌트를 사용 가능하지만 margin값을 넣자 렌더링에 문제가 생긴 것을 볼 수 있습니다.
View 컴포넌트의 경우 Text 컴포넌트 내부에 선언시 에러가 발생합니다.(스펙상 ios환경에서는 View가 Text의 자식으로 올 수 있다고 명시되어 있지만 현재 버전에서는 에러가 발생합니다)
따라서 Text 컴포넌트 내부에 다른 컴포넌트 사용은 최대한 지양하고 불가피하게 써야하는 경우에는 혹시 이슈가 없는지 꼼꼼히 살펴봐야합니다.

Text 컴포넌트는 중첩해서 사용할 수 있습니다. 자식 Text 컴포턴트는 부모 Text 컴포넌트의 스타일을 상속받으며 텍스트 하이라이팅 처리를 할 떄 주로 사용합니다.

import React, { Component } from 'react';
import { StyleSheet, Text, SafeAreaView } from 'react-native';


const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
});

export default class AppView extends Component {
  render() {
    return (
      <SafeAreaView style={styles.container}>
        <Text style={{fontWeight: 'bold'}}>
          I am bold
          <Text style={{color: 'red'}}>
            and red
          </Text>
        </Text>
      </SafeAreaView>
    );
  }
}

중첩된 Text 컴포넌트는 inline-level처럼 동작하는데 margin, padding, border 등 box-model에 관련된 스타일이 적용되지 않다는 것을 주의해주세요.

import React, { Component } from 'react';
import { StyleSheet, Text, SafeAreaView } from 'react-native';


const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  text: {
    // 중첩된 텍스트는 margin, padding, border가 적용되지 않음
    marginVertical: 50,
    padding: 20,
    borderWidth: 1,
    backgroundColor: 'yellow',
  },
});

export default class AppView extends Component {
  render() {
    return (
      <SafeAreaView style={styles.container}>
        <Text style={styles.text}>
          Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse corporis voluptatum quasi iste fugiat earum quas. Nisi adipisci hic, repudiandae culpa ab possimus fuga! Quasi neque dignissimos aliquid veritatis error!
        </Text>

        {/* 중첩된 텍스트 */}
        <Text>
          <Text style={styles.text}>
            Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse corporis voluptatum quasi iste fugiat earum quas. Nisi adipisci hic, repudiandae culpa ab possimus fuga! Quasi neque dignissimos aliquid veritatis error!
          </Text>
        </Text>
      </SafeAreaView>
    );
  }
}

Touchable

RN에는 View나 Text에도 터치 핸들링을 제공하고 있으며 button 역할을 하는 컴포넌트가 여러개 존재합니다.
다양한 Touchable 컴포넌트를 이용해 작업할 수 있지만 각 컴포넌트의 특징과 props를 알고 있어야 하고 앱 내 터치 요소에 통일성을 해칠 수 있습니다.
프로젝트에 적절한 하나의 Touchable 컴포넌트를 기본적으로 사용하고 다른 Touchable 컴포넌트는 필요한 경우에만 사용하는 방식을 고려해볼 수 있습니다.

  • Button
    기본적인 버튼 컴포넌트입니다. style props를 이용해 스타일링을 할 수 없기 때문에 UI개발시 활용도가 떨어집니다.
    • 주요 props
      color : ios의 경우 버튼 텍스트 color, 안드로이드의 경우 버튼 배경색이 변경됩니다.
  • TouchableHightlight
    터치시 하이라이트가 발생합니다. 내부에 반드시 하나의 자식 컴포넌트를 삽입해야합니다.
    여러개의 컴포넌트가 필요하다면 View나 flagment(<></>)를 이용해 그룹화해야합니다.
    • 주요 props
      underlayColor : 터치시 하이라이팅되는 색상을 지정합니다.
import React, { Component } from 'react';
import { StyleSheet, Image, Text, SafeAreaView, TouchableHighlight } from 'react-native';


const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  button: {
    flexDirection: 'row',
    justifyContent: 'center',
    alignItems: 'center',
    alignSelf: 'center',
    width: 150,
    height: 50,
    marginTop: 50,
    padding: 10,
    borderRadius: 5,
    backgroundColor: 'powderblue',
  },
  buttonText: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#fff'
  },
  icon: {
    width: 15,
    height: 15,
    marginRight: 5,
  }
});

export default class AppView extends Component {
  render() {
    return (
      <SafeAreaView style={styles.container}>
        <TouchableHighlight
          onPress={()=>{}}
          underlayColor="red"
          style={styles.button}
      >
          <> {/* 하나의 자식 요소만 올 수 있으므로 flagment로 그룹화 */}
            <Image style={styles.icon} source={require('~/assets/images/icon_coin.png')} />
            <Text style={styles.buttonText}>100코인 획득</Text>
          </>
        </TouchableHighlight>
      </SafeAreaView>
    );
  }
}
  • TouchableOpacity
    터치시 opacity값이 적용됩니다. 다른 Touchable 컴포넌트와 달리 여러개의 자식 요소가 올 수 있습니다.
    • 주요 props
      activeOpacity : 터치시 적용되는 opacity값을 설정합니다.(0~1)
 render() {
    return (
      <SafeAreaView style={styles.container}>
        <TouchableOpacity
          onPress={()=>{}}
          activeOpacity={0.3}
          style={styles.button}
      >
          <Image style={styles.icon} source={require('~/assets/images/icon_coin.png')} />
          <Text style={styles.buttonText}>100코인 획득</Text>
        </TouchableOpacity>
      </SafeAreaView>
    );
  }
  • TouchableNativeFeedback
    터치시 사용자가 정의한 피드백을 표현할 수 있으며 단일 View 컴포넌트만 자식 요소로 가질 수 있습니다. Android 환경만 지원합니다.

Image

html의 img태그처럼 이미지를 표현할 때 사용하는 컴포넌트입니다. source props에 이미지 경로를 전달해 사용합니다.
앱 내부의 이미지파일을 불러올 땐 require를 이용하며 widthheight를 적용하지 않으면 이미지 원본의 사이즈대로 렌더링됩니다.
네트워크 이미지나 데이터 url 이미지를 사용할 때는 image의 widthheight 등 영역을 확보해주지 않으면 영역이 잡히지 않습니다.

render() {
    return (
      <SafeAreaView style={styles.container}>
        {/* 내부 이미지 호출 방식*/}
        <Image
          source={require('~/assets/images/icon_coin.png')}
        />

        {/* 네트워크 이미지 호출 방식*/}
        <Image
          style={{width: 50, height: 50}}
          source={{uri: 'https://reactnative.dev/img/tiny_logo.png'}}
        />

        {/* data URI 이미지 호출 방식*/}
        <Image
          style={{width: 66, height: 58}}
          source={{uri: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADMAAAAzCAYAAAA6oTAqAAAAEX...'}} 
        />
      </SafeAreaView>
    );
}

3.2 HTML에서 보지못했던 새로운 기능을 가진 컴포넌트

ImageBackground

background 관련 스타일은 backgroundColor만 존재하는 리액트 네이티브에서 배경이미지를 필요할 때 사용하는 컴포넌트 입니다.
CSS의 background-image를 적용했을 때와 같이 동작하며 Image 컴포넌트와 사용방법이 유사합니다.

import React, { Component } from 'react';
import { StyleSheet, ImageBackground, Text, SafeAreaView,  } from 'react-native';

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  textWrap: {
    height: 300,
    justifyContent: 'center',
    alignItems: 'center',
  },
  text: {
    fontSize: 30,
    fontWeight: 'bold',
    color: '#fff'
  },
});

export default class AppView extends Component {
  render() {
    return (
      <SafeAreaView style={styles.container}>
        <ImageBackground style={styles.textWrap} source={{ uri: 'https://images.pexels.com/photos/255379/pexels-photo-255379.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500'}}>
          <Text style={styles.text}>Lorem ipsum dolor sit amet.</Text>
        </ImageBackground>
      </SafeAreaView>
    );
  }
}

SafeAreaView

SafeAreaView는 기기의 안전한 영역 경계 내에서 콘텐츠를 렌더링할 때 사용합니다.
만약 SafeAreaView를 적용하지 않는다면 기기의 둥근 코너나 카메라 노치와 같은 화면의 물리적인 부분과, 탐색 모음, 탭 바, 시간, 배터리 등 도구 모음 등의 뷰가 렌더링되는 내용과 겹치는 현상이 발생하게됩니다.
SafeAreaView는 자동으로 padding을 적용하여 위와 같은 문제를 해결하고 안전한 영역에 콘텐츠가 렌더링될 수 있도록 합니다.
현재 ios 11 버전 이상의 ios기기에만 적용되며 보통 페이지의 최상위 래퍼 컴포넌트로 많이 사용합니다.

import React, { Component } from 'react';
import { StyleSheet, View, SafeAreaView } from 'react-native';

const styles = StyleSheet.create({
  wrap: {
    flex: 1,
  },
  container: {
    flex: 1,
    backgroundColor: 'powderblue',
  }
});

export default class AppView extends Component {
  render() {
    return (
      <SafeAreaView style={styles.wrap}>
        <View style={styles.container}>

        </View>
      </SafeAreaView>
    );
  }
}

* 주의사항 *
SafeAreaView 영역의 높이와 디바이스의 높이와 일치하지 않는다면 padding이 적용되지 않습니다. (margin 적용 등)

ScrollView

RN은 웹 브라우저처럼 컨텐츠가 길어지면 자동적으로 스크롤을 생성하지 않습니다.
스크롤이 필요하다면 ScrollView 컴포넌트를 사용하며 CSS에서 요소에 overflow: auto을 선언한 것과 유사하게 동작합니다.
RN은 position: 'fixed'를 지원하지 않지만 ScrollView를 사용해야만 스크롤이 생성되기 때문에 뷰포트에 고정된 UI를 구현할 수 있습니다.
또한 horizontal props를 통해 가로 스크롤을 제어할 수 있습니다.
ScrollView는 flex: 1이 기본 값으로 설정되어있습니다.

import React, { Component } from 'react';
import { StyleSheet, SafeAreaView, View, Text, ScrollView } from 'react-native';

const styles = StyleSheet.create({
  wrap: {
    flex: 1,
  },
  header: {
    height: 60,
    borderBottomWidth: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  headerText: {
    fontSize: 20,
    fontWeight: 'bold',
  },
});

export default class AppView extends Component {
  render() {
    return (
      <SafeAreaView style={styles.wrap}>
        <View style={styles.header}>
          <Text style={styles.headerText}>Header</Text>
        </View>
        <ScrollView>
        {[...Array(100).keys()].map(()=> (
          <Text>Lorem ipsum dolor sit amet.</Text>
        ))}
        </ScrollView>
      </SafeAreaView>
    );
  }
}

KeyboardAvodingView

KeyboardAvodingView는 키보드가 올라올 경우 컨텐츠가 키보드에 가려지는 문제를 해결하는 방법을 제공하는 컴포넌트입니다.
behavior props를 이용해 키보드 회피 방법을 정할 수 있습니다.
input이 제공되어 키보드를 사용해야하는 페이지에서 필수적으로 사용되는 컴포넌트입니다.

FlatList

FlatList는 목록형 UI를 렌더링 할 때 유용한 기능을 제공하는 컴포넌트입니다. 목록이 길어지면 자동적으로 내부에 ScrollView가 적용되므로 데이터에 따라 스크롤 처리가 필요하다면 ScrollView보다 좋은 선택일 수 있습니다. header, footer, 데이터가 없을 경우 노출될 UI, 아래로 당겼을 때 refresh 기능 등을 props를 통해 설정할 수 있으며 행 별로 스타일 제어가 가능해 그리드 UI를 구현할 때도 매우 편리합니다. data와 renderItem props는 필수적으로 선언해주어야합니다

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

const styles = StyleSheet.create({
  wrap: {
    flex: 1,
  },
  header: {
    height: 60,
    borderBottomWidth: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  headerText: {
    fontSize: 20,
    fontWeight: 'bold',
  },
  imageRow: {
    justifyContent: 'space-between',
  },
  image: {
    width: 120,
    height: 120,
  },
});

const data = [
  {
    id: 1,
    uri: 'https://i.ytimg.com/vi/ZYvvmsrDOj8/maxresdefault.jpg'
  },
  {
    id: 2,
    uri: 'https://i.ytimg.com/vi/ZYvvmsrDOj8/maxresdefault.jpg'
  },
  {
    id: 3,
    uri: 'https://i.ytimg.com/vi/ZYvvmsrDOj8/maxresdefault.jpg'
  },
  {
    id: 4,
    uri: 'https://i.ytimg.com/vi/ZYvvmsrDOj8/maxresdefault.jpg'
  }
]

export default class AppView extends Component {
  render() {
    return (
      <SafeAreaView style={styles.wrap}>
        <FlatList 
          data={data}
          ListHeaderComponent={()=>
            <View style={styles.header}>
              <Text style={styles.headerText}>Header</Text>
            </View>
          }
          renderItem={({ item })=>
            <Image style={styles.image} source={{ uri: item.uri }} />
          }
          columnWrapperStyle={styles.imageRow}
          numColumns={3}
        />
      </SafeAreaView>
    );
  }
}

위 예시를 보고 FlatList를 어떻게 사용하는지 살펴봅시다.
ListHeaderComponent props를 이용해 헤더를 생성하고 data, renderItem props로 컨텐츠를 렌더링하였습니다.
numColumns으로 한 행에 3개의 아이템이 위치하도록 설정하였고 columnWrapperStyle로 각 행의 스타일을 추가했습니다. (numColumns 설정시 각 행은 flexDirection: 'row'가 설정됩니다.)
ScrollView와 비교해서 관리해야할 props는 많지만 그 만큼 지원하는 기능이 많기 때문에 UI에 맞게 적절하게 사용해주시는게 좋습니다. FlatList는 ScrollView의 모든 props를 사용할 수 있습니다.

ActiveIndicator

로딩시 로딩 인디케이터를 제공하는 컴포넌트입니다. OS마다 인디케이터 모양이 다르고 사이즈는 largesmall로만 제어할 수 있어 로딩 인디케이터 디자인이 따로 적용되어 있거나 정확한 사이즈를 조정해야한다면 사용하기 힘든 단점이 있습니다.

<SafeAreaView style={styles.wrap}>
    <ActivityIndicator size="large" color="#0000ff" />
    <ActivityIndicator size="small" color="#00ff00" />
</SafeAreaView>

Modal

모달창을 구현할 때 사용되는 컴포넌트입니다. 어디에 선언되든 부모와 상관없이 항상 뷰포트 전체 width, height값을 가지며 기존 레이아웃 위에 노출됩니다.

import React, { Component } from 'react';
import { StyleSheet, SafeAreaView, Modal, View, Text } from 'react-native';

const styles = StyleSheet.create({
  wrap: {
    flex: 1,
  },
  modal: {
    flex: 1,
    justifyContent: 'center',
    backgroundColor: 'rgba(0, 0, 0, 0.4)',
  },
  popup: {
    height: 300,
    marginHorizontal: 40,
    backgroundColor: '#fff',
  },
});

export default class AppView extends Component {
  render() {
    return (
      <SafeAreaView style={styles.wrap}>
        <Modal>
          <View style={styles.modal}>
            <View style={styles.popup}>
              <Text>popup</Text>
            </View>
          </View>
        </Modal>
      </SafeAreaView>
    );
  }
}

이 밖에도 RN에서 제공하는 컴포넌트가 많으니 앞서 설명드린 컴포넌트의 자세한 사용법과 다른 컴포넌트들에 알고 싶으시다면 https://facebook.github.io/react-native/ 를 참고해주세요.

3.3 커스텀 컴포넌트 사용하기

RN에서 제공하는 컴포넌트를 사용하면서 컴포넌트 내에 항상 들어가는 코드가 있다면 커스텀 컴포넌트를 사용을 고려해 볼 수 있습니다.
반복적인 코드나 스타일 리셋 코드 등을 넣어 컴포넌트를 사용한다면 코드를 매번 삽입할 필요가 없어집니다.
저희 서비스에서는 안드로이드 환경에서 폰트에 들어가는 padding값을 없애기 위해서 RN에서 제공하는 Text를 커스텀해 사용하였습니다.

import * as React from 'react'
import { Text, Platform, TextProps } from 'react-native'

const DefaultStyle = Platform.select({
  ios: {},
  android: { includeFontPadding: false },
})

const wrappedText: React.SFC<TextProps> = ({ children, style, ...bypass }) => (
  <Text {...bypass} style={[DefaultStyle, style]}>
    {children}
  </Text>
)

export default wrappedText

이 밖에도 Touchable 컴포넌트 사용시 opacity나 underlayColor 등 앱내 버튼 피드백 통일, ActiveIndicator 컴포넌트 로딩 인디케이터 기본 색상 값 지정 등에서 활용해 볼 수 있습니다.

4. Props

앞선 예시에서 확인하셨듯이 RN은 컴포넌트에 props를 전달할 수 있습니다. 다만 주의해야할 점은 style props 이외에도 리액트 네이티브는 UI 렌더링에 영향을 주는 props들이 많다는 것입니다.
props로 제공하는 기능을 모르고 있으면 구조상 처리가 불가능하거나 억지로 구현하기 위해 좋지 않은 코드가 삽입될 수 있기 때문에 협업이나 유지보수에 어려움을 겪을 수 있습니다.
따라서 작업하기 전 사용하고자 하는 컴포넌트에서 style에 관련된 props가 있는지, 어떻게 사용하는지를 확인해야합니다.
UI개발시 자주 사용하는 몇 가지 props에 대해 알아보겠습니다.

numberOfLines, elliipsizeMode

RN은 텍스트의 말줄임 처리시 스타일이 아닌 Text 컴포넌트의 numberOfLines와 ellipsizeMode props를 사용합니다.

import React, { Component } from 'react';
import { StyleSheet, SafeAreaView, Text } from 'react-native';

const styles = StyleSheet.create({
  wrap: {
    flex: 1,
  },
  text: {
    padding: 20,
    fontSize: 20,
    fontWeight: 'bold',
    lineHeight: 24,
  },
});

export default class AppView extends Component {
  render() {
    return (
      <SafeAreaView style={styles.wrap}>
        <Text style={styles.text} numberOfLines={2}>
          Lorem ipsum dolor sit amet consectetur adipisicing elit. Sit ex eos asperiores perspiciatis a facere recusandae ea, reiciendis nulla provident!
        </Text>
      </SafeAreaView>
    );
  }
}

numberOfLines props에 말줄임이 될 라인수를 전달하면 간단하게 말줄임을 구현할 수 있습니다.
그렇다면 elliipsizeMode는 어디에 사용될까요?
elliipsizeMode는 "head", "middle", "tail", "clip" 네 가지 값을 가질 수 있으며 말줄임의 위치나 방식을 조정합니다.

render() {
    return (
      <SafeAreaView style={styles.wrap}>
        <Text style={styles.text} numberOfLines={1} ellipsizeMode="head">
          ellipsizeMode is "head" ellipsizeMode is "head" ellipsizeMode is "head" ellipsizeMode is "head" ellipsizeMode is "head" ellipsizeMode is "head"
        </Text>
        <Text style={styles.text} numberOfLines={1} ellipsizeMode="middle">
          ellipsizeMode is "middle" ellipsizeMode is "middle" ellipsizeMode is "middle" ellipsizeMode is "middle" ellipsizeMode is "middle" 
        </Text>
        <Text style={styles.text} numberOfLines={1} ellipsizeMode="tail">
          ellipsizeMode is "tail" ellipsizeMode is "tail" ellipsizeMode is "tail" ellipsizeMode is "tail" ellipsizeMode is "tail" 
        </Text>
        <Text style={styles.text} numberOfLines={1} ellipsizeMode="clip">
          ellipsizeMode is "clip" ellipsizeMode is "clip" ellipsizeMode is "clip" ellipsizeMode is "clip" ellipsizeMode is "clip" 
        </Text>
      </SafeAreaView>
    );
  }

head는 말줄임의 위치가 텍스트의 처음 부분, middle은 가운데, tail은 뒷 부분이 말줄임되며 clip은 ‘…’ 표시 없이 텍스트가 잘리게 됩니다.
보통 head,middleclip은 잘 사용되지 않고 tail이 기본 값이기 때문에 실제 말줄임을 적용할 때는 numberOfLines props 하나만 설정할 때가 많습니다.

contentContainerStyle

ScrollView 사용할 때 컨텐츠의 내용이 부족하더라도 ScrollView 영역만큼의 영역을 확보해야할 경우가 있습니다. (ex) 데이터 없을 때 결과 없음 텍스트 가운데 정렬 등)
이 경우 ScrollView의 안쪽 View에 flex: 1이나 height: 100% 등을 선언하여도 영역을 확보할 수가 없습니다.
실제 ScrollView를 사용할 때 ScrollView 내부에 접근할 수 없는 컨테이너가 생성되기 때문인데요. 이 때 contentContainerStyle props를 이용해 이 컨테이너의 스타일을 부여할 수 있습니다.

import React, { Component } from 'react';
import { StyleSheet, SafeAreaView, View, Text, ScrollView } from 'react-native';

const styles = StyleSheet.create({
  wrap: {
    flex: 1,
  },
  header: {
    height: 60,
    borderBottomWidth: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  headerText: {
    fontSize: 20,
    fontWeight: 'bold',
  },
  empty: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'powderblue',
  }
});

export default class AppView extends Component {
  render() {
    return (
      <SafeAreaView style={styles.wrap}>
        <View style={styles.header}>
          <Text style={styles.headerText}>Header</Text>
        </View>
        <ScrollView contentContainerStyle={{ flexGrow: 1 }}>
          <View style={styles.empty}>
            <Text>contentContainer를 설정하면 영역이 확보됩니다.</Text>
          </View>
        </ScrollView>
      </SafeAreaView>
    );
  }
}

여기서 주의할 점은 contentContainerStyle props에 flesBasis를 0으로 설정하면 스크롤이 되지 않는 이슈가 있습니다.
따라서 contentContainerStyle에서 영역 확보시 flexBasis값을 0으로 만드는 flex: 1대신 flexGrow: 1을 사용합니다.

hitSlop

UI작업을 하다보면 모바일에서 터치 영역을 눈에 보이는 영역보다 확장해야할 경우가 자주 있습니다. 웹에서는 주로 padding을 이용하지만 padding을 사용하면 주변 요소에 영향을 주지 않도록 수치 계산이 필요합니다. hitSlop props를 이용하면 주변 요소의 렌더링에 영향을 주지 않고 터치 영역만 원하는 만큼 증가시킬 수 있습니다.

import React, { Component } from 'react';
import { StyleSheet, SafeAreaView, TouchableOpacity, Text } from 'react-native';

const styles = StyleSheet.create({
  wrap: {
    flex: 1,
  },
  button: {
    marginTop: 50,
    alignSelf: 'center',
    borderWidth: 1,
    borderRadius: 4,
    padding: 20,
  },
});

export default class AppView extends Component {
  render() {
    return (
      <SafeAreaView style={styles.wrap}>
        <TouchableOpacity style={styles.button}>
          <Text>버튼</Text>
        </TouchableOpacity>
        <TouchableOpacity hitSlop={{ top: 10, right: 10, bottom: 10, left: 10 }} style={styles.button}>
          <Text>버튼</Text>
        </TouchableOpacity>
      </SafeAreaView>
    );
  }
}

위의 예시에서 hitSlop을 통해 렌더링에 영향을 미치지 않고 터치 영역이 확장된 것을 볼 수 있습니다.
hitSlop은 object내에 toprightbottomleft 값을 통해 조정할 수 있습니다.

5. 추천 오픈 소스 및 디버깅 툴 

RN은 아직 베타버전이기 때문에 자잘한 버그가 꽤 많습니다. 또 처음 리액트 RN UI 개발을 진행하면 어색하고 불편함을 느낄 수 있습니다.
하지만 Object-C나 Java 등 플랫폼 별 언어를 몰라도 ECMA Script와 리액트를 알고 있다면 안드로이드와 iOS 환경에서 네이티브 UI 동시 개발이 가능하고 그 방식이 웹에서의 작업과 닮아있습니다.
UI개발이 마크업, 웹 프레임워크를 넘어서 네이티브 영역으로도 확대될 수 있기 때문에 기회가 된다면 시도해보는 것은 어떨까요?
업무나 개인 공부 등 여러 가지 이유로 RN을 처음 경험하는 분들에게 이 글이 도움이 되었으면 좋겠습니다.
글 내용이나 RN에 대해 궁금한게 있으시면 언제든지 말씀해주세요~

긴 글 읽어주셔서 감사합니다.

6. 참고 자료


3개의 댓글

samslow · 2020년 3월 24일 4:28 오후

안녕하세요. 글 잘 보았습니다.
RN개발을 5개월 정도 했는데 그동안 제가 정리하고 싶었던 가려운 부분들을 대부분 긁어주는 글이라 너무 좋네요.
더불어서 제가 몰랐던 꿀팁들도 많구요.
자주 읽으면 생산성도 올라갈 것 같습니다.
긴 글 써주셔서 감사합니다.

햄복지수 · 2020년 7월 17일 2:34 오후

앱쪽은 잼병인데 좋은 글 덕분에 기본적인 개념을 잡고 갑니다.
경험에 우러나오는 유익한 정보 정말 감사합니다!!

k · 2020년 7월 22일 10:15 오전

이렇게 좋은 글 남겨주셔서 감사드립니다! 정말 큰 도움이 됐습니다.

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다