상세 컨텐츠

본문 제목

[리액터 스타터2] project 2 [할 일 관리] 앱 만들기 1

23-24/React.js 2

by YUZ 유즈 2023. 12. 1. 10:00

본문

728x90

 

1. 프로젝트 준비하기

 

<요구사항 분석하기>

[할 일 관리] 앱의 최종 구현 모습

 

  • 기능
    1. 오늘의 날짜를 요일, 월, 일, 연도순으로 표시한다. 
    2. 할 일(Todo)을 작성하는 입력 폼이 있고, <추 가> 버튼을 클릭하면 할 일 아이템을 생성한다.
    3. [할 일 관리] 앱은 생성한 아이템을 페이지 하단에 리스트로 표시하는데, 키워드 검색으로 원하는 할 일만 추출할 수 있다. 
    4. 리스트로 표시하는 낱낱의 할 일 아이템은 일을 마쳤는지 여부를 표시하는 체크박스, 아이템 이름, 등록 날짜, 그리고 <삭제> 버튼으로 이루어져 있다. 

 

컴포넌트로 본 [할 일 관리] 앱

 

  • [할 일 관리] 앱의 UI 요소의 컴포넌트
    • Header: 오늘의 날짜를 표시 형식에 맞게 보여 준다.
    • TodoEditor: 새로운 할 일 아이템을 등록한다.
    • TodoList: 검색어에 맞게 필터링된 할 일 리스트를 렌더링한다. (만약 검색 폼이 공백이면 필터링하지 않는다.)
    • TodoItem: 낱낱의 할 일 아이템에는 기본 정보 외에도 체크박스와 <삭제> 버튼이 있다. 체크박스를 클릭하면 할 일을 마쳤는지 여부가 토글되고, <삭제> 버튼을 클릭하면 해당 아이템을 삭제한다.

 

 

<리액트 앱 만들기>

1. 새로운 폴더 ‘project2’를 만든다.

2. 터미널을 열고 npx create-react-app . 명령을 입력해 리액트 앱을 생성한다.

3. 다음 4개의 불필요한 파일은 삭제한다. 

  • src/App.test.js 
  • src/logo.svg 
  • src/reportWebVitals.js
  • setupTest.js

 

  • src/App.js 수정
import "./App.css";
2
3function App() {
4  return <div className="App"></div>;
5}
6export default App;
  • src/index.js 수정
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);

 

터미널에서 npm run start를 입력해 리액트 앱을 시작한다.

 

 

 

 


2. UI 구현하기

 

<페이지 레이아웃 만들기>

[할 일 관리] 앱의 최종 모습

 

  • App.js에 <h2> 태그를 추가

좌우 여백이 넓으며 페이지의 정중앙에 자리에 위치하게 한다.

import "./App.css";

function App() {
  return (
    <div className="App">
      <h2>헬로 리액트</h2>
    </div>
  );
}
export default App;
  • index.css 수정
body {
  margin: 0px; 
}
  • App.css 수정
.App {
  max-width: 500px; 
  width: 100%; 
  margin: 0 auto; 
  box-sizing: border-box; 
  padding: 20px; 
  border: 1px solid gray; 
}

 

결과 레이아웃 화면

 

 

App에는 3개의 자식 컴포넌트 Header, TodoEditor, TodoList를 각각 세로로 배치하게 한다.

  • App.js 수정
import "./App.css";

function App() {
  return (
    <div className="App">
      <div>Header</div>
      <div>Todo Editor</div>
      <div>Todo List</div>
    </div>
  );
}
export default App;
  • App.css 수정
.App {
  (...)
  display: flex; 
  flex-direction: column; 
  gap: 30px;
}

 

렌더링 결과

 

 

<Header 컴포넌트 만들기>

 

페이지 최상단에 위치할 Header 컴포넌트를 만든다.

 

1. 우선 src에 component 폴더를 만든다.

2. Header.js를 생성한다.

 

  • src/component/Header.js 수정

Header 컴포넌트가 오늘의 날짜를 렌더링한다.

