Skip to main content

26 posts tagged with "engineering"

View All Tags

Introducing Create React Native App

· 4 min read
Adam Perry
Software Engineer at Expo

오늘 우리는 Create React Native App을 발표한다. 이 새로운 도구는 React Native 프로젝트를 시작하는 과정을 크게 단순화한다. 이 도구는 Create React App의 디자인에서 큰 영감을 받았으며, FacebookExpo(이전의 Exponent)의 협력으로 만들어졌다.

많은 개발자들이 특히 Android를 위해 React Native의 네이티브 빌드 의존성을 설치하고 설정하는 데 어려움을 겪는다. Create React Native App을 사용하면 Xcode나 Android Studio를 사용할 필요가 없으며, Linux나 Windows에서도 iOS 기기를 위한 개발이 가능하다. 이는 Expo 앱을 통해 이루어진다. Expo 앱은 순수 JavaScript로 작성된 CRNA 프로젝트를 네이티브 코드를 컴파일하지 않고도 로드하고 실행한다.

새로운 프로젝트를 생성해 보자(yarn이 설치되어 있다면 적절한 yarn 명령어로 대체 가능):

$ npm i -g create-react-native-app
$ create-react-native-app my-project
$ cd my-project
$ npm start

이 명령어는 React Native 패키저를 시작하고 QR 코드를 출력한다. Expo 앱에서 이 QR 코드를 열어 JavaScript를 로드할 수 있다. console.log 호출은 터미널로 전달된다. 모든 표준 React Native API와 Expo SDK를 사용할 수 있다.

네이티브 코드는 어떻게 처리할까?

많은 React Native 프로젝트는 컴파일이 필요한 Java 또는 Objective-C/Swift 의존성을 포함한다. Expo 앱은 카메라, 비디오, 연락처 등의 API를 제공하며, Airbnb의 react-native-maps나 Facebook 인증과 같은 인기 라이브러리도 포함한다. 하지만 Expo가 제공하지 않는 네이티브 코드 의존성이 필요한 경우, 직접 빌드 환경을 구성해야 한다. Create React App과 마찬가지로 CRNA에서도 "이젝트(eject)" 기능을 지원한다.

npm run eject 명령어를 실행하면 react-native init으로 생성한 프로젝트와 유사한 구조를 얻을 수 있다. 이 시점부터는 react-native init으로 시작한 경우와 마찬가지로 Xcode나 Android Studio가 필요하다. react-native link로 라이브러리를 추가할 수 있으며, 네이티브 코드 컴파일 과정을 완전히 제어할 수 있다.

궁금한 점이나 피드백이 있나요?

Create React Native App은 이제 일반 사용에 충분히 안정화되었습니다. 여러분의 사용 경험을 듣고 싶습니다! 트위터를 통해 연락하거나 GitHub 저장소에 이슈를 열어주세요. 풀 리퀘스트도 환영합니다!

Using Native Driver for Animated

· 12 min read
Janic Duplessis
Software Engineer at App & Flow

지난 1년 동안, 우리는 Animated 라이브러리를 사용한 애니메이션의 성능을 개선하기 위해 노력해 왔다. 애니메이션은 아름다운 사용자 경험을 만드는 데 매우 중요하지만, 올바르게 구현하기 쉽지 않다. 우리는 개발자가 성능 저하를 일으킬 수 있는 코드를 걱정하지 않고도 효율적인 애니메이션을 쉽게 만들 수 있도록 하고 싶다.

이 기능의 목적

Animated API는 매우 중요한 제약 조건을 염두에 두고 설계되었다. 바로 직렬화 가능하다는 점이다. 이는 애니메이션이 시작되기 전에 모든 정보를 네이티브로 보낼 수 있음을 의미한다. 따라서 네이티브 코드가 UI 스레드에서 애니메이션을 수행할 때, 매 프레임마다 브리지를 거칠 필요가 없다. 이 기능은 매우 유용한데, 애니메이션이 시작된 후에도 JS 스레드가 블로킹되더라도 애니메이션이 부드럽게 실행될 수 있기 때문이다. 실제로 사용자 코드가 JS 스레드에서 실행되고 React 렌더링이 JS를 오랫동안 잠글 수 있기 때문에 이러한 상황은 자주 발생할 수 있다.

약간의 역사...

이 프로젝트는 약 1년 전 Expo가 Android용 li.st 앱을 개발하면서 시작되었다. Krzysztof Magiera는 Android에서 초기 구현을 담당했다. 결과는 성공적이었고, li.st는 Animated를 사용해 네이티브로 구동되는 애니메이션을 탑재한 첫 번째 앱이 되었다. 몇 달 후, Brandon Withrow가 iOS에서 초기 구현을 완성했다. 이후 Ryan Gomba와 나는 Animated.event 지원 같은 누락된 기능을 추가하고, 실제 프로덕션 앱에서 발견한 버그를 수정하는 작업을 진행했다. 이는 진정한 커뮤니티의 노력이었으며, 관련된 모든 분들과 개발의 상당 부분을 후원한 Expo에 감사드린다. 이제 이 기술은 React Native의 Touchable 컴포넌트와 최근 출시된 React Navigation 라이브러리의 네비게이션 애니메이션에서 사용되고 있다.

