자바스크립트 로딩 최적화
JavaScript 코드를 파싱하고 실행하려면 메모리와 시간이 필요하다. 따라서 앱이 커질수록 코드를 처음 필요할 때까지 로딩을 미루는 것이 유용한 경우가 많다. React Native는 기본적으로 활성화된 몇 가지 표준 최적화 기능을 제공하며, 여러분의 코드에서 React가 앱을 더 효율적으로 로드할 수 있도록 도와주는 기법을 적용할 수 있다. 또한 매우 큰 앱에 적합한 몇 가지 고급 자동 최적화 기능(각각의 장단점이 있음)도 존재한다.
권장: Hermes 사용하기
Hermes는 새로운 React Native 앱의 기본 엔진으로, 효율적인 코드 로딩을 위해 최적화되어 있다. 릴리스 빌드에서는 JavaScript 코드가 사전에 바이트코드로 완전히 컴파일된다. 바이트코드는 필요할 때 메모리에 로드되며, 일반 JavaScript처럼 파싱할 필요가 없다.
React Native에서 Hermes 사용에 대해 더 알아보려면 여기를 참고한다.
권장: 대형 컴포넌트 지연 로딩
초기 렌더링 시 사용될 가능성이 낮은 코드나 의존성이 많은 컴포넌트가 있다면, React의 lazy
API를 사용해 첫 렌더링 시점까지 코드 로딩을 지연시킬 수 있다. 일반적으로 앱의 화면 단위 컴포넌트를 지연 로딩하는 것이 좋다. 이렇게 하면 새로운 화면을 추가해도 앱의 시작 시간이 늘어나지 않는다.
React 공식 문서에서 Suspense를 활용한 컴포넌트 지연 로딩에 대해 더 자세히 알아보고, 예제 코드를 확인할 수 있다.
팁: 모듈 사이드 이펙트 피하기
컴포넌트 모듈이나 그 의존성에서 전역 변수를 수정하거나 컴포넌트 외부에서 이벤트를 구독하는 등의 사이드 이펙트가 발생하면, 지연 로딩을 사용할 때 앱의 동작이 바뀔 수 있다. 대부분의 React 앱에서 모듈은 사이드 이펙트를 발생시키지 않아야 한다.
import Logger from './utils/Logger';
// 🚩 🚩 🚩 사이드 이펙트! 이 코드는 SplashScreen 컴포넌트가 렌더링되기 전에 실행되며,
// 나중에 SplashScreen을 지연 로딩하기로 결정하면 앱의 다른 부분에서 예기치 못한 문제를 일으킬 수 있다.
global.logger = new Logger();
export function SplashScreen() {
// ...
}
고급: 인라인에서 require
호출하기
lazy
나 비동기 import()
를 사용하지 않고, 처음 사용할 때까지 일부 코드의 로딩을 지연시키고 싶은 경우가 있다. 이때 파일 상단에 정적 import
를 사용하는 대신 require()
함수를 활용할 수 있다.
import {Component} from 'react';
import {Text} from 'react-native';
// ... 매우 무거운 모듈을 임포트
export default function VeryExpensive() {
// ... 많은 렌더링 로직
return <Text>Very Expensive Component</Text>;
}
import {useCallback, useState} from 'react';
import {TouchableOpacity, View, Text} from 'react-native';
// 일반적으로 정적 임포트를 사용함:
// import VeryExpensive from './VeryExpensive';
let VeryExpensive = null;
export default function Optimize() {
const [needsExpensive, setNeedsExpensive] = useState(false);
const didPress = useCallback(() => {
if (VeryExpensive == null) {
VeryExpensive = require('./VeryExpensive').default;
}
setNeedsExpensive(true);
}, []);
return (
<View style={{marginTop: 20}}>
<TouchableOpacity onPress={didPress}>
<Text>Load</Text>
</TouchableOpacity>
{needsExpensive ? <VeryExpensive /> : null}
</View>
);
}
고급: require
호출 자동 인라인 처리
React Native CLI를 사용해 앱을 빌드하면, 코드 내부와 사용 중인 서드파티 패키지(node_modules
) 안의 require
호출이 자동으로 인라인 처리된다. 단, import
문은 이 기능에 포함되지 않는다.
import {useCallback, useState} from 'react';
import {TouchableOpacity, View, Text} from 'react-native';
// 이 최상위 require 호출은 아래 컴포넌트의 일부로 지연 평가된다.
const VeryExpensive = require('./VeryExpensive').default;
export default function Optimize() {
const [needsExpensive, setNeedsExpensive] = useState(false);
const didPress = useCallback(() => {
setNeedsExpensive(true);
}, []);
return (
<View style={{marginTop: 20}}>
<TouchableOpacity onPress={didPress}>
<Text>Load</Text>
</TouchableOpacity>
{needsExpensive ? <VeryExpensive /> : null}
</View>
);
}
일부 React Native 프레임워크는 이 기능을 비활성화한다. 특히 Expo 프로젝트에서는 기본적으로 require
호출이 인라인 처리되지 않는다. 이 최적화를 활성화하려면 프로젝트의 Metro 설정을 수정하고 getTransformOptions
에서 inlineRequires: true
로 설정하면 된다.
인라인 require
사용 시 주의사항
require
호출을 인라인으로 처리하면 모듈이 평가되는 순서가 바뀌고, 심지어 일부 모듈이 전혀 평가되지 않을 수도 있다. 일반적으로 자바스크립트 모듈은 부작용(side effect)이 없도록 작성되기 때문에, 이 방식은 자동으로 적용해도 안전하다.
하지만 모듈 중에 부작용이 있는 경우 - 예를 들어 로깅 메커니즘을 초기화하거나 코드 전반에서 사용하는 전역 API를 수정하는 경우 - 예상치 못한 동작이나 심지어 크래시가 발생할 수 있다. 이런 경우에는 특정 모듈을 최적화에서 제외하거나, 아예 이 기능을 비활성화하는 것이 좋다.
모든 require
호출의 자동 인라인 처리를 비활성화하려면:
metro.config.js
파일에서 inlineRequires
변환 옵션을 false
로 설정한다.
module.exports = {
transformer: {
async getTransformOptions() {
return {
transform: {
inlineRequires: false,
},
};
},
},
};
특정 모듈만 require
인라인 처리에서 제외하려면:
inlineRequires.blockList
와 nonInlinedRequires
두 가지 변환 옵션이 있다. 아래 코드 예제를 참고해 각 옵션을 어떻게 사용하는지 확인한다.
module.exports = {
transformer: {
async getTransformOptions() {
return {
transform: {
inlineRequires: {
blockList: {
// `DoNotInlineHere.js` 파일 내의 require() 호출은 인라인 처리되지 않는다.
[require.resolve('./src/DoNotInlineHere.js')]: true,
// 다른 곳의 require() 호출은 인라인 처리된다.
// 단, nonInlinedRequires에 매칭되는 경우는 제외된다.
},
},
nonInlinedRequires: [
// 'react' 모듈의 require() 호출은 어디에서도 인라인 처리되지 않는다.
'react',
],
},
};
},
},
};
require
인라인 처리 설정을 더 세밀하게 조정하려면 Metro의 getTransformOptions
문서를 참고한다.
고급: 랜덤 액세스 모듈 번들 사용 (Hermes 미사용)
Hermes 사용 시 지원되지 않음. Hermes 바이트코드는 RAM 번들 형식과 호환되지 않으며, 모든 사용 사례에서 동일하거나 더 나은 성능을 제공한다.
랜덤 액세스 모듈 번들(RAM 번들)은 앞서 언급한 기법들과 함께 작동하여 파싱되고 메모리에 로드되어야 하는 자바스크립트 코드의 양을 줄인다. 각 모듈은 별도의 문자열(또는 파일)로 저장되며, 모듈이 실행되어야 할 때만 파싱된다.
RAM 번들은 물리적으로 여러 파일로 분할될 수도 있고, 단일 파일 내에 여러 모듈의 조회 테이블로 구성된 인덱스 형식을 사용할 수도 있다.
- Android
- iOS
Android에서 RAM 형식을 활성화하려면 android/app/build.gradle
파일을 편집한다. apply from: "../../node_modules/react-native/react.gradle"
줄 앞에 project.ext.react
블록을 추가하거나 수정한다:
project.ext.react = [
bundleCommand: "ram-bundle",
]
단일 인덱스 파일을 사용하려면 Android에서 다음 줄을 추가한다:
project.ext.react = [
bundleCommand: "ram-bundle",
extraPackagerArgs: ["--indexed-ram-bundle"]
]
iOS에서는 RAM 번들이 항상 인덱스 형식(단일 파일)으로 생성된다.
Xcode에서 RAM 형식을 활성화하려면 "Bundle React Native code and images" 빌드 단계를 편집한다. ../node_modules/react-native/scripts/react-native-xcode.sh
앞에 export BUNDLE_COMMAND="ram-bundle"
를 추가한다:
export BUNDLE_COMMAND="ram-bundle"
export NODE_BINARY=node
../node_modules/react-native/scripts/react-native-xcode.sh
RAM 번들 빌드 설정 및 세부 조정에 대한 자세한 내용은 Metro의 getTransformOptions
문서를 참고한다.