const Header = () => {
  return (
    <div className="Header">
      <h3>오늘은 📅</h3> 
      <h1>{new Date().toDateString()}</h1> 
    </div>
  );
};
export default Header;
  • src/App.js 수정

Header 컴포넌트를 페이지에 렌더링하려면 App의 자식으로 배치한다.

import "./App.css";
import Header from "./component/Header";

function App() {
  return (
    <div className="App">
      <Header />
      <div>Todo Editor</div>
      <div>Todo List</div>
    </div>
  );
}
export default App;
  • component 폴더에 Header 컴포넌트를 스타일링하기 위한 Header.css 생성
.Header h1 { 
  margin-bottom: 0px;
  color: #1f93ff;
}
  • src/component/Header.js 첫 줄에 추가

Header.css를 Header.js에서 불러온다.

import "./Header.css";

 

Header 컴포넌트의 스타일 적용

 

 

 

<TodoEditor 컴포넌트 만들기>

 

 

TodoEditor 컴포넌트의 모습

 

 

 

component 폴더에 컴포넌트와 스타일을 정의할 TodoEditor.js와 TodoEditor.css를 각각 생성한다.

 

  • src/component/TodoEditor.js에 작성
import "./TodoEditor.css";

const TodoEditor = () => {
  return <div className="TodoEditor">TodoEditor Component</div>;
};
export default TodoEditor;

 

  • src/App.js에 작성
import "./App.css";
import Header from "./component/Header";
import TodoEditor from "./component/TodoEditor";

function App() {
  return (
    <div className="App">
      <Header />
      <TodoEditor />
      <div>Todo List</div>
    </div>
  );
}
export default App;

 

TodoEditor 컴포넌트 배치

 

  • src/component/TodoEditor.js 작성

TodoEditor의 UI를 만든다.

import "./TodoEditor.css";

const TodoEditor = () => {
  return (
    <div className="TodoEditor">
      <h4>새로운 Todo 작성하기 ✏️ </h4>
      <div className="editor_wrapper">
        <input placeholder="새로운 Todo..." />
        <button>추가</button>
      </div>
    </div>
  );
};
export default TodoEditor;
  • src/component/TodoEditor.css 작성
.TodoEditor .editor_wrapper {
  width: 100%;
  display: flex;
  gap: 10px;
}

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

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

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

 

TodoEditor 컴포넌트의 스타일 지정

 

 

<TodoList, TodoItem 컴포넌트 만들기>

 

1. TodoList 컴포넌트 만들기

 

TodoList 컴포넌트의 모습

 

 

 

 

컴포넌트와 스타일을 정의하는 TodoList.js와 TodoList.css를 각각 생성한다.

 

  • src/component/TodoList.js 작성
import "./TodoList.css";

const TodoList = () => {
  return <div className="TodoList">TodoList Component</div>;
};
export default TodoList;
  • src/App.js 작성

TodoList를 App 컴포넌트의 자식으로 배치한다.

import "./App.css";
import Header from "./component/Header";
import TodoEditor from "./component/TodoEditor";
import TodoList from "./component/TodoList";

function App() {
  return (
    <div className="App">
      <Header />
      <TodoEditor />
      <TodoList />
    </div>
  );
}
export default App;

 

TodoList 컴포넌트 배치

 

 

 

TodoList 컴포넌트는 크게 할 일 아이템을 조회하는 검색 폼과 조회한 할 일 아이템을 목록 형태로 보여주는 리스트 두 부분으로 구성한다.

 

TodoList 컴포넌트 상단에 위치할 검색 폼부터 만들자.

 

  • src/component/TodoList.js 작성
import "./TodoList.css";

const TodoList = () => {
  return (
    <div className="TodoList">
      <h4>Todo List 🌱</h4>
      <input className="searchbar" placeholder="검색어를 입력하세요" />
    </div>
  );
};
export default TodoList;
  • src/component/TodoList.css 작성
