일정 관리 웹앱 만들기

나혜수·2023년 3월 21일
0

리액트

목록 보기
13/23

✅프로젝트 준비

📄 프로젝트 생성 및 라이브러리 설치

$ create react-app todo-app
$ cd todo-app
$ yarn add node-sass classnames react-icons
  • 라이브러리
    node-sass, classnames, react-icons
  1. react-icons 라이브러리를 사용하면 SVG 형태의 아이콘을 리액트 컴포넌트처럼 쉽게 사용할 수 있다.
  2. classnames 모듈은 여러 클래스를 추가할 때 뿐만 아니라, 특정 값이 true/false임에 따라 클래스명을 추가 or 추가하지 않도록 하는 것을 간단히 구현할 수 있게 해준다.

📄 prettier 설정
앞에서 배웠던 Prettier를 설정해 코드 스타일을 정리하자. 프로젝트 최상위 디렉토리에 .prettierrc 파일을 다음과 같이 생성한다.

{
  "singleQuote": true,
  "semi": true,
  "useTabs": false,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 80
}


📄 index.css 수정
프로젝트 글로벌 스타일 파일이 들어 있는 index.css를 수정한다.

body {
  margin: 0;
  padding: 0;
  background: white;
}


📄 App 컴포넌트 초기화

import React from "react";

const App = () => {
  return <div>Todo App을 만들자!</div>
}

export default App;


✅UI 구성하기

컴포넌트
src 디렉토리에 components 디렉토리를 생성하여 다음 4개의 파일을 저장한다.

  1. TodoTemplate
    화면을 가운데로 정렬시켜 주며, 앱 타이틀을 보여준다.
  2. TodoInsert
    새로운 항목을 입력하고 추가할 수 있는 컴포넌트. state를 통해 input의 상태를 관리한다.
  3. TodoListItem
    각 할 일에 대한 정보를 보여주는 컴포넌트. todo 객체를 props로 받아와 상태에 따라 다른 스타일의 UI를 보여준다.
  4. TodoList
    todos 배열을 props로 받고, 이를 map을 사용해 여러 개의 TodoListItem 컴포넌트로 변환하여 보여준다.

1. TodoTemplate 만들기

🏷️TodoTemplate.js

import React from "react";
import './TodoTemplate.scss'

const TodoTemplate = ({children}) => {
    const today = new Date()       // js 기본 제공 함수 
    const time = {
        year: today.getFullYear(),  //현재 년도
        month: today.getMonth() + 1, // 현재 월
        date: today.getDate(), // 현재 날짜
        day: today.getDay(), // 요일, 숫자로 출력됨
    };

    const week = ['일','월','화','수','목','금','토'] // 숫자를 요일로 바꿔주기 위함

    return (
        <div className="TodoTemplate">
            <h1>{time.year}{time.month}{time.date}</h1>
            <div className="day">{week[time.day]}요일</div>
            <div className="content">{children}</div>
        </div>
    )
}

export default TodoTemplate

🏷️TodoTemplate.scss
components 디렉토리에 TodoTemplate.scss 파일을 생성한다.

.TodoTemplate{
    padding-top: 48px;
    padding-left: 32px;
    padding-right: 32px;
    padding-bottom: 24px;
    border-bottom: 1px solid #e9ecef;

        
    h1 {
        margin: 0;
        font-size: 36px;
        color: #343a40;
    }    

    .day {
        margin-top: 4px;
        color: #868e96;
        font-size: 21px;
    }    
    

    .content{
        background-color: white;
        color: #20c997;
        font-size: 18px;
        margin-top: 40px;
        font-weight: bold;
    }
}

🏷️App.js

import React from "react";
import TodoTemplate from "./components/TodoTemplate";

const App = () => {
  return <TodoTemplate>Todo App을 만들자!</TodoTemplate>
}

export default App;

2. TodoInsert 만들기

🏷️TodoInsert.js

import React from 'react'
import {MdOutlineAdd} from 'react-icons/md'
import './TodoInsert.scss'

const TodoInsert = () => {
    return (
        <form className='TodoInsert'>
            <input placeholder='할 일을 입력하세요.'/>
            <button type='submit'><MdOutlineAdd/></button> 
        </form>
    )
}

export default TodoInsert

🏷️TodoInsert.scss
components 디렉토리에 TodoTemplate.scss 파일을 생성한다.

