[처음부터 배우는 리액트 네이티브]할 일 관리 어플리케이션

최다연·2025년 11월 21일

ReactNative

목록 보기
3/8

할 일 관리 애플리케이션

📎 https://github.com/choi-day/react-native-todo

프로젝트 준비하기

프로젝트를 생성한다

expo init react-naitive-todo

스타일드 컴포넌트 라이브러리와 prop-types 라이브러리를 설치한다

npm install styled-components prop-types

src/App.js 작성

import React from "react";
import styled, {ThemeProvider} from 'styled-components/native'
import {theme} from './theme';

const Container = styled.View`
    flex: 1;
    background-color: ${({theme}) => theme.background};
    align-items: center;
    justify-content: center;
`;

export default function App() {
    return (
        <ThemeProvider theme={theme}>
            <Container></Container>
        </ThemeProvider>
    )
}

App.js 수정

import App from './src/App'

export default App;

📝 현재 동작
1. App을 ThemeProvider로 감싼다.
2. theme 안에 정의된 값들이 모든 styled-component에 전달된다.
3. Container는 theme.background 색을 배경으로 사용한다.
4. flex 1로 화면 전체를 채우고, 자식을 중앙에 정렬한다.

타이틀 만들기

App.js 수정(title 삽입)

import React from "react";
import { StatusBar } from "react-native";
import styled, {ThemeProvider} from 'styled-components/native'
import {theme} from './theme';

const Container = styled.SafeAreaView`
    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: 20px;
`;

export default function App() {
    return (
        <ThemeProvider theme={theme}>
            <Container>
                <StatusBar
                barStyle="light-content"
                backgroundColor={theme.background}
                />
                <Title>TODO List</Title>
            </Container>
        </ThemeProvider>
    );
}

📝 현재 동작

  1. 화면은 SafeArea 위에서 시작됨 (노치/상단바 안 겹침)
  2. StatusBar 텍스트는 흰색 (light-content)
  3. Android에서는 StatusBar 배경도 theme.background 색으로 변경됨
  4. 배경은 theme.background 색
  5. TODO List 제목은 왼쪽 상단에 크게 표시됨

Input 컴포넌트 만들기

component/input.js 작성

import React from 'react';
import styled from 'styled-components/native';
import { Dimensions, useWindowDimensions } from 'react-native';
import PropTypes from 'prop-types';
import { theme } from '../theme';

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, value, onChangeText, onSubmitEditing,}) => {
    const width = useWindowDimensions().width

    return( <StyledInput 
            width = {width} 
            placeholder={placeholder} 
            maxLength={50}
            autoCapitalize='none'
            autoCorrect={false}
            returnKeyType='done'
            keyboardAppearance='dark'
            value={value}
            onChangeText={onChangeText}
            onSubmitEditing={onSubmitEditing}
            />
        );
};

Input.propTypes = {
    placeholder: PropTypes.string,
    value: PropTypes.string.isRequired,
    onChangeText: PropTypes.func.isRequired,
    onSubmitEditing: PropTypes.func.isRequired,
  };

export default Input;

📝 Input 컴포넌트 동작

  1. 현재 화면 너비를 자동으로 가져옴 (useWindowDimensions)
    → 기기 크기에 따라 입력창 너비 자동 조절
  2. 입력창 폭을 화면 너비 - 40px 로 설정
  3. placeholder 색, 배경색, 글자색을 theme에서 가져와 적용
  4. 입력 이벤트를 외부에서 제어하도록 설정됨
    • value → 현재 입력값
    • onChangeText → 입력할 때마다 실행
    • onSubmitEditing → 엔터(완료) 눌렀을 때 실행
  5. TextInput 공통 설정 적용됨
    • 자동대문자 없음
    • 자동완성/자동수정 꺼짐
    • returnKeyType=‘done’
    • iOS 다크 키보드 사용

App.js 수정

