여기서는 expo를 사용하도록 하겠다.
> expo init todoApp
> cd todoApp
> code .
//npm start
<View>
<Heading />
<Input />
<TodoList />
<Button />
<TabBar />
</View>
import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { StyleSheet, Text, View, ScrollView } from 'react-native';
export default function App() {
return (
<View style={styles.container}>
<ScrollView keyboardShouldPersistTaps ='always' style={styles.content}>
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5'
},
content: {
flex: 1,
paddingTop: 60
}
});
ScrollView 컴포넌트
플랫폼을 감싸는 것으로 스크롤이 가능한 View 컴포넌트이다.
속성 keyboardShouldPersistTaps ='always' 는 키보드가 열려있으면 닫아서 UI가 onPress 이벤트를 모두 처리하게 한다.
flex: 1은 스타일 값으로 해당 컴포넌트가 상위 ㅋ넌테이너 영역 전체를 채우도록 해준다.
class App extends Component() {
constructor(){
super()
this.state={
inputValue: '',
todos: [],
type: 'All'
}
...
}
...
먼저 App을 함수형에서 클래스로 변경해주었다.
나중에 필요한 몇 가지를 초기 state로 지정했다.
여러 todo를 다루어야하기 때문에 배열이 필요하기에 이름은 todos로 설정하였다.
todo들을 추가하는 TextInput의 현재 state를 저장하는 값이 필요하기에 이름은 inputValue로 설정하였다.
현재 보고 있는 todo의 타입을 저장할 값(All, Current, Active)이 필요하기에 이름은 type으로 설정하였다.
import React from 'react';
import {View, Text, StyleSheet} from 'react-native';
const Heading = () => {
//{}아닌 return 사용하지 않고 ()로 감싸도 된다.
return(
<View style={styles.header}>
<Text style={styles.headerText}>
todos
</Text>
</View>
)
}
const styles = StyleSheet.create({
header: {
marginTop: 80
},
headerText: {
textAlign: 'center',
fontSize: 72,
color: 'rgba(175, 47, 47, 0.25)',
fontWeight: '100'
}
})
export default Heading
이제 App.js에 Heading 컴포넌트를 가져와 사용해보자
import React,{Component} from 'react';
import {View, ScrollView, StyleSheet} from 'react-native';
import Heading from './Heading';
class App extends Component{
constructor(){
super()
this.state={
inputValue: '',
todos: [],
type: 'All'
}
render(){
return(
<View style={styles.container}>
<ScrollView keyboardShouldPersistTaps='always' style={styles.content}>
<Heading />
</ScrollView>
</View>
)
}
}
const styles = StyleSheet.create({
constainer: {
flex:1,
backgroundColor: '#f5f5f5'
},
content: {
flex: 1,
paddingTop: 60
}
})
export default App
import React from 'react';
import {View, TextInput, StyleSheet} from 'react-native';
const Input = () => (
<View>
<TextInput
style={StyleSheet.inputContainer}
placeholder='What nees to be done?'
placeholderTextColor='#CACACA'
selectionColor='#666666'/>
</View>
)
const styles = StyleSheet.create({
inputContainer:{
marginLeft: 20,
marginRight: 20,
shadowOpacity: 0.2,
shadowRadius: 3,
shadowColor:'#000000',
},
input:{
height:60,
backgroundColor: '#ffffff',
paddingLeft: 10,
paddingRight: 10
}
})
export default Input
TextInput
HTML의 input 태그와 유사하다.
placeholder는 사용자가 텍스트를 입력하기 전에 보여주는 텍스트를 지정한다.
placeholderTextColor는 플레이스 홀더의 텍스트 스타일을 지정한다.
selectionColor는 TextInput의 커서 스타일을 지정한다.
다음으로는 TextInput의 값을 가져오기 위해서 기능을 연결하고, App 컴포넌트의 상태에 이를 저장한다.
그리고 App.js 파일로 가서 inputChange라는 새로운 메서드를 생성자 아래와 render 메서드 위 사이에 추가한다.
이 메서드는 전달받은 inputValue의 값을 이용해서 state를 갱신하고 제대로 동작하는 지를 console.log()를 이용해서 출력한다.
import React,{Component} from 'react';
import {View, ScrollView, StyleSheet} from 'react-native';
import Heading from './Heading';
import Input from './Input';
class App extends Component{
constructor(){
super()
this.state={
inputValue: '',
todos: [],
type: 'All'
}
inputChange(inputValue){
console.log('Input Value:', inputValue)
this.setState({inputValue})
}
render(){
const {inputValue} = this.state
return(
<View style={styles.container}>
<ScrollView keyboardShouldPersistTaps='always' style={styles.content}>
<Heading />
<Input
inputValue={inputValue}
inputChange={(text) => this.inputChange(text)}/>
</ScrollView>
</View>
)
}
}
const styles = StyleSheet.create({
constainer: {
flex:1,
backgroundColor: '#f5f5f5'
},
content: {
flex: 1,
paddingTop: 60
}
})
export default App
Input 컴포넌트를 App.js 파일로 가져와 TextInput에 메서드를 연결한다.
TextInput은 Input 컴포넌트에 prop으로 전달되고, state를 저장한 inputValue는 Input 컴포넌트의 prop으로 전달된다.
inputChange 메서드는 인수가 하나로 TextInput의 값을 전달한다.
이 메서드는 TextInput에서 반환된 값으로 inputValue를 갱신한다.
...
const Input = ({inputValue, inputChange}) => (
<View>
<TextInput
value={inputValue}
style={StyleSheet.inputContainer}
placeholder='What nees to be done?'
placeholderTextColor='#CACACA'
selectionColor='#666666'
onChangeText={inputChange}/>
</View>
...
TextInput 컴포넌트를 새로운 inputChange 메서드와 inputValue prop으로 갱신한다.
상태를 유지하지 않는 컴포넌트를 만들면서 속성(prop)으로 전달된 inputValue와 inputChange를 구조분해할당 처리를 한다.
TextInput의 값이 변경되면 inputChange 메서드가 호출되고, 이 값은 부모 컴포넌트로 전달되어 inputValue의 상태를 지정하게 된다.
onChangeText 메서드 는 TextInput의 컴포넌트의 값이 변경될 때마다 이 메서드가 호출되고, TextInput의 값이 전달된다.
inputValue의 값이 state로 저장되어 있으므로 이제 todo 목록에 항목을 추가하는 버튼을 만들어야 한다.
전처럼 버튼에 바인딩되는 메서드를 작성하는데 이 메서드는 새로운 todo를 생성자에서 정의했던 todos 배열에 추가해 준다.
이 메서드의 이름은 submitTodo로 하고 inputChange 메서드 뒤면서 render 메서드 앞에 두도록 한다.
import React,{Component} from 'react';
import {View, ScrollView, StyleSheet} from 'react-native';
import Heading from './Heading';
import Input from './Input';
class App extends Component{
constructor(){
super()
this.state={
inputValue: '',
todos: [],
type: 'All'
}
}
inputChange(inputValue){
console.log('Input Value:', inputValue)
this.setState({inputValue})
}
submitTodo(){
//inputValue가 비어있는지 확인
if(this.state.inputValue.match(/^\s*$/)){
return
}
//inputValue가 비어있지 않으면 todo 변수를 생성하고 title, todoINdex, complete 객체를 할당
const todo = {
title: this.state.inputValue,
todoIndex,
complete: false
}
//인덱스 증가
todoIndex++
//새로운 todo를 기존 배열에 추가
const todos = [...this.state.todos, todo]
//todo의 state를 지정해 this.state.todos의 갱신된 배열과 일치하게 만들고 inputValue를 빈 문자열로 재지정
this.setState({todos, inputValue:''}, () => {
console.log('State: ', this.state)
})
}
render(){
const {inputValue} = this.state
return(
<View style={styles.container}>
<ScrollView keyboardShouldPersistTaps='always' style={styles.content}>
<Heading />
<Input
inputValue={inputValue}
inputChange={(text) => this.inputChange(text)} />
</ScrollView>
</View>
)
}
}
const styles = StyleSheet.create({
constainer: {
flex:1,
backgroundColor: '#f5f5f5'
},
content: {
flex: 1,
paddingTop: 60
}
})
export default App
마지막으로 import 문 아래면서 App.js 파일의 맨 위에 todoIndex 변수를 생성한다.
...
import Input from './Input';
//전역으로 선언
let todoIndex = 0
class App extends Component{
...
submitTodo 메서드를 작성했으므로 Button.js 파일을 만들어서 submitTodo 메서드와 연결버튼이 작동되게 한다.
import React from 'react'
import { View, Text, StyleSheet, TouchableHighlight } from 'react-native'
const Button = ({ submitTodo }) => (
<View style={styles.buttonContainer}>
<TouchableHighlight underlayColor='#efefef' style={styles.button} onPress={submitTodo}>
<Text style={styles.submit}>
Submit
</Text>
</TouchableHighlight>
</View>
)
const styles = StyleSheet.create({
buttonContainer: {
alignItems: 'flex-end'
},
button: {
height: 50,
paddingLeft: 20,
paddingRight: 20,
backgroundColor: '#ffffff',
width: 200,
marginRight: 20,
marginTop: 15,
borderWidth: 1,
borderColor: 'rgba(0,0,0,.1)',
justifyContent: 'center',
alignItems: 'center'
},
submit: {
color: '#666666',
fontWeight: '600'
}
})
export default Button
TouchableHighlight
리액트 네이티브에서 버튼을 만들 때의 한 가지 방법
뷰들을 감싸는 게 가능하고 이들 뷰가 터치 이벤트에 적절히 대응하게 해준다.
버튼을 누르면 디폴트 backgroundColor는 지정된 underlayColor로 바뀌며, prop으로 지정한다.
TouchableHighlight는 자식 컴포넌트 하나만을 다룬다
import Button from './Button'
let todoIndex = 0
...
constructor(){
...
this.submitTodo = this.submitTodo.bind(this)
}
...
<Input inputValue={inputValue}
inputChange={(text) => this.inputChange(text)}/>
<Button submitTodo={this.submitTodo} />
</ScrollView>
</View>
)
}
}
...
Button 컴포넌트를 가져와 render 메서드 내 Input 컴포넌트 아래에 두었다.
this.submitTodo라는 prop으로 Button에 submitTodo를 전달하였다.
이제 앱을 새로고치고 todo를 추가하면 TextInput은 지워지고 앱의 state는 콘솔에 기록되면서 새로운 todo가 todos배열에 보이게 된다.
todo 배열에 todo들을 추가했으므로 화면에 랜더링해야 한다.
렌더링을 하려면 TodoList와 Todo를 만들어야 한다.
TodoList는 todo 목록을 렌더링하고 각각의 todo에 대해서는 Todo 컴포넌트를 사용한다.
import React from 'react'
import { View, Text, StyleSheet } from 'react-native'
import TodoButton from './TodoButton'
const Todo = ({ todo }) => (
<View style={styles.todoContainer}>
<Text style={styles.todoText}>
{todo.title}
</Text>
</View>
)
const styles = StyleSheet.create({
todoContainer: {
marginLeft: 20,
marginRight: 20,
backgroundColor: '#ffffff',
borderTopWidth: 1,
borderRightWidth: 1,
borderLeftWidth: 1,
borderColor: '#ededed',
paddingLeft: 14,
paddingTop: 7,
paddingBottom: 7,
shadowOpacity: 0.2,
shadowRadius: 3,
shadowColor: '#000000',
shadowOffset: { width: 2, height: 2 },
flexDirection: 'row',
alignItems: 'center'
},
todoText: {
fontSize: 17
},
})
export default Todo
계속해서 TodoList 컴포넌트를 만들자
import React from 'react'
import { View } from 'react-native'
import Todo from './Todo'
const TodoList = ({ todos }) => {
todos = todos.map((todo, i) => {
return (
<Todo
key={i}
todo={todo} />
)
})
return (
<View>
{todos}
</View>
)
}
export default TodoList
TodoList 컴포넌트는 현재 하나의 속성인 todos 배열을 갖는다.
키를 지정해 각 컴포넌트에 해당하는 키로 todo 항목의 인덱스를 전달했다.
key 속성은 가상 DOM으로 diff가 구해지면서 바뀌게 되는 항목을 리액트가 식별하는 데 도움이 된다.
마지막으로 할 작업은 TodoList 컴포넌트를 App.js 파일로 가져와 속성으로 todos를 전달하는 것이다.
import TodoList from './TodoList'
...
class App extends Component {
...
render () {
const { todos, inputValue } = this.state
return(){
<View
style={styles.container}>
<ScrollView
keyboardShouldPersistTaps='always'
style={styles.content}>
<Heading />
<Input
inputValue={inputValue}
inputChange={(text) => this.inputChange(text)} />
<TodoList todos={todos} />
<Button
submitTodo={this.submitTodo} />
</ScrollView>
</View>
)
}
}
다음 작업은 todo를 complete로 표시하면서 todo를 제거하는 것이다.
App.js 파일을 열어 submitTodo 메서드 아래에 toggleComplete와 deleteTodo 메서드를 작성한다.
toggleComplete 메서드는 todo가 완료되었는지를 전환해주고, deleteTodo 메서드는 todo를 제거해준다.
...
constructor(){
...
this.toggleComplete = this.toggleComplete.bind(this)
this.deleteTodo = this.deleteTodo.bind(this)
}
...
deleteTodo (todoIndex) {
let { todos } = this.state
todos = this.state.todos.filter((todo) => {
return todo.todoIndex !== todoIndex
})
this.setState({ todos })
}
toggleComplete (todoIndex) {
let { todos } = this.state
todos.forEach((todo) => {
if (todo.todoIndex === todoIndex) {
todo.complete = !todo.complete
}
})
this.setState({ todos })
}
deleteTodo는 todoIndex를 인수로 하며, todos를 필터링해 전달된 인덱스의 todo를 제외한 모든 todo들을 반환, 그런 다음 state를 나머지 todos로 재지정
toggleComplete는 todoIndex를 인수로하며, 주어진 인덱스의 todo를 만날 때까지 todos를 반복, complete 부울 값을 현재와 반대되게 바꾸고 todos의 state를 재지정
이들 메서드를 연결하려면 todo에 전달할 버튼 컴포넌트를 만들어야 한다.
import React from 'react'
import { Text, TouchableHighlight, StyleSheet } from 'react-native'
const TodoButtton = ({ onPress, complete, name }) => (
<TouchableHighlight onPress={onPress} underlayColor='#efefef' style={styles.button}>
<Text style={[ styles.text, complete ? styles.complete : null, name === 'Delete' ? styles.deleteButton : null ]}>
{name}
</Text>
</TouchableHighlight>
)
const styles = StyleSheet.create({
button: {
alignSelf: 'flex-end',
padding: 7,
borderColor: '#ededed',
borderWidth: 1,
borderRadius: 4,
marginRight: 5
},
text: {
color: '#666666'
},
complete: {
color: 'green',
fontWeight: 'bold'
},
deleteButton: {
color: 'rgba(175, 47, 47, 1)'
}
})
export default TodoButton
render(){
...
<TodoList
type={type}
toggleComplete={this.toggleComplete}
deleteTodo={this.deleteTodo}
todos={todos} />
<Button submitTodo={this.submitTodo} />
...
}
...
const TodoList = ({ type, todos, deleteTodo, toggleComplete }) => {
todos = todos.map((todo, i) => {
return (
<Todo
key={i}
deleteTodo={deleteTodo}
toggleComplete={toggleComplete}
todo={todo} />
)
})
...
마지막으로 Todo.js 파일을 열어서 Todo 컴포넌트를 갱신해 새로운 TodoButton 컴포넌트와 버트 ㄴ컨테이너의 스타일을 적용
import TodoButton from './TodoButton';
...
const Todo = ({ todo, toggleComplete, deleteTodo }) => (
<View style={styles.todoContainer}>
<Text style={styles.todoText}>
{todo.title}
</Text>
<View style={styles.buttons}>
<TodoButton name='Done' complete={todo.complete} onPress={() => toggleComplete(todo.todoIndex)} />
<TodoButton name='Delete' onPress={() => deleteTodo(todo.todoIndex)} />
</View>
</View>
)
...
const styles = StyleSheet.create({
...
buttons: {
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-end',
alignItems: 'center'
},
...
})
마지막 과정에서는 탭 바 필터를 만들겠습니다. 이 필터는 todo 목록 전체를 표시해주거나, 완료되었거나 작업중인 todo만을 선택적으로 표시해 줍니다.
생성자에서 앱을 처음 만들 때 state인 type 변수를 'All'로 지정했는데 이제 setType이라는 메서드를 만드는데 이 메서드는 인수로 type을 가지며 state인 type을 갱신해 줍니다.
이 메서드는 App.js 파일의 toggleComplete 메서드 아래에 둡니다.
...
constructor(){
...
this.setType = this.setType.bind(this)
}
...
setType(type){
this.setState({type})
}
다음으로는 TabBar와 TabBarItem 컴포넌트를 만들어야 합니다.
import React from 'react'
import { View, StyleSheet } from 'react-native'
import TabBarItem from './TabBarItem'
const TabBar = ({ setType, type }) => (
<View style={styles.container}>
<TabBarItem
type={type}
title='All'
setType={() => setType('All')} />
<TabBarItem
type={type}
border
title='Active'
setType={() => setType('Active')} />
<TabBarItem
type={type}
border
title='Complete'
setType={() => setType('Complete')} />
</View>
)
const styles = StyleSheet.create({
container: {
height: 70,
flexDirection: 'row',
borderTopWidth: 1,
borderTopColor: '#dddddd'
}
})
export default TabBar
이 컴포넌트는 setType과 type을 속성으로 가집니다.
아직 정의하지 않은 TabBarItem 컴포넌트를 가져옵니다.
TabBarItem 컴포넌트는 속성이 셋으로 title, type, setType입니다.
border라는 속성은 Boolean 값을 가지며, 지정하면 왼쪽 테두리 스타일을 추가해 줍니다.
import React from 'react'
import { Text, TouchableHighlight, StyleSheet } from 'react-native'
const TabBarItem = ({ border, title, selected, setType, type }) => (
<TouchableHighlight
underlayColor='#efefef'
onPress={setType}
style={[
styles.item, selected ? styles.selected : null,
border ? styles.border : null,
type === title ? styles.selected : null ]}>
<Text style={[ styles.itemText, type === title ? styles.bold : null ]}>
{title}
</Text>
</TouchableHighlight>
)
const styles = StyleSheet.create({
item: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
border: {
borderLeftWidth: 1,
borderLeftColor: '#dddddd'
},
itemText: {
color: '#777777',
fontSize: 16
},
selected: {
backgroundColor: '#ffffff'
},
bold: {
fontWeight: 'bold'
}
})
export default TabBarItem
TouchableHighlight 컴포넌트에서 몇 가지 속성들을 확인하고서 prop에 근거해 스타일을 지정합니다.
selected가 true면 스타일을 styles.seleted로
border가 true면 스타일을 styles.border로 지정
type이 title과 같으면 스타일을 styles.seleted로 지정합니다.
Text 컴포넌트에서 type이 title과 같으닞 확인하고, 그렇다면 스타일을 styles.bold로 지정
TabBar를 구현하기 위해 app/App.js 파일을 열어 TabBar 컴포넌트를 가져와 지정합니다.
그리고 this.state를 비구조화하는 일환으로 render 메서드에 type을 가져옵니다.
...
import TabBar from '.TabBar'
class App extends Component{
...
render(){
const { todos, inputValue, type } = this.state
return (
<View
style={styles.container}>
<ScrollView
keyboardShouldPersistTaps='always'
style={styles.content}>
<Heading />
<Input
inputValue={inputValue}
inputChange={(text) => this.inputChange(text)} />
<TodoList
type={type}
toggleComplete={this.toggleComplete}
deleteTodo={this.deleteTodo}
todos={todos} />
<Button
submitTodo={this.submitTodo} />
</ScrollView>
<TabBar
type={type}
setType={this.setType} />
</View>
)
}
}
TabBar 컴포넌트를 가져온 다음 state에서 type을 구조할당하고 새로운 TabBar 컴포넌트에 전달하고 TodoList 컴포넌트에도 전달합니다.
마지막으로 TodoList 컴포넌트를 열고 필터를 추가해서, 선택한 탭에 따라 지금 복원하려는 타입의 todo들만 반환하도록 해야 합니다.
TodoList.js 파일을 열고 prop 중 type을 비구조화하고 return 문 앞에 getVisibleTodos 메서드를 추가하도록 합니다.
...
const TodoList = ({ type, todos, deleteTodo, toggleComplete }) => {
const getVisibleTodos = (todos, type) => {
switch (type) {
case 'All':
return todos
case 'Complete':
return todos.filter((t) => t.complete)
case 'Active':
return todos.filter((t) => !t.complete)
}
}
todos = getVisibleTodos(todos, type)
todos = todos.map((todo, i) => {
...
switch 문을 사용해 현재 type이 무엇으로 지정되어 있는지 확인합니다.
다음으로 todos 변수를 getVisivleTodos 메서드가 반환하는 값으로 지정
잘 보고 잘 따라해 배워갑니다.
덕분에 React Native 가 어떻게 움직이는지 대략 알게 되었어요.