250x250
Notice
Recent Posts
Recent Comments
Link
«   2025/07   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
Archives
Today
Total
관리 메뉴

y.developer

[TIL] Day 23 React로 TodoList 만들기 1 본문

카테고리 없음

[TIL] Day 23 React로 TodoList 만들기 1

y.developer 2023. 11. 6. 21:45
728x90

2023.11.06 월

 

ver1, ver 4

 

리액트를 활용하여 Todo List 기능을 구현했다.

input 값을 추가하고, 불러와서 사용할 때 이용하는 useState를 처음 다뤄봐서 어려웠지만,

하나하나 뜯어가며 다시 분석해 보자

 

  • 초기 화면 구성하기
  • title 값 저장하기
  • content 값 저장하기
  • todos 값 저장하기

 

 

초기 화면 구성하기

최종적으로 구축하려고 하는 디자인을 잡아놓고 들어가는 것이 훨씬 작업에 수월했다.

기능이 작동할 것도 예상해서 미리 구성해 놓는다면 기능을 완성시켰을 때 바로 확인을 할 수 있고,

기능 구현에 실패하더라도 바로 시각적으로 확인 가능하고 다음 스텝의 그림을 그려볼 수 있다.

 

 

App.jsx

import './App.css';
import React from "react";

function App() {
  return (
    <div className='outline'>
      <div className='app_box'>
        <header>
          <div className='header_top'>
            <h1>My Todo List</h1>
            <p>React</p>
          </div>
          <form className='header_box'>
            <div className='header_input_box_outline'>
              <div className='header_input_box title'>
                <p>제목</p>
                <input/>
              </div>
              <div className='header_input_box content'>
                <p>내용</p>
                <input/>
              </div>
            </div>
            <div>
              <button className='add_btn'>추가하기</button>
            </div>
          </form>
        </header>
        <main>
          <div>
            <div className='working'>
              <h2>Working.. 🔥</h2>
              <div className='working_card_box'>
                <div className='card'>
                  <h3>title</h3>
                  <p>content</p>
                  <div className='card_btn_box'>
                    <button className='delete_btn'>삭제하기</button>
                    <button className='complete_btn'>완료</button>
                  </div>
                </div>
              </div>
            </div>
            <div className='done'>
              <h2>Done..! 🎉</h2>
              <div className='done_card_box'>
                <div className='card'>
                  <h3>title</h3>
                  <p>content</p>
                  <div className='card_btn_box'>
                    <button className='delete_btn'>삭제하기</button>
                    <button className='complete_btn'>취소</button>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </main>
      </div>
    </div>
  )
}

export default App;

 

이 코드는 ver1에서 컴포넌트를 나누기 전 하나의 파일 안에 전부 작성한 모습이다.

쉽게 말하자면 초안이라고 할 수 있다.

추후에 컴포넌트를 분리할 것이다.

이 과정을 생략하고 처음부터 컴포넌트를 분리해서 작성을 시작해도 좋다. (2, 3번째 글에 기재된 코드 참고!)

 

먼저 태그를 활용해서 구역을 나눠주고, className를 선언해 준다.

구상한 디자인에 가깝게 작성하고, CSS에서 위치를 잡기 편하게 구역을 설정한다.

추후에 컴포넌트를 나눌 때는 다 분리가 되겠지만, 초기에는 전체적인 구도를 보기 위해서 한 곳에 작성했다.

 

크게 input 값과 추가하기 버튼을 컨트롤할 header,

card를 뿌려줄 main으로 구성했다.

 

상단에서 적용해야 할  React와 CSS 파일을 불러온다.

참고로 js와 jsx의 기능적인 측면의 차이는 없다.

단지 협업하는 사람들과 js문법을 사용했는지, jsx문법을 사용했는지 구별하기 위한 소통 방법이다.

 

 

App.css

/* 세팅 */
p,
h1,
h2,
h3,
input,
button{
  margin: 0px;
  padding: 0px;
}