export default function App() {
    const [newTask, setNewTask] = useState('');

    const _addTask = () => {
        alert(`Add: ${newTask}`);
        setNewTask('');
    };

    const _handleTextChange = text => {
        setNewTask(text);
    }
    
    
...

<Input 
placeholder = "+ Add a Task"
value = {newTask}
onChangeText={_handleTextChange}
onSubmitEditing={_addTask}
/>

📝 수정된 동작 요약

  1. 사용자가 입력창에 적는 텍스트가 newTask 에 저장됨.

  2. 입력창에서 글자 입력할 때

    → onChangeText → _handleTextChange 실행

    → newTask 값이 실시간 업데이트됨.

  3. 엔터(Submit) 키를 누르면

    → onSubmitEditing → _addTask 실행

    → 현재 입력된 newTask를 alert로 표시

    → 입력이 완료되면 newTask 값을 빈 문자열로 초기화(입력창 비워짐).

  4. Input 컴포넌트는 newTask를 value로 받아 표시함

    → 상태 변화가 입력창에 실시간 반영됨.

할 일 목록 만들기

아이콘이 될 이미지를 assets/icons 아래 다운 받는다. 파일 명을 동일한 이름으로 사용하면서 뒤에 @2x, @3x 를 붙이면 자동으로 화면에 알맞은 이미지를 불러와 사용한다

src/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,
};

📝 images.js 동작 요약

  1. 각 아이콘 이미지를 파일에서 불러온다.
    • 체크박스 비활성
    • 체크박스 활성
    • 삭제 아이콘
    • 수정 아이콘
  2. 불러온 이미지들을 하나의 객체(images)에 정리해서 저장한다.

components/IconButton.js

import React from 'react';
import { TouchableOpacity } from 'react-native';
import styled from 'styled-components/native';
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}) => {
  return (
    <TouchableOpacity onPressOut={_onPressOut}>
      <Icon source={type} completed={completed} />
    </TouchableOpacity>
  );
};

IconButton.propTypes = {
  type: PropTypes.oneOf(Object.values(images)).isRequired,
  onPressOut: PropTypes.func,
};

export default IconButton;

📝 코드 동작 요약

  1. IconButton은 TouchableOpacity 를 사용해 터치 가능한 버튼을 만든다.
  2. 버튼 안에는 type으로 전달된 아이콘 이미지(source) 를 표시한다.
  3. 아이콘은 styled-components로 스타일링되어 30×30 크기 + margin 10 으로 표시된다.
  4. 버튼을 떼는 순간(onPressOut) 전달된 함수가 실행된다.
  5. type은 반드시 images 객체 안의 아이콘 중 하나여야 하고, PropTypes로 타입 검사를 한다.

components/task.js

import React, { useState } from 'react';
import styled from 'styled-components/native';
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;

📝 Task 컴포넌트 동작 요약

  1. Task는 한 줄짜리 할 일 아이템을 표시하는 컴포넌트이다.
  2. 왼쪽에는 완료 상태를 나타내기 위한 체크박스 아이콘이 배치된다.
  3. 가운데에는 text prop으로 전달된 할 일 내용이 표시되며, flex:1로 공간을 넓게 차지한다.
  4. 오른쪽에는 수정 아이콘과 삭제 아이콘이 각각 버튼 형태로 배치된다.
  5. text는 반드시 문자열이어야 하고 PropTypes로 타입 검사를 한다.

기능 구현하기

할 일 추가하기

App.js 수정

