[React] TodoList 만들기

뽕칠이·2024년 7월 2일
0

UI 분석하기

앱의 최종 UI

최종 UI 분석

  • TodoDate 컴포넌트
    오늘 날짜의 요일, 월, 일, 연도순으로 표현

  • TodoWrite 컴포넌트
    할 일을 입력하는 input 칸, 추가 버튼을 누르면 Todo List에 추가

  • TodoList 컴포넌트
    할 일 목록이 존재하고, 검색어를 입력하면 필터링되어 TodoList를 출력

  • TodoItem 컴포넌트
    체크박스, 할 일, 등록 날짜, 제거버튼이 존재

UI를 바라볼 때 컴포넌트 단위로 어떻게 나눌 것인지 설계하는 것이 중요하다.


UI 구현하기

페이지 레이아웃

TodoDate 컴포넌트

최상단에 위치할 컴포넌트이다. TodoDate 컴포넌트에는 문자열 오늘은 + 이모티콘(window + .)과 날짜로 구성된다.

- TodoDate.jsx

const TodoDate = () => {
    return(
        <div>
            <h3>오늘은 📅</h3>
            <h1>{new Date().toDateString()}</h1>
        </div>
    )
}

export default TodoDate;

- App.jsx

import TodoDate from './TodoDate'
import './App.css'

function App() {

  return (
    <div className="App">
      <TodoDate />
    </div>
  )
}

export default App

- App.css

.App {
  max-width: 500px;
  width: 100%;
  margin: 0 auto;
  box-sizing: border-box;
  padding: 20px;
  border: 1px solid gray;
  display: flex;
  flex-direction: column;
  gap: 30px;
}

max-width: 500px -> 최대 너비 500px로 고정

width: 100% -> 너비를 브라우저의 100%로 설정, 500px 이상 늘어나지 않음(줄어드는 건 가능)

margin: 0 auto -> 여백을 위아래는 0, 좌우는 auto로 설정

box-sizing: border-box -> 요소의 크기를 어떤 것을 기준으로 계산할 지 정하는 속성, border-box로 설정하면 내부 여백이 요소의 크기에 영향을 미치지 않음

padding: 20px -> 내부 여백을 20px로 설정

border: 1px solid gray -> 경계선을 1px 회색 실선으로 설정

display: flex -> 페이지의 요소를 브라우저에서 어떻게 보여줄지 결정, 기본값은 수직이고 flex로 설정하면 요소를 수평으로 배치, 주로 다른 요소들을 감싸는 컨테이너 용도로 사용됨

flex-direction: column -> 컨테이너에 있는 요소들의 배치 방향을 조절하는 속성, 기본 값은 row로 수평으로 배치하고 column으로 설정하면 수직으로 배치

gap: 30px -> 자식 컴포넌트 간의 여백을 조절


TodoWrite 컴포넌트

TodoWrtie 컴포넌트에는 문자열 새로운 Todo 작성하기+ 이모티콘(window+.)과 할 일을 적을 input추가버튼이 존재한다.

- TodoWrite.jsx

const TodoWrite = () => {
    return(
        <div>
            <h3>새로운 Todo 작성하기 🖋️</h3>
            <input 
                placeholder="새로운 Todo..."/>
            <button>추가</button>
        </div>
    )
}

export default TodoWrite;

- App.jsx

import TodoDate from './TodoDate'
import TodoWrite from './TodoWrite'
import './App.css'

function App() {

  return (
    <div className='App'>
      <TodoDate />
      <TodoWrite />
    </div>
  )
}

export default App

TodoWrite 컴포넌트에 css 적용하기

- TodoWrite.css

.TodoWrite .WriteContent {
    width: 100%;
    display: flex;
    gap: 10px;
}

.TodoWrite input {
    flex: 1;
    box-sizing: border-box;
    border: 1px solid rgb(220, 220, 220);
    border-radius: 5px;
    padding: 15px;
}

.TodoWrite input:focus{
    outline: none;
    border: 1px solid #1f93ff;
}

.TodoWrite button{
    cursor: pointer;
    width: 80px;
    border: none;
    background-color: #1f93ff;
    color: white;
    border-radius: 5px;
}

.WriteContent -> 입력 폼과 추가 버튼을 감싸는 요소의 className

flex: 1 -> 해당 요소의 너비가 브라우저의 크기에 따라 유연하게 늘어나고 줄어듬

border-radius: 5px -> 모서리의 둥근 정도 조절

