상세 컨텐츠

본문 제목

[React.js 1팀] Project2 [할 일 관리] 앱 만들기

24-25/React.js 1

by mingging17 2024. 12. 27. 10:00

본문

728x90

1. 프로젝트 준비하기

프로젝트 구현에 앞서 구현에 필요한 준비 작업을 진행하자. 프로젝트 1을 준비할 때처럼 먼저 앱의 요구사항을 분석하고, 이를 토대로 필요한 기능을 하나씩 구현하겠다.

 

 

1-(1) 요구사항 분석하기

최종 구현 페이지에서  [할 일 관리] 앱에는 4가지의 기능이 있다.

 

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

요구사항 분석에 맞게 페이지의 각 UI 요소를 역할에 따라 구분할 수 있도록 컴포넌트 단위로 나누겠다.

위 사진은 [할 일 관리] 앱의 UI 요소를 컴포넌트로 구분한 사진이다. 

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

 

1-(2) 리액트 앱 만들기

프로젝트 2를 위한 새 리액트 앱을 생성하고 불필요한 파일과 코드는 삭제한다. 이 과정은 [카운터] 앱을 만들 때와 동일하다.

 

 

 

2. UI 구현하기

프로젝트 준비를 모두 마쳤다면 UI를 구현하겠다. [할 일 관리] 앱의 UI 구현은 페이지의 전체 레이아웃 먼저 만들고, 세부 요소는 순서에 따라 차근차근 만들 예정이다. 

 

 

2-(1) 페이지 레이아웃 만들기

위 그림은 [할 일 관리] 앱의 최종 형태를 UI 관점에서 보여준다. [할 일 관리] 앱의 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: 0auto;
  box-sizing: border-box;
  padding: 20px;
  border: 1px solid gray;
}

 

 

App에는 3개의 자식 컴포넌트 Header, TodoEditor, TodoList를 각각 세로로 배치할 예정이다. App에 배치할 자식 컴포넌트를 아직 구현하지 않았으므로, 임시 요소를 만들어 대신 배치하겠다. 

 

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 컴포넌트의 display 속성을 이용하면, 요소의 배치 간격을 좀 더 보기 좋게 만들 수 있다.

 

App.css를 수정한다.

.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;
}

 

수정 후 렌더링 결과를 확인하면 요소의 간격이 적절히 떨어져 있음을 알 수 있다. 이렇게 App 컴포넌트의 스타일링을 모두 마무리했다.

 

 

2-(2) Header 컴포넌트 만들기

이번에는 페이지 최상단에 위치할 Header 컴포넌트를 만들겠다.

 

src에 이 프로젝트의 컴포넌트 파일을 한곳에 모아 둘 component 폴더 생성 후 component 폴더에 Header.js 생성한다.

 

-  App.js

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

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

function App() {
  return(
    <div className="App">
      <Header />
      <div>Header</div>
      <div>Todo Editor</div>
      <div>Todo List</div>
    </div>
  )
}

export default App;

 

- Header.js

Header 컴포넌트가 오늘의 날짜를 렌더링하도록 작성한다.

import "./Header.css" //css스타일 적용

const Header = () => {
    return(
        <div className="Header">
            <h3>오늘은 📆</h3>
            <h1>{new Date().toDateString()}</h1>
        </div>
    );
};
export default Header;

 

- Header.css

.Header h1 {
    margin-bottom: 0px;
    color: #1f93ff;
}

 

 

렌더링 결과를 확인하면 위 그림과 같다. 페이지에 렌더링 한 날짜는 당일 날짜가 표시된다.

 

 

2-(3) TodoEditor 컴포넌트 만들기

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

 

- TodoEditor.js

컴포넌트는 요소의 제목, 할 일 아이템을 생성하는 입력 폼, 클릭하면 실제 할 일 아이템을 생성하는 버튼으로 구성되어 있다.

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;

 

- 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;
}

 

  • flex를 1로 설정하여 해당 요소의 너비가 브라우저의 크기에 따라 유연하게 변경된다.
  • 입력 폼을 클릭했을 때의 스타일을 설정 -> outline 속성을 none으로 설정하여 두꺼운 경계선이 생기지 않음
  • cursor 속성을 pointer로 설정하여 버튼에 마우스 포인터를 올릴 때 모양이 손 모양으로 변경됨.

 

저장 후 렌더링 결과를 확인하면 다음과 같다.

 

2-(4) TodoList, TodoItem 컴포넌트 만들기

[할 일 관리] 앱의 TodoList에는 TodoItem 컴포넌트가 여러 개 있다.

 

TodoList 컴포넌트 만들기

- TodoList.js

import "./TodoList.css";

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

 

- 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;
}

 

저장 후 페이지에서 렌더링 결과를 확인하면 다음과 같다.

 

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

 

TodoItem 컴포넌트 만들기