원리 이해

먼저, JS 드라이버를 사용하는 Animated로 애니메이션이 어떻게 동작하는지 살펴보자. Animated를 사용할 때는 수행하려는 애니메이션을 나타내는 노드 그래프를 선언한 후, 드라이버를 사용해 미리 정의된 곡선에 따라 Animated 값을 업데이트한다. 또한 Animated.event를 사용해 View의 이벤트에 Animated 값을 연결해 업데이트할 수도 있다.

애니메이션의 단계와 각 단계가 어디서 일어나는지 정리하면 다음과 같다:

  • JS: 애니메이션 드라이버는 requestAnimationFrame을 사용해 매 프레임마다 실행되고, 애니메이션 곡선을 기반으로 계산한 새로운 값으로 드라이브되는 값을 업데이트한다.
  • JS: 중간 값이 계산되어 View에 연결된 props 노드로 전달된다.
  • JS: setNativeProps를 사용해 View가 업데이트된다.
  • JS에서 Native로 브리지를 거친다.
  • Native: UIView 또는 android.View가 업데이트된다.

보이는 것처럼 대부분의 작업은 JS 스레드에서 일어난다. JS 스레드가 블로킹되면 애니메이션이 프레임을 건너뛰게 된다. 또한 매 프레임마다 JS에서 Native로 브리지를 거쳐 네이티브 뷰를 업데이트해야 한다.

네이티브 드라이버는 이 모든 단계를 네이티브로 옮긴다. Animated가 애니메이션 노드 그래프를 생성하기 때문에, 애니메이션이 시작될 때 한 번만 네이티브로 직렬화하여 전송할 수 있다. 이렇게 하면 JS 스레드로 다시 콜백할 필요가 없어지고, 네이티브 코드가 매 프레임마다 UI 스레드에서 직접 뷰를 업데이트할 수 있다.

애니메이션 값과 보간 노드를 직렬화하는 예제를 살펴보자 (정확한 구현은 아니고 예제일 뿐이다).

네이티브 값 노드를 생성한다. 이 값이 애니메이션될 값이다:

NativeAnimatedModule.createNode({
id: 1,
type: 'value',
initialValue: 0,
});

네이티브 보간 노드를 생성한다. 이 노드는 네이티브 드라이버에게 값을 어떻게 보간할지 알려준다:

NativeAnimatedModule.createNode({
id: 2,
type: 'interpolation',
inputRange: [0, 10],
outputRange: [10, 0],
extrapolate: 'clamp',
});

네이티브 props 노드를 생성한다. 이 노드는 네이티브 드라이버에게 뷰의 어떤 prop에 연결되었는지 알려준다:

NativeAnimatedModule.createNode({
id: 3,
type: 'props',
properties: ['style.opacity'],
});

노드들을 연결한다:

NativeAnimatedModule.connectNodes(1, 2);
NativeAnimatedModule.connectNodes(2, 3);

props 노드를 뷰에 연결한다:

NativeAnimatedModule.connectToView(3, ReactNative.findNodeHandle(viewRef));

이렇게 하면 네이티브 애니메이션 모듈은 JS로 값을 계산할 필요 없이 네이티브 뷰를 직접 업데이트하는 데 필요한 모든 정보를 갖게 된다.

남은 일은 어떤 타입의 애니메이션 곡선을 사용할지와 어떤 애니메이션 값을 업데이트할지 지정해 애니메이션을 실제로 시작하는 것이다. 타이밍 애니메이션은 JS에서 미리 모든 프레임을 계산해 네이티브 구현을 더 작게 만들 수 있다.

NativeAnimatedModule.startAnimation({
type: 'timing',
frames: [0, 0.1, 0.2, 0.4, 0.65, ...],
animatedValueId: 1,
});

이제 애니메이션이 실행될 때 일어나는 일을 정리하면 다음과 같다:

  • Native: 네이티브 애니메이션 드라이버는 CADisplayLink 또는 android.view.Choreographer를 사용해 매 프레임마다 실행되고, 애니메이션 곡선을 기반으로 계산한 새로운 값으로 드라이브되는 값을 업데이트한다.
  • Native: 중간 값이 계산되어 네이티브 뷰에 연결된 props 노드로 전달된다.
  • Native: UIView 또는 android.View가 업데이트된다.

보이는 것처럼 더 이상 JS 스레드와 브리지가 필요 없으므로 애니메이션이 더 빨라진다! 🎉🎉

앱에서 사용하는 방법

일반적인 애니메이션의 경우 답은 간단하다. 애니메이션을 시작할 때 설정에 useNativeDriver: true를 추가하면 된다.

