-
React Native로 TodoList 만들기React/React Native 2022. 12. 30. 16:48
React Native로 TodoList 만들기
뼈대+CSS(styled component 사용)
- styled component는 install 설치해줘야 한다.
https://emotion.sh/docs/@emotion/native 참고
yarn add @emotion/react @emotion/native npm install @emotion/react @emotion/native
둘중 하나 원하는걸로 설치
import styled from '@emotion/native';
import 해오기
뼈대 코드
import {StatusBar} from 'expo-status-bar'; import styled from '@emotion/native'; import {TouchableOpacity} from 'react-native'; import {AntDesign, Feather} from '@expo/vector-icons'; const StyledTouchNav = styled.TouchableOpacity` width: 100px; background-color: gray; `; const StyleText = styled.Text` text-align: center; line-height: 50%; font-weight: 800; `; const NavContainer = styled.View` flex-direction: row; justify-content: space-between; `; const InputContainer = styled.View` justify-content: center; border-color: black; border-bottom-width: 3px; border-top-width: 3px; margin: 15px 0; padding: 15px 0; `; const SafeView = styled.SafeAreaView` width: 90%; margin: 0 auto; `; const StyledInput = styled.TextInput` width: 100%; height: 45px; border-width: 2px; font-size: 18px; padding: 0 15px; `; const ItemContainer = styled.View``; const Item = styled.View` flex-direction: row; background-color: #ddd; width: 100%; height: 50px; align-items: center; justify-content: space-between; padding: 0 10px; margin: 0 0 10px 0; `; const Icons = styled.View` flex-direction: row; width: 90px; justify-content: space-between; `; export default function App() { return ( // 스크롤뷰를 쓸 때는 style로 스타일링을 하는게 아니고 contentContainerStyle 아래처럼 써야함 // 노치 아래로 뷰를 나오게 하려면 SafeAreaView 사용 <SafeView> <StatusBar /> <NavContainer flexWay={'row'}> <StyledTouchNav> <StyleText>Javascript</StyleText> </StyledTouchNav> <StyledTouchNav> <StyleText>React</StyleText> </StyledTouchNav> <StyledTouchNav> <StyleText>Coding Text</StyleText> </StyledTouchNav> </NavContainer> <InputContainer> <StyledInput/> </InputContainer> <ItemContainer> <Item> <StyleText> Todo 들어갈 곳 </StyleText> <Icons> <TouchableOpacity> <AntDesign name='checksquare' size={24} color='black' /> </TouchableOpacity> <TouchableOpacity> <Feather name='edit' size={24} color='black' /> </TouchableOpacity> <TouchableOpacity> <AntDesign name='delete' size={24} color='black' /> </TouchableOpacity> </Icons> </Item> </ItemContainer> </SafeView> ); }
결과물
아이콘 사용하는 방법
1번은 import 하라는거고,
2번은 아이콘을 사용할곳에 복붙하면 된다.
시작하기 전 알고가면 좋을 태그
https://jeffgukang.github.io/react-native-tutorial/ 한번 읽어보면 좋을 것 같다.
<TextInput>
- 일반 HTML의 <input>태그와 React Native의 <TextInput> 태그는 동일하다.
- onsubmit 속성 대신 onSubmitEditing 속성을 사용한다. (버튼을 누르면 동작하는게 아니라 폰에서 엔터를 누르면 동작함)
<View>
- <div> 는 <View> 태그를 사용한다.
- 기본적으로 flex style이 내장되어 있고, 기본 정렬은 column(세로) 이다.
<Scroll View>
- 스크롤 되는 <div>는 style을 따로 하는게 아닌, <ScrollView> 태그를 사용한다.
<SafeAreaView>
- 휴대폰 노치가 화면을 가리는 경우를 방지하기 위해 <SafeAreaView> 태그를 사용하여 Container처럼 사용한다. (IOS만 해당)
- 노치 화면을 계산해 padding을 자동으로 주기 때문에 style에 padding 속성은 먹히지 않는다.
- 그래서 body처럼 사용하는게 일반적이다. 항상 외각에 위치해있는 컴포넌트이다.
- Android 개발만 할거라면 해당 태그는 사용하지 않아도 된다.
<StatusBar>
- 휴대폰 상단 시간, 배터리 충전여부 등 을 나타낼 수 있다. SafeAreaView 밑에 <StatusBar style='auto' /> 입력하면 된다.
<TouchableOpacity>
- React Native의 <Button> 태그는 style 지정도 안되는 개 쓰레기이니 <TouchableOpacity> 태그를 버튼태그 대신 사용하자.
Create, Read
export default function App() { const [text, setText] = useState(''); const [category, setCategory] = useState(''); // js, react, ct로 카테고리 나눌예정 const [todos, setTodos] = useState([]); // newTodo를 감싸줄 배열 // 엔터 클릭 시 저장될 Data 양식 const newTodo = { id: Date.now(), text, isDone: false, // 완료 표시를 위해 추가함 isEdit: false, // 수정 기능을 위해 추가함 category, }; // 글 추가 const addTodo = () => { setTodos([...todos, newTodo]); setText(''); }; return ( <SafeView> <StatusBar /> <NavContainer flexWay={'row'}> <StyledTouchNav> <StyleText>Javascript</StyleText> </StyledTouchNav> <StyledTouchNav> <StyleText>React</StyleText> </StyledTouchNav> <StyledTouchNav> <StyleText>Coding Text</StyleText> </StyledTouchNav> </NavContainer> <InputContainer> <StyledInput onSubmitEditing={addTodo} value={text} onChangeText={setText} /> </InputContainer> <ItemContainer> {todos.map((todo) => { return ( <Item key={todo.id}> <StyleText>{todo.text}</StyleText> <Icons> <TouchableOpacity> <AntDesign name='checksquare' size={24} color='black' /> </TouchableOpacity> <TouchableOpacity> <Feather name='edit' size={24} color='black' /> </TouchableOpacity> <TouchableOpacity> <AntDesign name='delete' size={24} color='black' /> </TouchableOpacity> </Icons> </Item> ); })} </ItemContainer> </SafeView> ); }
등록이 잘 됩니다.
console.log('todos', todos) 결과
팁
https://jsonformatter.curiousconcept.com/#
콘솔에 찍힌 데이터를 복붙해서 Process 버튼을 클릭하면
이렇게 데이터가 잘 정리되서 나온다.
Category 분류하기
<NavContainer flexWay={'row'}> <StyledTouchNav onPress={() => setCategory('js')}> <StyleText>Javascript</StyleText> </StyledTouchNav> <StyledTouchNav onPress={() => setCategory('react')}> <StyleText>React</StyleText> </StyledTouchNav> <StyledTouchNav onPress={() => setCategory('ct')}> <StyleText>Coding Text</StyleText> </StyledTouchNav> </NavContainer>
각 Category 선택 시 배경색상 변경해주기
<NavContainer flexWay={'row'}> <StyledTouchNav onPress={() => setCategory('js')} style={{backgroundColor: category === 'js' ? '#0FBCF9' : 'grey'}} > <StyleText>Javascript</StyleText> </StyledTouchNav> <StyledTouchNav onPress={() => setCategory('react')} style={{backgroundColor: category === 'react' ? '#0FBCF9' : 'grey'}} > <StyleText>React</StyleText> </StyledTouchNav> <StyledTouchNav onPress={() => setCategory('ct')} style={{backgroundColor: category === 'ct' ? '#0FBCF9' : 'grey'}} > <StyleText>Coding Text</StyleText> </StyledTouchNav> </NavContainer>
- 카테고리가 ㅇㅇ면 #0FBCF9 색상으로 백그라운드컬러 변경 : 아니면 grey
각 category에 해당하는 글만 보이게 하기
<ItemContainer> {todos.map((todo) => { if (category === todo.category) { //해당 todo의 category에 해당하는 category에 글이 업로드 되도록 if문 추가 return ( <Item key={todo.id}> <StyleText>{todo.text}</StyleText> <Icons> <TouchableOpacity> <AntDesign name='checksquare' size={24} color='black' /> </TouchableOpacity> <TouchableOpacity> <Feather name='edit' size={24} color='black' /> </TouchableOpacity> <TouchableOpacity> <AntDesign name='delete' size={24} color='black' /> </TouchableOpacity> </Icons> </Item> ) } })} </ItemContainer>
Done 값으로 완료한 todo 구분하기
const setDone = (id) => { const newTodos = [...todos]; // 얕은복사로 todos를 복사해온다. const idx = newTodos.findIndex((todo) => todo.id === id); // findIndex로 todo.id와 id가 동일한 것을 idx 변수에 담는다. newTodos[idx].isDone = !newTodos[idx].isDone; // 선택한 객체의 isDone 값을 현재 isDone의 반대값(flase->true / true->false)로 바꿔준다. setTodos(newTodos); };
<ItemContainer> {todos.map((todo) => { if (category === todo.category) { return ( <Item key={todo.id}> <StyleText style={{ textDecorationLine: todo.isDone ? 'line-through' : 'none', // isDone값이 true면 line-through를 주고, 아니면 아무효과 안줌 }} > {todo.text} </StyleText> <Icons> <TouchableOpacity onPress={() => setDone(todo.id)}> // 위 setDone 함수에서 id를 매개변수로 받고있기때문에 todo.id를 매개변수로 넘겨주는 코드 <AntDesign name='checksquare' size={24} color='black' /> </TouchableOpacity> <TouchableOpacity> <Feather name='edit' size={24} color='black' /> </TouchableOpacity> <TouchableOpacity> <AntDesign name='delete' size={24} color='black' /> </TouchableOpacity> </Icons> </Item> ); } })} </ItemContainer>
여기서 알아야 될 것
- findIndex : 판별 함수를 만족하는 첫 식별자를 반환
Delete
const deleteTodo = (id) => { Alert.alert('Todo 삭제', '정말 삭제하시겠습니까?', [ { text: '취소', style: 'cancel', }, { text: '삭제', style: 'destructive', onPress: () => { const newTodos = todos.filter((todo) => todo.id !== id); // filter 메소드는 immutable이라 todos에 영향을 미치지 못해서 얕은복사를 하지 않았다. setTodos(newTodos); }, }, ]); };
<TouchableOpacity onPress={() => deleteTodo(todo.id)}> {/* 삭제버튼에 delete 기능 추가 */} <AntDesign name='delete' size={24} color='black' /> </TouchableOpacity>
Alert 속성 참고 - https://reactnative.dev/docs/alert
Update
// 글 수정 (isEdit이 true일때 ㅇㅇㅇ해라 라고 할거기때문에 isDone이랑 로직이 동일함) const isEdit = (id) => { const newTodos = [...todos]; const idx = newTodos.findIndex((todo) => todo.id === id); newTodos[idx].isEdit = !newTodos[idx].isEdit; setTodos(newTodos); }; // 수정 text 입력 후 엔터 눌렀을 때 실행 const editTodo = (id) => { const newTodos = [...todos]; const idx = newTodos.findIndex((todo) => todo.id === id); newTodos[idx].text = editText; newTodos[idx].isEdit = false; // isEdit값을 닫아준다 setTodos(newTodos); setEditText(''); };
<Item key={todo.id}> {todo.isEdit ? ( // todo.isEdit이 true이면 TextInput, false면 styleText를 출력해라 <TextInput onSubmitEditing={() => editTodo(todo.id)} value={editText} onChangeText={setEditText} style={{backgroundColor: 'white', flex: 1}} /> ) : ( <StyleText style={{ textDecorationLine: todo.isDone ? 'line-through' : 'none', }} > {todo.text} </StyleText> )}
CRUD 끝!
전체코드
import {StatusBar} from 'expo-status-bar'; import styled from '@emotion/native'; import {Alert, TextInput, TouchableOpacity} from 'react-native'; import {AntDesign, Feather} from '@expo/vector-icons'; import {useState} from 'react'; const StyledTouchNav = styled.TouchableOpacity` width: 100px; background-color: gray; `; const StyleText = styled.Text` text-align: center; line-height: 50%; font-weight: 800; `; const NavContainer = styled.View` flex-direction: row; justify-content: space-between; `; const InputContainer = styled.View` justify-content: center; border-color: black; border-bottom-width: 3px; border-top-width: 3px; margin: 15px 0; padding: 15px 0; `; const SafeView = styled.SafeAreaView` width: 90%; margin: 0 auto; `; const StyledInput = styled.TextInput` width: 100%; height: 45px; border-width: 2px; font-size: 18px; padding: 0 15px; `; const ItemContainer = styled.View``; const Item = styled.View` flex-direction: row; background-color: #ddd; width: 100%; height: 50px; align-items: center; justify-content: space-between; padding: 0 10px; margin: 0 0 10px 0; `; const Icons = styled.View` flex-direction: row; width: 90px; justify-content: space-between; `; export default function App() { const [text, setText] = useState(''); const [category, setCategory] = useState(''); const [todos, setTodos] = useState([]); const [editText, setEditText] = useState(''); console.log('editText:', editText); const newTodo = { id: Date.now(), text, isDone: false, isEdit: false, category, }; // 글 추가 const addTodo = () => { setTodos([...todos, newTodo]); setText(''); }; // 완료 표시 const setDone = (id) => { const newTodos = [...todos]; const idx = newTodos.findIndex((todo) => todo.id === id); newTodos[idx].isDone = !newTodos[idx].isDone; setTodos(newTodos); }; // 글 삭제 const deleteTodo = (id) => { Alert.alert('Todo 삭제', '정말 삭제하시겠습니까?', [ { text: '취소', style: 'cancel', }, { text: '삭제', style: 'destructive', onPress: () => { const newTodos = todos.filter((todo) => todo.id !== id); // filter 메소드는 immutable이라 todos에 영향을 미치지 못해서 얕은복사를 하지 않았다. setTodos(newTodos); }, }, ]); }; // 글 수정 (isEdit이 true일때 ㅇㅇㅇ해라 라고 할거기때문에 isDone이랑 로직이 동일함) const isEdit = (id) => { const newTodos = [...todos]; const idx = newTodos.findIndex((todo) => todo.id === id); newTodos[idx].isEdit = !newTodos[idx].isEdit; setTodos(newTodos); }; // 수정 text 입력 후 엔터 눌렀을 때 실행 const editTodo = (id) => { const newTodos = [...todos]; const idx = newTodos.findIndex((todo) => todo.id === id); newTodos[idx].text = editText; newTodos[idx].isEdit = false; // isEdit값을 닫아준다 setTodos(newTodos); setEditText(''); }; return ( <SafeView> <StatusBar /> <NavContainer flexWay={'row'}> <StyledTouchNav onPress={() => setCategory('js')} style={{backgroundColor: category === 'js' ? '#0FBCF9' : 'grey'}} > <StyleText>Javascript</StyleText> </StyledTouchNav> <StyledTouchNav onPress={() => setCategory('react')} style={{backgroundColor: category === 'react' ? '#0FBCF9' : 'grey'}} > <StyleText>React</StyleText> </StyledTouchNav> <StyledTouchNav onPress={() => setCategory('ct')} style={{backgroundColor: category === 'ct' ? '#0FBCF9' : 'grey'}} > <StyleText>Coding Text</StyleText> </StyledTouchNav> </NavContainer> <InputContainer> <StyledInput onSubmitEditing={addTodo} value={text} onChangeText={setText} /> </InputContainer> <ItemContainer> {todos.map((todo) => { if (category === todo.category) { return ( <Item key={todo.id}> {todo.isEdit ? ( // todo.isEdit이 true이면 TextInput, false면 styleText를 출력해라 <TextInput onSubmitEditing={() => editTodo(todo.id)} value={editText} onChangeText={setEditText} style={{backgroundColor: 'white', flex: 1}} /> ) : ( <StyleText style={{ textDecorationLine: todo.isDone ? 'line-through' : 'none', }} > {todo.text} </StyleText> )} <Icons> <TouchableOpacity onPress={() => setDone(todo.id)}> <AntDesign name='checksquare' size={24} color='black' /> </TouchableOpacity> <TouchableOpacity onPress={() => isEdit(todo.id)}> <Feather name='edit' size={24} color='black' /> </TouchableOpacity> <TouchableOpacity onPress={() => deleteTodo(todo.id)}> {/* 삭제버튼에 delete 기능 추가 */} <AntDesign name='delete' size={24} color='black' /> </TouchableOpacity> </Icons> </Item> ); } })} </ItemContainer> </SafeView> ); }
'React > React Native' 카테고리의 다른 글
React Native로 영화소개 서비스 만들기-1. UI 만들기(1) (0) 2023.01.03 TodoList와 firebase 연결 및 세팅 (0) 2023.01.02 TodoList에 AsyncStorage 사용하기 (0) 2023.01.02 React Native 설치 및 로컬환경 작업부터 배포까지 (2) 2022.12.29