.TodoInsert {
    display: flex;
    background: whitesmoke ;

    input {
    	// 기본 스타일 초기화
        background: none;
        outline: none;
        border: none;
        padding: 0.5rem;
        font-size: 1.125rem;
        line-height: 1.5;
        color: black;
        &::placeholder{
            color: #abb7d0
        }
        // 버튼을 제외한 영역 모두 차지
        flex: 1;
    }

    button{
        // 기본 스타일 초기화
        background: none;
        outline: none;
        border: none;
        background: #adb5db;
        color: white;
        padding-left: 1rem;
        padding-right: 1rem;
        font-size: 1.5rem;
        display: flex;
        align-items: center;
        cursor: pointer;
        transition: 0.1s background ease-in;
        
        // 버튼에 마우스를 대면 색깔이 바뀐다. 
        &:hover{
            background: #d2d8f6;
        }

    }
}

🏷️App.js

import React from "react";
import TodoTemplate from "./components/TodoTemplate";
import TodoInsert from "./components/TodoInsert";

const App = () => {
  return <TodoTemplate><TodoInsert/></TodoTemplate>
}

export default App;

3. TodoListItem & TodoList 만들기

🏷️TodoListItem.js

import React from "react";
import {MdCheckBoxOutlineBlank, MdCheckBox, MdRemoveCircleOutline} from 'react-icons/md'
import './TodoListItem.scss'

const TodoListItem = () => {
    return (
        <div>
            <div className="checkbox">
                <MdCheckBoxOutlineBlank/>
                <div className="text">할 일</div>
            </div>
            <div className="remove"><MdRemoveCircleOutline/></div>
        </div>
    )
}

export default TodoListItem

🏷️TodoList.js

import React from "react";
import TodoListItem from "./TodoListItem";
import './TodoList.scss'

const TodoList = () => {
    return (
        <div className="TodoList">
            <TodoListItem/>
            <TodoListItem/>
            <TodoListItem/>
        </div>
    )
}

export default TodoList

🏷️TodoListItem.scss
components 디렉토리에 TodoListItem.scs 파일을 생성한다.

.TodoListItem {
    padding: 1rem;
    display: flex;
    align-items: center; // 세로 중앙 정렬
    &:nth-child(even){
        background: #f8f9fa;
    }

    .checkbox {
        cursor: pointer;
        flex: 1; // 차지할 수 있는 영역 모두 차지
        display: flex;
        align-items: center; // 세로 중앙 정렬

        svg {
            // 아이콘
            font-size: 1.5rem;
        }

        .text {
            margin-left: 0.5rem;
            flex: 1; // 차지할 수 있는 영역 모두 차지
        }

        // 체크 시 보여 줄 스타일 
        &.checked {
            svg {
                color: #22b8cf
            }
            .text {
                color: #adb5bd;
                text-decoration: line-through;
            }
        }
    }

    .remove {
        display: flex;
        align-items: center;
        font-size: 1.5rem;
        color: #ff6b6b;
        cursor: pointer;
        &:hover {
            color: #ff8787
        }
    }

    // 앨리먼트와 앨리먼트 사이 테두리 넣어줌
    & + & {
        border-top: 1px solid #dee2e6;
    }
}

🏷️TodoList.scss
components 디렉토리에 TodoList.scss 파일을 생성한다.

.TodoList {
    min-height: 320px;
    max-height: 513px;
    overflow-y: auto;
}

🏷️App.js

import React from "react";
import TodoTemplate from "./components/TodoTemplate";
import TodoInsert from "./components/TodoInsert";
import TodoList from "./components/TodoList";

const App = () => {
  return (
    <TodoTemplate>
      <TodoInsert/>
      <TodoList/>
    </TodoTemplate>
  )
}

export default App;


✅기능 구현하기

1. App에서 todos 상태 사용하기

App에서 useState를 사용해 todos라는 상태를 정의하고, todos를 TodoList의 props로 전달할 것이다. todos 배열 안에는 각항목의 " id, text, 완료 여부 값 "이 포함되어 있다. TodoList에서 이 값을 받아온 후 TodoItem으로 변환하여 렌더링할 것이다.

🏷️App.js

import React, { useState } from "react";
import TodoTemplate from "./components/TodoTemplate";
import TodoInsert from "./components/TodoInsert";
import TodoList from "./components/TodoList";

const App = () => {
  const [todos, setTodos] = useState([
    {
      id: 1,
      text: "리액트 기초 배우기",
      checked: true
    },
    {
      id: 2,
      text: "방 청소하기",
      checked: true
    },
    {
      id: 3,
      text: "이상협 때리기",
      checked: false
    }
  ])

  return (
    <TodoTemplate>
      <TodoInsert/>
      <TodoList todos={todos}/>
    </TodoTemplate>
  )
}

export default App;

🏷️TodoList.js
props로 받아온 todos 배열을 map을 통해 TodoListItem으로 이루어진 배열로 변환하여 렌더링해주었다. map을 사용해 컴포넌트로 변환할 때는 key props를 전달해 주어야 한다.