/* 검색 폼에 스타일 적용 */
.TodoList .searchbar {
  margin-bottom: 20px;
  width: 100%;
  border: none;
  border-bottom: 1px solid rgb(220, 220, 220);
  box-sizing: border-box;
  padding-top: 15px;
  padding-bottom: 15px;
}

/* 검색 폼을 클릭했을 때의 스타일 적용 */
.TodoList .searchbar:focus {
  outline: none;
  border-bottom: 1px solid #1f93ff;
}

 

TodoList 컴포넌트의 검색 폼 스타일 지정

 

 

검색 폼에서 검색어를 입력하면 조건에 일치하는 할 일 아이템이 하단에 리스트로 출력된다. 아직 TodoItem 컴포넌트를 만들지 않았으므로 할 일 아이템을 출력하지는 못한다.

 

 

 

 

2. TodoItem 컴포넌트 만들기

 

TodoItem 컴포넌트의 모습

 

component 폴더에서 컴포넌트와 스타일을 정의하는 TodoItem.js와 TodoItem.css를 각각 생성한다.

 

  • src/component/TodoItem.js 작성
import "./TodoItem.css";

const TodoItem = () => {
  return (
    <div className="TodoItem">
      <div className="checkbox_col"> 
        <input type="checkbox" />
      </div>
      <div className="title_col">할 일</div> 
      <div className="date_col">{new Date().toLocaleDateString()}</div> 
      <div className="btn_col"> 
        <button>삭제</button>
      </div>
    </div>
  );
};
export default TodoItem;

 

 

 

 

3. TodoList에 TodoItem 컴포넌트 배치하기

 

  • TodoList.js를 수정
import TodoItem from "./TodoItem";
import "./TodoList.css";

const TodoList = () => {
  return (
    <div className="TodoList">
      <h4>Todo List 🌱</h4>
      <input className="searchbar" placeholder="검색어를 입력하세요" />
      <div className="list_wrapper"> 
        <TodoItem />
        <TodoItem />
        <TodoItem />
      </div>
    </div>
  );
};
export default TodoList;

TodoItem 컴포넌트 3개 배치

 

 
아이템 사이에 적절한 간격을 주려면 부모 컴포넌트인 TodoList에서 여러 개의 TodoItem을 감싸고 있는 ‘list_wrapper’ 요소에 스타일링을 적용해야한다.
  • TodoList.css 추가
(...)
TodoList .list_wrapper {
  display: flex;
  flex-direction: column;
  gap: 20px;
}
  • TodoItem.css 수정
/* 할 일 아이템 박스 스타일 적용 */
.TodoItem {
  display: flex;
  align-items: center;
  gap: 20px;
  padding-bottom: 20px;
  border-bottom: 1px solid rgb(240, 240, 240);
}

/* 체크박스를 감싼 박스에 스타일 적용 */
.TodoItem .checkbox_col {
  width: 20px;
}

/* 할 일 텍스트를 감싼 박스에 스타일 적용 */
.TodoItem .title_col {
  flex: 1;
}

/* 할 일 아이템 등록 시간을 감싼 박스에 스타일 적용 */
.TodoItem .date_col {
  font-size: 14px;
  color: gray;
}

/* 삭제 버튼에 스타일 적용 */
.TodoItem .btn_col button {
  cursor: pointer;
  color: gray;
  font-size: 14px;
  border: none;
  border-radius: 5px;
  padding: 5px;
}
  • App.css의 border 속성 제거 또는 주석 처리
.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;
}

 

[할 일 관리] 앱의 최종 UI

 

 


3. 기능 구현 준비하기

 

먼저 컴포넌트별로 어떤 기능을 구현해야 하는지 다시 살펴 보자.
  • App 컴포넌트: 할 일 데이터 관리하기
  • Header 컴포넌트: 오늘의 날짜 표시
  • TodoEditor 컴포넌트: 새로운 할 일 아이템 생성
  • TodoList 컴포넌트: 검색에 따라 필터링된 할 일 아이템 렌더링
  • TodoItem 컴포넌트: 할 일 아이템의 수정 및 삭제

 

