예상 UI
todo list 만들기(1)에서는 기본 UI 구성만 할 것이고, todo list 만들기(2)에서는 기능 구현을 할 것이다.
프로젝트 생성
expo init [project name]
프로젝트에서 사용할 라이브러리 설치 : styled-components, prop-types
cd [project name]
npm i styled-components prop-types
src 폴더 생성 -> App.js 파일 생성
src 외부에 있는 App.js
import App from './src/App';
export default App;
src/App.js
import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
export default function App() {
return (
<View style={styles.container}>
<Text>start</Text>
<StatusBar style="auto" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
src 폴더 아래에 component 폴더 생성
Title component 만들기
src/components/Title.js
import React from 'react';
import { StyleSheet, Text } from 'react-native';
const Title = ({ title }) => {
return <Text style={styles.title}>{title}</Text>;
};
const styles = StyleSheet.create({
title: {
fontSize: 40,
fontWeight: '600',
color: 'black',
margin: 30,
},
});
export default Title;
src/App.js
import Title from './components/Title';
export default function App() {
return (
<View style={styles.container}>
<Title title="todo list"></Title>
<Text>start</Text>
<StatusBar style="auto" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'flex-start',
justifyContent: 'flex-start',
},
});
문제점
상단바와 메인 화면(컴포넌트)이 겹치는 문제 발생
해결책
SafeAreaView 컴포넌트 사용 (iOS)
자동으로 padding 값이 적용되어 노치 디자인 문제를 해결한 수 있는 컴포넌트이다.
StatusBar 컴포넌트 사용 (Andriod)
리액트 네이티브에서는 상태 바를 제어할 수 있는 StatusBar 컴포넌트를 제공한다. 이 컴포넌트를 사용하면 상태 바의 스타일을 변경할 수 있고, 서로 겹치는 문제를 해결할 수 있다.
https://reactnative.dev/docs/textinput
src/components/Input.js
import React from 'react';
import { Dimensions, StyleSheet, Text, TextInput } from 'react-native';
const Input = () => {
return (
<TextInput style={styles.input} placeholder="+ Add a task" maxLength={50} />
);
};
const styles = StyleSheet.create({
input: {
width: Dimensions.get('window').width - 15,
fontSize: 20,
backgroundColor: '#f1f3f5',
borderWidth: 1,
borderRadius: 5,
margin: 5,
padding: 8,
alignItems: 'center',
},
});
export default Input;
RN에서는 크기가 다양한 모바일 기기에 대응하기 위해 현재 화면의 크기를 알 수 있는 2가지를 제공한다.
위의 두 기능 모두 현재 기기의 화면 크기를 알 수 있고, 이를 이용해 다양한 크기의 키키에 동일한 모습으로 적용될 수 있도록 코드를 작성할 수 있다.
- Dimensions
처음 값을 받아왔을 때의 크기로 고정되지 때문에 기기를 회전해서 화면이 전환되면 변환된 화면의 크기와 일치하지 않을 수 있다. 이런 상황을 위해 이벤트 리스너를 등록하여 화면의 크기 변화에 대응할 수 있도록 기능을 제공한다.
const styles = StyleSheet.create({
input: {
width: Dimensions.get('window').width - 15,
//생략
},
});
- useWindowDimensions
RN에서 제공하는 Hooks중 하나로, 화면의 크기가 변경되면 화면의 크기, 너비, 높이를 자동으로 업데이트 한다.
width : useWindowDImensions().width - 15;
useState 사용
useState를 사용하여 newTask 상태 변수와 setNewTask 세터 함수를 생성.
_handleTextChange()
: Input 컴포넌트에서 값이 변할 때마다 newTask에 저장하도록 작성했다.
_addTask()
: 완료버튼을 누르면 입력된 내용을 확인하고 Input 컴포넌트를 초기화하도록 만들었다.
app.js
export default function App() {
const [newTask, setNewTask] = useState('');
const _addTask = () => {
alert(`add : ${newTask}`);
setNewTask('');
};
const _handleTextChange = (text) => {
setNewTask(text);
};
return (
<View style={styles.container}>
<StatusBar style="auto" />
<Title title="Todo List✔️"></Title>
<Input
value={newTask}
onChangeText={_handleTextChange}
onSubmitEditing={_addTask}
/>
<Button
title="enter"
onPress={() => {
_addTask();
}}
/>
</View>
);
}
components/Input.js
const Input = ({ value, onChangeText, onSubmitEditing }) => {
return (
<TextInput
style={styles.input}
placeholder="+ Add a task"
maxLength={50}
value={value}
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}
/>
);
};
삽질 : 처음에 props 받아오는걸 {} 이 괄호를 안쳐서 값이 제대로 안나왔다. 이상하다 하면서 구글링만 한참하다가 나중에 깨달음...🥲
vector-icons
는 Expo 프로젝트에서 기본적으로 설치되는 라이브러리이다.
여기에서 아이콘을 확인할 수 있다.
오른쪽 Filters를 눌러보면 다양한 아이콘 종류가 있는 것을 확인 할 수 있다. 나는 이 프로젝트에서 MaterialCommunityIcons만을 사용했지만 선호도에 따라 자유롭게 선택할 수 있다. 대신 import를 꼭 해주어야 한다.
여기에서 아이콘 이미지를 다운로드 받는다.
아이콘을 다운 받으면 이미지와 함께 2배, 3배 사이즈의 아이콘이 함께 있는 것을 볼 수 있다. 다운로드가 완료된 3개의 이미지를 각각 '파일명.png', '파일명@2x.png'로 변경한다. 이렇게 파일명을 동일한 이름으로 사용하면서 뒤에 @2x, @3x를 붙이면 RN에서 화면 사이즈에 알맞은 크기의 이미지를 자동으로 불러와 사용한다.
아이콘 이미지는 assets 폴더 아래 icons 폴더를 생성하여 넣어둔다.
react native docs
RN에서 image component를 사용하는 방법은 위 링크에서 확인할 수 있다. 프로젝트에 있는 이미지 파일의 경로나 URL을 이용해서 원격에 있는 이미지를 렌더링할 수 있다.
먼저 이미지를 관리할 images.js 파일을 생성한다.
images.js
import CheckBoxOutline from '../assets/icons/outline_check_box_outline_blank_black_24dp';
import CheckBox from '../assets/icons/outline_check_box_black_24dp';
import Delete from '../assets/icons/outline_delete_black_24dp';
import Edit from '../assets/icons/outline_edit_black_24dp';
export const images = {
uncompleted: CheckBoxOutline,
completed: CheckBox,
delete: Delete,
edit: Edit,
};
이미지 종류별로 컴포넌트를 만들지 않고 IconButton 컴포넌트를 호출 할 때 원하는 이미지의 종류를 props에 type으로 전달하도록 작성한다.
src/components/IconButton.js
import { images } from '../images';
const IconButton = ({ type }) => {
return (
<TouchableOpacity style={styles.iconbutton}>
<Image source={type} />
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
iconbutton: {
margin: 10,
},
});
export default IconButton;
src/App.js
//생략
import IconButton from './components/IconButton';
import { images } from './images';
export default function App() {
//생략
return (
<View style={styles.container}>
<StatusBar style="auto" />
<Title title="Todo List✔️"></Title>
<Input
value={newTask}
onChangeText={_handleTextChange}
onSubmitEditing={_addTask}
/>
<IconButton type={images.uncompleted} />
<IconButton type={images.completed} />
<IconButton type={images.delete} />
<IconButton type={images.edit} />
</View>
);
}
결과물
flexDirection: 'row'
: 컴포넌트가 옆으로 쌓여야 한다.width: Dimensions.get('window').width - 15
: 넓이는 input 컴포넌트와 동일하게 설정marginLeft: 7
: 예쁘게 만들기 위해 margin 설정alignItems: 'center', justifyContent: 'space-around'
: 정렬 설정Text
컴포넌트 : fontSize는 Input 컴포넌트와 동일하게, 예쁘게 정렬하기 위해 flex :1
로 설정해준다.src/components/Task.js
const Task = ({ text }) => {
return (
<View style={styles.container}>
<IconButton type={images.uncompleted} />
<Text style={{ fontSize: 20, flex: 1 }}>{text}</Text>
<IconButton type={images.edit} />
<IconButton type={images.delete} />
</View>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
width: Dimensions.get('window').width - 15,
marginLeft: 7,
alignItems: 'center',
justifyContent: 'space-around',
},
});
src/App.js
export default function App() {
return (
<View style={styles.container}>
<StatusBar style="auto" />
<Title title="Todo List✔️"></Title>
<Input
value={newTask}
onChangeText={_handleTextChange}
onSubmitEditing={_addTask}
/>
<ScrollView>
<Task text="todo list 1" />
<Task text="todo list 2" />
<Task text="todo list 3" />
<Task text="todo list 4" />
<Task text="todo list 5" />
</ScrollView>
</View>
);
}
결과물