input:focus -> 입력 폼을 클릭했을 때 반응을 설정

outline: none -> 두꺼운 선 제거

cursor: pointer -> 마우스를 버튼에 올렸을 때 마우스의 모양이 변경됨

TodoItem 컴포넌트

TodoItem 컴포넌트는 체크박스 + 할 일 내용 + 작성 날짜 + 제거 버튼으로 구성된다.

- TodoItem.jsx

import "./TodoItem.css"

const TodoItem = () => {
    return(
        <div className="TodoItem">
            <input type="checkbox" />
            <div className="Content">할 일</div>
            <div className="WriteDate">{new Date().toLocaleDateString()}</div>
            <button>제거</button>
        </div>
    )
}

export default TodoItem;

TodoList 컴포넌트

TodoList 컴포넌트에는 문자열 Todo List + 이모티콘(window+.) + Search Bar +TodoItem 컴포넌트로 구성된다.

- TodoList.jsx

import "./TodoList.css"
import TodoItem from "./TodoItem";

const TodoList = () => {
    return(
        <div>
            <h3>Todo List 🍀</h3>
            <input 
                className="SearchBar"
                placeholder="검색어를 입력하세요"/>
            <div className="ItemList">
                <TodoItem />
                <TodoItem />
                <TodoItem />
            </div>
        </div>
    )
}

export default TodoList;

TodoItem 컴포넌트와 TodoList 컴포넌트에 css 적용

- TodoItem.css

.TodoItem {
    display: flex;
    align-items: center;
    gap: 20px;
    padding-bottom: 20px;
    border-bottom: 1px solid rgb(240, 240, 240);
    margin-top: 20px;
}

.Content {
    flex: 1;
}

.WriteDate{
    color: gray;
}

Content에 flex: 1을 주게 되면

flex-grow: 1;
flex-shrink: 1;
flex-basis: 0%

3가지 의미를 모두 갖는다.
-> 점유 크기를 0으로 지정한 후, 화면 비율에 따라 유연하게 늘어나거나 줄어든다.

- TodoList.jsx

.SearchBar{
        width: 100%;
        border: none;
        box-sizing: border-box;
        border-bottom: 1px solid rgb(240, 240, 240);
        padding-bottom: 10px;
}

UI 구성은 끝났다. 이제 기능 구현으로 넘어가보겠다.

기능 구현하기

기능 구현 설계

CRUD에 기초하여
Create : TodoItem 생성하기
Read : TodoItem을 TodoList에 보여주고, 필터링 기능 구현하기
Update : 내용 수정은 포함하지 않으므로 checkBox 체크유무 구현하기
Delete : TodoList에서 TodoItem 삭제하기

TodoItem의 초기 데이터 설정하기

데이터의 구조는 체크박스, 할 일 내용, 작성 날짜, 제거버튼으로 구성되어있다. 제거버튼을 제외한 내용은 작성자가 작성했을 때 받아와야 하는 정보이다. 초기 설정을 위해 mock data를 작성할 것이다.

- App.jsx

import './App.css'

const mockData = [
  {
    id:0,
    isDone:false,
    content: "React 공부하기",
    writeDate: new Date().getTime()
  },
  {
    id:1,
    isDone:false,
    content: "Spring 공부하기",
    writeDate: new Date().getTime()
  },
   
    id:2,
    isDone:false,
    content: "Django 공부하기",
    writeDate: new Date().getTime()
  }
]

function App() { ... }

id : 고유번호
isDone : 체크유무
content : 할 일 내용
writeDate : 작성 날짜


Create

할 일을 추가하는 기능이다.

- App.jsx

function App() {
  const [todo, setTodo] = useState(mockData);
  const idRef = useRef(3);
  const onCreate = (content) => {
    const newData = {
      id: idRef.current,
      isDone: false,
      content,
      writeDate: new Date().getTime()
    };
    setTodo([newData, ...todo]);
    idRef.current += 1;
  };

  return (
    <div className='App'>
      <TodoDate />
      <TodoWrite onCreate={onCreate}/>
      <TodoList />
    </div>
  )
}

