본문 바로가기
  • 프론트엔드 개발자 세오세오 | Frontend dev Seo
Learn to Code

[React] React hooks 연습: 간단한 To-Do 리스트

by CEOSEO 2021. 5. 18.
728x90
반응형

React 함수 컴포넌트로만 만든 간단한 Todo 리스트

 

리액트를 배운 뒤 모두가 한번쯤은 만들어본다는 To-do list를 만들었다. 다양한 hooks를 연습하고 싶었지만 아무래도 to-do 리스트는 많은 기능이 없기 때문에 useState()만을 사용하게 되었다. 최상단 App 컴포넌트 아래에 List 컴포넌트, 그리고 그 안에 Todo 작성 Form과 To-do 리스트만 있는 아주 콤팩트한 앱이다.  요 to-do list의 간단한 기능은 아래와 같다.

 

 

오늘의 매우 중요한 할 일을 작성하고 [추가]를 누르면...

 

오늘의 할 일이 리스트에 추가된다.

 

리스팅을 다시 클릭하면 완료 여부를 표시할 수 있다.

 

완료 상태로 되어 있을 땐 리스팅의 오른편에 완료된 할일을 삭제할 수 있는 X 버튼이 생긴다.
X 버튼을 클릭하면 해당 항목이 삭제된다.

 

 

 

 

 

최상단 <App> 컴포넌트

 

최상단 컴포넌트인 App에 초기값으로 initialTodos를 만들었다. 각 todo엔 key값으로 써줄 id와 todo 내용, 그리고 완료 여부를 표시하는 completed만 있다.

 

import './App.css'
import List from './List'
import { useState } from 'react'
import { v4 as uuid } from 'uuid'

const initialTodos = [
  {
    id: uuid(),
    todo: '리액트 공부하기',
    completed: false
  },
  {
    id: uuid(),
    todo: '블로그 쓰기',
    completed: false
  }
]

function App() {
  const [todos, setTodo] = useState(initialTodos)

  const handleSubmit = (newTodo) => {
    const todo = {
      id: uuid(),
      todo: newTodo,
      completed: false
    }
    setTodo( (prevState) =>[...prevState, todo])
  }

  const deleteTodo = (id) => {
    setTodo( (prevState) => prevState.filter(todo=> todo.id!==id))
  }

  return (
    <div className="App">
      <h1 id="heading">To-Do List</h1>
      <List 
        todos={todos} 
        handleSubmit={handleSubmit} 
        handleClickTodo={setTodo}
        deleteTodo={deleteTodo}
      />
    </div>
  );
}

export default App;

 

 

<List> 컴포넌트

<List>컴포넌트는 새로운 To-do를 작성하는 부분인 <TodoForm>과 state에 있는 개별 todo들이 <SingleTodo>가 되어 map으로 그려지는 두 부분으로 나뉘어 있다.

 

import TodoForm from './TodoForm'
import SingleTodo from './SingleTodo'

export default function List ( {todos, handleSubmit, handleClickTodo, deleteTodo} ) {
  const handleClick = (e, id) => {
    handleClickTodo(todos.map(todo => {
      if (todo.id === id) {
        todo.completed = !todo.completed
      }
      return todo
    }))
  }

  return (
    <div className="List">
      <TodoForm handleSubmit={handleSubmit}/>
      <ul className="todo-list">
        {todos.map( todo => 
          <SingleTodo 
            todo={todo} 
            handleClick={handleClick}
            deleteTodo={deleteTodo}
            key={todo.id}
          />
        )}
        
      </ul>
    </div>
  )
}

 

 

<TodoForm>

새로운 to-do를 작성하는 부분이다. e.target의 [0]번째는 <input>이며, 이 <input>의 value를 찝어다가 조상님으로부터 전해받은 props인 handleSubmit에 전달해준다.

 

한 번 to-do를 작성한 다음 input 칸을 깨끗하게 비워주는 것은 당연지사!!

 

export default function TodoForm ( { handleSubmit } ) {
  const addNewTodo = (e, newTodo) => {
    e.preventDefault()
    handleSubmit(newTodo)
    e.target[0].value = ''
  }

  return (
    <form className="TodoForm" onSubmit={(e)=> addNewTodo(e, e.target[0].value)}>
      <input type="text"/>
      <button type="submit">추가</button>
    </form>
  )
}

 

 

<SingleTodo>

