React Native(리액트 네이티브, 이하 RN)는 Meta가 개발한 오픈소스 크로스플랫폼 프레임워크로, React의 컴포넌트 모델을 모바일 네이티브 앱 개발로 확장한 프레임워크다.
리액트 문법 기반으로 한 낮은 러닝 커브, 리액트 특유의 풍부한 생태계 상당 부분을 사용할 수 있는 유연함 등의 이유로 빠른 인력 투입 및 개발을 필요로 한 스타트업에서 많이 애용되고 있으며, 나 역시 실무에서 대부분의 앱 개발을 RN으로 진행했다.
Expo는 RN의 네이티브 복잡성을 크게 줄여 “설치→코딩→빌드”까지 일관된 개발자 경험(DX)을 제공하도록 만들어진 툴킷이자 플랫폼으로, RN이 첫 출시된 뒤(2015년) "초기 RN의 네이티브 빌드 번거로움"을 해결하고자 하는 비교적 가벼운(?) 접근으로 빠르게 등장했다.
하지만 Expo는 미리 정의된 네이티브 모듈과 API를 사용하도록 설계되었기 때문에 추가로 매우 특수하거나 복잡한 네이티브 기능이 필요할 경우에는 이를 직접 구현하거나 Expo 환경 밖에서 빌드해야만 했다. 이에 따라 초기에는 "Expo에서 시작하면 추후 네이티브 확장성이 부족하다"는 편견이 생기기도 했었다.
Expo는 이러한 편견을 극복하기 위해 RN 오픈소스에 꾸준히 기여하며, Meta와의 긴밀한 협업을 지속하고자 노력했다. 이에 따라 expo-dev-client와 expo-config-plugins 등의 도구 및 워크플로우를 개발하였고, 개발자가 Expo 관리 워크플로에서 벗어나지 않고도 원하는 네이티브 확장이 가능하도록 개선하기 위한 노력을 멈추지 않았다.
그 결과로, 2024년 React Conf에서 RN의 새로운 아키텍처를 도입하는 과정에서 Meta는 “React Native 앱을 새로 시작할 땐 반드시 Expo(혹은 Expo와 같은 framework)를 써라”라는 공식 입장을 밝히기도 했다. React Native 공식문서에서도 Expo로 프로젝트를 시작하도록 아예 가이드라인이 잡혀있는 것을 확인할 수 있다.
최근 사이드 프로젝트로 Android 환경에 집중한 RN 기반의 앱을 작업하고 있는데, 공식 문서의 가이드라인을 통해 Expo를 활용하고 있고, 2023년도라는 늦은 시간에 Expo를 써본 내가 느끼기에도 불과 몇 년 사이에도 React 생태계를 잘 활용할 수 있도록 안정성이 많이 높아졌다고 느껴졌다.
그렇다면 2024년 React conf에서 도입된 RN의 새로운 아키텍처는 어떤 구조이며, Expo는 이 부분에서 어떻게 핵심 역할을 수행했는지 알아보도록 하자.
새로운 아키텍처가 필요했던 이유 (기존 아키텍처의 문제점)
RN은 기존 방식의 구조적 한계와 퍼포먼스, 최신 React 기능 호환성 부족을 극복하기 위해 새롭게 아키텍처를 구성하여 발표했다. 이는 성능·유연성·차세대 React 기능 지원을 위해 도입된 근본적인 내부 재설계였다.
먼저 기존 아키텍처의 문제점들을 짚어보며 왜 새로운 아키텍처 설계가 필요했는지 알아보자.
RN은 모든 데이터·이벤트·명령이 JS와 네이티브(runtime) 간에 "Bridge"라는 비동기 통신 채널을 통해 주고받는다. 이때, Bridge 구조는 JS 측에서 호출된 명령이나 데이터를 JSON 형태로 직렬화하여 브리지로 전송하고, 네이티브 측에서 다시 역직렬화하여 처리하는 과정을 거친다.
위 과정 덕분에 우리는 기존의 React와 거의 동일한 방식으로 코드를 구성할 수 있고, 리액트를 알고 있다면 별도의 네이티브 언어(Kotlin, Swift 등)를 배우지 않아도 앱을 개발할 수 있게 되었다.
하지만, 문제점도 있었다.
Brideg 구조는 명령이 Bridge로 전달될 때마다 큐(Queue)에 쌓여 순차적으로 처리하기 때문에 브리지를 경유하는 데이터 전달에 따른 직렬화/역직렬화 비용, 큐 대기 등으로 UI/이벤트 처리에 지연이 발생한다. 또한, 앱 실행 시 모든 네이티브 모듈이 한 번에 초기화(Eager loading)되어 필요 여부와 관계없이 메모리를 사용하기 때문에 메모리나 성능적으로도 리소스가 낭비된다.
동기화에 대한 문제도 꾸준히 언급되었다.
기존 아키텍처에서는 JS와 네이티브 코드 간의 데이터·이벤트 주고받기가 항상 비동기(Async) Bridge로만 처리되었다. RN이 처음 설계되었을 때에는 JS와 네이티브에 완전한 타입/메모리 일치를 보장하기 어려워 안전을 위해 브리지 큐에서 메시지 단위로만 통신하도록 설계했기 때문이다. 이로 인해 사용자가 스크롤·드래그·제스처 같은 민감한 UI 동작을 할 때, JS와 네이티브 간 실시간으로 바로 메시지를 주고받을 수 없고, 항상 큐에서 기다리며 처리되어 “미세하게 느린 느낌" 또는 "프레임 드랍”을 느낄 수 있었다.
비동기 Bridge 구조는 React Concurrent Rendering(동시성 렌더링), Suspense, Transitions처럼 최신 웹 React에서 도입된 기능들의 사용에 제한되는 문제점도 발생시켰다. Concurrent Rendering은 여러 렌더링 과정을 동시에 처리하면서 필요한 UI만 우선 렌더링 할 수 있게 해 주는데, 기존 브리지 구조에선 JS와 네이티브 간 비동기 큐를 거치기 때문에 React의 동시 작업/우선순위 조정 등이 네이티브 UI에 바로 반영되지 못했다.
또한 구 아키텍처에는 자동 인터페이스 생성(Codegen) 기능이 없었고, JS와 네이티브 간 타입 체크나 추상화 계층이 부족했기 때문에 JS에서 네이티브 함수를 호출하려면 두 언어(예: JavaScript와 Java/ObjC)에서 동일한 이름·타입·시그니처를 각각 따로 구현해야 했다.
당연스럽게도 코드 관리 및 버그 수정에 리소스가 많이 들었고, RN은 개발자 경험(DX)의 변화가 필요했다는 결론에 도달했다.
RN의 New Architecture
RN의 새로운 아키텍처의 가장 핵심이 되는 변경점은 JSI(JavaScript Interface)의 도입이다.
JSI는 자바스크립트와 네이티브 두 레이어 사이를 직접 연결해 주는 추상화(통신) 계층으로, 기존의 Bridge 레이어를 대체했다.
앞서 언급했듯, Bridge 레이어는 비동기로만 이루어졌기 때문에 UI 랜더링에 영향을 주었다. 이에 반해 JSI는 메모리 레벨에서 객체와 함수에 직접 접근하고, 런타임/네이티브가 서로를 바로 호출·제어할 수 있도록 설계했기 때문에 동기/비동기 모두 지원된다.
JavaScript 엔진(Hermes, JSC, V8 등)은 C++ 객체로 구성되어 있으며, 네이티브(안드로이드/아이폰) 역시 C++ 바인딩을 통해 메모리 공간을 일부 공유한다. 이때 JSI가 React Native 코어와 각 플랫폼 네이티브 모듈(ObjC/Java/C++) 사이에 C++로 작성된 공통 인터페이스 계층을 제공한다.
공통 인터페이스 계층은 다음과 같은 방식으로 구성된다.
// 네이티브 모듈에서
jsi::Function myNativeFunc = ...;
globalObject.setProperty(runtime, "myNativeFunc", myNativeFunc);
먼저 JavaScript 객체/함수를 jsi::Object, jsi::Function 등 C++ 객체 형태로 감싼다. (jsi::Function, jsi::Object는 C++ 레이어에서의 래퍼 객체)
// C++ 네이티브 코드에서
auto jsFunc = globalObject.getPropertyAsFunction(runtime, "myJSFunc");
jsFunc.call(runtime, { ... 파라미터 ... });
반대로 네이티브 코드에서 JSI Function 객체를 즉시 실행할 수 있다.
즉, 네이티브와 JS는 함수/객체가 C++ 포인터 및 객체로 직접 래핑 되었기 때문에 동일 메모리에서 함수/객체에 포인터로 접근해 호출/데이터 전송이 가능하다.
굉장히 좋은 기능인데, 한 가지 의문이 든다.
이렇게 좋은 기능을 왜 이제서야?
먼저, 기존 아키텍처의 설계 철학과 한계를 생각해 볼 수 있다.
2015년 RN 초창기에는 다양한 JS 엔진과 플랫폼(iOS/Android)에서 최대한 안전하고 호환성 높은 구조가 필요했다. 당시 가장 보편적이고 안전한 JS-네이티브 연결 방식은 직렬화 → 큐(브리지) → 역직렬화 패턴이었다. 기술적으로 구현이 쉽고, 런타임이 예측 가능했으며, 플랫폼별 충돌 여지를 최소화할 수 있는 선택지였기 때문이다.
기술적 어려움도 한몫했다.
당시에는 모바일 JS 엔진(Hermes, JSC, V8 등)이 각기 다른 인터페이스와 성능, 안정성을 보여 JS-네이티브 "직접 메모리 공유"가 쉽지 않았다. JS와 네이티브가 메모리 오브젝트/포인터를 공유하기 위해서는 안전한 가비지 컬렉션/메모리 관리를 보장해야 하는데, 플랫폼 간 일관성이 부족했기 때문이다. 따라서 당시로서는 구현 난이도, 메모리 누수 위험, 예측불가 버그 등 실현 장벽이 높은 현실적인 문제도 있었다.
다양한 OS, 다양한 JS 엔진, 다양한 네이티브 API를 C++ 추상화 계층으로 안전하게 통합하는 기술은 오랜 시간과 커뮤니티·Core 팀의 협업, 여러 버전의 릴리즈와 실전 테스트가 필요했다. Expo, Meta, 커뮤니티의 오랜 준비, JS 엔진(Hermes)의 고도화, TurboModules 모듈 시스템, Fabric 렌더러 등 지속적인 개발과 노력 끝에 인프라가 준비된 최근에서야 충분히 안정적으로 제공할 수 있게 된 것이다. 사랑해요 연예가ㅈ...아니고 RN!
TurboModules와 Fabric 개념에 대해서도 잠깐 짚고 넘어가자.
TurboModules는 RN의 새로운 네이티브 모듈 시스템으로, JSI를 활용해 네이티브 모듈과 JS 사이의 직접·동기 호출이 가능하게 한다.
이를 통해 필요한 시점에만 모듈을 동적으로 로드(lazy loading)해 앱 시작 속도와 메모리 사용량을 대폭 개선시킬 수 있었다. 또한, TypeScript/Flow, Codegen을 활용해 네이티브 모듈의 명세(interface)와 실제 네이티브 코드/JS가 자동으로 동기화되게 도와준다. 즉, 네이티브-자바스크립트 간 커뮤니케이션 병목지점 제거 역할을 맡고 있는 셈이다.
Fabric은 React Native의 새로운 UI 렌더러로, JSI 및 공통 C++ 코어를 활용해 RN의 UI 렌더링을 플랫폼(iOS/Android/web) 간 일관성 있게, 그리고 동기적으로 처리해 준다.
덕분에 UI 업데이트를 브리지 없이 동기 처리하기 때문에 프레임 드롭이나 잔상 현상을 최소화할 수 있게 되었고, 렌더 트리(Shadow tree) 구조가 불변(immutable)하게 관리되어 동시성 이슈 및 메모리 문제도 해결해 주었다. 즉, Fabric은 React의 렌더링 최신 기능(동시성, 우선순위 관리 등)을 네이티브에서 제공하는 신규 UI 엔진이다.
앞서 언급한 JSI, TurboModules, Fabric 세 요소가 합쳐져서 RN의 새로운 아키텍처의 프레임워크적 토대를 이룬 것이다.
Expo팀은 어떤 실질적 기여와 기술적 주도권을 보여줬는가?
앞서 언급했듯, Expo는 RN 출시 초기부터 생태계 발전에 기여하며 표준의 중심이 되고자 실질적이고 다양한 도움을 제공했다.
특히 초창기의 복잡한 네이티브 빌드 추상화를 통해 환경 설정 없이도 앱을 빠르게 만들고 띄워볼 수 있게 했고, 핵심 네이티브 API를 빌트인으로 제공해 모바일 기능을 별도 네이티브 수정 없이 사용할 수 있도록 Expo SDK로 패키징/배포했다. 이 외에도 적극적인 오픈소스 기여, 다양한 샘플, 문서, 포럼 및 컨퍼런스 참여로 RN 커뮤니티 성장 및 확장에 크게 기여했다.
이러한 노력 덕에 새로운 아키텍처 개발 과정에 Expo 팀이 포함되어 있는 것은 놀랄 일이 아니었다.
Expo 팀은 RN 팀과 긴밀히 협력하여 TurboModules, Fabric 등 신구조를 앞장서서 도입해 사용성 및 호환성 테스트가 가능하게 했다. 덕분에 New Architecture 완성 전부터 수많은 앱을 실제 Expo 환경에서 돌려볼 수 있었고, 문제점, 버그, 불안정 요소들을 빠르게 파악하고 수정할 수 있었다.
또한, Expo의 네이티브 확장성 한계를 전반적으로 해소하기 위한 장기 프로젝트들인 네이티브 확장/직접 빌드를 위한 도구(expo-dev-client), 프로젝트 설정 자동화 플러그인(expo-config-plugins) 등 개발이 완료되며 기존의 RN+Expo 조합의 한계를 해소했다. 이 과정에서 CI/CD, 앱스토어 업로드, OTA(Over-the-air) 업데이트 등 새로운 아키텍처에 필요한 실전 빌드·배포·운영 인프라를 EAS(Expo Application Services)로 빠르게 제공했고, 앱 빌드·배포 자동화 실현에 Expo가 많은 기여를 했다는 평가가 많다.
덕분에 Expo는 RN에서 2024년 이후 새롭게 제시한 프레임워크 기준을 충족하며 컨퍼런스를 통해 "공식 프레임워크"가 될 수 있었다. 이는 RN 공식 Getting Started, 환경설정, 공식 문서 등에서 Expo 기반 생성이 표준으로 안내되는 것을 통해 알 수 있다.
마치며
개인적으로 RN 개발을 하면서 가장 힘들었던 부분 중 하나가 라이브러리에 따른 호환성 문제였다. 각 라이브러리들의 안정성이 천차만별이라 Node, Java, RN 버전에 따른 호환성을 유지하기가 어려웠고, 새로운 프로젝트에서 기존에 사용했던 라이브러리를 그대로 사용하기 어려웠다.
이 때문에 새로운 프로젝트에서 기존에 사용하던 라이브러리를 그대로 사용하기가 힘들었고, 프로덕트를 기한 내에 완성하기 위해 이슈가 해결되지 않은 상태에서도 우회하거나 직접 코드를 수정해서 사용하는 일이 생각보다 빈번하게 일어났다. 이로 인한 리소스 소모도 꽤 컸다고 생각한다.
Expo는 그런 개발상의 어려움을 가장 쉽고 효과적으로 해결해준 프레임워크였다.
npx expo install {패키지명}
Expo 프로젝트 내에서 위 명령어 한 줄이면 현재 RN 버전에서 호환성을 유지해 줄 수 있는 버전들을 일괄적으로 관리하고 설치해주기 때문이다.
새롭게 바뀐 Expo Router 같은 좋은 기능도 많지만, Expo가 제공하는 버전 관리와 패키지 호환성 일괄 설치 기능이 Expo가 "프레임워크"로서 가진 핵심적인 강점이라고 생각한다.
물론 그만큼 Expo가 RN 초기부터 끊임없이 발전하고자 한 노력이 있었기 때문이라고 생각한다.
JS의 프레임워크에 큰 축을 담당하는 React, 그런 React가 만든 앱 개발 프레임워크 React Native.
React가 JS 생태계를 대표하는 프레임워크가 되기 위해 노력하는 것처럼, Expo는 React Native 생태계 내에서 비슷한 중심적 역할을 하고 싶은 게 아닐까?
참조
- React Conf Keynote (Day 2)
- Embracing Expo: The New Standard for Creating React Native Apps
- About the New Architecture, Fabric, Turbo Native Modules: iOS
- React Conf 20242 Recap
- Deep dive into React Native JSI
- Understanding React Native's New Architecture: Fabric and TurboModules Explained
'Front-End > React + Native' 카테고리의 다른 글
[React]useReducer와 useContext로 알아보는 전역 상태 관리 (1) | 2025.02.01 |
---|---|
[React]useState vs. useReducer (0) | 2025.02.01 |
[React]왜 리액트는 뮤텍스 잠금(Mutex Lock)이 없을까? (2) | 2024.12.15 |
[React-Native]데이터 인코딩 기본 개념, 근데 이제 RNFS를 곁들인 (1) | 2024.11.24 |
[React]React 16 버전에서의 주요 변화점과 동작 원리 (4) | 2024.10.13 |
댓글