이전 코드:

Animated.timing(this.state.animatedValue, {
toValue: 1,
duration: 500,
}).start();

변경 후 코드:

Animated.timing(this.state.animatedValue, {
toValue: 1,
duration: 500,
useNativeDriver: true, // <-- 이 부분 추가
}).start();

애니메이션 값은 하나의 드라이버와만 호환된다. 따라서 어떤 값에 대해 애니메이션을 시작할 때 네이티브 드라이버를 사용한다면, 해당 값에 대한 모든 애니메이션도 네이티브 드라이버를 사용해야 한다.

이것은 Animated.event에서도 동작한다. 스크롤 위치를 따라야 하는 애니메이션에 매우 유용하다. 네이티브 드라이버를 사용하지 않으면 React Native의 비동기적 특성 때문에 제스처보다 항상 한 프레임 뒤처지게 된다.

이전 코드:

<ScrollView
scrollEventThrottle={16}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: this.state.animatedValue } } }]
)}
>
{content}
</ScrollView>

변경 후 코드:

<Animated.ScrollView // <-- Animated ScrollView 래퍼 사용
scrollEventThrottle={1} // <-- 이벤트가 누락되지 않도록 1로 설정
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: this.state.animatedValue } } }],
{ useNativeDriver: true } // <-- 이 부분 추가
)}
>
{content}
</Animated.ScrollView>

주의사항

Native Animated에서 Animated의 모든 기능을 사용할 수 있는 것은 아니다. 주요 제한 사항은 레이아웃 속성이 아닌 프로퍼티만 애니메이션으로 처리할 수 있다는 점이다. 예를 들어 transformopacity는 작동하지만, 플렉스 박스와 위치 관련 속성은 지원하지 않는다. 또 다른 제한은 Animated.event인데, 이벤트 버블링이 아닌 직접 이벤트에서만 작동한다. 즉, PanResponder와는 호환되지 않지만 ScrollView#onScroll 같은 이벤트에서는 정상적으로 동작한다.

Native Animated는 React Native에 오랫동안 포함되어 있었지만, 실험적인 기능으로 간주되어 문서화되지 않았다. 따라서 이 기능을 사용하려면 React Native의 최신 버전(0.40 이상)을 사용해야 한다.

리소스

애니메이션에 대해 더 자세히 알고 싶다면 Christopher Chedeau이 강연을 추천한다.

애니메이션을 네이티브로 오프로딩하면 사용자 경험이 어떻게 개선되는지 깊이 있게 알고 싶다면 Krzysztof Magiera이 강연도 참고하면 좋다.

Right-to-Left Layout Support For React Native Apps

· 13 min read
Mengjue (Mandy) Wang
Software Engineer Intern at Facebook

앱 스토어에 앱을 출시한 후, 다음 단계는 더 많은 사용자를 확보하기 위해 국제화를 진행하는 것이다. 전 세계 20개 이상의 국가와 수많은 사람들이 오른쪽에서 왼쪽으로 쓰는 언어(RTL)를 사용한다. 따라서 앱이 RTL을 지원하도록 만드는 것이 필요하다.

React Native가 RTL 레이아웃을 지원하도록 개선되었다는 소식을 전하게 되어 기쁘다. 이 기능은 현재 react-native 마스터 브랜치에서 사용할 수 있으며, 다음 RC 버전인 v0.33.0-rc에서도 제공될 예정이다.

이를 위해 RN이 사용하는 핵심 레이아웃 엔진인 css-layout과 RN 코어 구현, 그리고 특정 오픈소스 JS 컴포넌트를 RTL 지원을 위해 변경했다.

RTL 지원을 실제 환경에서 테스트하기 위해, 최신 버전의 Facebook Ads Manager 앱(첫 번째 크로스 플랫폼 100% RN 앱)이 아랍어와 히브리어로 iOSAndroid에서 RTL 레이아웃으로 제공된다. 다음은 RTL 언어로 된 앱의 모습이다:

RN의 RTL 지원을 위한 주요 변경 사항

css-layout은 이미 레이아웃을 위해 startend 개념을 도입했다. Left-to-Right(LTR) 레이아웃에서 startleft를 의미하고, endright를 의미한다. 하지만 RTL 레이아웃에서는 startright를, endleft를 의미한다. 이를 통해 RN은 startend 계산에 의존해 position, padding, margin을 포함한 올바른 레이아웃을 계산할 수 있다.

또한 css-layout은 각 컴포넌트의 방향을 부모로부터 상속받도록 설계했다. 따라서 루트 컴포넌트의 방향을 RTL로 설정하면 전체 앱이 뒤집힌다.

아래 다이어그램은 이러한 변경 사항을 높은 수준에서 설명한다:

주요 변경 사항은 다음과 같다:

이 업데이트를 통해 앱에서 RTL 레이아웃을 허용하면:

  • 모든 컴포넌트 레이아웃이 수평으로 뒤집힌다.
  • RTL을 지원하는 OSS 컴포넌트를 사용 중이라면 일부 제스처와 애니메이션이 자동으로 RTL 레이아웃을 적용한다.
  • 앱을 완전히 RTL 지원으로 만들기 위해 필요한 추가 작업은 최소화된다.

앱을 RTL 지원으로 만들기

  1. RTL을 지원하려면 먼저 앱에 RTL 언어 번들을 추가한다.

    • iOSAndroid의 일반 가이드를 참고한다.
  2. 앱이 RTL 레이아웃을 지원할 수 있도록 네이티브 코드 시작 부분에서 allowRTL() 함수를 호출한다. 이 유틸리티는 앱이 준비되었을 때만 RTL 레이아웃을 적용한다. 예제는 다음과 같다.

    iOS:

    // AppDelegate.m 파일에서
    [[RCTI18nUtil sharedInstance] allowRTL:YES];

    Android:

    // MainActivity.java 파일에서
    I18nUtil sharedI18nUtilInstance = I18nUtil.getInstance();
    sharedI18nUtilInstance.allowRTL(context, true);
  3. Android의 경우 AndroidManifest.xml 파일의 <application> 엘리먼트에 android:supportsRtl="true"를 추가한다.

이제 앱을 다시 컴파일하고 기기 언어를 RTL 언어(예: 아랍어 또는 히브리어)로 변경하면 앱 레이아웃이 자동으로 RTL로 변경된다.

RTL 대응 컴포넌트 작성하기

일반적으로 대부분의 컴포넌트는 이미 RTL(오른쪽에서 왼쪽) 대응이 되어 있다. 예를 들어:

  • 왼쪽에서 오른쪽으로 배치된 레이아웃
  • 오른쪽에서 왼쪽으로 배치된 레이아웃

하지만 몇 가지 주의해야 할 경우가 있으며, 이때는 I18nManager를 사용해야 한다. I18nManager에는 isRTL이라는 상수가 있어 앱의 레이아웃이 RTL인지 아닌지를 확인할 수 있다. 이를 통해 레이아웃에 따라 필요한 변경을 적용할 수 있다.

방향성을 가진 아이콘 처리

컴포넌트에 아이콘이나 이미지를 사용할 때, RN은 소스 이미지를 자동으로 뒤집지 않기 때문에 LTR과 RTL 레이아웃에서 동일하게 표시된다. 따라서 레이아웃 스타일에 따라 아이콘을 직접 뒤집어야 한다.

  • 좌에서 우로(LTR) 레이아웃
  • 우에서 좌로(RTL) 레이아웃

아이콘의 방향을 조정하는 두 가지 방법은 다음과 같다:

  • 이미지 컴포넌트에 transform 스타일 추가:

    <Image
    source={...}
    style={{transform: [{scaleX: I18nManager.isRTL ? -1 : 1}]}}
    />
  • 또는, 방향에 따라 이미지 소스 변경:

    let imageSource = require('./back.png');
    if (I18nManager.isRTL) {
    imageSource = require('./forward.png');
    }
    return <Image source={imageSource} />;

제스처와 애니메이션

안드로이드와 iOS 개발에서 RTL 레이아웃으로 변경하면, 제스처와 애니메이션이 LTR 레이아웃과 반대 방향으로 동작한다. 현재 리액트 네이티브(RN)에서는 제스처와 애니메이션이 RN 코어 코드 수준에서 지원되지 않고, 컴포넌트 수준에서 지원된다. 다행히 일부 컴포넌트는 이미 RTL을 지원하고 있으며, 예를 들어 SwipeableRowNavigationExperimental가 있다. 그러나 제스처를 사용하는 다른 컴포넌트들은 수동으로 RTL을 지원해야 한다.

제스처 RTL 지원을 잘 보여주는 예로 SwipeableRow를 들 수 있다.

제스처 예제
// SwipeableRow.js
_isSwipingExcessivelyRightFromClosedPosition(gestureState: Object): boolean {
// ...
const gestureStateDx = IS_RTL ? -gestureState.dx : gestureState.dx;
return (
this._isSwipingRightFromClosed(gestureState) &&
gestureStateDx > RIGHT_SWIPE_THRESHOLD
);
},
애니메이션 예제
// SwipeableRow.js
_animateBounceBack(duration: number): void {
// ...
const swipeBounceBackDistance = IS_RTL ?
-RIGHT_SWIPE_BOUNCE_BACK_DISTANCE :
RIGHT_SWIPE_BOUNCE_BACK_DISTANCE;
this._animateTo(
-swipeBounceBackDistance,
duration,
this._animateToClosedPositionDuringBounce,
);
},

RTL 지원 앱 유지 관리