export default App
  1. 할 일 데이터를 다룰 state를 생성한다.
  2. 추가 버튼을 누르면 새로운 할 일을 생성하는 함수 onCreate를 선언한다.
  3. 새로운 데이터를 담을 변수 newData 변수를 선언한다.
  4. 새로운 데이터를 기존 데이터에 추가하기 위해 setTodo...연산자를 통해 기존 데이터에 추가한다.
  5. id 값의 변화는 렌더링이 필요하지 않으므로 useRef를 사용하여 변경한다.
  6. onCreate추가 버튼이 클릭되어야지 호출되므로 TodoWrite 컴포넌트에 props로 전달한다.

- TodoWrite.jsx

import { useRef, useState } from "react";
import "./TodoWrite.css"

const TodoWrite = ({ onCreate }) => {
  const [content, setContent] = useState("");
  const onChangeContent = (event) => {
      setContent(event.target.value);
  }
  const onSubmit = () => {
      onCreate(content);
  }

    return(
        <div className="TodoWrite">
            <h3>새로운 Todo 작성하기 🖋️</h3>
            <div className="WriteContent">
                <input
                    value={content}
                    onChange={onChangeContent}
                    placeholder="새로운 Todo..."/>
                <button onClick={onSubmit}>추가</button>
            </div>
        </div>
    )
}

export default TodoWrite;
  1. 입력창의 데이터를 저장하기 위한 state를 선언한다.
  2. 입력창의 변화를 보여주기 위한 함수 onChangeContent를 작성하고 input 태그의 valuecontent로 선언한다.
  3. 입력창의 내용을 저장하기 위해 추가 버튼이 클릭되었을 때 onCreate 함수를 호출하여 새로운 데이터를 저장한다.

빈 입력 제한하기

- TodoWrite.jsx

import { useRef, useState } from "react";
import "./TodoWrite.css"

const TodoWrite = ({ onCreate }) => {
  ( ... )
  const inputRef = useRef();
  const onSubmit = () => {
      if (!content) {
      	inputRef.current.focus();
        return;
      }
      onCreate(content);
  }
  
  return(
        <div className="TodoWrite">
            <h3>새로운 Todo 작성하기 🖋️</h3>
            <div className="WriteContent">
                <input
                    ref={inputRef}
                    value={content}
                    onChange={onChangeContent}
                    placeholder="새로운 Todo..."/>
                <button onClick={onSubmit}>추가</button>
            </div>
        </div>
    )
}

export default TodoWrite;
  1. onSubmit 함수에서 content가 비어있다면 inputRef의 현재값을 저장한 요소에 포커스한다.

Read

  1. 할 일을 추가하면 렌더링을 통해 페이지에 보여준다.
  2. 검색 기능을 구현한다.

추가한 할 일을 페이지에 렌더링하기

- App.jsx

function App() {
  const [todo, setTodo] = useState(mockData);
  (...)

  return (
    <div className='App'>
      <TodoDate />
      <TodoWrite onCreate={onCreate}/>
      <TodoList todo={todo}/>
    </div>
  )
}

export default App

TodoList 컴포넌트는 todo를 리스트로 렌더링해야 한다. 리액트에서 배열 데이터를 렌더링할 때는 map 메서드를 주로 사용한다. map 메서드를 사용하면 컴포넌트를 순회하면서 매 요소를 반복하여 렌더링한다.

- TodoList.jsx

import "./TodoList.css"
import TodoItem from "./TodoItem";

const TodoList = ({ todo }) => {
    return(
        <div>
            <h3>Todo List 🍀</h3>
            <input 
                className="SearchBar"
                placeholder="검색어를 입력하세요"/>
            <div className="ItemList">
                {todo.map((it) => (
                    <TodoItem key={it.id} {...it} />
                ))}
            </div>
        </div>
    )
}

export default TodoList;

map 메서드를 사용한 부분을 보면, 매개변수로 it을 받는데 이것은 하나의 요소를 의미한다.
it 내부에는 id, isDone, content, writeDate가 존재한다. 이것을 전달하기 위해 ...연산자를 통해 TodoItem 컴포넌트에 props로 전달한다.
key는 어떤 컴포넌트를 업데이트할지 결정하는 요소다. 그러므로 고유의 값을 할당해야 한다. -> id

- TodoItem.jsx

import "./TodoItem.css"

const TodoItem = ({id, isDone, content, writeDate}) => {
    return(
        <div className="TodoItem">
            <input 
                checked={isDone}
                type="checkbox" />
            <div className="Content">{content}</div>
            <div className="WriteDate">{new Date(writeDate).toLocaleDateString()}</div>
            <button>제거</button>
        </div>
    )
}

export default TodoItem;

