자바스크립트 로딩 최적화
자바스크립트 코드를 파싱하고 실행하려면 메모리와 시간이 필요하다. 이 때문에 앱이 커질수록 코드를 처음 필요할 때까지 로딩을 미루는 것이 유용한 경우가 많다. React Native는 기본적으로 활성화된 몇 가지 표준 최적화 기능을 제공하며, 여러분의 코드에서 React가 앱을 더 효율적으로 로드할 수 있도록 도와주는 기법들을 적용할 수 있다. 또한 매우 큰 규모의 앱에 적합한 고급 자동 최적화 기법들도 있다(물론 각각의 장단점이 있다).
권장: Hermes 사용하기
Hermes는 새로운 React Native 앱의 기본 엔진으로, 효율적인 코드 로딩을 위해 최적화되어 있다. 릴리스 빌드에서 JavaScript 코드는 미리 바이트코드로 완전히 컴파일된다. 바이트코드는 필요할 때 메모리에 로드되며, 일반 JavaScript처럼 파싱할 필요가 없다.
React Native에서 Hermes 사용에 대해 더 알아보려면 여기를 참조한다.
권장: 대형 컴포넌트 지연 로딩
초기 렌더링 시 사용될 가능성이 낮은 많은 코드나 의존성을 가진 컴포넌트가 있다면, React의 lazy
API를 사용해 첫 렌더링이 될 때까지 해당 코드의 로딩을 지연할 수 있다. 일반적으로 앱의 화면 단위 컴포넌트를 지연 로딩하는 것을 고려해야 한다. 이를 통해 새로운 화면을 추가하더라도 앱의 시작 시간이 늘어나지 않는다.
React 문서에서 Suspense를 이용한 컴포넌트 지연 로딩에 대해 더 자세히 알아보고, 예제 코드도 확인할 수 있다.
팁: 모듈의 부작용을 피하자
컴포넌트 모듈이나 그 의존성이 전역 변수를 수정하거나 컴포넌트 외부의 이벤트를 구독하는 등의 **부작용(side effects)**을 가지고 있다면, 지연 로딩 컴포넌트가 앱의 동작을 바꿀 수 있다. 리액트 앱에서 대부분의 모듈은 부작용을 가져서는 안 된다.
import Logger from './utils/Logger';
// 🚩 🚩 🚩 부작용! 이 코드는 SplashScreen 컴포넌트를 렌더링하기도 전에 실행되며,
// 나중에 SplashScreen을 지연 로딩하기로 결정하면 앱의 다른 부분에서 예기치 못한
// 문제를 일으킬 수 있다.
global.logger = new Logger();
export function SplashScreen() {
// ...
}
고급: 인라인에서 require
호출하기
특정 코드를 처음 사용할 때까지 로딩을 미루고 싶을 때가 있다. 이때 lazy
나 비동기 import()
를 사용하지 않고도 require()
함수를 활용할 수 있다. 이 방법은 파일 상단에 정적 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>
);
}
이 예제에서는 VeryExpensive
컴포넌트를 처음 필요로 할 때까지 로딩을 지연시킨다. require()
를 사용해 동적으로 모듈을 불러오는 방식이다. 이렇게 하면 초기 로딩 시간을 줄이고, 실제로 해당 컴포넌트가 필요한 시점에만 리소스를 사용할 수 있다.
고급: require
호출 자동 인라인 처리
React Native CLI로 앱을 빌드할 때, require
호출(단, import
는 제외)은 여러분의 코드와 사용 중인 서드파티 패키지(node_modules
) 내부에서 자동으로 인라인 처리된다.
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
호출을 인라인 처리하면 모듈 평가 순서가 바뀌며, 심지어 일부 모듈이 아예 평가되지 않을 수도 있다. 일반적으로 JavaScript 모듈은 사이드 이펙트가 없도록 작성되기 때문에 이 작업은 자동으로 수행해도 안전하다.
하지만 모듈 중에 사이드 이펙트가 있는 경우, 예를 들어 로깅 메커니즘을 초기화하거나 코드 전반에서 사용하는 글로벌 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',
],
},
};
},
},
};
더 자세한 설정과 미세 조정 방법은 Metro의 getTransformOptions
문서를 참고한다.
고급: 랜덤 액세스 모듈 번들 사용 (Hermes 미사용)
Hermes 사용 시 지원되지 않는다. Hermes 바이트코드는 RAM 번들 형식과 호환되지 않으며, 모든 사용 사례에서 동일하거나 더 나은 성능을 제공한다.
랜덤 액세스 모듈 번들(RAM 번들)은 앞서 언급한 기법들과 함께 사용해 파싱하고 메모리에 로드해야 하는 자바스크립트 코드의 양을 제한한다. 각 모듈은 별도의 문자열(또는 파일)로 저장되며, 해당 모듈이 실행될 때만 파싱된다.
RAM 번들은 물리적으로 여러 파일로 분리될 수도 있고, 단일 파일 내에 여러 모듈의 조회 테이블로 구성된 인덱스 형식을 사용할 수도 있다.
- Android
- iOS
Android에서는 android/app/build.gradle
파일을 수정해 RAM 형식을 활성화한다. 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에서 "Bundle React Native code and images" 빌드 단계를 수정해 RAM 형식을 활성화한다. ../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
문서를 참고한다.