/* 아웃라인 */
.outline {
  display: flex;
  justify-content: center;
  
  width: 100%;
  height: 100%;
}

/* app 공간 */
.app_box {
  display: flex;
  flex-direction: column;

  position: relative;

  width: 1200px;
  height: 800px;
  margin-top: 10px; 
}

/* header */
header {
  width: 100%;
  height: 130px;
}

header h1 {
  font-size: 24px;
}

.header_top {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;

  margin: 10px 15px;
}

.header_box {
  display: flex;
  justify-content: space-between;
  align-items: center;

  width: 100%;
  height: 100px;
  margin-top: 20px;

  background-color: rgb(238, 238, 238);
  border-radius: 10px;
}

.header_input_box_outline {
  display: flex;
  flex-direction: row;

  margin: 0 20px;
}

.header_input_box {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 10px;
}

.header_input_box.content {
  margin-left: 30px;
}

.header_input_box input {
  width: 250px;
  height: 35px;

  border: none;
  border-radius: 8px;
}

.add_btn {
  width: 130px;
  height: 35px;
  margin-right: 20px;
  padding: 5px;

  color: aliceblue;
  border: none;
  border-radius: 8px;
  background-color: rgb(36, 126, 127);
}

/* footer */
footer {
  width: 100%;
  height: 100%;
  margin-top: 40px;
}

.card_top {
  margin: 30px 15px;
}

.card_box {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  gap: 20px;

  margin-top: 20px;
}

.card {
  display: flex;
  justify-content: center;
  flex-direction: column;
  gap: 20px;

  width: 270px;
  height: 150px;
  padding: 10px 20px;

  border-radius: 10px;
  border: 5px solid rgb(21, 117, 118);
}

.card_btn_box {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-direction: row;
  gap: 15px;
}

.card_btn_box button {
  width: 130px;
  height: 40px;

  background: transparent;
  border-radius: 10px;
}

.delete_btn {
  border: 3px solid rgb(246, 83, 63);
}

.complete_btn {
  border: 3px solid rgb(16, 106, 0);  
}

 

ver4 기준 App.css 코드

위의 ver1 App.jsx에서의 ClassName과 상이할 수 있음

 

설정한 className을 가지고 CSS를 적용한다.

 

특히 card_box의 부분이 중요하다.

display: flex; 로 flex를 활성화하고
flex-direction: row; 카드는 가로로 배치
flex-wrap: wrap; 화면 오른쪽 끝부분에 다 담지 못할 경우 밑으로 오도록 설정
gap: 20px; 카드 사이의 간격을 설정합니다.
margin-top: 20px; 그리고 마진값은 여분의 공간을 확보하기 위해서 설정.

 

 

 

App.jsx

import './App.css';
import React from "react";
import { useState } from "react";