isDone : 체크박스 상태를 변경하기 위한 props
content : 내용을 페이지에 보여주기 위한 props
writeDate : 저장된 writeDate를 Date 객체에 전달함으로써 해당 날짜를 Date 객체로 생성한다.

검색 기능 구현하기

SearchBar에 텍스트를 입력하면 필터링되어 TodoList가 출력된다.

- TodoList.jsx

import "./TodoList.css"
import TodoItem from "./TodoItem";
import { useState } from "react";

const TodoList = ({ todo }) => {
    const [search, setSearch] = useState("");
    const onChangeSearch = (event) => {
        setSearch(event.target.value);
    }
    const getSearchResult = () => {
        return search === ""
            ? todo
            : todo.filter((it) => it.content.toLowerCase().includes(search.toLowerCase())
            )
    }
    return(
        <div>
            <h3>Todo List 🍀</h3>
            <input
                value={search}
                onChange={onChangeSearch}
                className="SearchBar"
                placeholder="검색어를 입력하세요"/>
            <div className="ItemList">
                {getSearchResult().map((it) => (
                    <TodoItem {...it} />
                ))}
            </div>
        </div>
    )
}
  1. SearchBar의 텍스트를 저장할 state를 선언한다.
  2. SearchBar의 변화를 감지하고 state에 저장할 onChangeSearch 함수를 작성한다.
  3. 저장한 state를 토대로 검색 기능을 구현한 getSearchResult 함수다.
    -> search가 비어있으면 todo 전체를 출력하고, 비어있지 않다면 todo의 content에 search가 들어있는 요소만 필터링하여 출력한다.

Update

체크 박스를 클릭하여 상태를 변경한다.

체크박스 상태 변경하기

- App.jsx

function App() {
  const [todo, setTodo] = useState(mockData);
  ( ... )
  const onUpdate = (targetId) => {
    setTodo(
      todo.map((it) => 
          it.id === targetId ? {...it, isDone : !it.isDone} : it
      )
    )
  }

  return (
    <div className='App'>
      <TodoDate />
      <TodoWrite onCreate={onCreate}/>
      <TodoList todo={todo} onUpdate={onUpdate}/>
    </div>
  )
}

export default App
  1. 체크박스에 이벤트가 발생했을 때 사용할 onUpdate 함수를 작성한다.
  2. todo를 수정하기 위해 map 함수를 통해 targetId와 같은 id를 갖는 요소를 찾는다.
  3. id값이 같다면 요소를 펼치고 isDone의 현재값과 반대의 값으로 변경한다.
  4. id값이 다르다면 변경하지 않는다.

Delete

할 일 리스트에서 할 일을 삭제한다.

할 일 삭제하기

- App.jsx

function App() {
  const [todo, setTodo] = useState(mockData);
  ( ... )

  const onDelete = (targetId) => {
    setTodo(
      todo.filter((it) => it.id !== targetId)
    )
  }

  return (
    <div className='App'>
      <TodoDate />
      <TodoWrite onCreate={onCreate}/>
      <TodoList todo={todo} onUpdate={onUpdate} onDelete={onDelete}/>
    </div>
  )
}

export default App
  1. 삭제를 위한 함수 onDelete를 작성한다.
  2. onDelete함수는 targetId에 해당하는 id를 제외한 요소들을 필터링하여 todo에 저장한다.

- TodoList.jsx

const TodoList = ({ todo, onUpdate, onDelete}) => {
    (...)
     
    return(
        <div>
            <h3>Todo List 🍀</h3>
            <input
                value={search}
                onChange={onChangeSearch}
                className="SearchBar"
                placeholder="검색어를 입력하세요"/>
            <div className="ItemList">
                {getSearchResult().map((it) => (
                    <TodoItem {...it} onUpdate={onUpdate} onDelete={onDelete}/>
                ))}
            </div>
        </div>
    )
}

- TodoItem.jsx

const TodoItem = ({id, isDone, content, writeDate, onUpdate, onDelete}) => {
    (...)

    const onDeleteBtn = () =>  {
        onDelete(id);
    }
    return(
        <div className="TodoItem">
            <input 
                checked={isDone}
                onChange={onUpdateCheck}
                type="checkbox" />
            <div className="Content">{content}</div>
            <div className="WriteDate">{new Date(writeDate).toLocaleDateString()}</div>
            <button onClick={onDeleteBtn}>제거</button>
        </div>
    )
}

export default TodoItem;

0개의 댓글