“React Native는 div 대신 View, p 대신 Text를 씁니다.”
이 글에서는 Expo 앱의 화면을 구성하는 핵심 컴포넌트와 스타일링 방법을 코드와 함께 살펴봅니다.
웹 vs React Native 컴포넌트 비교
React Native는 브라우저가 아닌 네이티브 환경에서 실행되기 때문에,
HTML 태그 대신 React Native 전용 컴포넌트를 사용합니다.
| 웹 (HTML) | React Native | 역할 |
|---|---|---|
<div> | <View> | 레이아웃 컨테이너 |
<p>, <span> | <Text> | 텍스트 표시 |
<img> | <Image> | 이미지 표시 |
<input> | <TextInput> | 텍스트 입력 |
<button> | <TouchableOpacity> / <Pressable> | 버튼 / 터치 이벤트 |
<ul> / <li> | <FlatList> | 리스트 |
overflow: scroll | <ScrollView> | 스크롤 영역 |
View — 레이아웃의 기본
모든 화면 구성의 기본 단위입니다. 웹의 <div>와 같습니다.
import { View, Text, StyleSheet } from 'react-native';
export default function HomeScreen() {
return (
<View style={styles.container}>
<View style={styles.box}>
<Text>박스 안의 텍스트</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
padding: 16,
},
box: {
backgroundColor: '#fff',
borderRadius: 8,
padding: 16,
marginBottom: 12,
shadowColor: '#000',
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2, // Android 그림자
},
});
Text — 텍스트 표시
React Native에서는 반드시 <Text> 컴포넌트 안에 텍스트를 넣어야 합니다.
View 안에 직접 문자열을 쓰면 오류가 발생합니다.
import { Text, View, StyleSheet } from 'react-native';
export default function TextExample() {
return (
<View style={styles.container}>
<Text style={styles.title}>제목 텍스트</Text>
<Text style={styles.body}>본문 텍스트입니다. 길게 쓰면 자동으로 줄바꿈됩니다.</Text>
<Text style={styles.caption} numberOfLines={1} ellipsizeMode="tail">
한 줄만 표시하고 넘치면 말줄임표로 처리됩니다...
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: { padding: 16 },
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 8,
},
body: {
fontSize: 16,
lineHeight: 24,
color: '#333',
},
caption: {
fontSize: 12,
color: '#999',
marginTop: 4,
},
});
Image — 이미지 표시
로컬 이미지와 원격 이미지 모두 사용할 수 있습니다.
import { Image, View, StyleSheet } from 'react-native';
export default function ImageExample() {
return (
<View style={styles.container}>
{/* 로컬 이미지 */}
<Image
source={require('./assets/images/logo.png')}
style={styles.localImage}
/>
{/* 원격 이미지 */}
<Image
source={{ uri: 'https://picsum.photos/200' }}
style={styles.remoteImage}
/>
</View>
);
}
const styles = StyleSheet.create({
container: { padding: 16, gap: 12 },
localImage: {
width: 100,
height: 100,
},
remoteImage: {
width: '100%',
height: 200,
borderRadius: 8,
resizeMode: 'cover', // cover, contain, stretch
},
});
원격 이미지는 반드시
width와height를 지정해야 합니다.
TextInput — 텍스트 입력
import { TextInput, View, Text, StyleSheet } from 'react-native';
import { useState } from 'react';
export default function InputExample() {
const [value, setValue] = useState('');
return (
<View style={styles.container}>
<TextInput
style={styles.input}
placeholder="이름을 입력하세요"
value={value}
onChangeText={setValue}
/>
<Text>입력값: {value}</Text>
{/* 비밀번호 입력 */}
<TextInput
style={styles.input}
placeholder="비밀번호"
secureTextEntry
/>
{/* 여러 줄 입력 */}
<TextInput
style={[styles.input, styles.multiline]}
placeholder="내용을 입력하세요"
multiline
numberOfLines={4}
/>
</View>
);
}
const styles = StyleSheet.create({
container: { padding: 16, gap: 12 },
input: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: 12,
fontSize: 16,
backgroundColor: '#fff',
},
multiline: {
height: 100,
textAlignVertical: 'top', // Android에서 텍스트를 위에서 시작
},
});
TouchableOpacity / Pressable — 버튼 & 터치
import { TouchableOpacity, Pressable, Text, View, StyleSheet } from 'react-native';
export default function ButtonExample() {
return (
<View style={styles.container}>
{/* TouchableOpacity: 터치 시 투명도 변화 */}
<TouchableOpacity
style={styles.button}
onPress={() => alert('버튼 클릭!')}
activeOpacity={0.7}
>
<Text style={styles.buttonText}>TouchableOpacity 버튼</Text>
</TouchableOpacity>
{/* Pressable: 더 세밀한 터치 제어 (최신 권장 방식) */}
<Pressable
style={({ pressed }) => [
styles.button,
pressed && styles.buttonPressed,
]}
onPress={() => alert('Pressable 클릭!')}
>
<Text style={styles.buttonText}>Pressable 버튼</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
container: { padding: 16, gap: 12 },
button: {
backgroundColor: '#007AFF',
padding: 14,
borderRadius: 8,
alignItems: 'center',
},
buttonPressed: {
backgroundColor: '#005EC4',
},
buttonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
});
ScrollView — 스크롤 영역
화면을 넘어가는 콘텐츠를 스크롤할 수 있게 합니다.
import { ScrollView, View, Text, StyleSheet } from 'react-native';
export default function ScrollExample() {
return (
<ScrollView style={styles.container}>
{Array.from({ length: 20 }, (_, i) => (
<View key={i} style={styles.item}>
<Text>아이템 {i + 1}</Text>
</View>
))}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
item: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
});
리스트가 많을 경우 ScrollView 대신 FlatList를 사용하세요.
ScrollView는 모든 항목을 한 번에 렌더링해 성능에 부담을 줄 수 있습니다.
FlatList — 성능 좋은 리스트
많은 데이터를 효율적으로 렌더링할 때 사용합니다.
화면에 보이는 항목만 렌더링하기 때문에 성능이 뛰어납니다.
import { FlatList, View, Text, StyleSheet } from 'react-native';
const DATA = [
{ id: '1', title: '첫 번째 항목' },
{ id: '2', title: '두 번째 항목' },
{ id: '3', title: '세 번째 항목' },
// ...
];
export default function ListExample() {
return (
<FlatList
data={DATA}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.item}>
<Text style={styles.title}>{item.title}</Text>
</View>
)}
ItemSeparatorComponent={() => <View style={styles.separator} />}
/>
);
}
const styles = StyleSheet.create({
item: {
padding: 16,
backgroundColor: '#fff',
},
title: {
fontSize: 16,
},
separator: {
height: 1,
backgroundColor: '#eee',
},
});
StyleSheet — 스타일링
React Native의 스타일은 CSS와 유사하지만 몇 가지 차이가 있습니다.
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
// Flexbox가 기본 레이아웃 (웹과 동일한 개념)
container: {
flex: 1, // 남은 공간 모두 차지
flexDirection: 'column', // 기본값 (웹은 row가 기본)
justifyContent: 'center', // 주축 정렬
alignItems: 'center', // 교차축 정렬
gap: 12, // 자식 간격
},
// 크기
box: {
width: 100, // 고정 크기
width: '50%', // 비율
height: 100,
},
// 여백
spacing: {
margin: 16, // 외부 여백 (전체)
marginHorizontal: 16, // 좌우 여백
marginVertical: 8, // 상하 여백
padding: 16, // 내부 여백
},
// 텍스트
text: {
fontSize: 16,
fontWeight: 'bold', // 'normal', 'bold', '100'~'900'
color: '#333',
lineHeight: 24,
textAlign: 'center',
},
// 테두리
border: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
},
});
웹 CSS와 다른 점
| 항목 | 웹 CSS | React Native |
|---|---|---|
| 단위 | px, %, rem 등 | 숫자 (dp 단위) |
| flexDirection 기본값 | row | column |
| 클래스 적용 | className | style |
| 캐스케이딩 | 있음 | 없음 (독립적) |
| 하이픈 속성명 | background-color | backgroundColor (camelCase) |
실전 예제 — 프로필 카드
지금까지 배운 컴포넌트를 조합한 예제입니다.
import { View, Text, Image, TouchableOpacity, StyleSheet } from 'react-native';
export default function ProfileCard() {
return (
<View style={styles.card}>
<Image
source={{ uri: 'https://picsum.photos/80' }}
style={styles.avatar}
/>
<View style={styles.info}>
<Text style={styles.name}>홍길동</Text>
<Text style={styles.bio}>React Native 개발자입니다 👋</Text>
</View>
<TouchableOpacity style={styles.button} onPress={() => alert('팔로우!')}>
<Text style={styles.buttonText}>팔로우</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
card: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#fff',
padding: 16,
borderRadius: 12,
margin: 16,
shadowColor: '#000',
shadowOpacity: 0.08,
shadowRadius: 8,
elevation: 3,
gap: 12,
},
avatar: {
width: 56,
height: 56,
borderRadius: 28,
},
info: {
flex: 1,
},
name: {
fontSize: 16,
fontWeight: '700',
marginBottom: 2,
},
bio: {
fontSize: 13,
color: '#666',
},
button: {
backgroundColor: '#007AFF',
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 20,
},
buttonText: {
color: '#fff',
fontSize: 13,
fontWeight: '600',
},
});
정리
| 컴포넌트 | 용도 |
|---|---|
View | 레이아웃 컨테이너 |
Text | 텍스트 표시 |
Image | 이미지 표시 |
TextInput | 텍스트 입력 |
TouchableOpacity / Pressable | 버튼 / 터치 이벤트 |
ScrollView | 스크롤 영역 (소량 콘텐츠) |
FlatList | 리스트 (대량 데이터) |
이 7가지 컴포넌트만 잘 활용해도 대부분의 화면을 구성할 수 있습니다.
다음 편에서는 Expo Router로 여러 화면 간 이동을 구현하는 방법을 살펴보겠습니다.
시리즈 목차
- 1편 Expo 개발 환경 세팅 & 첫 앱 실행
- 2편 화면 구성 & 핵심 컴포넌트 ← 현재 글
- 3편 화면 이동 구현 (Expo Router)
- 4편 상태 관리 & API 연동
- 5편 디바이스 기능 활용 (카메라, 위치, 알림)
- 6편 Firebase 연동
- 7편 빌드 & 앱스토어 배포