function App() {
  const [title, setTitle] = useState("");
  const [content, setContent] = useState("");
  const [todo, setTodo] = useState([]);
  const [doneTodo, setDoneTodo] = useState([]);

  // 추가하기 버튼 클릭
  const submitClickHandler = (e) => {
    e.preventDefault();

    const newTodo = {
      id: Date.now(),
      title: title,
      content: content,
      isDone: false,
    }

    if (title === "" || content === "") {
      alert("제목 및 내용을 입력해 주세요.");
    } else {
      setTodo([...todo, newTodo]);
    }

    setTitle("");
    setContent("");
  };

  // title input 값
  const titleChangeHandler = e => {
    setTitle(e.target.value)
  };

  // content input 값
  const contentChangeHandler = e => {
    setContent(e.target.value)
  };
  
  

  return (
    <div className='outline'>
      <div className='app_box'>
        <header>
          <div className='header_top'>
            <h1>My Todo List</h1>
            <p>React</p>
          </div>
          <form className='header_box'>
            <div className='header_input_box_outline'>
              <div className='header_input_box title'>
                <p>제목</p>
                <input
                  type='text'
                  value={title}
                  onChange={titleChangeHandler}
                />
              </div>
              <div className='header_input_box content'>
                <p>내용</p>
                <input
                  type='text'
                  value={content}
                  onChange={contentChangeHandler}
                />
              </div>
            </div>
            <div>
              <button className='add_btn' onClick={submitClickHandler}>추가하기</button>
            </div>
          </form>
        </header>
        .
        .
        .

이 또한 ver1 에서 작성한 코드

수정 및 최종 적용한 코드를 보고 싶다면 2, 3번째 글을 참고.

초안을 작성했을 때에는 uuid대신 Date.now를 통해서 id값을 부여했다.

const [doneTodo, setDoneTodo] = useState([]) 라는 다소 불필요한 과정이 있었다.

아래 내용은 초안을 바탕으로 한다.

ver4를 기반으로한 코드는 2번째 글을 참고하자.

컴포넌트를 분리한 후 index.jsx에서 작성했다.

 

title, constent 값 저장하기

이번 작업에서 핵심으로 사용할 리액트 hook인 useState를 불러온다.

App함수 안쪽에 useState 함수를 활용해서 titlecontent 그리고 todo, donetodo를 저장할 변수를 선언한다.

그리고 titleChangeHandlercontentChangeHandler 함수도 선언하고 각 input 태그에 연결한다.

submitClickHandler를 선언해서 추가하기 버튼에 연결한다. 

 

title 값을 보면 e.target.value 값이 title에 세팅된다.

content도 똑같이 작동한다.

 

추가하기 버튼이 감싸고 있는 태그를 form으로 선언해서 버튼을 누르지 않고 엔터만 치더라도 작동한다.

그러나 추가적인 설정을 해주지 않으면 새로고침이 되는 문제가 있다.

submitClickHandler 함수에 매개변수를 넣고 preventDefault()를 활용해서 자동 새로고침을 방지한다.

 

 

todo 값 저장하기

newTodo를 선언해서 새롭게 생성될 todo의 기본 형태를 잡아준다.

id는 독립적인 값이 들어가도록 Date.now()를 활용했다.

 

if문을 추가하여 input 창이 모두 작성이 되지 않으면 alert 창이 뜨도록 했다.

 

setTodo에 Spread Operator(스프레드 오퍼레이터)(...)를 활용해서 기존 값을 표현했고,

이후 새로운 newTodo가 뒤로 오도록 설정했다.

 

버튼이 활성화된 후 input창을 비우기 위해서

setTitle과 setContent에 빈칸을 설정해 줬다.

 

이로써 input 값을 받고 doto에 원하는 형태로 저장을 완료했다.

이제 다음 시간에 저장한 값을 추출하여 카드 형태로 나타내는 것을 정리해 보자.

 

 

 

Pro Tips

1. JSX 문법이란 무엇일까요? 

Javascript에 XML을 추가한 확장한 문법입니다.
- 리액트로 프로젝트를 개발할 때 사용됩니다.
- 브라우저에서 실행하기 전에 바벨을 사용하여 일반 자바스크립트 형태의 코드로 변환됩니다.
- 하나의 파일에 자바스크립트와 HTML을 동시에 작성하여 편리합니다.
- 자바스크립트에서 HTML을 작성하듯이 하기 때문에 가독성이 높고 작성하기 쉽습니다.

 

JSX 스타일링
 - JSX에서 자바스크립트 문법을 쓰려면 {}를 써야 하기 때문에, 스타일을 적용할 때에도 객체 형태로 넣어 주어야 한다.
 - 카멜 표기법으로 작성해야 한다. (font-size => fontSize)

class 대신 className
 - 일반 HTML에서 CSS 클래스를 사용할 때에는 class 라는 속성을 사용한다.
 - JSX에서는 class가 아닌 className 을 사용한다.

주석
 - JSX 내에서 {/*…*/} 와 같은 형식을 사용한다.

 

선언형 화면
 - 개발자가 JSX를 작성하기만 하면, 리액트 엔진은 JSX를 기존 자바스크립트로 해석하여 준다.
이를 '선언형 화면' 기술이라고 한다. 앞으로 사용할 react의 기본문법이니 숙지하고 가도록 하자.

 

 

 

2. 사용자가 입력하는 값, 또는 이미 입력된 값, 투두의 타이들과 같은 애플리케이션의 상태를 관리하기 위해 리액트의 어떤 기능을 사용하셨나요?

useState Hook

컴포넌트의 상태를 간편하게 생성하고 업데이트해 주는 도구를 제공해 줍니다.
리액트 16.8 이전 버전에서는 함수형 컴포넌트에서는 상태를 관리할 수 없었지만,
리액트 16.8 에서 Hooks 라는 기능이 도입되면서 함수형 컴포넌트에서도 상태를 관리할 수 있게 되었습니다.

 

 

 

3. 애플리케이션의  상태 값들을 컴포넌트 간 어떤 방식으로 공유하셨나요?
props, 상태 리프팅

두 컴포넌트에서 state를 제거하고 가장 가까운 공통 부모로 이동한 다음 props를 통해 전달합니다.

이를 상태 리프팅이라고 합니다.

 

 

 

4. 기능 구현을 위해 불변성 유지가 필요한 부분이 있었다면 하나만 설명해 주세요.
title과 content 내용은 독립적인 요소로 각 카드를 생성하기 위해서 불변성이 지켜져야 합니다.

 

 

5. 반복되는 컴포넌트를 파악하고 재사용할 수 있는 컴포넌트로 분리해 보셨나요? 그렇다면 어떠한 이점이 있었나요?
App에서 작동하는 Header, MainLogic, Todo 컴포넌트를 분리하였습니다.
코드를 수정할 때 가독성을 높일 수 있고, 반복적으로 사용되는 부분에서 이점이 있습니다.
코드를 작성할 때도 도움이 되지만, 유지 보수할 때 큰 장점이 있다고 생각합니다.

 

 

 

6. 리드미(README)란?

README는 쉽게 말해서 프로젝트 소개글이라고 할 수 있습니다.

일반적으로 디렉토리나 압축 파일에 포함된 기타 파일에 대한 정보를 포함하고 있으며, 일반적으로 소프트웨어와 함께 배포됩니다.

또한 현재 Git과 같은 코드 저장소에서도 해당 파일을 기본값으로 생성되게 하여 해당 저장소에 대한 설명을 기입하도록 유도하고 있습니다.

다른 확장자를 사용할 수도 있겠으나 기본적인 README의 확장자는 md인데 이것은 Markdown(마크다운)의 약자입니다. 

 

 


하루를 마치며

리액트의 기능을 십분 발휘하여 처음 만들어본 개인 프로젝트였다. input 값을 하나씩 받아서 저장하는 방법은 쉬웠지만, 이를 todo에 저장하는 방법을 알기까지 시간이 오래 걸렸고 이해하기에도 쉽지 않았다. 처음에는 title값을 다시 titleArray를 만들고 이를 활용해서 todo를 만들어야 하는 줄 알고 멀리 돌아갔었다. setTodo를 하는 과정에서도 스프레드 오퍼레이터를 사용하는 방법이 익숙하지 않았지만, 이제는 완벽하게 이해가 되었다.

만약 카드의 순서를 바꿔서 새로운 카드가 앞쪽에 배치되도록 하려면 setTodo([newTodo, ...todo]);로 설정하면 된다는 사실도 알았다.

전체적으로 리액트가 돌아가는 로직을 이해하고 활용할 수 있게 되었다는 게 신기하다.

 

 

오늘의 한 줄
오늘의
Todo List
정복하기
728x90