- 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;
  • 할 일 아이템 가장 왼쪽에는 할 일 완료 여부를 표시하는 체크박스를 배치
  • 사용자가 작성한 할 일을 렌더링할 요소를 배치 -> 지금은 임시로 '할 일'이라는 문자열을 렌더링
  • 할 일 아이템이 작성된 시간을 렌더링할 요소를 배치 -> 지금은 임시로 현재 시각을 렌더링
  • 할 일을 삭제하는 버튼을 배치

 

 

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;

 

- 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;
  }

 

마지막으로 렌더링 결과를 확인하면 UI 구현을 완료한 것을 알 수 있다. 이제 App 컴포넌트에서 경계를 확인할 필요가 없으니, App.css의 border 속성은 제거 또는 주석 처리한다.

최종적으로 구성된 UI는 위 사진과 같다. UI 구현을 흔히 '퍼블리싱' 또는 'UI 개발'이라고 한다.

 

 

 

3. 기능 구현 준비하기

컴포넌트별로 어떤 기능을 구현해야 하는지 다시 살펴보자.

  • App 컴포넌트: 할 일 데이터 관리하기
  • Header 컴포넌트: 오늘의 날짜 표시
  • TodoEditor: 컴포넌트: 새로운 할 일 아이템 생성
  • TodoList 컴포넌트: 검색에 따라 필터링된 할 일 아이템 렌더링
  • TodoItem 컴포넌트: 할 일 아이템의 수정 및 삭제

구현할 컴포넌트의 기능을 잘 살펴보면 주로 데이터를 추가(생성)하고, 조회하고, 수정하고, 삭제하는 기능으로 이루어져 있음을 알 수 있다. 이렇듯 데이터를 다루는 4개의 기능, 즉 추가(Create), 조회(Read), 수정(Update), 삭제(Delete) 기능을 앞 글자만 따서 CRUD라고 한다. CRUD는 데이터 처리의 기본 기능으로, 웹 서비스라면 기본적으로 갖추고 있어야 한다.

 

  • Create: 할 일 아이템 생성
  • Read: 할 일 아이템 렌더링
  • Update: 할 일 아이템 수정
  • Delete: 할 일 아이템 삭제

 

3-(1) 기초 데이터 설정하기

- App.js

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

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

  return (
    (...)
  );
}
export default App;
  • useState를 이용해 할 일 아이템의 상태를 관리할 State를 만든다. 함수 useState에서 인수로 빈 배열을 전달해 State 변수 todo의 기본 값을 빈 배열로 초기화한다.

 

3-(2) 데이터 모델링하기

현실의 사물이나 개념을 프로그래밍 언어의 객체와 같은 자료구조로 표현하는 행위를 '데이터 모델링'이라고 한다. 데이터 모델링을 하는 이유는 [할 일 관리] 앱의 '할 일'처럼 현실 세계의 사물이나 개념을 프로그래밍 언어로 표현하고 다뤄야 하기 때문이다.

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

 

하나의 할 일 아이템에는 일의 완료 여부(isDone), 일의 종류(content), 생성 날짜(createdDate) 등 3가지 정보가 담겨 있다. 할 일 아이템에는 페이지에 렌더링 하지는 않지만, id라는 고유 식별자가 있다. 모든 아이템에는 해당 아잉템을 구별하기 위한 고유 식별자가 필요하기 때문이다. 고유 식별자가 없으면 특정 아이템을 삭제하거나 수정하는 등의 연산이 불가능하다.

 

{
  id: 0, 
  isDone: false, 
  content: "React 공부하기", 
  createdDate: new Date().getTime(), 
}

모델링한 정보를 토대로 할 일 아이템을 자바스크립트 객체로 만들면 위와 같다.

  • id는 특정 아이템을 식별하는 고유한 값이다.
  • isDone은 불리언 자료형으로 현재 상황에서 할 일이 완료되었는지 여부를 확인할 때 이용한다.
  • contnet는 할 일이 무엇인지 알려주는 문자열이다.
  • createDate는 할 일의 생성 시간이다. new Date()로 Date 객체를 만들고 getTime 메서드를 이용해 이 객체를 타임 스탬프값으로 변환한다.

데이터를 모델링하는 이유는 데이터를 어떻게 관리할지 생각하기 위함이다. 

 

 

3-(3) 목 데이터 설정하기

목(Mock) 데이터란 모조품 데이터라는 뜻이다. 기능을 완벽히 구현하지 않은 상태에서 테스트를 목적으로 사용하는 데이터다. 임시 데이터라 표현하기도 한다. 기능을 아직 개발하지 않아 데이터가 없는 상황일 때 목 데이터를 사용한다.

 

- App.js

(...)
const mockTodo = [ // 3개의 객체를 저장하는 배열 목 데이터 생성
  {
    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(),
  },
];