상태에 저장된 개별 todo가 하나씩 해당되는 컴포넌트이다. 깔끔해보였던 아이콘들을 React icons에서 가져와서 사용했다. todo 상태의 완료 체크 여부(todo.completed)에 따라 아이콘이 바뀐다.

 

투두 내용의 취소선 CSS 같은 경우 그냥 삼항연산자로 쓰려니 너무 길어져서 따로 빼서 밑에 함수로 만든 다음 가져다 사용했다.

 

import { BsCircle, BsCheckCircle, BsXCircleFill } from 'react-icons/bs'

export default function SingleTodo ( {todo, handleClick, deleteTodo} ) {
  return (
    <li className="SingleTodo" 
        onClick={e=>{if (e.target.localName !== 'path') 
        handleClick(e, todo.id)}}>
      <div>
        {todo.completed ? <BsCheckCircle/> : <BsCircle/>}
        <div style={completed(todo.completed)}>{todo.todo}</div>
      </div>
      <button className="deleteBtn" onClick={()=>deleteTodo(todo.id)}>
        {todo.completed ? <BsXCircleFill/> : ''}
      </button>
    </li>
  ) 
}

function completed (boolean) {
  if (boolean === true) {
    return {textDecoration:"line-through",color:"#d3d3d3"}
  } else {
    return {textDecoration:""}
  }
}

 

위 코드 중에 아래와 같은 부분이 있는데, 저 if문을 넣어준 구조상의 이유가 있다. 구조상 <li>안에 <div>와 <button>, 그리고 해당 투두의 완료 여부에 따라 나타나는 삭제 버튼이 있는데, handleClick props를 실행시키는 onClick이 <li>에 걸려있기 때문에, 삭제 버튼이 생성되었을 때 해당 버튼을 눌러도 삭제가 되지 않고 handleClick이 호출되는 onClick이 실행되기 때문이다.

 

 

onClick={e=>{if (e.target.localName !== 'path') 
        handleClick(e, todo.id)}}>

 

따라서, li가 클릭이 되어 onClick이 발동되었지만 만약 li의 자식들 중 delete 버튼인 이 눌러진 경우라면 (즉, e.target.localName이 'path'라면) handleClick이 발동되지 않게 해놨다.

 

 

 

추가:  CSS

@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;700&display=swap');

:root {
  --dark-color: #444444;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: 'Poppins', sans-serif;
}

body {
  width: 100vw;
  height: 100vh;
  display: grid;
  place-content: center;
}

.App {
  width: 100vw;
  height: 100vh;
  padding: 2rem;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  align-items: center;
  background-color: var(--dark-color);
}

#heading {
  text-align: center;
  margin-bottom: 1rem;
  color: #fff;
}

.List {
  flex: 1 1 auto;
  min-width: 350px;
  max-width: 400px;
  height: 80%;
  padding: 1rem 1rem;
  list-style: none;
  background-color:rgb(248, 243, 236);
  border-radius: 15px;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  align-items: center;
  box-shadow: 5px 5px 20px rgba(0,0,0,0.2);
}

.List > li {
  width: 100%;
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  column-gap: 0.5rem;
  
}

.TodoForm {
  width: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  column-gap: 0.2rem;
}

.TodoForm > input[type=text] {
  flex: 1 1 auto;
  padding: 0.2rem 0.5rem;
  color: var(--dark-color);
  border: none;
  border-radius: 5px;
}

.TodoForm > input[type=text]:focus {
  outline: none;
}

.TodoForm > button {
  background-color: var(--dark-color);
  border: none;
  border-radius: 5px;
  padding: 0.2rem;
  color: #fff;
}

.TodoForm > button:hover {
  cursor: pointer;
  background-color: rosybrown;
}

.todo-list {
  width: 100%;
  display: flex;
  flex-direction: column;
  row-gap: 0.5rem;
  margin: 0.5rem 0;
}

.SingleTodo {
  list-style: none;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0 0.5rem;
  border-radius: 10px;
  transition: 0.2s;
}

.SingleTodo:hover {
  cursor: pointer;
  background-color:rosybrown;
  transform: translateY(-3px);
}

.SingleTodo > div {
  color: var(--dark-color);
  display: flex;
  column-gap: 0.5rem;
  justify-content: flex-start;
  align-items: center;
}

.SingleTodo:hover >div {
  color: #fff;
}

.deleteBtn {
  border: none;
  background: transparent;
  z-index: 10;
  display: inline;
}

.deleteBtn:hover {
  cursor: pointer;
  color: #fff;
}

 

 

 

728x90
반응형

댓글