데이터를 다루는 4개의 기능, 즉 추가(Create), 조회(Read), 수정(Update), 삭제(Delete) 기능을 앞글자만 따서 CRUD라고 한다. CRUD는 데이터 처리의 기본 기능으로, 웹 서비스라면 기본적으로 갖추고 있어야 합니다. 
  • Create: 할 일 아이템 생성 
  • Read: 할 일 아이템 렌더링 
  • Update: 할 일 아이템 수정 
  • Delete: 할 일 아이템 삭제 

 

<기초 데이터 설정하기>

아이템부터 생성하자.

 

  • App.js 수정
import { useState } from "react";
(..)

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

  return (
    (...)
  );
}
export default App;

 

함수 useState는 리액트 훅으로 react 라이브러리에서 불러온다. 리액트에서는 보통 리스트 형태의 데이터를 보관할 때 배열을 이용한다.
State 변수 todo는 [할 일 관리] 앱에서 데이터를 저장하는 배열이면서 동시에 일종의 데이터베이스 역할을 수행한다.

 

 
 

<데이터 모델링하기>

 

이렇게 현실의 사물이나 개념을 프로그래밍 언어의 객체와 같은 자료구조로 표현하는 행위를 ‘데이터 모델링’이라 한다.

 

 

[할 일 관리] 앱의 할 일 아이템 데이터 모델링

 

 

하나의 할 일 아이템에는 일의 완료 여부, 일의 종류, 생성 날짜 등 3가지 정보가 담겨 있다. 세 요소는 각각 isDone, content, createdDate라는 별도의 이름으로 구분한다.

 

<목 데이터 설정하기>

목(Mock) 데이터란 모조품 데이터라는 뜻이다. 기능을 완벽히 구현하지 않은 상태에서 테스트를 목적으로 사용하는 데이터이다.

 

  •  App.js 수정
(...)
const mockTodo = [ 
  {
    id: 0,
    isDone: false,
    content: "React 공부하기",
    createdDate: new Date().getTime(),
  },
  {
    id: 1,
    isDone: false,
    content: "빨래 널기",
    createdDate: new Date().getTime(),
  },
  {
    id: 2,
    isDone: false,
    content: "노래 연습하기",
    createdDate: new Date().getTime(),
  },
];b

function App() {
  const [todo, setTodo] = useState(mockTodo); 
b
  return (
    (...)
  );
}
export default App;
아직 TodoList 컴포넌트에 목 데이터를 전달하지 않았기 때문에 데이터를 페이지에 렌더링하지는 않는다. 따라서 지금은 리액트 개발자 도구를 이용해 데이터가 잘 설정되는지 확인해야 한다.
개발자 도구에서 [Components] 탭을 연다. App 컴포넌트의 hooks 항목에서 State의 값을 확인한다.
 

 

 

 


4. Create: 할 일 추가하기

 

CRUD의 첫 번째 기능인 Create를 구현하자.

 

<기능 흐름 살펴보기>

[할 일 관리] 앱의 Create 기능 흐름

 

 

 

<아이템 추가 함수 만들기>

TodoEditor 컴포넌트에서 <추가> 버튼을 클릭하면 App에 사용자가 입력한 할 일 데이터를 전달하고 추가 이벤트가 발생했음을 알려야 한다.

 

  • App 컴포넌트에서 새 할 일 아이템을 추가하는 함수 onCreate 생성
(...)
function App() {
  const [todo, setTodo] = useState(mockTodo);

  const onCreate = (content) => { 
    const newItem = {
      id: 0,
      content,
      isDone: false,
      createdDate: new Date().getTime(),
    };
    setTodo([newItem, ...todo]); 
  };

  return (
    (...)
  );
}
export default App;

 