초기 RTL 호환 앱 출시 이후에도 새로운 기능을 반복적으로 추가해야 할 경우가 많다. 개발 효율성을 높이기 위해 I18nManager는 테스트 기기의 언어 설정을 변경하지 않고도 빠르게 RTL을 테스트할 수 있는 forceRTL() 함수를 제공한다. 앱 내에 이 기능을 간단히 전환할 수 있는 스위치를 추가하는 것도 좋은 방법이다. 다음은 RNTester의 RTL 예제에서 가져온 코드다:

<RNTesterBlock title={'Quickly Test RTL Layout'}>
<View style={styles.flexDirectionRow}>
<Text style={styles.switchRowTextView}>forceRTL</Text>
<View style={styles.switchRowSwitchView}>
<Switch
onValueChange={this._onDirectionChange}
style={styles.rightAlignStyle}
value={this.state.isRTL}
/>
</View>
</View>
</RNTesterBlock>;

_onDirectionChange = () => {
I18nManager.forceRTL(!this.state.isRTL);
this.setState({isRTL: !this.state.isRTL});
Alert.alert(
'Reload this page',
'Please reload this page to change the UI direction! ' +
'All examples in this app will be affected. ' +
'Check them out to see what they look like in RTL layout.',
);
};

새로운 기능을 개발할 때 이 버튼을 쉽게 토글하고 앱을 리로드해 RTL 레이아웃을 확인할 수 있다. 이 방법의 장점은 언어 설정을 변경하지 않고도 테스트가 가능하다는 점이다. 하지만 다음 섹션에서 설명하듯 일부 텍스트 정렬은 변경되지 않을 수 있다. 따라서 앱 출시 전에 반드시 RTL 언어로 테스트하는 것이 좋다.

한계와 향후 계획

RTL(오른쪽에서 왼쪽) 지원은 앱의 대부분의 사용자 경험을 커버해야 한다. 하지만 현재 몇 가지 제한 사항이 존재한다:

  • 텍스트 정렬 방식이 Android와 iOS에서 다르게 동작한다.
    • iOS에서는 기본 텍스트 정렬이 활성 언어 번들에 따라 결정되며, 항상 한쪽으로 고정된다. Android에서는 텍스트 콘텐츠의 언어에 따라 달라진다. 예를 들어, 영어는 왼쪽 정렬, 아랍어는 오른쪽 정렬된다.
    • 이론적으로는 플랫폼 간 일관성을 유지해야 하지만, 사용자가 앱을 사용할 때 한 동작을 다른 동작보다 선호할 수 있다. 텍스트 정렬에 대한 최적의 방식을 찾기 위해 더 많은 사용자 경험 연구가 필요할 수 있다.
  • "진짜" 왼쪽/오른쪽 개념이 없다.

    앞서 논의한 것처럼, JS 측면에서 left/right 스타일을 start/end로 매핑한다. RTL 레이아웃에서 코드의 모든 left는 화면에서 "오른쪽"이 되고, 코드의 right는 화면에서 "왼쪽"이 된다. 이는 제품 코드를 크게 변경할 필요가 없어 편리하지만, 코드에서 "진짜 왼쪽"이나 "진짜 오른쪽"을 지정할 방법이 없다는 의미이다. 향후에는 언어와 무관하게 컴포넌트가 방향을 제어할 수 있도록 하는 것이 필요할 수 있다.

  • 제스처와 애니메이션에 대한 RTL 지원을 개발자 친화적으로 개선한다.

    현재 제스처와 애니메이션이 RTL과 호환되도록 하려면 여전히 프로그래밍 노력이 필요하다. 앞으로는 제스처와 애니메이션의 RTL 지원을 더 개발자 친화적으로 만드는 방법을 찾는 것이 이상적일 것이다.

직접 해보기!

RTL 지원에 대해 더 깊이 이해하려면 RNTesterRTLExample을 확인해 보세요. 여러분의 경험을 공유해 주시면 감사하겠습니다!

마지막으로, 이 글을 읽어 주셔서 감사합니다! React Native의 RTL 지원이 여러분의 앱을 전 세계 사용자에게 더 나은 경험을 제공하는 데 도움이 되길 바랍니다!

Dive into React Native Performance

· 3 min read
Pieter De Baets
Software Engineer at Facebook

React Native를 사용하면 React와 Relay의 선언적 프로그래밍 모델을 통해 JavaScript로 Android와 iOS 앱을 개발할 수 있다. 이를 통해 더 간결하고 이해하기 쉬운 코드를 작성할 수 있으며, 컴파일 과정 없이 빠르게 반복 작업을 진행할 수 있다. 또한 여러 플랫폼 간에 코드를 쉽게 공유할 수 있다. 이렇게 하면 더 빠르게 앱을 출시할 수 있고, 앱의 외관과 사용자 경험을 훌륭하게 만드는 데 집중할 수 있다. 성능 최적화는 이 과정에서 매우 중요한 부분이다. 다음은 React Native 앱의 시작 속도를 두 배로 향상시킨 과정에 대한 이야기이다.

왜 속도가 중요한가?