export default function App() {
  const width = Dimensions.get('window').width;
  const [newTask, setNewTask] = useState('');
  const [tasks, setTasks] = useState({
    '1': { id: '1', text: 'Hanbit', completed: false },
    '2': { id: '2', text: 'React Native', completed: true },
    '3': { id: '3', text: 'React Native Sample', completed: false },
    '4': { id: '4', text: 'Edit TODO Item', completed: false },
  });

  const _addTask = () => {
    const ID = Date.now().toString();
    const newTaskObject = {
      [ID]: { id: ID, text: newTask, completed: false },
    };
    setNewTask('');
    setTasks({ ...tasks, ...newTaskObject });
    
    ...
    
   return (
    ...
    <List width={width}>
      {Object.values(tasks)
        .reverse()
        .map(item => (
          <Task key={item.id} text={item.text} />
        ))}
    </List>
    ...
  );
}

📝  ****추가 기능 동작 요약

  1. 기존 newTask만 관리하던 상태에 tasks라는 할 일 목록 상태 객체를 추가한다.

    → 각 할 일은 id, text, completed 필드를 가진다.

  2. _addTask 함수는 Date.now().toString()으로 고유 ID를 생성하고,

    그 ID를 key로 갖는 새 할 일 객체(newTaskObject)를 만든다.

  3. 새로 만든 할 일을 기존 tasks와 합쳐서 상태를 갱신한다.

    → 입력창은 setNewTask('')로 비워져서 다시 입력할 수 있게 된다.

  4. 영역에서는Object.values(tasks)로 상태에 있는 모든 할 일을 꺼내 동적 렌더링한다.

  5. .reverse()를 사용해 가장 최근에 추가한 할 일일수록 위에 보이도록 목록 순서를 뒤집은 뒤 렌더링한다.

수정하기

App.js

const _deleteTask = id => {
  const currentTasks = Object.assign({}, tasks);
  delete currentTasks[id];
  setTasks(currentTasks);
};

...

<Task 
  key={item.id}
  item={item}
  deleteTask={_deleteTask}
/>

Task.js

<IconButton 
  type={images.delete}
  id={item.id}
  onPressOut={deleteTask}
/>

IconButton.js

const IconButton = ({ type, onPressOut, id }) => {
  const _onPressOut = () => {
    if (typeof onPressOut === 'function') onPressOut(id);
  };

  return (
    <TouchableOpacity onPressOut={_onPressOut}>
      <Icon source={type} />
    </TouchableOpacity>
  );
};

📝 삭제 기능 동작 요약

  1. 삭제 아이콘을 누르면 IconButton 내부에서 id를 포함한 onPressOut이 실행된다.
  2. Task 컴포넌트는 deleteTask 함수에 해당 id를 전달한다.
  3. App.js의 deleteTask 함수는 tasks 객체를 복사한 뒤 해당 id를 가진 항목을 삭제한다.
  4. 삭제된 tasks 객체를 setTasks로 다시 저장하여 화면이 갱신된다.
  5. 결과적으로 터치한 할 일 항목이 목록에서 사라진다.

완료 기능 구현

App.js

const _toggleTask = id => {
  const currentTasks = Object.assign({}, tasks);
  currentTasks[id]['completed'] = !currentTasks[id]['completed'];
  setTasks(currentTasks);
};

...

<Task 
  key={item.id} 
  item={item}
  deleteTask={_deleteTask}
  toggleTask={_toggleTask}
/>

Task.js

<IconButton 
  type={item.completed ? images.completed : images.uncompleted}
  id={item.id}
  onPressOut={toggleTask}
  completed={item.completed}
/>

...

<Contents completed={item.completed}>{item.text}</Contents>

...

const Contents = styled.Text`
  flex: 1;
  font-size: 24px;
  color: ${({ theme, completed }) => (completed ? theme.done : theme.text)};
  text-decoration-line: ${({ completed }) => 
    completed ? 'line-through' : 'none'};
`;

IconButton.js

const IconButton = ({ type, onPressOut, id, completed }) => {
  const _onPressOut = () => {
    if (typeof onPressOut === 'function') onPressOut(id);
  };

  return (
    <TouchableOpacity onPressOut={_onPressOut}>
      <Icon source={type} completed={completed} />
    </TouchableOpacity>
  );
};

📝 완료 기능 동작 요약

  1. 체크 아이콘을 누르면 IconButton 내부에서 id를 포함한 onPressOut 함수가 실행된다.
  2. Task 컴포넌트는 전달받은 toggleTask 함수에 해당 id를 전달한다.
  3. App.js의 toggleTask 함수는 tasks 객체에서 해당 id의 completed 값을 반전시킨다.
  4. setTasks로 업데이트된 tasks를 저장하면 UI가 자동으로 다시 렌더링된다.
  5. 완료된 항목은 체크박스 이미지가 변경되고, 텍스트 색상과 스타일이 line-through로 바뀐다.

수정 기능 구현

App.js

const [isEditing, setIsEditing] = useState(false);
const [currentTask, setCurrentTask] = useState({});

...

const _startEditing = item => {
  setIsEditing(true);
  setCurrentTask(item);
};

...

const _updateTask = () => {
  const updatedTasks = Object.assign({}, tasks);
  updatedTasks[currentTask.id] = currentTask;
  saveTasks(updatedTasks);
  setIsEditing(false);
  setCurrentTask({});
};

Task.js

<IconButton 
  type={images.update}
  onPressOut={() => startEditing(item)}
/>

Input.js

<Input
  placeholder="+ Add a Task"
  value={isEditing ? currentTask.text : newTask}
  onChangeText={text =>
    isEditing
      ? setCurrentTask({ ...currentTask, text })
      : setNewTask(text)
  }
  onSubmitEditing={isEditing ? updateTask : addTask}
/>

📝 수정 기능 동작 요약

  1. 수정 기능을 위해 isEditing 상태와 현재 수정 중인 할 일을 저장하는 currentTask 상태를 추가한다.
  2. 수정 버튼을 누르면 startEditing이 실행되어 수정 모드로 전환되고, 입력창에는 해당 할 일의 텍스트가 채워진다.
  3. 입력창에서 텍스트를 변경하면 currentTask.text 값만 업데이트된다.
  4. 엔터(Submit)를 누르면 updateTask가 실행되어 해당 id의 할 일을 새로운 텍스트로 덮어쓴다.
  5. 수정 후에는 isEditing이 false가 되어 입력창이 다시 기본 입력 모드로 돌아간다.

부가기능

데이터 저장하고 불러오기

AsyncStorage를 이용해 로컬에 데이터를 저장하고 불러올 수 있다. 비동기로 동작하며 문자열로 된 키-값의 형태의 데이터를 기기에 저장하고 불러오는 기능을 제공한다.

expo install @react-native-community/async-storage
const _saveTasks = async tasks => {
    try {
    await AsyncStorage.setItem('tasks', JSON.stringify(tasks));
    setTasks(tasks);
    } catch (e) {
    console.
    error(e);
    }
}

이후 setTasks를 _saveTasks로 바꿔준다

📝 데이터 저장 동작 요약

  1. AsyncStorage에서 'tasks' 키로 저장된 데이터를 가져온다.
  2. 불러온 문자열 데이터를 JSON.parse 해서 tasks 상태에 넣고, 에러가 나면 콘솔에 출력한다.
  3. 할 일을 추가/삭제/완료 토글할 때마다 _saveTasks를 통해 tasks 상태를 업데이트하면서 동시에 AsyncStorage에 다시 JSON.stringify(tasks) 형태로 저장한다.
const _loadTasks = async () => {
    const loadedTasks = await AsyncStorage.getItem('tasks');
    setTasks(JSON.parse(loadedTasks || '{}'));
};

📝 데이터 불러오기 동작 요약

  1. AsyncStorage에서 'tasks' 키로 저장된 데이터를 읽고, 없으면 빈 객체 {}를 기본값으로 사용한다.
  2. 가져온 문자열 데이터를 JSON.parse 후 tasks 상태에 넣어 화면에서 사용할 수 있게 만든다.


중간에 stylecomponents 오류가 나서 한참을 삽질하다가 아래 글을 참고하여 고쳤다... 버전때문에 오류가 많이 나는 듯 하다

https://github.com/styled-components/styled-components/issues/5576

0개의 댓글