반응형
🤍 목표 기능
- 등록 : 할 일 추가
- 수정 : 완료되지 않은 할 일 항목 수정
- 삭제 : 할 일 삭제
- 완료 : 할 일 항목의 완료 상태 관리 기능
🤍 프로젝트 준비
프로젝트 생성
exop init react-native-todo //프로젝트 생성
cd react-native-todo
npm install styled-components prop-types //프로젝트에서 사용할 라이브러리 설치
프로젝트에서 사용할 색상 정의 - theme.js
export const theme = {
background: '#101010',
itemBackground: '#313131',
main: '#778bdd',
text: '#cfcfcf',
done: '#616161',
}
1.타이틀 만들기
import React from 'react';
import styled, {ThemeProvider} from 'styled-components';
import { theme } from './theme';
const Container = styled.View`
flex: 1;
background-color: ${({theme}) => theme.background};
align-items: center;
justify-content: flex-start;
`;
const Title = styled.Text`
font-size: 40px;
font-weight: 600;
color: ${({theme}) => theme.main};
align-self: flex-start;
margin: 0px 20px;
`;
export default function App() {
return (
<ThemeProvider theme={theme}>
<Container>
<Title>TODO List</Title>
</Container>
</ThemeProvider>
);
};
아이폰처럼 노치 디자인이 있는 기기는 Title 컴포넌트의 일부가 가려지는 현상이 발생함. (내 핸드폰은 아이폰 12pro이다.)
리액트 네이티브에서 제공하는 SafeAreaView 컴포넌트 사용
- SafeAreaView : 자동으로 padding값이 적용되어 노치 디자인 문제를 해결할 수 있음.
2.Input 컴포넌트 만들기
Input 컴포넌트는 할 일 항목 추가 & 수정 시 사용될 예정이다. 화면에 너무 꽉차 답답해 보이지 않도록 Dimensions 를 사용해 본다.
- Dimensions : 리액트 네이티브에서는 크기가 다양한 모바일 기기에 대응하기 위해 현재 화면의 크기를 알 수 있는 Dimensions, useWindowDimensions를 제공함. 두 기능 모두 현재 기기의 화면 크기를 알 수 있으며 Dimensions는 처음 받아왔을 때의 크기로 고정되기 때문에 화면 회전 시에는 일치하지 않을 수 있다. 이런 상황을 위해 이벤트 리스너(event listener)를 등록하여 화면 크기 변화에 대응할 수 있는 기능을 제공한다.
- useWindowDimensions : 리액트 네이티브에서 제공하는 Hooks 중 하나이며 화면 크기 변경 시 크기, 너비, 높이를 자동으로 업데이트함.
Dimensions를 활용해 화면 너비를 구하고, props로 전달하여 스타일 작성 시 화면 너비를 이용할 수 있도록 구현. -> 크기가 다른 기기에서도 항상 동일한 좌우 공백이 나타나게 됨.
import React from "react";
import styled from "styled-components";
import { Dimensions } from "react-native";
const StyledInput = styled.TextInput.attrs(({theme})=>({
placeholderTextColor: theme.main,
}))`
width: ${({width}) => width - 40}px;
height: 60px;
margin: 3px 0;
padding: 15px 20px;
border-radius: 10px;
background-color: ${({ theme }) => theme.itemBackground};
font-size: 25px;
color: ${({ theme }) => theme.text};
`;
const Input = ({placeholder}) => {
const width = Dimensions.get('window').width;
};
export default Input;
Input 컴포넌트에 다양한 속성 설정하기
placeholder에 적용할 문자열을 props로 받아 설정하고 입력 글자수는 50자로 제한
TextInput 컴포넌트에서 제공하는 속성을 이용해 키보드 설정 변경
return (
<StyledInput
width={width}
placeholder={placeholder}
maxLength={50}
autoCapitalize="none" /* 자동으로 대문자 전환 off */
autoCorrect={false} /* 자동 수정 off */
returnKeyType="done" /* 키보드 완료 버튼 설정 */
keyboardAppearance="dark" /* 아이폰 키보드 색상 어둡게 설정 */
value={value}
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}/>
);
💻 실행 화면 : 키보드 색상이 어둡게 잘 변경되었다.
3.이벤트
useState를 이용하여 newTask 상태 변수, 세터 함수 생성하고 Input 컴포넌트에서 값이 변경될 때마다 newTask에 저장하도록 구현.
import React, {useState} from 'react';
export default function App() {
const [newTask, setNewTask] = useState('');
const _addTask = () => {
alert(`Add: ${newTask}`);
setNewTask('');
};
const _handleTextChange = text => {
setNewTask(text);
};
return (
<ThemeProvider theme={theme}>
<Container>
<StatusBar barStyle="light-content" backgroundColor={theme.background}/>
<Title>TODO List</Title>
<Input
placeholder="+ Add a Task"
value={newTask}
onChangeText={_handleTextChange}
onSubmitEditing={_addTask}
/>
</Container>
</ThemeProvider>
);
};
Input 컴포넌트에서 props로 전달된 값을 이용하도록 수정
import PropTypes from 'prop-types';
...
const Input = ({placeholder, value, onChangeText, onSubmitEditing}) => {
const width = Dimensions.get('window').width;
return (
<StyledInput
...
value={value}
onChangeText={onChangeText}
onSubmitEditing={onSubmitEditing}/>
);
};
Input.prototype = {
placeholder: PropTypes.string,
value: PropTypes.string.isRequired,
onChangeText: PropTypes.func.isRequired,
onSubmitEditing: PropTypes.func.isRequired,
}
export default Input;
💻 실행 화면 : Input 컴포넌트에 입력된 값이 alert창으로 나타남.
4. 할 일 목록 만들기
- IconButton 컴포넌트 : 완료,수정,삭제 버튼으로 사용할 컴포넌트
- Task 컴포넌트 : 목록의 각 항목으로 사용할 컴포넌트
버튼에 사용할 아이콘 이미지를 구글 폰트에서 다운 받는다.
https://fonts.google.com/icons?selected=Material+Icons
다운로드 완료 후 프로젝트의 assets 폴더에 하위 폴더 icons 생성 후 복사
리액트 네이티브에서 제공하는 Image 컴포넌트는 프로젝트에 있는 이미지 파일의 경로, URL을 이용하여 원격 이미지 렌더링 가능 - images.js
import CheckBoxOutline from '../assets/icons/check_box_outline.png';
import CheckBox from '../assets/icons/check_box.png';
import DeleteForever from '../assets/icons/delete_forever.png';
import Edit from '../assets/icons/edit.png';
export const images = {
uncompleted: CheckBoxOutline,
completed: CheckBox,
delete: DeleteForever,
update: Edit,
};
IconButton 컴포넌트 호출 시 원하는 이미지를 props에 type으로 전달하도록 구현함, 아이콘 색은 입력되는 텍스트와 동일한 색을 사용하도록 스타일 적용 - IconButton.js
import React from "react";
import { TouchableOpacity } from "react-native";
import styled from "styled-components";
import PropTypes from 'prop-types';
import { images } from "../images";
const Icon = styled.Image`
tint-color: ${({theme, completed}) => completed ? theme.done : theme.text};
width: 30px;
height: 30px;
margin: 10px;
`;
const IconButton = ({ type, onPressOut, id, completed}) => {
return (
<TouchableOpacity onPressOut={_onPressOut}>
<Icon source={type}/>
</TouchableOpacity>
);
};
IconButton.propTypes = {
type: PropTypes.oneOf(Object.values(images)).isRequired,
onPressOut: PropTypes.func,
};
export default IconButton;
💻 실행 화면 : 완성된 IconButton 컴포넌트를 App에서 적용 시 아래와 같은 화면이 출력됨
Task.js
- 완료 여부 확인 버튼
- 입력된 할 일 내용
- 항목 삭제 버튼
- 수정 버튼으로 구성
할 일 내용은 props로 전달되어 오는 값을 활용, 체크박스, 수정, 삭제 버튼은 IconButton 컴포넌트를 이용해 만듦.
import React from "react";
import styled from "styled-components";
import PropTypes from 'prop-types';
import IconButton from "./IconButton";
import { images } from "../images";
const Container = styled.View`
flex-direction: row;
align-items: center;
background-color: ${({theme})=>theme.itemBackground};
border-radius: 10px;
padding: 5px;
margin: 3px 0px;
`;
const Contents = styled.Text`
flex: 1;
font-size: 24px;
color: ${({theme})=>theme.text};
`;
const Task = ({text})=>{
return (
<Container>
<IconButton type={images.uncompleted}/>
<Contents>{text}</Contents>
<IconButton type={images.update} />
<IconButton type={images.delete} />
</Container>
);
};
Task.propTypes = {
text: PropTypes.string.isRequired,
};
export default Task;
💻 실행 화면 : App 컴포넌트에서 Task 컴포넌트를 이용해 할 일 목록을 생성하면 아래와 같은 화면이 출력됨.
🤍 프로젝트 기능 구현하기
1. 추가 기능
-useState를 이용해 할 일 목록을 저장하고 관리할 tasks 변수 생성.
-최신 항목이 가장 앞에 보이도록 tasks를 역순으로 랜더링되도록 작성.
=> key는 리액트에서 컴포넌트 배열 렌더링 시, 어떤 아이템이 추가, 수정, 삭제되었는지 식별하는 것을 돕는 고유값이며 자식 컴포넌트의 props로 전달되지 않음. key를 지정하지 않을 경우 경고 메시지가 나타나므로 각 항목마다 고유한 id를 key로 지정함.
-addTask 함수 호출 시 tasks에 새로운 할 일 항목이 추가됨.
export default function App() {
const width = Dimensions.get('window').width;
const [newTask, setNewTask] = useState('');
const [tasks, setTasks] = useState({
'1' : {id: '1',text: '리액트 공부', completed: false},
'2' : {id: '1',text: '리액트 블로그 정리', completed: true},
'3' : {id: '1',text: '운동', completed: false},
'4' : {id: '1',text: '알고리즘 문제 풀기', completed: false},
});
//할 일 추가
const _addTask = () => {
const ID = Date.now().toString();
const newTaskObject = {
[ID] : {id: ID, text: newTask, completed: false},
};
setNewTask('');
setTasks({...tasks, ...newTaskObject});
};
return (
<ThemeProvider theme={theme}>
<Container>
<StatusBar barStyle="light-content" backgroundColor={theme.background}/>
<Title>TODO List</Title>
<Input
placeholder="+ Add a Task"
value={newTask}
onChangeText={_handleTextChange}
onSubmitEditing={_addTask}
/>
<List width={width}>
{Object.values(tasks)
.reverse()
.map(item => (
<Task key={item.id} text={item.text} />
))}
</List>
{/* <IconButton type={images.uncompleted} />
<IconButton type={images.completed} />
<IconButton type={images.delete} />
<IconButton type={images.update} /> */}
</Container>
</ThemeProvider>
);
};
💻 실행 화면
2. 삭제 기능
_deleteTask : 삭제 버튼 클릭 시 항목 id를 이용하여 tasks에서 해당 항목을 삭제
//할 일 삭제
const _deleteTask = id => {
const currentTasks = Object.assign({},tasks);
delete currentTasks[id];
setTasks(currentTasks);
};
return (
<ThemeProvider theme={theme}>
<Container>
<StatusBar barStyle="light-content" backgroundColor={theme.background}/>
<Title>TODO List</Title>
<Input
placeholder="+ Add a Task"
value={newTask}
onChangeText={_handleTextChange}
onSubmitEditing={_addTask}
/>
<List width={width}>
{Object.values(tasks)
.reverse()
.map(item => (
<Task key={item.id} item={item} deleteTask={_deleteTask}/>
))}
</List>
</Container>
</ThemeProvider>
);
};
Task 컴포넌트를 전달받은 내용이 사용되도록 수정
//Task.js
<IconButton type={images.delete} id={item.id} onPressOut={deleteTask}/>
props로 전달된 deleteTask 함수는 삭제 버튼으로 전달, 함수에서 필요한 항목 id도 함께 전달
IconButton 컴포넌트에서 잔달된 함수를 이용하도록 수정.
=> props로 onPrssOut이 전달되지 않았을 때에도 문제가 발생하지 않도록 defaultProps를 이용해 onPressOut의 기본값 지정
const IconButton = ({ type, onPressOut, id }) => {
const _onPressOut = () => {
onPressOut(id);
};
return (
<TouchableOpacity onPressOut={_onPressOut}>
<Icon source={type} />
</TouchableOpacity>
);
};
IconButton.defaultProps = {
onPressOut : () => {},
}
IconButton.propTypes = {
type: PropTypes.oneOf(Object.values(images)).isRequired,
onPressOut: PropTypes.func,
id: PropTypes.string,
};
export default IconButton;
💻 실행 화면
3. 완료 기능
항목을 완료 상태로 만들어도 다시 미완료 상태로 돌아올 수 있도록 구현
...
const _toggleTask = id => {
const currentTasks = Object.assign({},tasks);
currentTasks[id]['completed']=!currentTasks[id]['completed'];
setTasks(currentTasks);
}
...
return (
<ThemeProvider theme={theme}>
<Container>
...
<List width={width}>
{Object.values(tasks)
.reverse()
.map(item => (
<Task key={item.id} item={item} deleteTask={_deleteTask} toggleTask={_toggleTask} />
))}
</List>
</Container>
</ThemeProvider>
);
함수 호출 시마다 완료 여부를 나타내는 completed 값이 전달되는 함수 작성
const Task = ({item, deleteTask, toggleTask})=>{
return (
<Container>
<IconButton type={item.completed ? images.completed : images.uncompleted}
id={item.id}
onPressOut={toggleTask}
completed={item.completed} />
<Contents completed={item.completed}>{item.text}</Contents>
{item.completed || <IconButton type={images.update} />}
{/* <IconButton type={images.update} /> */}
<IconButton type={images.delete} id={item.id} onPressOut={deleteTask} completed={item.completed}/>
</Container>
);
};
Task.propTypes = {
//text: PropTypes.string.isRequired,
item: PropTypes.object.isRequired,
deleteTask: PropTypes.func.isRequired,
toggleTask: PropTypes.func.isRequired,
};
export default Task;
💻 실행 화면
4. 수정 기능
수정 버튼 클릭 시 해당 항목이 Input 컴포넌트로 변경되면서 내용 수정 가능하도록 구현
=> 수정 완료된 항목 전달 시 tasks에서 해당 항목을 변경하는 함수 작성
//할 일 수정
const _updateTask = item => {
const currentTasks = Object.assign({},tasks);
currentTasks[item.id] = item;
_saveTasks(currentTasks);
};
...
return isReady ? (
<ThemeProvider theme={theme}>
<Container>
...
<List width={width}>
{Object.values(tasks)
.reverse()
.map(item => (
<Task
key={item.id}
item={item}
deleteTask={_deleteTask}
toggleTask={_toggleTask}
updateTask={_updateTask}
/>
))}
</List>
</Container>
</ThemeProvider>
);
};
수정 상태를 관리하기 위해 isEditing 변수 생성, 수정 버튼 클릭 시 값이 변하도록 작성
import Input from "./Input";
...
const Task = ({item, deleteTask, toggleTask, updateTask})=>{
const [isEditing, setIsEditing] = useState(false);
const [text, setText] = useState(item.text);
const _handleUpdateButtonPress = () => {
setIsEditing(true);
};
const _onSubmitEditing = () => {
if (isEditing) {
const editedTask = Object.assign({},item,{text});
setIsEditing(false);
updateTask(editedTask);
}
};
const _onBlur = () => {
if (isEditing) {
setIsEditing(false);
setText(item.text);
}
};
return isEditing ? (
<Input
value={text}
onChangeText={text=>setText(text)}
onSubmitEditing={_onSubmitEditing}
onBlur={_onBlur}
/>
) : (
<Container>
...
{item.completed || (
<IconButton
type={images.update}
onPressOut={_handleUpdateButtonPress}
/>
)}
...
</Container>
);
};
Task.propTypes = {
//text: PropTypes.string.isRequired,
item: PropTypes.object.isRequired,
deleteTask: PropTypes.func.isRequired,
toggleTask: PropTypes.func.isRequired,
updateTask: PropTypes.func.isRequired
};
export default Task;
+ 데이터 저장, 저장 데이터 불러오기 기능 추가
약간의 충돌이 있었지만 git pull로 해결하고 깃허브에도 커밋 완료
반응형
'💻 my code archive > ✨React-Native' 카테고리의 다른 글
[리액트 네이티브] ContextAPI, Consumer, Provider 실습 (0) | 2022.07.14 |
---|---|
[리액트 네이티브] Hooks, useEffect, useRef, useMemo, 커스텀 Hooks 만들기 (0) | 2022.07.13 |
[리액트 네이티브 스타일링] flex 정렬, 스타일드 컴포넌트(styled-components), ThemeProvider (0) | 2022.07.06 |
[리액트 네이티브] 내장 컴포넌트, props, useState (0) | 2022.07.03 |
리액트 네이티브 개발환경 준비하기, 프로젝트 생성 (0) | 2022.07.03 |