[React] 초간단 투두리스트 만들기

hsecode·2023년 6월 14일
25

react

목록 보기
1/1
post-thumbnail

리액트를 어떻게 공부하면 좋을까 해서 시작한 TO DO LIST..🤔


대략 이런 형태의 리스트를 만들어 보았다.

  • UI - JSX로 HTML 작성 & CSS 입혀주기
  • 추가 - input에 입력한 리스트 생성하기
  • 삭제 - 입력한 리스트 삭제하기

이 순서로 진행했고, 처음엔 클래스형으로 작성했었는데 대세를 좇기위해 함수형으로 바꿨다.

1. UI - HTML, CSS

가장 먼저 UI. 디자인을..내가 했어야했다는 점만 제외하면 어렵지 않게 작성할 수 있었다. 디자인은 너무 어렵다..💦

index.js 수정

우선...
React18 버전에서는 ReactDOM.render()가 사용되지 않는다.
Warning: ReactDOM.render is no longer supported in React 18. 에러가 나므로 index.js 파일을 수정해줬다.

import React from 'react';
import * as ReactDOM from 'react-dom/client';
import './App.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById("root"));

root.render(<App />);

reportWebVitals();

기본 구조 작성

App.js

App.js의 경우 기능이 직접적으로(?) 적용되지 않을 구조를 위주로 작성했고,
기능이 추가될 todoList 영역을 따로 만들어 import 한다.

import {useState} from 'react';
import TodoList from './component/todoList';

function TodoHead(){
  return (
    <div className={'headingArea'}>
      <h1 className={'heading__h1'}>TodoList</h1>
      <p className={'desctext'}>Enter your to-do and Complete!!</p>
    </div>
  )
}

function TodoContent() {
  return (
    <div className={'listArea'}>
      <div className={'listBox'}>
        <h2 className={'heading__note'}>NOTE.</h2>
        <TodoList></TodoList>
      </div>
    </div>
  )
}


function App() {
  return (
    <div className={'todolistWrap'}>
      <TodoHead></TodoHead>
      <TodoContent></TodoContent>
    </div>
  )
}

export default App;

todoList.js

todoList.js도 처음에는 아무런 기능 없이 구조만 작성했다.
텍스트 입력 영역을 따로 관리하고 싶어서 todoCreate를 따로 만들어 todoList에서 import했다.
그리고 이후에... 문법에 익숙치 않아서 연결할때 헤멤

import {useState} from 'react';
import TodoCreate from './todoCreate';

function TodoList() {
  return (
    <div className={'todoListBox'}>
      <ul className={'todoList'}>
          <li className={'todoItem'}>
            <label>
              <input type="checkbox"/>
              <span className={'checkIcon'}></span>
              <span className={'labelText'}>Sample List item</span>
            </label>
            <button type={'button'} className={'btnDel'}>삭제</button>
          </li>
      </ul>
      <TodoCreate />
    </div>
  )
}

export default TodoList;

todoCreate.js

function TodoCreate() {
  return(
    <form>
      <div className={'createArea'}>
        <input type='text' name='list' className={'inputText'} placeholder={'Entering to-do list~!'}></input>
        <button className={'btnEnter'}><span className={'arrowIcon'}></span>Enter</button>
      </div>
    </form>
  )
}

export default TodoCreate;

CSS 작성