function App() {
  const [todo, setTodo] = useState(mockTodo); 
	// todo의 기본값으로 목 데이터를 전달

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

 

비주얼 스튜디오 코드에 "(변수명) is assigned a value but never used"라는 경고 메시지가 나오지만 선언된 변수를 어디에도 사용하지 않을 때 발생하는 경고로 오류는 아니다. 따라서 무시해도 괜찮다.

 

아직 TodoList 컴포넌트에 목 데이터를 전달하지 않았기 때문에 데이터를 페이지에 렌더링하지는 않는다. 따라서 지금은 리액트 개발자 도구의  [Components] 탭울 통해 App 컴포넌트의 hooks 항목에서 State의 값을 확인해 데이터가 잘 설정되는지 확인해야 한다.

 

[Components] 탭에서 목 데이터 확인하기

 

 

4. Create: 할 일 추가하기

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

 

4-(1) 기능 흐름 살펴보기

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

위 사진은 [할 일 관리] 앱에서 할 일이 추가되는 과정을 도식화한 것이다.

 

  1. 사용자가 새로운 할 일을 입력한다.
  2. TodoEditor 컴포넌트에 있는 <추가> 버튼을 클릭한다.
  3. TodoEditor 컴포넌트는 부모인 App에게 아이템 추가 이벤트가 발생했음을 알리고 사용자가 추가한 할 일 데이터를 전달한다.
  4. App 컴포넌트는 TodoEditor 컴포넌트에서 받은 데이터를 이용해 새 아이템이 추가된 배열을 만들고 State 변수 todo 값을 업데이트한다.
  5. TodoEditor 컴포넌트는 자연스러운 사용자 경험을 위해 할 일 입력 폼을 초기화한다.

 

4-(2) 아이템 추가 함수 만들기

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

 

- App 컴포넌트

새 할 일 아이템을 추가하는 함수 onCreate 만들기.

(...)
function App() {
  const idRef = useRef(3); // 앞에 작성한 목 데이터 id가 0, 1, 2이라 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;

 

위 함수 onCreate에는 아이템을 새로 추가할 때마다 중복 id가 만들어지는 문제가 발생한다. 이를 Ref 객체를 사용하여 해결한다.

 

- App.js

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

 

 

4-(3) 아이템 추가 함수 호출하기

- TodoEditor 컴포넌트 수정

import { useState } from "react";
import "./TodoEditor.css";

const TodoEditor = ({ onCreate }) => {
  const [content, setContent] = useState(""); 
  const onChangeContent = (e) => {
    setContent(e.target.value);
  };
  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;
  • 사용자가 입력 폼에 입력한 데이터를 저장할 State 변수 content를 만든다.
  • 입력 폼의 onChange 이벤트 핸들러 onChangeContent를 만든다.
  • 입력 폼의 value 속성으로 content 값을 설정하고, 이벤트 핸들러로 onChangeContent를 설정한다.

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

(...)
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] 탭에서 직접 확인해야 한다.

 

 

 

4-(4) Create 완성도 높이기

빈 입력 방지하기

빈 입력은 말뜻 그대로 아무것도 입력하지 않은 상태에서 <추가> 버튼을 누르는 행위다. 아무것도 입력하지 않은 상태로 아이템을 추가하는 것을 방지하기 위한 방법은 여럿 있으나, 이 프로젝트에서는 웹 서비스들이 일반적으로 채택하고 있는 빈 입력란에 포커스를 주는 기능을 구현한다. 

 

- TodoEdiotr 컴포넌트 수정

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;
  • 할 일 입력 폼을 제어할 객체 inputRef를 생성한다.
  • 함수 onSubmit은 현재 content 값이 빈 문자열이면, inputRef가 현재값(current)으로 저장한 요소에 포커스 하고 종료한다.
  • 할 일 입력 폼의 ref에 inputRef를 설정한다. 이제 inputRef는 현잿값으로 이 요소를 저장한다.

 

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

입력 폼을 초기화하는 기능이 없다면 의도치 않게 <추가> 버튼을 클릭해 중복 아이템을 생성할 수 있다. 또한 다른 할 일을 추가하려는 사용자에게는 기존에 작성했던 항목이 지워지지 않고 남아 있어 이를 지워야 하는 불편함이 생긴다.

 

- TodoEditor 컴포넌트의 함수 onSubmit 수정

(...)
const TodoEditor = ({ onCreate }) => {
  (...)
  const onSubmit = () => {
    if (!content) {
      inputRef.current.focus();
      return;
    }
    onCreate(content);
    setContent("");
  };
  (...)
};
export default TodoEditor;
  • 함수 setContent를 호출해 인수로 빈 문자열을 전달한다. 그러면 새 아이템을 추가하고 난 후, content 값은 빈 문자열이 되고 입력 폼 역시 초기화된다.

 

Ender 키를 눌러 아이템 추가하기

- TodoEditor 컴포넌트 수정

(...)
const TodoEditor = ({ onCreate }) => {
  (...)
  const onKeyDown = (e) => { 
    if (e.keyCode === 13) {
      onSubmit();
    } // 13은 <Enter> 키를 의미함
  };
  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;
  • 함수 onKeyDown은 사용자가 Enter 키를 눌렀을 때 호출할 이벤트 핸들러다.

 

 


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

Corner React.js 1

Editor: Ninanyu

728x90

관련글 더보기