#1. React Native 준비작업
React Native 는, 처음 발표된 지 벌써 6년이 넘었지만, 2024.11 현재까지 버전이 아직 0.76 에 그치고 있죠.
버전이 아직도 1.0 에 이르지 못하고 있다는 것은, RN 이 아직 안정화 되지 못한 단계에 머무르고 있다는 의미이기도 하겠습니다.
이 때문에, RN 의 개발방식은 아직까지도 뚜렷하게 정형화 되지 않고 있는 것 같습니다.
공식 버전이 이미 19, 15 에 도달한 React & NextJS 에서는, 그 개발 방식이 이미 고도로 정형화되어 있어서, 세계의 거의 모든 개발자들이, 거의 똑같이 진행되는 개발방식에서 똑같은 알고리즘에 관해 매우 활발한 교류를 나누고 있습니다. 하지만 RN 의 개발방식은 아직 그렇게까지 진화하지는 않은 것 같습니다.
이러한 배경이 있기 때문에… React Native 의 개발 방식에는 아직까지 정형화된 공식은 없으며, RN 의 개발 방식과 프레임웍 & 라이브러리, 패키지 선호도 등은 매우 다양하게 흩어져 있고, 어느 한 쪽이 정답이거나 절대적인 상황은 아니다…. 라는 관점에서, 본 프로젝트에서 진행하고 사용할 라이브러리와 개발환경 등을 이해하고 받아들이시면 좋겠습니다. ( 문장이 좀.. 전형적으로 비굴함을 각오한 자의 문장이군요.)
.
.
.
이 프로젝트는 React 사전지식과 경험을 필요로 합니다.
React 에 충분히 익숙하지 않으시다면, 제 이전 프로젝트 — React 를 배워봅시다. — 를 진행하신 후에 본 프로젝트를 따라오시는 것이 좋겠습니다.
.
.
React Native 시작하기
React Native 프로젝트의 개발을 시작하는 방법으로는, 크게 두가지 방식이 잘 알려져 있습니다.
React Native CLI
React Native FrameWork : Expo
React Native Cli 는, Command Line Interface 로 모든 설정을 일일이 잡아줘야 한다는 허들이 있습니다. 반면에 프레임웍: Expo 는 나름 편리한 패키지와 프레임웍이 구성되어 있지만 그동안에는 여러 제약과 불만사항들이 있어왔습니다.
하지만, 2024년 11월 현재, Expo 에 쏠리던 비판은 많이 줄어들었습니다. RN 의 공식 페이지에서도 Expo 로 시작하는 것을 강력하게 추천하고 있습니다.
우리의 프로젝트는 Expo 를 기반으로 진행하겠습니다.
어떤 편리함이 있는지, 그리고 어떤 제약이 있는지는, Expo 를 사용한 프로젝트를 진행하면서 조금씩 알아보기로 하죠.
.
.
Create Expo Project
Expo 프로젝트를 생성하는 커맨드는, React 개발자에게 익숙한, create-react-app, create-next-app 과 비슷합니다. expo 역시 react-native 기반 프레임웍이기 때문에, 그냥 Expo 프로젝트를 생성해주기만 하면 됩니다.
먼저 React Native 프로젝트 폴더를 로컬에 생성해주고,
mkdir c:\local workspace\_react_native\react_native_project_2024
프로젝트 디렉토리로 이동한 후에,
cd \_react_native\react_native_project_2024
vscode 를 실행하고,
code .
vscode 의 커맨드 창에서 create-expo-app 을 실행해줍니다.
여기서 잠깐…
Expo 의 템플릿은 여러가지 베네핏을 주는데요, 특히 TypeScript 기본설정을 알아서 해주고 있습니다.
TS 에 익숙하시다면 템플릿 옵션을 사용하시고,
npx create-expo-app@latest . --template default
(기본 template 은 몇가지가 제공되는데, default 는 NextJS 개발자들에게 익숙한 app 디렉토리와 함께 기본 그룹을 만들고 TypeScript 를 설정해줍니다. ‘default’ 라는 이름이 붙어 있다는 건, 왠만하면 이걸 선택하라는 지시로 느껴지는데, 역시 그렇습니다.)
템플릿 선택은
https://docs.expo.dev/more/create-expo/
를 참고하시고, TS 가 부담이라면 템플릿 없이 설치하세요.
npx create-expo-app@latest .
이 프로젝트의 이후 과정은 디폴트 TypeScript 로 진행하지만, JavaScript 로 따라오셔도 어려움이 없도록 하겠습니다.
.
.
Expo for Android & IOS
개발 브라우저에서 표현되는 모바일 화면과, 실제 모바일 화면에서 표현되는 화면의 차이가 걱정될 수 있습니다. 실제로 많은 디바이스에서 적지 않은 차이를 보이고 있죠. 때로는 표현-렌더링 반응과정을 모니터링 해야 할 필요도 생깁니다.
모바일 디바이스에서 실제로 표현되는 화면을 모니터링 하면서 개발을 진행할 수 있도록 도와주는 모바일 Expo 앱이 있습니다.
Expo 홈페이지에서, 모바일 디바이스용 Expo 앱의 다운로드 링크를 스캔합니다.
https://docs.expo.dev/get-started/set-up-your-environment/
여기에서 안드로이드 또는 IOS 디바이스용 QR 코드를 선택하고 스크롤을 내리면 QR 코드가 나옵니다.
휴대폰 또는 태블릿에서 코드를 스캔하고, 플레이스토어 등에서 앱을 다운로드 받고 설치해줍니다.
Expo 앱이 설치되어 있어야 실제 모바일 디바이스로 디버깅을 할 수 있는데요… 앱을 실행하면, 모바일 Expo 로 앱을 띄울 수 있는 QR 코드가 생성됩니다.
.
.
Start Application
RN Expo 앱을 실행시키는 커맨드는 아래와 같습니다.
npx expo start
npm run dev 와는 조금 다르죠? Dev 서버 빌드 커맨드입니다.
커맨드를 실행하고 시간이 조금 지나면, 커맨드 창에 모바일 앱에서 앱을 띄우고 디버깅할 수 있는 QR 코드와 localhost 주소가 나타납니다.
모바일 기기에서 QR 코드를 스캔하면 모바일 Expo 로 기기에서 내이티브 앱을 띄울 수 있고,
커맨드 창에서 ‘w’ 를 치면 로컬 브라우저로 앱을 띄울 수 있습니다.
여기서 주의하셔야 할 점이 있는데, 개발중인 개발서버와 모바일 디바이스는 같은 로컬 네트워크에 접속하고 있어야 모바일 Expo 에서 앱을 띄울 수 있습니다.
.
.
추가 VSCode 플러그인
VSCode 의 익스텐션 설치 로 가셔서
ES7+React/Redux/React-Native
를 설치해주세요.
ES7 구문을 React-Native 프로젝트에서 사용할 수 있게 합니다.
.
.
.
.
React Native 와 Expo 의 중요 내장 컴포넌트들
React Native 에는 DOM 이 없습니다. 브라우저 기반 웹앱이 아니라 네이티브 앱이기 때문이죠.
따라서 <div /> 등의 DOM 태그와 컴포넌트를 사용할 수 없습니다.
<p>text</p> 같은 표현은 사용할 수 없고, 대신에 RN 또는 확장 라이브러리 등이 제공하는 고유 컴포넌트를 사용해야 합니다:
<Text>text</Text>
앞으로 RN 에서 자주 사용하게 될, 가장 기본적인 컴포넌트들을 몇가지 알아보고 다음 장으로 진행합시다.
.
.
React Native 내장 Core Components
View
<View />
는, RN 에서 UI 를 구성하는 가장 기본적인, fundamental 컴포넌트입니다.
View 는 기본적인 flexbox, style, some touch handling, and accessibility 등을 제공합니다.
RN 에는 DOM 이 없기 때문에, UI 의 기본 구성은 View 로부터 구성해야 합니다.
// /app/index.tsx
import {
View,
Text,
StyleSheet,
ImageBackground,
Pressable,
} from "react-native";
import React from "react";
import icedCoffee from "@assets/images/iced-coffee.png";
import { Link } from "expo-router";
const app = () => {
return (
<View style={styles.container}>
<ImageBackground
source={icedCoffee}
resizeMode="cover"
style={styles.image}
>
<Text style={styles.text}>Decent Coffee shop</Text>
<Link href="/contact" style={{ marginHorizontal: "auto" }} asChild>
<Pressable style={styles.button}>
<Text style={styles.buttonText}>Contact Us</Text>
</Pressable>
</Link>
</ImageBackground>
</View>
);
};
export default app;
.
Text
<Text />
는 텍스트를 표현하기 위한 RN 의 내장 컴포넌트입니다.
import React, {useState} from 'react';
import {Text, StyleSheet} from 'react-native';
import {SafeAreaView, SafeAreaProvider} from 'react-native-safe-area-context';
const TextInANest = () => {
const [titleText, setTitleText] = useState("Bird's Nest");
const bodyText = 'This is not really a bird nest.';
const onPressTitle = () => {
setTitleText("Bird's Nest [pressed]");
};
return (
<SafeAreaProvider>
<SafeAreaView style={styles.container}>
<Text style={styles.baseText}>
<Text style={styles.titleText} onPress={onPressTitle}>
{titleText}
{'\\n'}
{'\\n'}
</Text>
<Text numberOfLines={5}>{bodyText}</Text>
</Text>
</SafeAreaView>
</SafeAreaProvider>
);
};
Style 지정이 가능하기 때문에, <span>, <p> 와 비교해도 일단 큰 불편은 없을 것 같습니다.
.
SafeAreaView
<SafeAreaView />
는, IPhone 의 notch 로 인한 레이아웃 깨짐을 방지하기 위해 제공되는 RN 컴포넌트입니다.
RN 기본 패키지에는 포함되어 있지 않고, ‘react-native-safe-area-context’ 패키지를 설치한 후에 사용할 수 있습니다.
일부러 IPhone 지원을 하지 않는 네이티브 앱을 만들 사람은 없겠죠. 어차피 꼭 써야만 하는 컴포넌트라서, 앱의 가장 바깥영역은 무조건 <View />
대신에 <SafeAreaView />
로 감싸줘야 한다고 외우고 있으면 되겠습니다.
import React from 'react';
import {StyleSheet, Text } from 'react-native';
import {SafeAreaView, SafeAreaProvider} from 'react-native-safe-area-context';
export default function App() {
return (
<SafeAreaView style={styles.container}>
<Text style={styles.text}>Page content</Text>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
text: {
fontSize: 25,
fontWeight: '500',
},
});
.
Pressable
네이티브 앱에서의 press 이벤트는 일반 웹앱에서의 click 이벤트와는 다르고, 다양합니다.
<Pressable />
컴포넌트는 다양한 press 이벤트를 감지하고 상호작용할 수 있는, RN 의 코어 컴포넌트입니다.
Doc:
https://reactnative.dev/docs/pressable
LongPress:
Pressable 예제코드
import React, {useState} from 'react';
import {Pressable, StyleSheet, Text, View} from 'react-native';
import {SafeAreaView, SafeAreaProvider} from 'react-native-safe-area-context';
const App = () => {
const [timesPressed, setTimesPressed] = useState(0);
let textLog = '';
if (timesPressed > 1) {
textLog = timesPressed + 'x onPress';
} else if (timesPressed > 0) {
textLog = 'onPress';
}
return (
<SafeAreaProvider>
<SafeAreaView style={styles.container}>
<Pressable
onPress={() => {
setTimesPressed(current => current + 1);
}}
style={({pressed}) => [
{
backgroundColor: pressed ? 'rgb(210, 230, 255)' : 'white',
},
styles.wrapperCustom,
]}>
{({pressed}) => (
<Text style={styles.text}>{pressed ? 'Pressed!' : 'Press Me'}</Text>
)}
</Pressable>
<View style={styles.logBox}>
<Text testID="pressable_press_console">{textLog}</Text>
</View>
</SafeAreaView>
</SafeAreaProvider>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
},
text: {
fontSize: 16,
},
wrapperCustom: {
borderRadius: 8,
padding: 6,
},
logBox: {
padding: 20,
margin: 10,
borderWidth: StyleSheet.hairlineWidth,
borderColor: '#f0f0f0',
backgroundColor: '#f9f9f9',
},
});
.
.
Expo Components
Stack
<Stack />
은, Expo Router 에서 제공하는 네비게이션 컴포넌트입니다.
Stack 은 이름 그대로, 앱의 페이지들이 차곡차곡 쌓여있는 공간이라고 이해하시면 되겠습니다.
그리고 웹에서는 ‘뒤로가기’ 가 기본적으로 제공되지만, 네이티브 앱에서는 그렇지 않습니다. 네이티브 앱에서 ‘뒤로가기’ 는, 직접 구현해야 할 과제이죠. Stack 은, 이 과제를 도와주는 네비게이션 컴포넌트입니다.
app/
├── _layout.js // 홈 레이아웃
├── index.js // 홈 화면
├── about.js // 다른 화면
└── stack_example/ // Stack으로 그룹화된 화면
├── _layout.js // Stack 그룹을 설정해줍니다.
├── index.js // Stack 내 Link 를 생성합니다.
├── profile.js
└── settings.js
<Stack>
화면 그룹을 정의하며, 자식 요소로
<Stack.Screen>
을 포함합니다.Stack은 파일 기반 라우팅을 지원하며, 화면의 폴더 구조를 자동으로 네비게이션 트리로 변환합니다.
Expo Router는 기본적으로
Stack
에서 뒤로가기 버튼과, iOS 의 뒤로가기 스와이프를 지원하고, 특정 상황에서는 이를 오버라이드하거나 맞춤 동작을 추가할 수 있습니다.
<Stack.Screen>
각 화면의 설정을 담당하며, 다음 속성을 사용할 수 있습니다:
name
: 해당 화면 파일의 이름.options
: 화면에 대한 개별 옵션을 설정 (e.g., 타이틀, 헤더 스타일).
// /app/index.js
import { Link } from 'expo-router';
import { View, Text } from 'react-native';
export default function Home() {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Welcome to the Home Screen!</Text>
<Link href="/stack_example">
<Text style={{ color: 'blue', marginTop: 20 }}>Go to Stack Screen</Text>
</Link>
</View>
);
}
위 코드에서, stack_example
링크는, stack_example/index.js
로 이어집니다. 그리고 stack_example/_layout.js
는 이하 스택 그룹을 포함하고 있죠. 스택 그룹에서는 스택 고유의 규칙이 적용됩니다.
.
<Stack /> 과 <Stack.Screen /> 의 중첩구조를 기억해둡시다.
// /app/stack_example/_layout.js
// Stack 에 index, profile, settings, 총 3개의 페이지가 준비됩니다.
import { Stack } from 'expo-router';
export default function StackLayout() {
return (
<Stack>
<Stack.Screen name="index" options={{ title: 'Stack Home' }} />
<Stack.Screen name="profile" options={{ title: 'Profile' }} />
<Stack.Screen name="settings" options={{ title: 'Settings' }} />
</Stack>
);
}
.
// /app/stack_example/index.js
import { View, Text } from 'react-native';
import { Link } from 'expo-router';
export default function StackHome() {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>Welcome to Stack Home!</Text>
<Link href="/stack_example/profile">
<Text style={{ color: 'blue', marginTop: 20 }}>Go to Profile</Text>
</Link>
<Link href="/stack_example/settings">
<Text style={{ color: 'blue', marginTop: 20 }}>Go to Settings</Text>
</Link>
</View>
);
}
Stack 을 사용하면,
각 화면을 스택 자료구조처럼 관리하여, 이전 화면으로 돌아가는 기능을 제공합니다.
Nested Layout 을 사용할 수 있습니다.
안드로이드, IOS 에서 ‘뒤로가기’ 비헤이비어에 자동으로 반응하는 앱이 됩니다.
Stack 을 그룹으로 관리할 때는 분명한 정책이 필요합니다. 예를 들어, 로그인 플로우와 사용자 대시보드 등은 별도의 Stack 으로 관리하는,.
Stack 그룹의 정책적 결정은, 앱의 최적화에 있어서 예민한 첨단에 닿아있는 문제이기도 합니다.
서로 다른 여러개의 Stack 들이 존재할 때, 이들의 유기적 연관과 비연관성을 판단하고 앱을 설계해야 할 중요한 문제가 있습니다. 이 문제는 나중에 좀 더 다뤄보죠.
.
.
Link
<Link />
는 링크를 표현하기 위해 Expo Router 에서 지원하는 컴포넌트입니다.
이미 React Router, NextJS 등의 Link 에 익숙하신 분들은 그와 매우 비슷하다고 생각하면 될 것 같습니다.
<Link href="/contact" style={{ marginHorizontal: "auto" }} asChild>
<Pressable style={styles.button}>
<Text style={styles.buttonText}>Contact Us</Text>
</Pressable>
</Link>
.
.
Slot
<Slot />
컴포넌트는, nested layout 환경에서 레이아웃을 유지하는 Expo 컴포넌트인데요..
마치, NextJS 나 RRD 의 nested route 환경에서 layout 에 사용되는 children, Outlet 같은 컴포넌트라고 이해하시면 되겠습니다.
import { Slot } from 'expo-router';
// 라우팅 이벤트 발생시, Header 와 Footer 는 고정상태가 되고,
// Slot 내부에서만 라우팅과 렌더링이 발생합니다.
export default function HomeLayout() {
return (
<>
<Header />
<Slot />
<Footer />
</>
);
}
Slot은 레이아웃을 유지하면서 하위 라우트를 동적으로 삽입할 수 있는 도구 입니다.
Stack 과 Slot 의 조합은, 앱을 더 풍부하게 표현할 수 있게 해줄 것입니다.
.
.
Tabs
<Tabs />
는, Expo Router 에서 제공하는 유틸리티 컴포넌트로, 앱에서 탭-바 구현을 쉽게 해줍니다.
Tabs 컴포넌트는 iOS와 Android의 네이티브 탭바 스타일을 자동으로 적용합니다. 디폴트 설정으로는 탭바가 하단에 고정됩니다.
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme ?? "light"].tint,
headerShown: false,
tabBarButton: HapticTab,
tabBarBackground: TabBarBackground,
tabBarStyle: Platform.select({
ios: {
// Use a transparent background on iOS to show the blur effect
position: "absolute",
},
default: {},
}),
}}
>
<Tabs.Screen
name="index"
options={{
title: "Home",
tabBarIcon: ({ color }) => (
<IconSymbol size={28} name="house.fill" color={color} />
),
}}
/>
</Tabs>
);
탭바를 상단 고정으로 커스터마이징하려면,
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme ?? "light"].tint,
headerShown: false,
tabBarButton: HapticTab,
tabBarBackground: TabBarBackground,
tabBarPosition: "top", // 탭바를 상단으로 이동
tabBarStyle: Platform.select({
ios: {
// Use a transparent background on iOS to show the blur effect
position: "absolute",
// iOS 추가지정
top: 0,
},
default: {},
}),
}}
>
<Tabs.Screen
name="index"
options={{
title: "Home",
tabBarIcon: ({ color }) => (
<IconSymbol size={28} name="house.fill" color={color} />
),
}}
/>
</Tabs>
);
어렵지 않죠?
.
.
Styling in RN
RN 에서 스타일링은, 베이식 스타일링과 Tailwind for RN, 즉 NativeWind, 이렇게 두가지 스타일링 방식을 사용할 수 있습니다.
Basic Styling
베이식 스타일링 방식은, 아래 코드와 같습니다. 직관적이죠.
const App = () => {
return(
<Link href="/menu" style={{ marginHorizontal: 'auto' }} asChild>
<Pressable style={styles.button}>
<Text style={styles.buttonText}>Our Menu</Text>
</Pressable>
</Link>
)
}
// 스타일 객체를 컴포넌트 바깥에 선언해두고 컴포넌트에서 style 프롭을 지정해주는 것이 일반적입니다.
const styles = StyleSheet.create({
button: {
height: 60,
width: 150,
borderRadius: 20,
justifyContent: 'center',
backgroundColor: 'rgba(0,0,0,0.75)',
padding: 6,
marginBottom: 50,
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
textAlign: 'center',
padding: 4,
}
});
NativeWind
그리고, NativeWind 를 사용하는 방식입니다. 설정 과정이 쉽진 않아요. 한 단계씩 확인하면서 잘 따라오시길 바랍니다.
Doc:
https://www.nativewind.dev/getting-started/expo-router
.
install
$ npm install nativewind tailwindcss react-native-reanimated react-native-safe-area-context
$ npx pod-install
config tailwind.config.ts
$ npx tailwindcss init
npx tailwindcss init 명령으로, tailwind.config.js 파일을 생성해줍니다. 그리고 아래 코드를 save 해줘야 합니다.
// tailwind.config.js
module.exports = {
// NOTE: Update this to include the paths to all of your component files.
content: ["./app/**/*.{js,jsx,ts,tsx}"],
presets: [require("nativewind/preset")],
theme: {
extend: {},
},
plugins: [],
}
global.css
/global.css 파일을 생성하고 아래 라인들을 붙여넣어줍니다.
파일의 경로에 주의해주세요. package.json 과 같은 root 경로에 넣어줬습니다. metro.config.js 에서도 참조하기 때문에 root 레벨에서 생성해줬습니다만, 필요하다면 /app/ 디렉토리에 생성해주셔도 됩니다. 단, 다른 참조 경로들도 그에 따라 수정해주셔야 합니다.
@tailwind base;
@tailwind components;
@tailwind utilities;
Babel Config
/babel.config.js 가 없으면 생성하고 다음 코드를 넣어줍니다.
module.exports = function (api) {
api.cache(true);
return {
presets: [
["babel-preset-expo", { jsxImportSource: "nativewind" }],
"nativewind/babel",
],
};
};
Metro Config
metro.config.js 파일이 없으면 생성해주고 아래 코드를 붙여넣어 줍니다
const { getDefaultConfig } = require("expo/metro-config");
const { withNativeWind } = require("nativewind/metro");
const config = getDefaultConfig(__dirname);
module.exports = withNativeWind(config, { input: "./global.css" });
이제 App 에서, global.css 파일을 임포트 해옵니다.
우리는, Expo 의 layout.tsx 파일을 사용하므로, /app/layout.tsx 파일에 CSS IMPORT 라인을 추가해줍니다.
...
import "./global.css";
마지막으로, app.json 에서 다음 내용을 확인합니다.
{
"expo": {
"web": {
"bundler": "metro"
}
}
}
TypeScript 설정
/nativewind-env.d.ts 파일을 생성하고, 아래 라인을 추가해줍니다.
이후 프로젝트에서 타입 자동완성과 타입-세이프티 가 활성화됩니다.
/// <reference types="nativewind/types" />
추가로, dev 서버를 종료하고 재시작합니다. -c 옵션을 붙여서 캐시를 지우고 시작합니다.
$ npx expo start -c
.
이것으로 React Native - Expo 프로젝트에서, NativeWind 를 사용할 준비가 되었습니다.
이제 앱에서, Tailwind 의 유틸리티 클래스를 사용할 수 있습니다.
/app/(tabs)/index.tsx 에서, Tailwind 의 유틸리티 클래스가 잘 작동하는지 살짝 만져봅시다.
<ThemedView style={styles.titleContainer}>
<ThemedText type="title">Welcome!</ThemedText>
<HelloWave />
</ThemedView>
<ThemedView style={styles.stepContainer}>
<ThemedText type="subtitle">
Step 1: <Text className="text-red-400">Try it</Text>
</ThemedText>
“Try it” 문구가 빨간색으로 예쁘게 표시되고 있나요? 제 환경에서는 잘 작동하는 군요.
여기까지의 설정을 저장한 스타터 깃을 깃허브에 올려두겠습니다. 깃 레포지토리를 포크, 복사, 다운로드 등 하셔서 프로젝트를 생성하면 저와 동일한 환경에서 개발을 시작하실 수 있을 겁니다.
React Native + Expo + NativeWind 스타터 Git :
https://github.com/KangWoosung/react_native_expo_nativewind_202411
.
.
앞으로 우리의 코드는, nativeWind 를 사용하면서, 필요한 경우 Basic 스타일 객체를 보조로 사용할 것입니다.
.
.
.
자 이렇게 RN 을 시작하기 위한 준비과정이 끝났습니다.