import React from "react";
import TodoListItem from "./TodoListItem";
import './TodoList.scss'

const TodoList = ({todos}) => {
    return (
        <div className="TodoList">
            {todos.map(todo => (
                <TodoListItem todo={todo} key={todo.id}/>
            ))}           
        </div>
    )
}

export default TodoList

🏷️TodoListItem.js

이 코드에서는 조건부 스타일링을 위해 classnames를 사용한다.
아래 코드에서 <div className={cn('checkbox', { checked })}> 부분에 classnames가 사용되었다.
checked 값이 true → className이 "checkbox checked "
checked 값이 false → checked가 적용이 되지 않아 className이 "checkbox "

import React from "react";
import {MdCheckBoxOutlineBlank, MdCheckBox, MdRemoveCircleOutline} from 'react-icons/md'
import './TodoListItem.scss'
import cn from 'classnames'

const TodoListItem = ({todo}) => {
    const {text, checked} = todo

    return (
        <div className="TodoListItem">
            <div className={cn("checkbox",{checked})}>
                {checked? <MdCheckBox/> : <MdCheckBoxOutlineBlank/>}
                <div className="text">{text}</div>
            </div>
            <div className="remove"><MdRemoveCircleOutline/></div>
        </div>
    )
}

export default TodoListItem

2. 항목 추가 기능 구현하기

TodoInsert에서는 input 상태를 관리하고, App 컴포넌트에서는 todos 배열에 새로운 객체를 추가하는 함수를 만들어야 한다.

🏷️App.js
onInsert 함수에서 id 값을 useRef를 사용해 관리한다. useState가 아닌 useRef를 사용하는 이유는 id 값은 렌더링 정보가 아닌 단순히 새로운 정보를 만들 때 참조되는 값이기 때문이다.

import React, { useCallback, useState, useRef } from "react";
import TodoTemplate from "./components/TodoTemplate";
import TodoInsert from "./components/TodoInsert";
import TodoList from "./components/TodoList";

const App = () => {
  const [todos, setTodos] = useState([
    {
      id: 1,
      text: "리액트 기초 배우기",
      checked: true
    },
    {
      id: 2,
      text: "방 청소하기",
      checked: true
    },
    {
      id: 3,
      text: "이상협 때리기",
      checked: false
    }
  ])

  const nextId = useRef(4)

  const onInsert = useCallback((text) => {
    const todo = {
      id: nextId.current,
      text,
      checked: false   
    }
    setTodos([...todos,
    todo])
    nextId.current += 1 
  },[todos])

  return (
    <TodoTemplate>
      <TodoInsert onInsert={onInsert}/>
      <TodoList todos={todos}/>
    </TodoTemplate>
  )
}

export default App;

🏷️TodoInsert.js

button 이벤트에 onClick을 사용해도 되지만 굳이 form과 onSubmit을 이용한 이유는, onSubmit 이벤트의 경우 input에서 엔터를 눌렀을 때도 이벤트가 발생한다.

import React, { useCallback, useState } from 'react'
import {MdOutlineAdd} from 'react-icons/md'
import './TodoInsert.scss'

const TodoInsert = ({onInsert}) => {
    const [value, setValue] = useState('')

    // 리렌더링 시마다 함수를 새로 만들지 않도록 useCallback 사용

    const onChange = useCallback((e) => {
        setValue(e.target.value)
    },[])

    const onSubmit = useCallback(
        (e) => {
            onInsert(value) 
            setValue('')
            e.preventDefault() // 브라우저 새로고침 방지
        }
    ,[value, onInsert])

    return (
        <form className='TodoInsert' onSubmit={onSubmit}>
            <input placeholder='할 일을 입력하세요.'
            value={value}
            onChange={onChange}/>
            <button type='submit' onSubmit={onSubmit}><MdOutlineAdd/></button> 
        </form>
    )
}

export default TodoInsert

3. 지우기 기능 구현하기

🏷️App.js
App 컴포넌트에서 id를 파라미터로 받아와서 같은 id를 가진 항목을 todos에서 지우는 onRemove 함수를 작성한다.

import React, { useCallback, useState, useRef } from "react";
import TodoTemplate from "./components/TodoTemplate";
import TodoInsert from "./components/TodoInsert";
import TodoList from "./components/TodoList";

