React Native 문서의 일부 내용을 번역하여 정리한 포스팅입니다.
React Native는 React와 플랫폼의 네이티브 기능을 사용하여 Android 및 iOS 애플리케이션을 구축하는 오픈 소스 프레임워크입니다
React.js + React Native
앱스토어에 실제 배포할 수 있는 앱을 만든다.
Introduction
Core Components and Native Components
Platform-Specific Code
Fast Refresh
Metro
Using Libraries
Debugging
Security
Performance Overview
먼저 React Native를 사용하려면 JavaScript의 기본 개념을 이해해야 합니다.
Android 개발에서는 Kotlin 또는 Java로 뷰를 작성하며,
iOS 개발에서는 Swift 또는 Objective-C를 사용함.
React Native를 사용하면, JavaScript로 이러한 뷰를 React 컴포넌트를 사용해 호출할 수 있습니다.
런타임 시 React Native는 해당 컴포넌트에 대한 Android 및 iOS 뷰를 생성합니다.
크롬에서 자바스크립트 엔진이 <div>
를 생성하는 것과 같은 원리
리액트 네이티브에서는 이를 Hermes 엔진 이라고 부릅니다.
Document.createElement('div')
-> <div></div>
Hermes
엔진의 특징일반적으로 브라우저는 파일을 하나로 합쳐야 실행 가능한 경우가 많다!
이러한 딜레마를 해결해 준 것이 바로 번들러
React에 webpack이 있다면 React Native 에는 Metro가 있다!
Metro는 React Native의 파일들을 번들링 하여 개발한 파일들을 하나의 파일로 만드는 작업을 해줍니다.
resolveRequest: (context, moduleName, platform) => {
return {
type: 'sourceFile',
filePath: /* 정확한 파일의 경로 문자열 */,
}
}
React Native의 새 버전을 출시할 때마다 Hermes 버전을 빌드합니다.
이렇게 하면 사용 중인 React Native 버전과 완벽하게 호환되는 Hermes 버전을 사용할 수 있습니다.
과거에는 Hermes 버전과 React Native 버전을 일치시키는 데 문제가 있었습니다.
이를 통해 이 문제가 완전히 해결되고 사용자에게 특정 React Native 버전과 호환되는 JS 엔진이 제공됩니다.
React Native에는 즉시 사용할 수 있는 필수적인 네이티브 컴포넌트들이 포함되어 있어, 바로 앱을 구축할 수 있습니다. 이러한 컴포넌트들을 React Native의 핵심 컴포넌트(Core Components)라고 부릅니다.
React Native에는 컨트롤에서 인터렉티브한 기능까지 다양한 핵심 컴포넌트가 있습니다.
React Native가 컴파일 하여 Native App으로 변환된 코드입니다!
UI 요소들과 다르게 javascript logic은 컴파일 되지 않고 구축한 네이티브앱 안에서 javascript가 스레드로 실행돼요.
크로스 플랫폼 앱을 개발할 때, 가능한 한 많은 코드를 재사용하고 싶을텐데요,
React Native는 코드를 플랫폼별로 분리하여 구성할 수 있는 방법을 제공합니다!
Platform.OS는 iOS에서 실행 중일 때는 ios로, Android에서 실행 중일 때는 android로 설정됩니다
import {Platform, StyleSheet} from 'react-native';
const styles = StyleSheet.create({
height: Platform.OS === 'ios' ? 200 : 100,
});
import {Platform, StyleSheet} from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
...Platform.select({
ios: {
backgroundColor: 'red',
},
android: {
backgroundColor: 'green',
},
default: {
// 다른 플랫폼, 예를 들어 웹
backgroundColor: 'blue',
},
}),
},
});
//플랫폼별 컴포넌트를 반환하도록 사용할 수도 있습니다.
const Component = Platform.select({
ios: () => require('ComponentIOS'),
android: () => require('ComponentAndroid'),
})();
<Component />;
//Android
import {Platform} from 'react-native';
if (Platform.Version === 25) {
console.log('Running on Nougat!');
}
// ios
import {Platform} from 'react-native';
const majorVersionIOS = parseInt(Platform.Version, 10);
if (majorVersionIOS <= 9) {
console.log('Work around a change in behavior');
}
모듈이 Android/iOS 차이점 없이 NodeJS/웹과 React Native 간에 공유되어야 할 경우 .native.js
확장자를 사용할 수 있습니다. 이는 React Native와 ReactJS 간의 공통 코드를 공유하는 프로젝트에 유용합니다.
예를 들어, 프로젝트에 다음과 같은 파일이 있는 경우:
import React from 'react';
import { TouchableOpacity, Text, StyleSheet } from 'react-native';
const Button = ({ onPress, title }) => {
return (
<TouchableOpacity style={styles.button} onPress={onPress}>
<Text style={styles.text}>{title}</Text>
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
button: {
backgroundColor: '#007AFF', // iOS 스타일
padding: 10,
borderRadius: 5,
},
text: {
color: '#FFFFFF',
fontSize: 18,
},
});
export default Button;
import Button from './Button';
// 플랫폼에 따라 자동으로 .ios.js 또는 .android.js 파일을 가져옴
export default Button;
이 방법을 통해 코드의 재사용성을 높이면서도 플랫폼별로 다른 UI/UX를 제공할 수 있습니다.
Fast Refresh는 React Native의 기능으로, 코드 변경 시 거의 즉각적인 피드백을 제공합니다. 기본적으로 활성화되어 있으며, React 컴포넌트의 수정 사항이 1~2초 내에 화면에 반영됩니다.
이 기능은 React Native에서 먼저 도입되었으며, 나중에 React 웹 애플리케이션에서도 사용할 수 있도록 확장되었습니다.
Expo 프로젝트에서 Fast Refresh는 기본적으로 활성화되어 있으며, 코드를 저장할 때마다 앱이 자동으로 변경 사항을 반영합니다
작동 방식
에러 처리
제한 사항
상태를 리셋하고 컴포넌트를 다시 마운트하려면 // @refresh reset
주석을 추가할 수 있습니다. 또한, Fast Refresh는 useState, useRef와 같은 Hooks의 상태를 유지하려고 하지만, useEffect와 같은 의존성을 가진 Hooks는 항상 업데이트됩니다.
예를 들어, useMemo(() => x * 2, [x])를 useMemo(() => x * 10, [x])
로 수정하면, x가 변경되지 않았더라도 다시 실행됩니다.
만약 React가 그렇게 하지 않았다면, 수정 사항이 화면에 반영되지 않았을 것입니다!
때로는 예상치 못한 결과가 발생할 수 있습니다. 예를 들어, 의존성이 없는 useEffect도 Fast Refresh 중에 한 번 다시 실행될 수 있습니다.
그러나 useEffect가 가끔 다시 실행되는 상황에 견딜 수 있도록 코드를 작성하는 것은 Fast Refresh 없이도 좋은 습관입니다.
이는 나중에 새로운 의존성을 추가할 때 더 쉽게 만들어줍니다.
개발서버 부터 프로덕션에 배포 할때까지 사용하는 도구
Metro는 React Native에서 JavaScript 코드와 자산을 빌드하는 도구입니다.
프로젝트의 metro.config.js
파일에서 Metro 설정을 커스터마이징할 수 있습니다.
Hot Module Replacement (HMR): Metro는 코드의 특정 부분이 변경될 때, 전체 애플리케이션을 다시 로드하지 않고 변경된 부분만 즉시 업데이트하는 기능을 지원합니다.
앞서 언급한 Fast Refresh 기능은 Metro에 의해 지원됩니다.
Metro는 대규모 자바스크립트 프로젝트를 효율적으로 번들링하는 데 최적화되어 있습니다. 리액트 네이티브의 특성상 수백 개의 파일이 하나의 앱으로 묶이게 되는데, Metro는 이를 빠르게 처리합니다.
React Native는 기본 제공되는 컴포넌트 외에도 커뮤니티에서 제공하는 다양한 라이브러리를 사용할 수 있습니다.
라이브러리는 npm이나 Yarn과 같은 패키지 관리자를 통해 설치할 수 있습니다.
React Native에서는 여러 디버깅 옵션을 제공하는 앱 내 개발자 메뉴(Dev Menu)를 사용할 수 있습니다. 이 메뉴에 접근하는 방법은 다음과 같습니다
iOS 시뮬레이터: Cmd ⌘ + D (또는 Device > Shake)
Android 에뮬레이터: Cmd ⌘ + M (macOS) 또는 Ctrl + M (Windo
ws 및 Linux)
Android 장치 및 에뮬레이터: 터미널에서 adb shell input keyevent 82 명령어 실행
React DevTools를 사용해 React 요소 트리, props, state 등을 확인할 수 있습니다.
npx react-devtools
Android와 iOS에서는 개발자 메뉴에서 "Perf Monitor"를 선택해 성능 오버레이를 토글할 수 있습니다. 이 기능은 가이드용으로 사용되며, 정확한 성능 측정을 위해서는 Android Studio와 Xcode의 네이티브 도구를 사용하는 것을 권장합니다.
애플리케이션을 여러가지 보안 사고로부터 보호하기 위해 얼마나 많은 노력을 기울이느냐에 따라 악의적인 공격의 피해를 입거나 보안 취약점이 드러날 가능성은 반비례합니다. 보통 자물쇠는 뚫릴 수 있지만, 캐비닛 훅보다는 훨씬 뚫기 어렵습니다!
이 가이드에서는 민감한 정보 저장, 인증, 네트워크 보안 및 애플리케이션 보안을 강화하는 데 도움이 되는 도구에 대해 배울 것입니다.
앱 코드에 민감한 API 키를 절대 저장하지 마세요.
앱에서 리소스에 접근하기 위해 API 키나 비밀이 반드시 필요하다면, 가장 안전한 방법은 앱과 리소스 사이에 조정 계층을 구축하는 것입니다.
ex) 서버리스 함수(AWS Lambda나 Google Cloud Functions를 사용)
Async Storage는 React Native에서 비동기적이고 암호화되지 않은 키-값 저장소를 제공하는 커뮤니티 유지 모듈입니다. Async Storage는 앱 간에 공유되지 않으며, 각 앱은 자체 샌드박스 환경을 가지고 있어 다른 앱의 데이터에 접근할 수 없습니다.
iOS - Keychain Services를 사용하면 사용자를 위해 민감한 정보를 안전하게 저장할 수 있습니다. 이는 인증서, 토큰, 비밀번호 등 민감한 정보를 저장하기에 이상적입니다.
Android
1. Secure Shared Preferences 안드로이드의 지속적인 키-값 데이터 저장소입니다. Shared Preferences의 데이터는 기본적으로 암호화되지 않지만, Encrypted Shared Preferences는 Shared Preferences 클래스를 감싸 안드로이드에서 키와 값을 자동으로 암호화합니다.
2. Keystore 시스템을 사용하면 암호화 키를 컨테이너에 저장하여 장치에서 추출하기 어렵게 만듭니다.
딥 링크는 외부 소스에서 네이티브 애플리케이션으로 데이터를 직접 보내는 방법입니다.
app://처럼 생겼으며, app은 앱의 스킴이고 // 뒤에 오는 것은 내부적으로 요청을 처리하는 데 사용될 수 있습니다.
예를 들어, 이커머스 앱을 구축하고 있다면 app://products/1 을 사용해 딥 링크로 앱을 열고 ID가 1인 제품 상세 페이지로 이동할 수 있습니다.
===
딥 링크는 안전하지 않으며, 민감한 정보를 절대 보내서는 안 됩니다.
딥 링크가 안전하지 않은 이유는 URL 스킴을 등록하는 중앙화된 방법이 없기 때문입니다. 애플리케이션 개발자는 iOS에서는 Xcode에서, 안드로이드에서는 인텐트를 추가하여 거의 모든 URL 스킴을 선택할 수 있습니다.
-> Apple은 후속 iOS 버전(iOS 11)에서 이를 해결하기 위해 '선착순' 원칙을 도입했지만, 이 취약점은 여전히 다른 방식으로 악용될 수 있습니다. 이 문제에 대한 자세한 내용은 여기를 참조하세요. iOS에서 안전하게 콘텐츠를 앱 내로 링크하기 위해서는 유니버설 링크를 사용하세요.
OAuth2 인증 프로토콜은 현재 매우 인기가 많으며, 가장 완전하고 안전한 프로토콜로 자랑됩니다.
OAuth2에서는 사용자가 제3자를 통해 인증을 받도록 요청합니다. 인증이 완료되면 이 제3자는 요청한 애플리케이션으로 검증 코드를 리디렉션하며, 이 코드는 JWT(제이슨 웹 토큰)로 교환될 수 있습니다. JWT는 웹에서 정보를 안전하게 전송하기 위한 개방형 표준입니다.
PKCE(Pixy)는 Proof of Key Code Exchange의 약자이며 OAuth 2 스펙의 확장판입니다. 이는 인증과 토큰 교환 요청이 동일한 클라이언트에서 발생했음을 확인하는 추가적인 보안 계층을 추가하는 방법입니다.
보안을 처리하는 데 있어 완벽한 방법은 없습니다. 그러나 의식적인 노력과 성실함으로 애플리케이션에서 보안 침해 가능성을 크게 줄일 수 있습니다. 애플리케이션에 저장된 데이터의 민감도, 사용자 수, 해커가 계정에 접근할 경우 초래할 수 있는 피해에 비례해 보안에 투자하세요. 그리고 기억하세요: 요청하지 않은 정보는 접근하기 훨씬 어렵습니다.
React Native를 사용하여 WebView 기반 도구 대신 선택하는 주요 이유는 초당 60 프레임을 달성하고 네이티브 같은 느낌의 앱을 제공하기 위해서입니다.
JavaScript 스레드가 한 프레임 동안 반응하지 않으면 프레임이 드롭되며, 예를 들어 복잡한 애플리케이션의 루트 컴포넌트에서 this.setState를 호출하여 많은 하위 트리 컴포넌트를 다시 렌더링하면 200ms가 걸리고 12프레임이 드롭될 수 있습니다.
이로 인해 JavaScript에 의해 제어되는 애니메이션이 그 동안 멈춘 것처럼 보일 수 있습니다. 100ms 이상 걸리는 작업이 있다면 사용자가 느끼는 수준입니다!
JSFrame 속도가 떨어지면 애플리케이션에서 다음과 같은 문제가 발생할 수 있습니다
InteractionManager
사용: 긴 작업을 애니메이션이나 사용자 인터랙션 후로 연기하여 JSFrame 속도를 유지할 수 있습니다.import { InteractionManager } from 'react-native';
// 버튼 클릭 핸들러
const handleButtonClick = () => {
// 긴 작업을 예약
InteractionManager.runAfterInteractions(() => {
// 긴 작업 실행
performLongTask();
});
};
// 긴 작업 함수
const performLongTask = () => {
// 시간이 많이 걸리는 작업
console.log('긴 작업 실행 중...');
// 예시: API 호출, 대량 데이터 처리 등
};
import React, { useState, useEffect } from 'react';
import { View, Button } from 'react-native';
const SimpleAnimation = () => {
const [position, setPosition] = useState(0);
const startAnimation = () => {
const animate = () => {
setPosition(prevPosition => {
const newPosition = prevPosition + 5;
if (newPosition > 200) return 0; // 다시 처음으로
requestAnimationFrame(animate);
return newPosition;
});
};
requestAnimationFrame(animate);
};
return (
<View style={{ padding: 20 }}>
<Button title="Start Animation" onPress={startAnimation} />
<View
style={{
width: 50,
height: 50,
backgroundColor: 'blue',
marginTop: 20,
transform: [{ translateX: position }],
}}
/>
</View>
);
};
export default SimpleAnimation;
v0.74
function MyComponent(props) {
const [state1, setState1] = useState(false);
const [state2, setState2] = useState(false);
return (
<View>
<View
onLayout={() => {
setState1(true);
}}>
<View
onLayout={() => {
// When this event is executed, state1's new value is no longer observable here.
setState2(true);
}}>
</View>
</View>
);
}
import React, {memo} from 'react';
import {View, Text} from 'react-native';
const MyListItem = memo(
({title}: {title: string}) => (
<View>
<Text>{title}</Text>
</View>
),
(prevProps, nextProps) => {
return prevProps.title === nextProps.title;
},
);
export default MyListItem;
const renderItem = useCallback(({item}) => (
<View key={item.key}>
<Text>{item.title}</Text>
</View>
), []);
return (
// ...
<FlatList data={items} renderItem={renderItem} />;
// ...
);
많은 사람들이 NavigatorIOS의 성능이 Navigator보다 더 좋은 이유는 애니메이션이 메인 스레드에서 전적으로 실행되기 때문입니다. 따라서 JavaScript 스레드의 프레임 드롭이 애니메이션에 영향을 미치지 않습니다. 이와 유사하게 ScrollView는 JavaScript 스레드가 잠겨 있는 동안에도 스크롤이 가능하며, 이는 ScrollView가 메인 스레드에서 실행되기 때문입니다.
개발 모드에서 실행 중일 때 (dev=true): 개발 모드에서는 JavaScript 스레드 성능이 크게 저하됩니다. 더 많은 작업이 런타임에서 이루어지기 때문에, 성능 테스트는 항상 릴리스 빌드에서 수행해야 합니다.
as-is | to-be |
---|---|
이미지 크기 줄이기: 필요한 해상도에 맞는 이미지 크기를 사용하여 메모리 사용량을 줄입니다.
resizeMode 최적화: 이미지의 resizeMode 속성을 적절히 사용하여 성능을 개선할 수 있습니다. 예를 들어, resizeMode: 'cover' 대신 resizeMode: 'contain'을 사용할 수 있습니다.
shouldRasterizeIOS 및 renderToHardwareTextureAndroid 사용: 이미지가 반복적으로 움직이지 않을 경우, 이미지를 하드웨어에 렌더링하도록 하여 성능을 향상시킬 수 있습니다. 단, 이 속성들을 과도하게 사용하면 메모리 사용량이 증가할 수 있으므로 주의가 필요합니다.
<View renderToHardwareTextureAndroid=true />
<View shouldRasterizeIOS=true />
Animated.timing(fadeAnim, {
toValue: 1,
duration: 1000,
useNativeDriver: true, // 네이티브 드라이버 사용
}).start();
동작 원리
애니메이션이 GPU에서 직접 처리되므로, 복잡한 애니메이션이나 반복적인 애니메이션이 더 효율적으로 실행됩니다.
LayoutAnimation 사용: 네이티브 애니메이션을 활용하여 레이아웃 변경 시 발생하는 성능 저하를 방지합니다.
const styles = StyleSheet.create({
overlay: {
width: 300,
height: 300,
backgroundColor: 'rgba(0, 0, 0, 0.5)', // 투명 배경
justifyContent: 'center',
alignItems: 'center',
shouldRasterizeIOS: true, // iOS 성능 최적화
renderToHardwareTextureAndroid: true, // Android 성능 최적화
},
});
React Native에서 부드러운 성능을 유지하기 위해서는 JS 스레드와 UI 스레드의 역할을 이해하고, 불필요한 재렌더링과 부하를 줄이는 것이 중요합니다.
잘 구축된 네이티브 앱이 부드럽게 느껴지는 이유 중 하나는 상호작용 및 애니메이션 중에 비용이 많이 드는 작업을 피하기 때문입니다. React Native에서는 단일 자바스크립트 실행 스레드만 사용할 수 있는 제한이 있지만, InteractionManager를 사용하여 긴 작업이 모든 상호작용/애니메이션이 완료된 후에 시작되도록 할 수 있습니다.
애플리케이션은 다음과 같이 상호작용이 끝난 후 작업을 예약할 수 있습니다:
InteractionManager.runAfterInteractions(() => {
// ...긴 동기 작업...
});
requestAnimationFrame(): 시간이 지남에 따라 뷰를 애니메이션하는 코드를 실행합니다.
setImmediate/setTimeout/setInterval(): 나중에 코드를 실행하지만, 이는 애니메이션을 지연시킬 수 있습니다.
runAfterInteractions(): 나중에 코드를 실행하지만, 활성 애니메이션을 지연시키지 않습니다.
터치 처리 시스템은 하나 이상의 활성 터치를 '상호작용'으로 간주하며, 모든 터치가 끝나거나 취소될 때까지 runAfterInteractions() 콜백을 지연시킵니다.
InteractionManager는 또한 애플리케이션이 애니메이션을 등록하고 애니메이션이 시작될 때 '핸들'을 생성하고, 애니메이션이 완료되면 이를 지울 수 있게 합니다:
const handle = InteractionManager.createInteractionHandle();
// 애니메이션 실행 중... (`runAfterInteractions` 작업은 대기열에 추가됨)
// 나중에 애니메이션 완료 시:
InteractionManager.clearInteractionHandle(handle);
// 모든 핸들이 지워지면 대기 중인 작업이 실행됩니다.
Introduction
Core Components and Native Components
Platform-Specific Code
Fast Refresh
Metro
Using Libraries
Debugging
Security
Performance Overview
퍼포먼스 관련한 부분이 fabric에서도 아직 유효한건가요?