더 빠르게 동작하는 앱은 콘텐츠를 신속하게 로드한다. 사용자는 더 많은 시간을 앱과 상호작용할 수 있고, 부드러운 애니메이션은 앱 사용을 즐겁게 만든다. 2011년형 폰2G 네트워크가 대부분인 신흥 시장에서는 성능에 초점을 맞추는 것이 앱 사용 가능 여부를 결정짓는 중요한 요소가 된다.

React Native를 iOSAndroid에 출시한 이후, 우리는 리스트 뷰 스크롤 성능, 메모리 효율성, UI 반응성, 앱 시작 시간을 지속적으로 개선해왔다. 시작 시간은 앱의 첫인상을 결정짓고 프레임워크의 모든 부분에 부담을 주기 때문에, 가장 보람있고 도전적인 문제다.

이 글은 발췌문이다. 전체 글은 Facebook Code에서 확인할 수 있다.

Introducing Hot Reloading

· 16 min read
Martín Bigio
Software Engineer at Instagram

React Native의 목표는 최고의 개발자 경험을 제공하는 것이다. 그중 중요한 부분은 파일을 저장하고 변경 사항을 확인할 때까지 걸리는 시간이다. 앱이 커질수록 이 피드백 루프를 1초 미만으로 단축하는 것이 목표이다.

이 목표에 가까워지기 위해 세 가지 주요 기능을 도입했다:

  • 자바스크립트를 사용해 컴파일 시간을 줄인다.
  • es6/flow/jsx 파일을 VM이 이해할 수 있는 일반 자바스크립트로 변환하는 Packager 도구를 구현했다. 이 도구는 서버로 설계되어 중간 상태를 메모리에 유지해 빠른 증분 변경을 가능하게 하고, 멀티코어를 활용한다.
  • 파일 저장 시 앱을 다시 로드하는 Live Reload 기능을 구축했다.

현재 개발자들의 병목 현상은 앱을 다시 로드하는 시간이 아니라 앱 상태를 잃는 문제이다. 일반적인 시나리오는 런치 스크린에서 여러 화면을 거쳐야 하는 기능을 작업하는 경우이다. 매번 다시 로드할 때마다 동일한 경로를 반복해 클릭해야 하기 때문에 피드백 루프가 몇 초씩 길어지게 된다.

핫 리로딩

핫 리로딩의 핵심 개념은 앱을 계속 실행한 상태에서 수정한 파일의 새 버전을 런타임에 주입하는 것이다. 이 방식을 사용하면 앱의 상태를 잃지 않으면서 UI를 조정할 수 있다.

동영상 하나가 수많은 설명보다 효과적이다. 라이브 리로딩(기존 방식)과 핫 리로딩(새 방식)의 차이를 확인해 보자.

자세히 보면, 빨간색 박스에서 복구가 가능하고, 이전에 없던 모듈을 가져오더라도 전체 리로드 없이 시작할 수 있다는 것을 알 수 있다.

주의 사항: 자바스크립트는 상태를 많이 사용하는 언어이기 때문에 핫 리로딩을 완벽하게 구현할 수 없다. 실제로 현재 설정은 대부분의 일반적인 사용 사례에서 잘 작동하며, 문제가 발생할 경우 언제든지 전체 리로드를 사용할 수 있다.

핫 리로딩은 0.22 버전부터 사용 가능하며, 다음과 같이 활성화할 수 있다:

  • 개발자 메뉴를 연다.
  • "핫 리로딩 활성화"를 탭한다.

핵심 구현 과정

이제 핫 리로딩이 왜 필요한지, 어떻게 사용하는지 알았으니 실제 동작 원리를 살펴보자.

핫 리로딩은 Hot Module Replacement(HMR)라는 기능 위에 구축되었다. 이 기능은 webpack에서 처음 도입했고, React Native Packager 내부에 구현했다. HMR은 Packager가 파일 변경을 감지하고, 앱에 포함된 간단한 HMR 런타임으로 HMR 업데이트를 전송한다.

간단히 말해, HMR 업데이트는 변경된 JS 모듈의 새 코드를 포함한다. 런타임이 이를 받으면, 이전 모듈의 코드를 새 코드로 대체한다:

HMR 업데이트는 단순히 변경할 모듈의 코드만 포함하지 않는다. 코드를 교체하는 것만으로는 런타임이 변경 사항을 반영하기에 충분하지 않기 때문이다. 문제는 모듈 시스템이 이미 업데이트하려는 모듈의 _exports_를 캐시했을 가능성이다. 예를 들어, 다음과 같은 두 모듈로 구성된 앱이 있다고 가정하자:

// log.js
function log(message) {
const time = require('./time');
console.log(`[${time()}] ${message}`);
}

module.exports = log;
// time.js
function time() {
return new Date().getTime();
}

module.exports = time;

log 모듈은 time 모듈이 제공하는 현재 날짜를 포함해 메시지를 출력한다.

