원문출처: Nir Hadassi님의 Size Matters: How I used React Native to make my App look great on every device
예를 들어 아이폰7에서 개발한 리액트 네이티브 앱에 대한 디자인을 태블릿에서 실행하려고 하면 dryer에 너무 오래 방치된 것처럼 보입니다.
이는 리액트 네이티브의 모든 차원이 dp
(밀드 독립 픽셀)로 표현되는 단위가 없는 반면, 픽셀을 사용해서 설계가 만들어졌기 때문입니다.
간단히 말해서, 디바이스의 크기가 클수록 더 많은 dp
를 가지게 됩니다.
그래서 <View style={{width: 300, height: 450}}/>
은 아이폰7 화면의 대부분을 차지하지만, 태블릿 화면에서는 절반도 차지하지 않습니다.
컴포넌트를 다양한 화면 크기로 확장하는 몇 가지 방법을 알아보겠습니다.
import React from 'react';
import { View, Text, Dimensions, StyleSheet, TouchableOpacity } from 'react-native';
import { loremIpsum } from './contants';
const { width, height } = Dimensions.get('window');
const AwesomeComponent = () =>
<View style={styles.container}>
<View style={styles.box}>
<Text style={styles.title}>Awesome Blog Post Page</Text>
<Text style={styles.text}>{loremIpsum}</Text>
<View style={styles.buttonsContainer}>
<TouchableOpacity style={styles.button}>
<Text style={styles.buttonText}>Accept</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button}>
<Text style={styles.buttonText}>Decline</Text>
</TouchableOpacity>
</View>
</View>
</View>;
const styles = StyleSheet.create({
container: {
width: width,
height: height,
backgroundColor: '#E0E0E0',
alignItems: 'center',
justifyContent: 'center',
},
box: {
width: 300,
height: 450,
backgroundColor: 'white',
borderRadius: 10,
padding: 10,
shadowColor: 'black',
shadowOpacity: 0.5,
shadowRadius: 3,
shadowOffset: {
height: 0,
width: 0
},
elevation: 2
},
title: {
fontSize: 20,
fontWeight: 'bold',
marginBottom: 10,
color: 'black'
},
text: {
fontSize: 14,
color: 'black'
},
buttonsContainer: {
flex: 1,
justifyContent: 'flex-end',
alignItems: 'center'
},
button: {
width: 150,
height: 45,
borderRadius: 100,
marginBottom: 10,
backgroundColor: '#41B6E6',
alignItems: 'center',
justifyContent: 'center',
},
buttonText: {
fontWeight: 'bold',
fontSize: 14,
color: 'white'
}
});
export default AwesomeComponent;
보시다시피 스타일은 모두 dp단위이고, 화면에 따라 확대되지 않습니다.
유연하게 확장 가능한 컴포넌트를 개발할 때는 View의 크기와 여백을 상위 컴포넌트에 비례해서 변환해야 합니다.
예를 들어 Container의 너비가 375이고, Box의 너비가 300인 경우, Box의 너비는 부모(300/375)의 80%이고, 여백은 왼쪽 10%와 오른쪽의 10%입니다.
또는 dp로 표시된 여백을 일정하게 유지하고 flex:1
을 사용해서 사용가능한 공간에 분산시킬 수 있습니다.
// 컴포넌트를 flex로 조정한 예시
const FlexExample = () =>
<View style={[styles.container, {flex: 1}]}>
<View style={{flex: 16}}/>
<View style={{flexDirection: 'row', flex: 68}}>
<View style={{flex: 1}}/>
<View style={[styles.box, {flex: 8}]}>
<Text style={styles.title}>Awesome Blog Post Page</Text>
<Text style={styles.text}>{loremIpsum}</Text>
<View style={styles.buttonsContainer}>
<TouchableOpacity style={styles.button}>
<Text style={styles.buttonText}>Accept</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button}>
<Text style={styles.buttonText}>Decline</Text>
</TouchableOpacity>
</View>
</View>
<View style={{flex: 1}}/>
</View>
<View style={{flex: 16}}/>
</View>;
확장 가능한 레이아웃을 만들 때, 특히 전체 너비 또는 높이에 걸쳐 배치하고 싶거나 컴포넌트를 정의된 비율로 서로 다른 섹션으로 분할할 때 flex가 가장 좋습니다.
방향을 변경할 때도 다른 장치 간에 동일한 비율을 유지합니다.
flex는 확장이나 축소하는데에 있어서 훌륭한 툴이지만 항상 충분하지는 않습니다. flex를 통해 일부 컴포넌트는 쉽게 확장되지 않으며 width, height, margin, padding과 같은 특정 속성으로 제한됩니다. 따라서 fontSize, lineHeight, SVG size와 같은 항목은 flex될 수 없습니다.
이 방법을 사용하면 기본적으로 스타일시트에 축척할 모든 숫자를 디바이스 너비 또는 높이의 백분율로 변환할 수 있습니다.
디바이스 너비 = 375dp 인 경우
dox width
: 300dp = deviceWidth * 0.8 text fontSize
: 14dp = deviceWidth * 0.03 이 방법을 단순화할 수 있는 멋지고 간단한 라이브러리는 react-native-viewport-units
입니다.
import {vw, vh} from 'react-native-viewport-units';
const styles = StyleSheet.create({
container: {
...
},
box: {
width: 80 * vw,
height: 67 * vh,
padding: 2.6 * vw,
...
},
title: {
fontSize: 5.3 * vw,
marginBottom: 2.6 * vw,
fontWeight: 'bold',
color: 'black'
},
text: {
fontSize: 3.6 * vw,
color: 'black'
},
buttonsContainer: {
...
},
button: {
width: 40 * vw,
height: 10.7 * vw,
borderRadius: 27 * vw,
marginBottom: 2.6 * vw,
...
},
buttonText: {
fontWeight: 'bold',
fontSize: 3.6 * vw,
color: 'white'
}
});
이렇게 하면 우리가 기다려온 결과를 볼 수 있게 됩니다.
참고로 리액트 네이티브 0.42버전 이상에서 제공하는 new percentage support
를 사용하거나 PixelRatio.get()
을 사용해서 모든 것을 곱하면 거의 동일한 결과를 얻을 수 있습니다.
계산을 좀 해야하고 이상한 숫자를 가지긴 하지만 괜찮아보이지만 여전히 완벽하지는 않습니다.
디자이너가 태블릿화면을 봤을 때, button이 너무 커서 box의 너비를 줄여야 한다고 생각하면 어떻게 할까? 뷰포트를 줄이면 아이폰에도 영향을 줄 것이다.
이 때 생각할 수 있는 옵션은 픽셀 비율을 사용해서 HTML의 미디어쿼리와 같은 것을 사용하는 것입니다. 하지만 모든 것을 두 번 이상 사용하고 싶지 않을 땐 어떻게 해야할까요?
import { Dimensions } from 'react-native';
const { width, height } = Dimensions.get('window');
// 가이드라인 사이즈는 표준 "~5" 화면 모바일 장치를 기준으로 합니다.
const guidelineBaseWidth = 350;
const guidelineBaseHeight = 680;
// 뷰포트기반
const scale = size => width / guidelineBaseWidth * size;
// 높이기반
const verticalScale = size => height / guidelineBaseHeight * size;
// factor값 제어
const moderateScale = (size, factor = 0.5) => size + ( scale(size) - size ) * factor;
export {scale, verticalScale, moderateScale};
이 기능들은 표준 핸드폰에서 하나의 디자인을 가져와 다른 디스플레이 크기에 적용할 수 있도록 하는 것이 목적입니다.
scale
함수는 매우 간단하며 뷰포트를 사용하는 것과 동일한 결과를 반환합니다.
verticalScale
는 scale과 비슷하지만, 너비가 아닌 높이에 기반하기 때문에 유용할 수 있습니다.
진짜 마법은 moderateScale
에서 일어납니다. 이 기능의 장점은 기본값이 0.5으로 지정된 resize factor를 제어할 수 있다는 것입니다.
만약 300dp 너비로 View사이즈를 조정하려면 아이폰7에서 다음과 같은 값을 얻을 수 있습니다.
유틸메서드(dp) | 아이폰7(px) | 태블릿(px) |
---|---|---|
scale(300) | 320 | 300 + 360 = 660 |
moderateScale(300) | 310 | 300 + 360/2 = 480 |
moderateScale(300, 0.25) | 305 | 300 + 360/4 = 390 |
이것은 우리가 태블릿에서 거대하고 부피가 커 보이지 않으면서 휴대폰에서 대략적으로 비슷한 크기를 유지하면, 단 한 번만 쓸 수 있게 해줍니다.
결과적으로, 아이폰과 태블릿이 동일한 비율을 유지하며 화면을 보여줄 수 있게 됩니다.
import { scale, moderateScale, verticalScale} from './scaling';
const styles = StyleSheet.create({
...
box: {
width: moderateScale(300),
height: verticalScale(450),
padding: scale(10),
...
},
title: {
fontSize: moderateScale(20, 0.4),
marginBottom: scale(10),
...
},
text: {
fontSize: moderateScale(14),
...
},
button: {
width: moderateScale(150, 0.3),
height: moderateScale(45, 0.3),
marginBottom: moderateScale(10),
...
},
buttonText: {
fontSize: moderateScale(14),
...
}
});
요약하자면, 컴포넌트를 확장하는 방법은 여러 가지가 있습니다.
가능한 한 flex 레이아웃을 만들고 확장 유틸리티를 사용하여 padding, button, text 및 SVG와 같은 다른 모든 부분을 확장하는 것이었습니다.
여기서 다루지 않은 것은 이미지 스케일링과 방향 변경 처리입니다.