그런데 지금의 함수 onCreate에는 한 가지 문제점이 있다. 모든 아이템은 고유한 id를 가져야 하는데, 새롭게 추가할 아이템의 id가 모두 0으로 고정되기 때문이다. 그럼 아이템을 추가할 때마다 중복 id가 만들어져 문제가 발생한다.
Ref 객체를 사용하면 이 문제를 간단히 해결할 수 있다. Ref 객체는 전에 살펴본 적이 있는데, 리액트 훅인 함수 useRef로 생성한다.

 

 

  • App.js에서 새로운 Ref 객체를 생성
import { useState, useRef } from "react";
(...)

function App() {
  const idRef = useRef(3);
  (...)
}
export default App;
초깃값이 3인 Ref 객체를 생성해 idRef에 저장한다.
 
  • 다음으로 idRef를 이용해 아이템을 생성할 때마다 id가 1씩 늘어나도록 수정
(...)

function App() {
  (...)
  const idRef = useRef(3);

  const onCreate = (content) => {
    const newItem = {
      id: idRef.current, 
      content,
      isDone: false,
      createdDate: new Date().getTime(),
    };
    setTodo([newItem, ...todo]);
    idRef.current += 1; 
  };

  return (
    (...)
  );
}
export default App;

 

  • App 컴포넌트에서 할 일 아이템을 생성하는 함수 onCreate 작성
(...)
function App() {
  (...)
  return (
    <div className="App">
      <Header />
      <TodoEditor onCreate={onCreate} />
      <TodoList />
    </div>
  );
}
export default App;

 

 

 

<아이템 추가 함수 호출하기>

사용자가 할 일 입력 폼에서 아이템을 입력하고 <추가> 버튼을 클릭한다.
그러면 TodoEditor 컴포넌트는 새 할 일을 생성하기 위해 App에서 Props로 받은 함수 onCreate를 호출하고 현재 사용자가 작성한 할 일을 인수로 전달한다. 

 

  • src/component/TodoEditor.js 수정
import "./TodoEditor.css";

const TodoEditor = ({ onCreate }) => { 
  return (
    <div className="TodoEditor">
      <h4>새로운 Todo 작성하기 🔏 </h4>
      <div className="editor_wrapper">
        <input placeholder="새로운 Todo..." />
        <button>추가</button>
      </div>
    </div>
  );
};
export default TodoEditor;

 

다음으로 TodoEditor 컴포넌트의 할 일 입력 폼에서 사용자가 입력하는 새 할 일 데이터를 저장할 State를 만든다.

  • src/component/TodoEditor.js 수정
import { useState } from "react";
import "./TodoEditor.css";

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

  return (
    <div className="TodoEditor">
      <h4>새로운 Todo 작성하기 ✏ </h4>
      <div className="editor_wrapper">
        <input ③
          value={content}
          onChange={onChangeContent}
          placeholder="새로운 Todo..."
        />
        <button>추가</button>
      </div>
    </div>
  );
};
export default TodoEditor;

 

[Components] 탭에서 App의 TodoEditor를 클릭해 State(content)에 사용자가 지금 입력한 내용이 제대로 반영되는지 확인한다.

 

 

다음으로 <추가> 버튼을 클릭하면, 함수 onCreate를 호출하는 버튼 클릭 이벤트 핸들러를 만든다.

  • src/component/TodoEditor.js 작성
(...)
const TodoEditor = ({ onCreate }) => {
  (...)
  const onSubmit = () => { 
    onCreate(content);
  };

  return (
    <div className="TodoEditor">
      <h4>새로운 Todo 작성하기 ✏ </h4>
      <div className="editor_wrapper">
        <input
          value={content}
          onChange={onChangeContent}
          placeholder="새로운 Todo..."
        />
        <button onClick={onSubmit}>추가</button> 
      </div>
    </div>
  );
};
export default TodoEditor;
 
아직 App 컴포넌트의 todo 값을 페이지에 렌더링하는 Read 기능은 개발하지 않았기 때문에 개발자 도구의 [Components] 탭에서 직접 확인해야 한다.
새 할 일 아이템으로 ‘독서하기’를 입력하고 <추가> 버튼을 클릭한다.

 

 

 

<Create 완성도 높이기>

 