앱이 번들링될 때, React Native는 __d 함수를 사용해 각 모듈을 모듈 시스템에 등록한다. 이 앱의 경우, 여러 __d 정의 중 log에 대한 정의가 하나 있을 것이다:

__d('log', function() {
... // 모듈의 코드
});

이 호출은 각 모듈의 코드를 익명 함수로 감싸며, 이를 일반적으로 팩토리 함수라고 부른다. 모듈 시스템 런타임은 각 모듈의 팩토리 함수, 이미 실행되었는지 여부, 그리고 실행 결과(exports)를 추적한다. 모듈이 필요할 때, 모듈 시스템은 이미 캐시된 exports를 제공하거나 모듈의 팩토리 함수를 처음 실행하고 결과를 저장한다.

앱을 시작하고 log를 요청한다고 가정하자. 이 시점에서 logtime의 팩토리 함수는 아직 실행되지 않았으므로 exports가 캐시되지 않았다. 그런 다음 사용자가 time을 수정해 날짜를 MM/DD 형식으로 반환하도록 변경한다:

// time.js
function bar() {
const date = new Date();
return `${date.getMonth() + 1}/${date.getDate()}`;
}

module.exports = bar;

Packager는 time의 새 코드를 런타임으로 전송한다(1단계). 그리고 log가 결국 요청되면, time의 변경 사항이 반영된 상태로 exported 함수가 실행된다(2단계):

이제 log 코드가 time을 최상위 require로 요청한다고 가정하자:

const time = require('./time'); // 최상위 require

// log.js
function log(message) {
console.log(`[${time()}] ${message}`);
}

module.exports = log;

log가 요청되면, 런타임은 logtime의 exports를 캐시한다(1단계). 그런 다음 time이 수정되면, HMR 프로세스는 단순히 time의 코드를 교체하는 것으로 끝날 수 없다. 그렇게 하면 log가 실행될 때 time의 캐시된 복사본(이전 코드)을 사용하게 된다.

logtime의 변경 사항을 반영하려면, log의 캐시된 exports를 지워야 한다. 왜냐하면 log가 의존하는 모듈 중 하나가 핫 스왑되었기 때문이다(3단계). 마지막으로 log가 다시 요청되면, 팩토리 함수가 실행되면서 time을 요청하고 새 코드를 가져온다.

HMR API

React Native의 HMR(Hot Module Replacement)은 hot 객체를 도입해 모듈 시스템을 확장한다. 이 API는 webpack의 HMR API를 기반으로 한다. hot 객체는 accept라는 함수를 제공하는데, 이 함수를 사용해 모듈이 핫 스왑될 때 실행될 콜백을 정의할 수 있다. 예를 들어, time의 코드를 다음과 같이 변경하면, time을 저장할 때마다 콘솔에 "time changed"가 출력된다:

// time.js
function time() {
... // 새로운 코드
}

module.hot.accept(() => {
console.log('time changed');
});

module.exports = time;

이 API를 직접 사용해야 하는 경우는 드물다. 대부분의 일반적인 사용 사례에서는 핫 리로딩이 기본적으로 동작한다.

HMR 런타임

앞서 살펴봤듯이, HMR 업데이트를 단순히 수락하는 것만으로는 충분하지 않다. 이미 실행된 모듈이 핫 스왑된 모듈을 사용하고 있을 수 있고, 해당 모듈의 임포트가 캐시된 상태일 수 있다. 예를 들어, 영화 앱 예제의 의존성 트리에서 최상위에 MovieRouter가 있고, 이 모듈이 MovieSearchMovieScreen 뷰에 의존하며, 이 뷰들은 이전 예제의 logtime 모듈에 의존한다고 가정해 보자.

사용자가 영화 검색 뷰에 접근했지만 다른 뷰에는 접근하지 않았다면, MovieScreen을 제외한 모든 모듈의 익스포트가 캐시된다. time 모듈에 변경이 발생하면, 런타임은 log의 익스포트를 지워 time의 변경 사항을 반영해야 한다. 이 프로세스는 여기서 끝나지 않는다. 런타임은 모든 부모 모듈이 업데이트를 수락할 때까지 이 과정을 재귀적으로 반복한다. 따라서 log에 의존하는 모듈들을 가져와 업데이트를 시도한다. MovieScreen의 경우 아직 필요하지 않았기 때문에 중단할 수 있다. MovieSearch의 경우 익스포트를 지우고 부모 모듈을 재귀적으로 처리해야 한다. 마지막으로 MovieRouter에 대해 동일한 작업을 수행하고, 더 이상 의존하는 모듈이 없으므로 프로세스를 종료한다.

런타임은 의존성 트리를 탐색하기 위해 HMR 업데이트 시 Packager로부터 역의존성 트리를 받는다. 이 예제에서 런타임은 다음과 같은 JSON 객체를 받게 된다:

{
"modules": [
{
"name": "time",
"code": /* time's new code */
}
],
"inverseDependencies": {
"MovieRouter": [],
"MovieScreen": ["MovieRouter"],
"MovieSearch": ["MovieRouter"],
"log": ["MovieScreen", "MovieSearch"],
"time": ["log"]
}
}

리액트 컴포넌트

리액트 컴포넌트는 핫 리로딩과 함께 사용하기가 조금 더 까다롭다. 문제는 기존 코드를 새로운 코드로 단순히 교체할 수 없다는 점이다. 그렇게 하면 컴포넌트의 상태를 잃어버리기 때문이다. 리액트 웹 애플리케이션의 경우, Dan Abramov가 이 문제를 해결하기 위해 웹팩의 HMR API를 사용하는 바벨 트랜스폼을 구현했다. 간단히 말해, 그의 솔루션은 _트랜스폼 시점_에 모든 리액트 컴포넌트에 대한 프록시를 생성하는 방식으로 동작한다. 프록시는 컴포넌트의 상태를 유지하고, 실제 컴포넌트에 라이프사이클 메서드를 위임한다. 이 실제 컴포넌트가 핫 리로딩의 대상이 된다:

프록시 컴포넌트를 생성하는 것 외에도, 이 트랜스폼은 리액트가 컴포넌트를 리렌더링하도록 강제하는 코드를 포함한 accept 함수를 정의한다. 이 방식을 통해 앱의 상태를 잃지 않고 렌더링 코드를 핫 리로딩할 수 있다.

리액트 네이티브에 기본으로 포함된 트랜스포머babel-preset-react-native를 사용한다. 이 프리셋은 웹팩을 사용하는 리액트 웹 프로젝트와 동일한 방식으로 react-transform을 사용하도록 설정되어 있다.

Redux 스토어

Redux 스토어에서 핫 리로딩을 활성화하려면, webpack을 사용하는 웹 프로젝트에서와 유사하게 HMR API를 사용하면 된다:

// configureStore.js
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import reducer from '../reducers';

export default function configureStore(initialState) {
const store = createStore(
reducer,
initialState,
applyMiddleware(thunk),
);

if (module.hot) {
module.hot.accept(() => {
const nextRootReducer = require('../reducers/index').default;
store.replaceReducer(nextRootReducer);
});
}

return store;
};

리듀서를 변경하면, 해당 리듀서를 수락하는 코드가 클라이언트로 전송된다. 그런 다음 클라이언트는 리듀서가 스스로를 수락하는 방법을 모른다는 것을 인식하고, 해당 리듀서를 참조하는 모든 모듈을 찾아 수락하려고 시도한다. 결국, 이 흐름은 단일 스토어인 configureStore 모듈에 도달하여 HMR 업데이트를 수락하게 된다.

결론

핫 리로딩 기능을 개선하는 데 관심이 있다면, Dan Abramov가 쓴 핫 리로딩의 미래에 관한 글을 읽고 기여해 보길 권한다. 예를 들어, Johny Days는 여러 연결된 클라이언트와 함께 작동하도록 만드는 작업을 진행 중이다. 이 기능을 유지하고 발전시키는 데 여러분의 도움이 필요하다.

React Native를 통해 우리는 더 나은 개발자 경험을 제공하기 위해 앱을 만드는 방식을 재고할 수 있다. 핫 리로딩은 퍼즐의 한 조각에 불과하다. 더 나은 경험을 만들기 위해 어떤 창의적인 해결책을 적용할 수 있을까?

React Native 앱 접근성 구현하기

· 2 min read
Georgiy Kassabli
Facebook 소프트웨어 엔지니어

웹에서 React, 모바일에서 React Native가 출시되면서 개발자들이 제품을 구축할 수 있는 새로운 프론트엔드 프레임워크를 제공했다. 견고한 제품을 만들기 위한 핵심 요소 중 하나는 시각 장애가 있거나 다른 장애를 가진 사람들을 포함해 누구나 사용할 수 있도록 보장하는 것이다. React와 React Native의 접근성 API를 사용하면 시각 장애인을 위한 스크린 리더 같은 보조 기술을 사용하는 사람들도 React 기반 경험을 활용할 수 있다.

이 글에서는 React Native 앱에 초점을 맞춘다. React 접근성 API는 Android와 iOS API와 유사하게 디자인했다. Android, iOS 또는 웹을 위해 접근성 있는 애플리케이션을 개발해본 경험이 있다면 React AX API의 프레임워크와 용어에 익숙할 것이다. 예를 들어, UI 엘리먼트를 _accessible_로 설정해(따라서 보조 기술에 노출) 엘리먼트에 대한 문자열 설명을 제공하기 위해 _accessibilityLabel_을 사용할 수 있다:

<View accessible={true} accessibilityLabel="This is simple view">

Facebook의 React 기반 제품 중 하나인 Ads Manager 앱을 살펴보며 React AX API를 조금 더 깊이 있게 적용하는 방법을 알아보자.

이 글은 발췌문이다. Facebook Code에서 나머지 내용을 읽어보자.