React 개발자를 위한 React Native 가이드
React 개발자를 위한 React Native 가이드
서론
React는 선언적 UI, 컴포넌트 기반 아키텍처, 상태 관리라는 핵심 원칙을 공유하며 웹과 모바일 개발 생태계 모두에서 강력한 영향력을 발휘하고 있습니다. 이미 React에 능숙한 개발자에게 React Native는 매력적인 다음 단계처럼 보일 수 있습니다. 그러나 동일한 React 철학을 공유함에도 불구하고, 두 환경의 근본적인 차이는 새로운 사고 모델을 요구합니다. 이 보고서는 숙련된 React 웹 개발자가 React Native로 효과적으로 전환하는 데 필요한 심층적인 기술 지식과 아키텍처 이해를 제공하는 것을 목표로 합니다.
React와 React Native의 모든 주요 차이점이 렌더링 대상의 근본적인 변화, 즉 브라우저의 문서 객체 모델(DOM)에서 모바일 플랫폼의 네이티브 UI 툴킷으로의 전환에서 비롯된다는 것입니다. 이 패러다임 전환은 렌더링 아키텍처, UI 프리미티브, 스타일링, 내비게이션, 그리고 개발 생태계 전반에 걸쳐 연쇄적인 영향을 미칩니다. 본 문서는 이러한 차이점을 체계적으로 분석하여, React 개발자가 기존 지식을 기반으로 React Native의 세계를 깊이 있게 이해할 수 있도록 돕는 포괄적인 가이드가 될 것입니다.
섹션 1: 근본적인 패러다임 전환: 웹 DOM에서 네이티브 UI로
1.1 React의 렌더링 추상화
React와 React Native의 관계를 이해하는 첫걸음은 React 자체의 본질을 다시 정의하는 것에서 시작합니다. 많은 개발자들이 두 가지를 별개의 프레임워크로 인식하지만, 더 정확한 모델은 이들을 동일한 핵심 React 라이브러리의 서로 다른 *대상(target)*으로 보는 것입니다. react 패키지 자체는 컴포넌트, 상태(state), 속성(props)을 관리하는 플랫폼에 구애받지 않는 "엔진"입니다. 실제 차이는 이 엔진의 출력을 각 플랫폼이 이해할 수 있는 언어로 번역하는 "렌더러(renderer)" 패키지에서 발생합니다.
- React (웹): 웹 개발에서 이 렌더러는 react-dom입니다. React의 재조정(reconciliation) 프로세스가 완료되면, UI의 가벼운 인메모리 표현인 가상 DOM(Virtual DOM)이 생성됩니다. react-dom은 이 가상 DOM과 브라우저의 실제 DOM을 효율적으로 비교하여, 변경이 필요한 최소한의 노드만 실제 DOM에 적용합니다. 이 모든 과정은 브라우저라는 샌드박스 환경 내에서, 브라우저가 제공하는 기능의 한계 안에서 이루어집니다.
- React Native (모바일): 반면, react-native 렌더러는 DOM을 대상으로 하지 않으며 HTML을 사용하지 않습니다. React Native 애플리케이션의 JavaScript 코드는 자체적인 스레드에서 실행됩니다. 재조정 과정이 끝나면, 그 결과물은 가상 DOM이 아니라 네이티브 UI 컴포넌트를 생성, 업데이트, 또는 제거하라는 일련의 직렬화된 명령어입니다. 이 명령어들은 네이티브 플랫폼으로 전송되어 iOS의 UIView나 안드로이드의 ViewGroup과 같은 실제 네이티브 위젯으로 변환됩니다.
이러한 렌더링 대상의 차이가 모든 후속 차이점의 근원입니다. 우리가 React Native에서 <div> 대신 <View>를 사용하는 이유는 react-native 렌더러가 <div>가 무엇인지 모르기 때문입니다. 대신, 이 렌더러는 네이티브 UIView 또는 ViewGroup을 생성하는 방법을 알고 있습니다. 스타일링 방식이 다른 이유 역시 네이티브 UI 툴킷이 CSS를 사용하지 않기 때문입니다. 이처럼 렌더러의 차이라는 단일 원인에서 모든 차이점이 파생된다는 점을 이해하면, 학습 과정은 "새로운 프레임워크를 배우는 것"에서 "기존 React 지식을 활용하여 새로운 렌더링 대상과 그에 맞는 API를 배우는 것"으로 전환되어 훨씬 효율적이 됩니다. 결과적으로 React Native 앱의 UI는 웹뷰(WebView)에 의존하는 일부 하이브리드 프레임워크와 달리, 진정한 네이티브 위젯으로 구성되어 네이티브 앱과 동일한 외관, 느낌, 그리고 성능 프로필을 갖게 됩니다.
섹션 2: 아키텍처 심층 분석: JavaScript와 네이티브 영역 간의 통신
React Native의 핵심 아키텍처는 JavaScript 코드와 네이티브 플랫폼 코드라는 두 개의 분리된 세계를 어떻게 연결하는지에 관한 것입니다. 이 통신 방식은 시간이 지남에 따라 크게 발전했으며, 레거시 아키텍처와 새로운 아키텍처의 차이를 이해하는 것은 React Native의 성능 특성을 파악하는 데 매우 중요합니다.
2.1 레거시 브릿지 아키텍처: 비동기적 대화
초기 React Native를 구동했던 것은 '브릿지(Bridge)'라고 불리는 아키텍처입니다. 이는 두 세계가 서로 메시지를 주고받는 통신 모델에 가깝습니다. 마치 워키토키처럼, 한쪽이 말을 마치면 다른 쪽이 응답하는 방식입니다.
- 세 개의 스레드 모델: 레거시 아키텍처는 주로 세 개의 스레드에 의존하여 작동합니다.
- JavaScript 스레드: 모든 React 코드, 비즈니스 로직, 상태 업데이트가 실행되는 곳입니다.
- 네이티브/UI 스레드: 네이티브 UI 렌더링과 사용자 제스처(터치, 스크롤 등)를 처리하는 메인 스레드입니다.
- 섀도우 스레드(Shadow Thread): JavaScript에서 정의된 Flexbox 레이아웃을 계산하기 위해 Yoga 레이아웃 엔진을 실행하는 백그라운드 스레드입니다.
- 브릿지 메커니즘: 브릿지는 이 세 스레드, 특히 JS 스레드와 네이티브 스레드 간의 통신 채널 역할을 합니다. 그 본질은 비동기적(asynchronous)이고 일괄 처리(batched)되는 메시지 큐입니다. JS 스레드에서 네이티브 UI를 변경하라는 요청이 발생하면(예: setState로 인한 리렌더링), 이 요청은 JSON 형식의 문자열로 직렬화(serialization)됩니다. 이 직렬화된 메시지는 브릿지를 통해 네이티브 스레드로 전송되고, 네이티브 스레드에서 다시 역직렬화(deserialization)되어 실제 네이티브 UI 변경 명령으로 실행됩니다. 이 과정은 사용자의 터치 이벤트가 네이티브에서 JS로 전달될 때처럼 양방향으로 일어납니다.
- 성능 병목 현상: 이 아키텍처의 치명적인 약점은 바로 브릿지 자체에 있습니다. 비동기적 특성과 JSON 직렬화/역직렬화 오버헤드는 고빈도 업데이트 상황에서 심각한 성능 저하를 유발합니다.
- 리스트의 빠른 스크롤: 사용자가 FlatList를 빠르게 스크롤하면, 수많은 스크롤 이벤트가 발생합니다. 각 이벤트는 메시지가 되어 브릿지를 건너야 하므로, 브릿지에 "교통 체증"이 발생합니다. 이로 인해 UI 업데이트가 지연되어 사용자는 스크롤 도중 빈 공간을 보게 되는 'Jank' 현상을 경험하게 됩니다.
- 복잡한 애니메이션: JS 스레드에서 직접 제어하는 애니메이션은 특히 취약합니다. 만약 JS 스레드가 다른 복잡한 연산(예: API 응답 처리)으로 바쁘면, 다음 애니메이션 프레임에 대한 업데이트 메시지를 제시간에 브릿지로 보낼 수 없습니다. 결과적으로 애니메이션이 뚝뚝 끊기거나 잠시 멈추는 현상이 발생합니다. 이를 해결하기 위한 일반적인 해결책은 애니메이션 옵션에서 useNativeDriver: true를 사용하는 것입니다. 이는 애니메이션의 계산과 실행을 전적으로 네이티브 UI 스레드에 위임하여, JS 스레드의 부하와 상관없이 부드러운 애니메이션을 보장하는 방식입니다.
2.2 새로운 아키텍처: 통합된, 동기적 미래를 향해
레거시 브릿지의 근본적인 한계를 극복하기 위해 React Native 팀은 새로운 아키텍처를 도입했습니다. 이는 단순한 성능 개선이 아니라, JS와 네이티브 간의 관계를 재정의하는 패러다임의 전환입니다. 이는 "통신" 모델에서 "상호운용성(interoperability)" 모델로의 전환을 의미합니다. 두 세계가 메시지를 주고받는 대신, 공유된 인터페이스를 통해 직접 서로의 기능을 호출하고 객체를 참조할 수 있게 된 것입니다.
- JSI (JavaScript Interface): 새로운 아키텍처의 심장부입니다. JSI는 브릿지를 대체하는 경량의 범용 C++ API입니다. JSI의 가장 혁신적인 특징은 JavaScript가 C++ 객체에 대한 참조(reference)를 직접 보유하고, 그 객체의 메서드를 동기적으로(synchronously) 호출할 수 있게 한다는 점입니다. 이는 더 이상 데이터를 JSON 문자열로 변환할 필요가 없음을 의미하며, 통신 오버헤드를 극적으로 감소시킵니다. VisionCamera 라이브러리가 JSI를 통해 초당 약 2GB에 달하는 카메라 프레임 데이터를 실시간으로 처리할 수 있다는 사실은, 레거시 브릿지로는 상상조차 할 수 없었던 수준의 상호운용성을 보여주는 강력한 예시입니다.
- Fabric: 새로운 렌더링 시스템입니다. Fabric은 JSI를 활용하여 UI 계층을 더욱 반응성 있고 스레드에 안전하게 만듭니다. 더 많은 렌더링 로직을 C++ 공유 코드로 통합하여, JS 스레드가 네이티브 UI를 직접 제어할 수 있게 합니다. 이로 인해 레거시 아키텍처에서 발생했던 레이아웃 측정 후 UI가 "깜빡이며" 이동하는 현상 없이 동기적인 레이아웃 업데이트가 가능해집니다. 또한, Fabric은 React 18의 동시성 렌더링(Concurrent Rendering)과 같은 현대적인 React 기능과 완벽하게 호환됩니다.
- TurboModules: 차세대 네이티브 모듈입니다. TurboModules 역시 JSI를 통해 통신하며, 가장 큰 차이점은 필요할 때만 모듈을 로드하는 지연 로딩(lazy loading) 방식을 채택했다는 것입니다. 레거시 아키텍처에서는 앱 시작 시 모든 네이티브 모듈을 로드했지만, TurboModules는 실제 해당 모듈이 사용되는 시점에 로드하여 앱의 시작 시간을 단축하고 메모리 사용량을 줄입니다.
섹션 3: 사용자 인터페이스 구축: 새로운 프리미티브 세트
React 웹 개발자는 HTML 태그라는 친숙한 어휘를 사용하여 UI 구조를 만듭니다. React Native로 전환할 때 가장 먼저 마주하는 변화는 이 어휘가 완전히 새로운 것으로 대체된다는 점입니다.
3.1 코어 컴포넌트: 네이티브 UI 툴킷
React Native는 HTML 태그 대신, 가장 일반적인 네이티브 UI 위젯에 직접 매핑되는 "코어 컴포넌트(Core Components)"라는 기본 빌딩 블록 세트를 제공합니다. 이는 단순한 이름 변경이 아니라, 렌더링 결과물이 브라우저의 DOM 요소가 아닌 실제 네이티브 뷰라는 사실을 반영합니다.
- <View>: 가장 기본적인 컨테이너 컴포넌트로, 스크롤 기능이 없는 <div>와 유사합니다. Flexbox를 사용한 레이아웃 구성의 핵심 요소입니다.
- <Text>: 모든 텍스트 콘텐츠를 감싸는 데 사용되는 필수 컴포넌트로, 웹의 <p>나 <span>에 해당합니다. 웹과 가장 큰 차이점 중 하나는, 텍스트가 <View> 안에 직접 위치할 수 없으며 반드시 <Text> 컴포넌트로 감싸야 한다는 것입니다.
- <Image>: <img> 태그에 해당하는 이미지 표시용 컴포넌트입니다.
- <TextInput>: <input type="text">와 동일한 역할을 하는 사용자 입력 필드입니다.
- <ScrollView>: 화면 크기를 초과할 수 있는 콘텐츠를 담기 위한 일반적인 스크롤 가능 컨테이너입니다.
- <TouchableOpacity> / <TouchableHighlight>: <div>와 같은 일반 뷰에 터치 이벤트를 추가하여 버튼처럼 작동하게 만드는 래퍼(wrapper) 컴포넌트입니다. 이는 웹의 <button> 태그와 유사한 역할을 수행합니다.
이러한 새로운 컴포넌트 세트는 처음에는 낯설게 느껴질 수 있습니다. 아래의 "번역 사전"은 웹 개발자의 기존 지식을 React Native의 새로운 어휘로 직접 매핑하여 학습 곡선을 완화하는 데 도움을 줄 것입니다.
| HTML 요소 | React Native 코어 컴포넌트 | 주요 고려사항 |
| <div> | <View> | 레이아웃을 위한 기본 컨테이너. Flexbox의 기본 단위. |
| <p>, <span>, <h1> | <Text> | 모든 텍스트는 반드시 이 컴포넌트로 감싸야 함. 스타일 상속은 중첩된 <Text> 내에서만 가능. |
| <img> | <Image> | 로컬 및 원격 이미지를 렌더링. width와 height 스타일을 명시적으로 지정해야 함. |
| <input type="text"> | <TextInput> | 텍스트 입력을 위한 컴포넌트. 다양한 props로 키보드 타입, 자동 완성 등을 제어. |
| <button> | <TouchableOpacity>, <Button> | TouchableOpacity는 커스텀 스타일링이 자유로운 범용 터치 래퍼. <Button>은 기본 스타일을 제공. |
| <ul> / <li> | <FlatList>, <SectionList> | 긴 목록 데이터를 효율적으로 렌더링하기 위한 가상화된 리스트 컴포넌트. |
3.2 스타일링과 레이아웃: CSS in JS
React Native의 스타일링 방식은 웹 CSS와 근본적으로 다릅니다. 이는 웹 개발자에게 가장 큰 혼란과 좌절의 원인이 되기도 합니다.
- CSS 파일의 부재: 가장 큰 차이점은 별도의 .css 파일이 없다는 것입니다. 모든 스타일은 JavaScript 객체 형태로 정의되며, 일반적으로 StyleSheet.create API를 사용하거나 컴포넌트에 인라인(inline) 객체로 직접 전달합니다. 이는 스타일과 컴포넌트의 위치를 가깝게 유지하는 장점이 있지만, 웹에서 사용하던 문법과 패러다임을 버려야 함을 의미합니다.
- 문법과 레이아웃의 차이:
- 문법: 스타일 속성은 background-color와 같은 kebab-case 대신 backgroundColor와 같은 camelCase로 작성해야 합니다.
- Flexbox: Flexbox는 선택이 아닌, React Native의 기본이자 핵심 레이아웃 시스템입니다. 웹과 달리 flexDirection의 기본값이 row가 아닌 column이라는 점은 매우 중요합니다. 이는 모바일 화면이 세로 방향 중심이라는 특성을 반영한 것입니다.
- 단위와 특이성: px, em, rem과 같은 웹 단위는 존재하지 않습니다. 대신, 모든 수치 값은 기본적으로 밀도 독립적 픽셀(density-independent pixel, dp)로 처리됩니다. 이는 다양한 화면 해상도와 픽셀 밀도에 걸쳐 일관된 크기를 유지하도록 설계된 논리적 단위입니다. 또한, CSS 선택자, 상속(중첩된 <Text> 컴포넌트 제외), 계단식 적용(cascading)과 같은 개념이 없습니다. 스타일은 컴포넌트에 직접 적용될 뿐입니다.
- 제한 사항: 웹에서 당연하게 사용되던 많은 CSS 기능들이 React Native에는 없습니다. CSS Grid 레이아웃, :hover와 같은 의사 클래스(pseudo-classes), 여러 값을 가질 수 있는 복잡한 box-shadow (대신 iOS와 Android에서 다르게 동작하는 간단한 shadow 속성 사용), 그리고 @keyframes 애니메이션 등이 대표적입니다.
이러한 차이점을 체계적으로 이해하기 위해 아래 비교표를 참조할 수 있습니다. 이 표는 웹 개발자가 스타일링에 대한 사고방식을 전환하는 데 필요한 핵심적인 변화들을 명확히 보여줍니다.
| 항목 | 웹 (CSS) | React Native (StyleSheet) |
| 문법 | 별도의 .css 파일에 작성 | JavaScript 파일 내에 객체로 정의 (StyleSheet.create) |
| 속성 작명 | kebab-case (예: font-size) | camelCase (예: fontSize) |
| 레이아웃 모델 | Flexbox, Grid, Block, Inline 등 다양 | Flexbox가 지배적인 기본 레이아웃 시스템 |
| 기본 flexDirection | row | column |
| 단위 | px, em, rem, %, vw, vh 등 | 밀도 독립적 픽셀(dp)이 기본, %는 문자열로 사용 |
| 선택자 및 상속 | 클래스, ID, 태그 선택자 및 계단식 상속 지원 | 지원 안 함 (스타일은 컴포넌트에 직접 적용). <Text> 내에서만 텍스트 관련 스타일 상속. |
| 미지원 주요 기능 | - | Grid 레이아웃, 의사 클래스(:hover), 복잡한 box-shadow, @keyframes 애니메이션 |
섹션 4: 모바일 환경 탐색: URL 기반 라우팅에서 상태 기반 내비게이션으로
웹 애플리케이션에서 페이지 간 이동은 브라우저의 URL을 중심으로 이루어집니다. 그러나 모바일 앱에는 이러한 URL 중심의 개념이 없습니다. 이 근본적인 차이로 인해 React Native의 내비게이션은 웹 라우팅과 완전히 다른 패러다임을 따릅니다.
4.1 개념적 분리: URL vs. 히스토리 스택
- 웹 라우팅 (React Router): 웹에서의 라우팅은 본질적으로 브라우저의 주소창에 표시되는 URL과 히스토리 API에 깊이 의존합니다. react-router-dom과 같은 라이브러리는 특정 URL 경로(path)를 특정 React 컴포넌트와 매핑하는 역할을 합니다. 사용자가 링크를 클릭하면 URL이 변경되고, 라우터는 그에 맞는 컴포넌트를 렌더링합니다. 여기서 진실의 원천(source of truth)은 URL이며, 라우팅 자체는 비교적 상태 비저장(stateless) 방식으로 작동합니다. 사용자 경험은 뚜렷하게 구분된 "페이지"들을 오가는 것입니다.
- 모바일 내비게이션 (React Navigation): 모바일 앱은 웹 브라우저처럼 내장된 전역 히스토리 스택이나 URL 개념을 가지고 있지 않습니다. 따라서 React Navigation과 같은 라이브러리는 이 개념을 소프트웨어적으로 구현해야 합니다. 모바일 내비게이션은 상태 저장(stateful) 방식으로 작동하며, "페이지"가 아닌 "화면(screen)"들의 스택(stack)으로 개념화됩니다. 새로운 화면으로 이동하는 것은 현재 화면 위에 새 화면을 쌓는(push) 행위와 같습니다. 사용자가 뒤로 가기 버튼을 누르면, 스택의 가장 위에 있는 화면이 제거되고(pop) 그 아래에 있던 이전 화면이 드러나는 방식입니다.
이러한 패러다임의 전환은 웹 개발자가 내비게이션을 단순한 "라우팅"이 아닌, 애플리케이션의 핵심적인 "상태 관리"의 한 형태로 바라봐야 함을 시사합니다. NavigationContainer는 앱의 전체 내비게이션 상태(현재 스택, 활성 경로, 파라미터 등)를 담고 있는 중앙 저장소와 같습니다. navigation.navigate와 같은 함수 호출은 이 상태를 변경하기 위한 액션(action)을 디스패치(dispatch)하는 것과 유사하며, 이로 인해 UI가 적절하게 리렌더링됩니다. Redux나 Zustand 같은 상태 관리 라이브러리에 익숙한 React 개발자라면, 이 비유를 통해 React Navigation의 작동 방식을 더 직관적으로 이해할 수 있습니다.
4.2 React Navigation의 핵심 내비게이션 패턴
React Navigation은 다양한 네이티브 앱의 UI 패턴을 구현하기 위한 여러 종류의 "내비게이터(Navigator)"를 제공합니다.
- NavigationContainer: 모든 내비게이터를 감싸는 최상위 컴포넌트입니다. 앱의 전체 내비게이션 트리와 상태를 관리하는 역할을 하므로, 앱의 루트 레벨에서 단 한 번만 사용되어야 합니다.
- 스택 내비게이터 (createNativeStackNavigator): 가장 일반적인 내비게이션 패턴입니다. 한 번에 하나의 화면만 보여주며, 새로운 화면으로 이동할 때 이전 화면 위로 쌓이는 구조를 가집니다. 이는 웹 브라우저의 히스토리 동작과 가장 유사하지만, iOS와 Android 고유의 화면 전환 애니메이션과 제스처를 제공한다는 점에서 차이가 있습니다.
- 탭 내비게이터 (createBottomTabNavigator): 일반적으로 화면 하단에 위치하며, 앱의 주요 섹션들을 전환하는 데 사용되는 탭 바를 생성합니다. 각 탭은 독립적인 내비게이션 스택을 가질 수 있습니다.
- 드로어 내비게이터 (createDrawerNavigator): 화면의 측면에서 슬라이드되어 나오는 패널(드로어)을 생성합니다. 주로 설정이나 자주 사용되지 않는 여러 화면으로의 링크를 포함하는 데 사용됩니다.
- 프로그래밍 방식의 내비게이션: 웹에서는 주로 <Link to="/path">와 같은 선언적(declarative) 컴포넌트를 사용하여 라우팅을 정의합니다. 반면, React Native에서는 버튼 클릭과 같은 이벤트 핸들러 내에서 navigation.navigate('RouteName', { params })와 같이 함수를 호출하는 명령형(imperative) 방식이 더 보편적으로 사용됩니다. 이는 특정 로직 수행 후 조건에 따라 다른 화면으로 이동시키는 등 더 복잡한 흐름을 제어하는 데 유용합니다.
섹션 5: 개발 생태계: 툴체인 선택하기
React Native 프로젝트를 시작할 때 개발자가 내려야 하는 가장 중요한 결정 중 하나는 개발 환경, 즉 툴체인을 선택하는 것입니다. 크게 두 가지 선택지가 있으며, 각각은 개발 경험, 유연성, 그리고 프로젝트의 복잡성에 지대한 영향을 미칩니다.
5.1 Expo: 관리형 프레임워크
Expo는 React Native 위에 구축된 프레임워크이자 플랫폼으로, 네이티브 빌드 과정의 복잡성을 상당 부분 추상화하여 "관리형 워크플로우(managed workflow)"를 제공합니다.
- 장점:
- 신속한 설정: Xcode나 Android Studio를 직접 설치하고 설정할 필요 없이 몇 분 만에 프로젝트를 시작하고 실행할 수 있습니다.
- 풍부한 SDK: 카메라, 푸시 알림, 위치 정보 등 일반적인 모바일 기능을 위한 사전 구축된 네이티브 모듈 라이브러리를 제공하여 개발 속도를 높여줍니다.
- Expo Go: 개발 중인 앱을 실제 모바일 기기에서 QR 코드 스캔만으로 즉시 테스트할 수 있는 편리한 클라이언트 앱입니다.
- OTA (Over-The-Air) 업데이트: 앱 스토어의 검토 과정을 거치지 않고도 사용자에게 버그 수정이나 작은 기능 업데이트를 즉시 배포할 수 있습니다.
- 초보자 친화적: 네이티브 개발 경험이 없는 개발자나 빠른 프로토타이핑이 필요한 프로젝트에 이상적입니다.
- 단점:
- 제한된 네이티브 모듈 접근: Expo SDK에 포함되지 않은 특정 서드파티 네이티브 모듈을 사용하거나 직접 네이티브 코드를 작성하는 데 제약이 있습니다. (최근에는 '개발 클라이언트'를 통해 이 단점이 많이 완화되었습니다.)
- 빌드 제어 부족: 네이티브 프로젝트의 빌드 설정을 세밀하게 제어하기 어려워, 앱 크기나 성능 최적화에 한계가 있을 수 있습니다.
- 앱 번들 크기: 기본적으로 여러 API가 포함되어 있어, 간단한 앱이라도 순수 React Native CLI로 빌드한 앱보다 번들 크기가 더 클 수 있습니다.
5.2 React Native CLI: 순수 워크플로우
이는 React Native의 표준 개발 방식으로, "순수 워크플로우(bare workflow)"라고도 불립니다. 개발자에게 iOS와 Android의 네이티브 프로젝트에 대한 완전한 접근 권한을 부여합니다.
- 장점:
- 완벽한 유연성과 제어: 모든 서드파티 네이티브 모듈을 자유롭게 통합할 수 있으며, 필요에 따라 Java/Kotlin 또는 Swift/Objective-C로 직접 커스텀 네이티브 코드를 작성할 수 있습니다.
- 최적화 가능성: 빌드 구성을 완전히 제어할 수 있어 앱의 성능과 크기를 프로젝트 요구사항에 맞게 세밀하게 최적화할 수 있습니다.
- 복잡한 앱에 적합: 특정 네이티브 기능이 필수적이거나, 고성능이 요구되는 대규모 프로덕션급 애플리케이션에 더 적합합니다.
- 단점:
- 복잡한 설정 및 유지보수: 개발을 위해 Xcode와 Android Studio를 반드시 설치하고 환경 설정을 마쳐야 합니다. 이는 시간이 많이 소요될 수 있습니다.
- 업그레이드의 어려움: React Native 버전을 업그레이드하는 과정이 매우 고통스럽고 시간을 많이 소모하는 작업이 될 수 있습니다. 네이티브 종속성 충돌로 인해 프로젝트가 깨지는 경우가 빈번하게 발생합니다.
이 두 툴체인 간의 선택은 프로젝트의 생애주기 전체에 영향을 미치는 중요한 결정입니다. 아래의 결정 매트릭스는 프로젝트의 요구사항에 따라 어떤 툴체인이 더 적합한지 판단하는 데 도움을 줄 것입니다.
| 결정 기준 | Expo | React Native CLI |
| 설정 복잡도 | 낮음 (Xcode/Android Studio 불필요) | 높음 (Xcode/Android Studio 및 관련 SDK 필수) |
| 개발 속도 (초기) | 매우 빠름 | 느림 |
| 네이티브 모듈 접근성 | 제한적 (Expo SDK 또는 개발 클라이언트 필요) | 무제한 (모든 네이티브 모듈 사용 가능) |
| 빌드 구성 제어 | 제한적 | 완전한 제어 |
| 앱 크기 | 상대적으로 큼 | 최적화 가능 |
| 업그레이드 용이성 | 비교적 쉬움 (Expo가 관리) | 매우 어려울 수 있음 |
섹션 6: 플랫폼 수용하기: iOS와 Android의 차이 관리
"한 번 작성하면, 어디서든 실행된다(Write Once, Run Anywhere)"는 React Native의 매력적인 슬로건이지만, 현실은 조금 더 복잡합니다. 대부분의 비즈니스 로직은 플랫폼 간에 공유될 수 있지만, 진정으로 네이티브다운 사용자 경험을 제공하기 위해서는 UI 및 인터랙션 패턴에서 플랫폼별 미세 조정이 거의 항상 필요합니다.
6.1 "한 번 작성, 어디서든 실행"의 현실
iOS와 Android는 각기 다른 디자인 가이드라인과 사용자 인터페이스 관례를 가지고 있습니다. 예를 들어, 내비게이션 전환 애니메이션, 경고창(Alert)의 모양, 날짜 선택기(Date Picker)의 UI 등은 플랫폼마다 다릅니다. React Native는 이러한 차이를 일부 추상화해주지만, 모든 것을 해결해주지는 않습니다. 따라서 개발자는 두 플랫폼의 차이점을 인지하고, 필요에 따라 코드를 분기하여 각 플랫폼 사용자에게 익숙한 경험을 제공해야 합니다.
6.2 플랫폼별 코드 작성 기법
React Native는 플랫폼별 코드를 체계적으로 관리할 수 있는 두 가지 주요 방법을 제공합니다.
- 플랫폼별 파일 확장자: 이 방법은 컴포넌트의 구현이 플랫폼에 따라 완전히 다를 때 이상적입니다. 예를 들어, MyComponent.ios.js와 MyComponent.android.js라는 두 개의 파일을 생성할 수 있습니다.
// MyComponent.android.js
// Android에 특화된 컴포넌트 구현
// MyComponent.ios.js
// iOS에 특화된 컴포넌트 구현
- 그런 다음, 다른 파일에서 이 컴포넌트를 불러올 때는 확장자 없이 import MyComponent from './MyComponent';와 같이 사용합니다. React Native의 번들러는 빌드 시점에 타겟 플랫폼을 감지하여 자동으로 올바른 파일을 로드합니다.
- Platform 모듈: 컴포넌트의 일부만 플랫폼에 따라 다를 경우, Platform 모듈을 사용하여 인라인 조건부 로직을 작성하는 것이 더 효율적입니다.
- Platform.OS: 현재 실행 중인 플랫폼을 나타내는 문자열('ios' 또는 'android')을 반환합니다. 스타일 속성 하나를 다르게 적용하는 것과 같은 작은 차이를 처리할 때 유용합니다.
import { Platform, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
paddingTop: Platform.OS === 'ios'? 20 : 0,
},
});
-
- Platform.select(): 플랫폼별로 다른 값을 제공하는 객체를 인자로 받아, 현재 플랫폼에 해당하는 값을 반환하는 더 구조적인 방법입니다. 스타일, 설정 값, 심지어는 컴포넌트 자체를 반환하는 데 사용할 수 있어 매우 유용합니다.
import { Platform, StyleSheet, Text } from 'react-native';
const styles = StyleSheet.create({
text: {
color: Platform.select({
ios: 'blue',
android: 'green',
}),
},
});
const PlatformSpecificComponent = Platform.select({
ios: () => require('./ComponentIOS').default,
android: () => require('./ComponentAndroid').default,
})();
결론: 지식의 종합과 전환 과정 탐색
React 웹 개발에서 React Native 모바일 개발로의 전환은 단순히 새로운 라이브러리를 배우는 것을 넘어, 근본적인 패러다임의 변화를 수용하는 과정입니다. 이 보고서에서 분석한 바와 같이, 그 변화의 핵심은 렌더링 대상이 브라우저 DOM에서 네이티브 UI로 바뀌는 데 있으며, 이는 아키텍처(브릿지 vs. JSI), UI 프리미티브(HTML vs. 코어 컴포넌트), 스타일링(CSS vs. StyleSheet), 그리고 내비게이션(URL vs. 스택)에 이르기까지 모든 측면에 영향을 미칩니다.
전환하는 웹 개발자가 겪는 일반적인 함정
숙련된 React 개발자라 할지라도 React Native로 전환하는 과정에서 몇 가지 일반적인 함정에 빠지기 쉽습니다. 이러한 문제점들을 미리 인지하는 것은 더 원활한 학습 곡선을 그리는 데 도움이 될 것입니다.
- 플랫폼 차이 과소평가: 코드가 양쪽 플랫폼에서 시각적으로나 기능적으로 동일하게 작동할 것이라고 가정하는 것은 가장 흔한 실수입니다. 실제로는 플랫폼별 테스트와 조건부 코드 작성이 필수적이며, 이를 무시하면 일관성 없는 사용자 경험을 초래할 수 있습니다.
- 네이티브 계층 무시: 특히 순수 워크플로우를 사용할 경우, 복잡한 앱을 개발하거나 디버깅, 성능 튜닝을 할 때는 결국 네이티브 빌드 시스템(Xcode, Gradle)과 각 플랫폼의 디버깅 도구에 대한 기본적인 이해가 필요하게 됩니다. JavaScript만으로 모든 것을 해결할 수 있다는 생각은 비현실적일 수 있습니다.
- 성능에 대한 오해: React Native의 성능은 자동으로 보장되지 않습니다. 웹에서 흔히 사용되는 패턴이 React Native에서는 안티패턴이 될 수 있습니다. 예를 들어, 리스트 항목에 인라인 함수나 스타일 객체를 사용하는 것, 최적화되지 않은 큰 이미지를 로드하는 것, 그리고 프로덕션 빌드에서 console.log 문을 제거하지 않는 것 등은 심각한 성능 저하를 유발할 수 있습니다.
- 종속성 및 업그레이드 문제: React Native 생태계는 빠르게 변화하며, 이는 종속성 관리와 버전 업그레이드를 특히 어렵게 만듭니다. 순수 워크플로우에서 React Native 버전을 업그레이드하는 것은 종종 수많은 네이티브 라이브러리와의 충돌을 해결해야 하는 중대한 과제가 될 수 있습니다.