const App = () => {
  const [todos, setTodos] = useState([
    {
      id: 1,
      text: "리액트 기초 배우기",
      checked: true
    },
    {
      id: 2,
      text: "방 청소하기",
      checked: true
    },
    {
      id: 3,
      text: "이상협 때리기",
      checked: false
    }
  ])

  const nextId = useRef(4)

  const onInsert = useCallback((text) => {
    const todo = {
      id: nextId.current,
      text,
      checked: false   
    }
    setTodos([...todos,
    todo])
    nextId.current += 1 
  }, [todos])

  const onRemove = useCallback((id) => {
    setTodos(todos.filter(todo => todo.id !== id))
  },[todos])

  return (
    <TodoTemplate>
      <TodoInsert onInsert={onInsert}/>
      <TodoList todos={todos} onRemove={onRemove}/>
    </TodoTemplate>
  )
}

export default App;

🏷️TodoList.js
App 컴포넌트에서 만든 onRemove 함수를 TodoListItem 컴포넌트에서 사용하기 위해선 TodoList 컴포넌트를 거쳐야한다.

import React from "react";
import TodoListItem from "./TodoListItem";
import './TodoList.scss'

const TodoList = ({todos, onRemove}) => {
    return (
        <div className="TodoList">
            {todos.map(todo => (
                <TodoListItem todo={todo} key={todo.id} onRemove={onRemove}/>
            ))}           
        </div>
    )
}

export default TodoList

🏷️TodoListItem.js

import React from "react";
import {MdCheckBoxOutlineBlank, MdCheckBox, MdRemoveCircleOutline} from 'react-icons/md'
import './TodoListItem.scss'
import cn from 'classnames'

const TodoListItem = ({todo, onRemove}) => {
    const {id, text, checked} = todo

    return (
        <div className="TodoListItem">
            <div className={cn("checkbox",{checked})}>
                {checked? <MdCheckBox/> : <MdCheckBoxOutlineBlank/>}
                <div className="text">{text}</div>
            </div>
            <div className="remove" onClick={()=>onRemove(id)}><MdRemoveCircleOutline/></div>
        </div>
    )
}

export default TodoListItem

4. 토글 기능 구현하기

토글 기능은 위에서 만든 삭제 기능과 꽤 비슷하다. onToggle 함수를 App에서 만들고 해당 함수를 TodoList, TodoListItem에 props로 넘겨주면 된다.

🏷️App.js

import React, { useCallback, useState, useRef } from "react";
import TodoTemplate from "./components/TodoTemplate";
import TodoInsert from "./components/TodoInsert";
import TodoList from "./components/TodoList";

const App = () => {
  const [todos, setTodos] = useState([
    {
      id: 1,
      text: "리액트 기초 배우기",
      checked: true
    },
    {
      id: 2,
      text: "방 청소하기",
      checked: true
    },
    {
      id: 3,
      text: "이상협 때리기",
      checked: false
    }
  ])

  const nextId = useRef(4)

  const onInsert = useCallback((text) => {
    const todo = {
      id: nextId.current,
      text,
      checked: false   
    }
    setTodos([...todos,
    todo])
    nextId.current += 1 
  }, [todos])

  const onRemove = useCallback((id) => {
    setTodos(todos.filter(todo => todo.id !== id))
  },[todos])

  const onToggle = useCallback((id)=>{
    setTodos(
      todos.map(todo => 
        todo.id === id ? {...todo, checked: !todo.checked} : todo))
  },[todos])

  return (
    <TodoTemplate>
      <TodoInsert onInsert={onInsert}/>
      <TodoList todos={todos} onRemove={onRemove} onToggle={onToggle}/>
    </TodoTemplate>
  )
}

export default App;

🏷️TodoList.js

import React from "react";
import TodoListItem from "./TodoListItem";
import './TodoList.scss'

const TodoList = ({todos, onRemove, onToggle}) => {
    return (
        <div className="TodoList">
            {todos.map(todo => (
                <TodoListItem todo={todo} key={todo.id} onRemove={onRemove}
                onToggle={onToggle}/>
            ))}           
        </div>
    )
}

export default TodoList

🏷️TodoListItem.js

import React from "react";
import {MdCheckBoxOutlineBlank, MdCheckBox, MdRemoveCircleOutline} from 'react-icons/md'
import './TodoListItem.scss'
import cn from 'classnames'

const TodoListItem = ({todo, onRemove, onToggle}) => {
    const {id, text, checked} = todo

    return (
        <div className="TodoListItem">
            <div className={cn("checkbox",{checked})} onClick={()=>onToggle(id)}>
                {checked? <MdCheckBox/> : <MdCheckBoxOutlineBlank/>}
                <div className="text">{text}</div>
            </div>
            <div className="remove" onClick={()=>onRemove(id)}><MdRemoveCircleOutline/></div>
        </div>
    )
}

export default TodoListItem


이번 프로젝트는 소규모이기 때문에 따로 컴포넌트 리렌더링 최적화 작업이 없어도 정상 작동한다. 😀

profile
오늘도 신나개 🐶

0개의 댓글