/* App.css */
body {margin: 0;font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;}code {font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;}* {margin: 0;padding: 0;box-sizing:border-box;}ol,ul,li {list-style: none;}.todolistWrap {display: -webkit-flex;display: -ms-flex;display: flex;flex-direction: column;justify-content: center;align-items: center;height: 100vh;background: #eee;}.heading__h1 {position: relative;font-size: 42px;color: #333;}.headingArea {margin-bottom: 17px;width: 430px;}.headingArea .heading__h1 {display: inline-block;min-width: 125px;transform: rotate(-17deg);}.headingArea .heading__h1:before {content: "TodoList";position: absolute;top: -0.15rem;left: 0.15rem;color: transparent;-webkit-text-stroke: 0.1rem #333;}.headingArea .desctext {text-align: center;font-size: 13px;color: #c1c1c1;}.listArea {position: relative;width: 350px;height: 500px;}.listArea:before {content: '';position: absolute;top: 10px;right: -10px;box-sizing: border-box;width: 100%;height: 100%;background: #4fcda4;border-radius: 10px;z-index: 0;}.countBox {display: flex;justify-content: flex-end;margin-bottom: 12px;}.btnCount {display: inline-block;vertical-align: top;position: relative;margin-left: 4px;width: 20px;height: 20px;border: 0;background: #d2ddff;border-radius: 3px;font-size: 9px;color: transparent;overflow: hidden;}.btnCount::before {content: '';position: absolute;top: 50%;left: 50%;border-bottom: 10px solid royalblue;border-left: 6px solid transparent;border-right: 6px solid transparent;transform: translate(-50%, -50%);}.btnCount--down::before {transform: translate(-50%, -50%) rotate(180deg);}.listBox {display: flex;flex-direction: column;position: relative;padding: 20px;height: 100%;border: 2px solid #555;background: #fff;border-radius: 10px;z-index: 1;}.heading__note {margin-bottom: 10px;}.checkIcon {position: relative;display: inline-block;vertical-align: top;width: 14px;height: 14px;border: 1px solid #333;background: #fff;border-radius: 3px;}input:checked ~ .checkIcon {background: #333;}input:checked ~ .checkIcon:before {content: '';position: absolute;top: 50%;left: 50%;margin-top: -4px;margin-left: -4px;width: 6px;height: 3px;border-left: 2px solid #fff;border-bottom: 2px solid #fff;transform: rotate(-45deg);}.todoCount {margin-bottom: 5px;text-align: right;font-size: 12px;}.todoListBox {display: flex;flex-direction: column;flex: 1;min-height: 0;}.todoList {flex: 1;margin-right: -20px;padding-right: 20px;overflow-y: auto;}.todoItem {display: -webkit-flex;display: -ms-flex;display: flex;justify-content: space-between;align-items: center;border-bottom: 1px solid #333;}.todoItem label {display: -webkit-flex;display: -ms-flex;display: flex;align-items: center;min-width: 0;height: 30px;}.todoItem input[type='checkbox'] {position: absolute;top: 0;left: 0;opacity: 0;}.todoItem .checkIcon {flex-shrink: 0;margin-right: 7px;}.todoItem .labelText {overflow: hidden;white-space: nowrap;text-overflow: ellipsis;}.todoItem .btnDel {flex-shrink: 0;position: relative;width: 20px;height: 20px;border: 0;background: none;font-size: 0;color: transparent;}.todoItem .btnDel:before {content: '';position: absolute;top: 50%;left: 50%;margin-left: -6px;width: 12px;height: 1px;background: #333;transform: rotate(45deg);}.todoItem .btnDel:after {content: '';position: absolute;top: 50%;left: 50%;margin-left: -6px;width: 12px;height: 1px;background: #333;transform: rotate(-45deg);}.createArea {position: relative;margin-top: 20px;}.inputText {display: inline-block;padding: 0 37px 0 13px;width: 100%;height: 35px;border: 1px dashed #333;background: none;border-radius: 20px;}.btnEnter {position: absolute;top: 50%;right: 5px;margin-top: -13px;width: 26px;height: 26px;border: 0;background: #333;border-radius: 50%;text-align: center;font-size: 0;color: transparent;overflow: hidden;}.btnEnter:hover .arrowIcon {transform: translateX(20px);animation: moveRight .9s ease infinite;animation-delay: .5s;}.btnEnter .arrowIcon {position: relative;display: inline-block;vertical-align: top;margin-top: -1px;width: 10px;height: 1px;background-color: #fff;transition: all ease .5s;}.btnEnter .arrowIcon:before {content: '';position: absolute;top: -3px;right: 0;width: 6px;height: 6px;border-top: solid 1px #fff;border-right: solid 1px #fff;-webkit-transform: rotate(45deg);transform: rotate(45deg);}@keyframes moveRight {0% {transform: translateX(-20px);}100% {transform: translateX(20px);}}

2. 추가 - 리스트 생성하기

입력

  • .inputText
    입력될 value값 즉 리스트명을 {listName}으로 지정한다.
  • .inputText onChange
    - input에 onChange 이벤트를 걸어 값이 변경될때마다 실행될 함수 setListName를 지정한다.
    - 이때 이벤트를 받는 대상을 value(input에 입력된 값)로 지정하기위해 event.target.value로 작성한다.
  • const [listName, setListName] = useState('');
    - 변경되는 상태 관리를 위해 useState를 사용한다.
    - listName에 입력된 값을 업데이트 하는 함수로 setListName을 사용하고,
    - 초기 값은 빈 문자열('')로한다.
  • form onSubmit={handleSubmit}
    form태그에 onSubmit속성으로 폼이 제출될 때 실행할 함수 handleSubmit을 지정한다.
  • const handleSubmit = (event) => {...}
    - event.preventDefault() : 폼을 제출하면 페이지가 다시 로드되는 현상 방지
    - trim : trim 메서드는 양쪽 끝 공백을 제거할때 사용한다. listName이 공백이 아니면 if문을 실행한다. (그냥 listName !== ''으로 작성하면 공백이 리스트로 등록된다)
    - setList([...list, listName]) : 새로 입력한 리스트 listName을 기존 리스트인 list 배열에 추가하기 위해 전개연산자 ...을 사용해서 새 배열을 만든다. setList함수를 사용해list(기존 list + 추가한 리스트 listName)를 새 배열로 업데이트한다.
    - setListName(''); 리스트를 입력하면 setListName을 빈 문자열('')로 설정해서 필드가 비워지도록 한다.
const [list, setList] = useState([]);
const [listName, setListName] = useState('');
const handleSubmit = (event) => {
  event.preventDefault();
  if (listName.trim() !== '') {
    setList([...list, listName]);
    setListName('');
  }
};
// todoCreate.js
<form onSubmit={handleSubmit}>
  <div className={'createArea'}>
    <input type='text' name='list' className={'inputText'} placeholder={'Entering to-do list~!'} value={listName} onChange={(event) => 
  setListName(event.target.value)}></input>
    <button className={'btnEnter'}><span className={'arrowIcon'}></span>Enter</button>
  </div>
</form>

리스트 생성

  • {list.map((item, index) => ... )} : list배열을 순회하면서 각 리스트 아이템을 li로 반환한다.
    - list : const [list, setList] = useState([])의 list를 참조한다. useState([])는 초기값으로 빈 배열을 설정한 것이고, 즉 list는 이곳에서 만들어진(?) 배열을 참조하는 것이다.
    - map((item, index) : 여기서 첫 번째 매개변수 item은 현재 순회중인 요소이고, 두 번째 매개변수 index는 현재 요소의 인덱스를 의미한다.
  • li의 key 값으로 index를 받는다.
// todoList.js
<div className={'todoListBox'}>
  <ul className={'todoList'}>
    {list.map((item, index) => (
      <li key={index} className={'todoItem'}>
        <label>
          <input type="checkbox"/>
          <span className={'checkIcon'}></span>
          <span className={'labelText'}>{item}</span>
        </label>
        <button type={'button'} className={'btnDel'}>삭제</button>
      </li>
    ))}
  </ul>
  <TodoCreate />
</div>

3. 삭제 - 리스트 삭제하기

  • .btnDel onClick : 삭제버튼 클릭 시 해당 리스트 삭제를 위해 버튼에 onClick 이벤트로 handleDelete를 호출시킨다.
  • handleDelete : handleDeleteindex를 매개변수로 받는다.
  • const newList = [...list] : 기존 list 배열을 복사해서 newList를 만든다. (splice 메서드가 원본 배열을 직접 수정하기 때문에)
  • newList.splice(index, 1) : splice 메서드의 첫 번째 매개변수로 수정할 요소의 index 값을 받고, 두 번째 매개변수로 삭제할 요소의 개수를 지정한다. 즉 newList배열에서 해당되는 index 위치의 아이템을 1개 삭제한다.
  • setList(newList) : setList(newList)를 호출하여 list를 업데이트 한다. 이때 컴포넌트가 재렌더링 되면서 삭제된 아이템이 반영된다.
const handleDelete = (index) => {
  const newList = [...list];
  newList.splice(index, 1);
  setList(newList);
};
// todoList.js
<div className={'todoListBox'}>
  <ul className={'todoList'}>
    {list.map((item, index) => (
      <li key={index} className={'todoItem'}>
        <label>
          <input type="checkbox"/>
          <span className={'checkIcon'}></span>
          <span className={'labelText'}>{item}</span>
        </label>
        <button type={'button'} className={'btnDel'} onClick={() => handleDelete(index)}>삭제</button>
      </li>
    ))}
  </ul>
  <TodoCreate />
</div>

+) 컴포넌트 연결

todoList.js와 todoCreate.js를 연결한다. 사실 처음에 연결이 생각처럼 잘 안돼서 한 파일에 작성했다가 나중에 고쳤다.

  • todoList.js에서 변수를 정의했으므로 이것을 todoCreate.js에 받아와야한다.
  • 수정중 중괄호 없이 (listName, setListName, handleSubmit)로 지정해서 value가 제대로 안나왔었는데 (ex. [object Object]) 이는 TodoCreate의 매개변수로 listName, setListName, handleSubmit 을 개별적으로 전달하지 않고, 객체 형식으로 전달해서 발생된 문제라는 것을 알게됐다.
  • 그래서 중괄호를 사용해 ({listName, setListName, handleSubmit})으로 수정해서 개별적으로 값을 받아오도록 했다.
  • <TodoCreate /> 또한 <TodoCreate listName={listName} setListName={setListName} handleSubmit={handleSubmit} /> 로 정의한다.
// todoCreate.js
function TodoCreate({listName, setListName, handleSubmit}) {...}
// todoList.js
function TodoList() {
  
  const [listName, setListName] = useState('');
  const [list, setList] = useState([]);
  const handleSubmit = (event) => {
    event.preventDefault();
    if (listName.trim() !== '') {
      setList([...list, listName]);
      setListName('');
    }
  };
  const handleDelete = (index) => {
    const newList = [...list];
    newList.splice(index, 1);
    setList(newList);
  };

  return (
    	...
    	<TodoCreate listName={listName} setListName={setListName} handleSubmit={handleSubmit} />
	)
}

👏🏻 마무리

🐱: 분명 초간단인데... 초간단하게 하진 않았다.. 찰떡같은 설명을 할 수 있는 그날까지 가보자고

profile
Markup Developer 💫

2개의 댓글

comment-user-thumbnail
2023년 6월 14일

항상 만들다 실패하는 TodoList..
상세한 설명 감사합니다. 저도 다시 도전해봐야겠어요!

1개의 답글