1. 빈 입력 방지하기

 

할 일 입력 폼을 관리할 Ref 객체를 하나 만들고, 함수 onSubmit에서 content 값이 비어 있으면 입력 폼에 포커스를 구현하는 방식이다.

 

  • src/component/TodoEditor.js 수정
import { useState, useRef } from "react";
import "./TodoEditor.css";

const TodoEditor = ({ onCreate }) => {
  const [content, setContent] = useState("");
  const inputRef = useRef(); 

  (...)
  const onSubmit = () => {
    if (!content) { 
      inputRef.current.focus();
      return;
    }
    onCreate(content);
  };

  return (
    <div className="TodoEditor">
      <h4>새로운 Todo 작성하기 ✏ </h4>
      <div className="editor_wrapper">
        <input
          ref={inputRef} 
          value={content}
          onChange={onChangeContent}
          placeholder="새로운 Todo..."
        />
        <button onClick={onSubmit}>추가</button>
      </div>
    </div>
  );
};
export default TodoEditor;

 

 

 

2. 아이템 추가 후 입력 폼 초기화하기

  • src/component/TodoEditor.js 수정
(...)
const TodoEditor = ({ onCreate }) => {
  (...)
  const onSubmit = () => {
    if (!content) {
      inputRef.current.focus();
      return;
    }
    onCreate(content);
    setContent(""); 
  };
  (...)
};
export default TodoEditor;

 

 

 

3. <Enter> 키를 눌러 아이템 추가하기

  • src/component/TodoEditor.js 수정
(...)
const TodoEditor = ({ onCreate }) => {
  (...)
  const onKeyDown = (e) => { 
    if (e.keyCode === 13) {
      onSubmit();
    }
  };
  return(
    <div className="TodoEditor">
      <h4>새로운 Todo 작성하기 ✏ </h4>
      <div className="editor_wrapper">
        <input
          ref={inputRef}
          value={content}
          onChange={onChangeContent}
          onKeyDown={onKeyDown} 
          placeholder="새로운 Todo..."
        />
        <button onClick={onSubmit}>추가</button>
      </div>
    </div>
  )
};
export default TodoEditor;

 

 

 

 

 


출처: 이정환, 『한 입 크기로 잘라먹는 리액트』, 프로그래밍인사이트(2023)

Editor: yunseul

 

Quiz

1(2,3,4,5). 데이터를 다루는 4개의 기능, 즉 (    ), (    ), (    ), (    ) 기능을 앞글자만 따서 (    )라고 한다.

6. 현실의 사물이나 개념을 프로그래밍 언어의 객체와 같은 자료구조로 표현하는 행위를 (    )이라 한다.

7. (    ) 데이터란 모조품 데이터라는 뜻이다.

8. (    )를 사용하면 아이템을 추가할 때마다 중복 id가 만들어져 문제를 해결할 수 있다.


 

1. 위에 프로젝트에서 App.js에 새로운 Ref 객체를 생성하도록 코드를 다시 작성해 보자. 초깃값이 3인 Ref 객체를 생성해 idRef에 저장하도록 해보자.

 

2. 아이템 추가 후 입력 폼 초기화하는 코드를 다시 작성해 보자.

 

 

 


 

 

 

추가(Create), 조회(Read), 수정(Update), 삭제(Delete), CRUD / 데이터 모델링 / 목(Mock) / Ref 객체   

import { useState, useRef } from "react";
(...)

function App() {
  const idRef = useRef(3);
  (...)
}
export default App;
(...)
const TodoEditor = ({ onCreate }) => {
  (...)
  const onSubmit = () => {
    if (!content) {
      inputRef.current.focus();
      return;
    }
    onCreate(content);
    setContent(""); 
  };
  (...)
};
export default TodoEditor;

 

 

 


 

출처: 이정환, 『한 입 크기로 잘라먹는 리액트』, 프로그래밍인사이트(2023), p2. 할 일 관리 앱 만들기 0~4장.

Editor: yunseul

 

728x90

관련글 더보기