상세 컨텐츠

본문 제목

[React.js 1] p2. 할 일 관리 앱 만들기(2)

23-24/React.js 1

by ssxb 2023. 12. 22. 10:00

본문

728x90

 

Read: 할 일 리스트 렌더링하기

배열을 리스트로 렌더링하기

App 컴포넌트의 State 변수 todo에는 배열 형태로 여러 개의 할 일 아이템이 저장되어 있습니다. 배열 todo를 TodoList 컴포넌트에 Props로 전달합니다.

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

 

TodoList 컴포넌트에서는 App에서 Props로 전달된 todo를 리스트로 렌더링해야 합니다. 리액트에서 배열 데이터를 렌더링할 때는 배열 메서드 map을 주로 이용합니다. map을 이용하면 HTML 또는 컴포넌트를 순회하면서 매 요소를 반복하여 렌더링합니다.

 

map을 이용한 HTML 반복

TodoList 컴포넌트에서 배열 메서드 map을 이용해 HTML 요소를 반복해 렌더링합니다. TodoList 컴포넌트를 다음과 같이 수정합니다.

import TodoItem from "./TodoItem";
import "./TodoList.css";

const TodoList = ({ todo }) => { ①
  return (
    <div className="TodoList">
      <h4>Todo List 🌱</h4>
      <input className="searchbar" placeholder="검색어를 입력하세요" />
      <div className="list_wrapper">
        {todo.map((it) => ( ②
          <div>{it.content}</div>
        ))}
      </div>
    </div>
  );
};
export default TodoList;
Props를 구조 분해 할당합니다.
map 메서드를 이용해 배열 todo의 모든 요소를 순차적으로 순회하며 HTML로 변환합니다. 이 식의 결괏값은 배열 todo에 저장된 모든 할 일을 <div> 태그로 감싼 것과 동일합니다.
 

페이지를 새로고침(<F5>)하고 렌더링 결과를 확인하면, todo에 저장된 3개의 할 일을 HTML로 반복해 페이지에 렌더링합니다.

 

map을 이용한 컴포넌트 반복

배열을 이용해 컴포넌트를 반복해 렌더링합니다.

import TodoItem from "./TodoItem";
import "./TodoList.css";

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

 

TodoItem 컴포넌트에 전달된 Props를 이 컴포넌트에서 사용할 수 있도록 다음과 같이 수정합니다.

import "./TodoItem.css";

const TodoItem = ({ id, content, isDone, createdDate }) => { ①
  return (
    <div className="TodoItem">
      <div className="checkbox_col">
        <input checked={isDone} type="checkbox" /> ②
      </div>
      <div className="title_col">{content}</div> ③
      <div className="date_col">
        {new Date(createdDate).toLocaleDateString()} ④
      </div>
      <div className="btn_col">
        <button>삭제</button>
      </div>
    </div>
  );
};
export default TodoItem;

저장한 다음, 결과를 확인할 수 있도록 할 일 입력 폼에 ‘독서하기’라는 새 아이템을 추가합니다.

새 아이템 렌더링하기

 

개발자 도구의 콘솔을 열면 여러 가지 경고 메시지가 출력되는 걸 볼 수 있습니다. 앞서 확인했던 것과 같은 경고 메시지입니다.
Each child in a list should have a unique "key" prop.
경고 메시지를 직역하면 “리스트의 모든 자식 요소는 key라는 고유한 prop을 반드시 가져야 한다”라고 해석할 수 있습니다. 그리고 다음과 같은 두 번째 경고 메시지도 발견할 수 있습니다.
You provided a 'checked' prop to a form without an 'onChange' handler …
이 메시지는 TodoItem 컴포넌트가 체크박스 입력 폼에 onChange 이벤트 핸들러를 설정하지 않아서 발생한 경고입니다. 나중에 이 체크박스에 onChange 이벤트 핸들러를 설정할 예정이므로 지금은 무시해도 됩니다.
 
 

key 설정하기

우리는 이미 아이템마다 고유한 id를 갖도록 데이터를 모델링했습니다. 그리고 App 컴포넌트의 할 일 아이템 생성 과정에서 Ref 객체를 이용해 아이템마다 고유 id를 갖도록 만들었습니다. 따라서 id를 key로 전달하면 문제를 간단히 해결할 수 있습니다.
TodoList 컴포넌트를 다음과 같이 수정합니다.
import TodoItem from "./TodoItem";
import "./TodoList.css";

const TodoList = ({ todo }) => {
  return (
    <div className="TodoList">
      (...)
      <div className="list_wrapper">
        {todo.map((it) => (
          <TodoItem key={it.id} {...it} /> ①
         ))}
      </div>
    </div>
  );
};
export default TodoList;

 

검색어에 따라 필터링 하기

먼저 사용자가 입력하는 검색어를 처리할 State 변수를 만든 다음, 검색 폼에서 사용자가 입력한 내용을 처리하는 기능을 만듭니다. TodoList.js를 다음과 같이 수정합니다.

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

const TodoList = ({ todo }) => {
  const [search, setSearch] = useState(""); 
  const onChangeSearch = (e) => { // ②
    setSearch(e.target.value);
  };

  return (
    <div className="TodoList">
      <h4>Todo List 🌱</h4>
      <input
        value={search} // ③
        onChange={onChangeSearch} // ④
        className="searchbar"
        placeholder="검색어를 입력하세요"
      />
      <div className="list_wrapper">
        {todo.map((it) => (
          <TodoItem key={it.id} {...it} />
        ))}
      </div>
    </div>
  );
};
export default TodoList;

사용자가 입력한 검색어에 따라 할 일 아이템을 필터링하는 기능을 만듭니다.

(...)
const TodoList = ({ todo }) => {
  (...)
  const getSearchResult = () => {
    return search === ""
      ? todo
      : todo.filter((it) => it.content.includes(search));
  };

  return (
    <div className="TodoList">
      (...)
      <div className="list_wrapper">
        {getSearchResult().map((it) => ( ②
          <TodoItem key={it.id} {...it} />
        ))}
      </div>
    </div>
  );
};
export default TodoList;

 

이제 검색 기능이 잘 구현되었습니다. 페이지에서 테스트 해 볼 수 있습니다.


Update: 할 일 수정하기

기능 흐름 살펴보기

[할 일 관리] 앱의 Update 기능 흐름
  1. 사용자가 TodoItem의 체크박스에 틱(체크 표시)합니다.
  2. TodoItem 컴포넌트는 함수 onUpdate를 호출하고 어떤 체크박스에 틱이 발생했는지 해당 아이템의 id를 인수로 전달합니다. 물론 그 전에 함수 onUpdate를 App 컴포넌트에서 Props로 TodoItem에 전달해야 합니다. 
  3. App 컴포넌트의 함수 onUpdate는 틱이 발생한 아이템의 상태(완료 또는 미완료)를 토글하기 위해 State 값을 업데이트합니다. 
  4. App 컴포넌트의 State 값이 변경되면 TodoList에 전달하는 Props의 값 또한 변경됩니다. 
  5. TodoList는 변경된 State 값을 다시 리스트로 렌더링합니다. 결과적으로 수정 사항이 반영됩니다.

아이템 수정 함수 만들기

할 일 생성을 위해 함수 onCreate를 만들었듯이 수정을 위해 함수 onUpdate를 만듭니다. 그리고 이 함수를 TodoItem 컴포넌트에 전달해야 합니다. 
다음과 같이 App에 할 일 수정 함수 onUpdate를 생성하고 TodoList 컴포넌트에 Props로 전달합니다.
(...)
function App() {
  (...)
  const onUpdate = (targetId) => { ①
    setTodo(
      todo.map((it) => { ②
        if (it.id === targetId) {
          return {
            ...it,
            isDone: !it.isDone,
          };
        } else {
          return it;
        }
      })
    );
  };

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

 

이제 TodoList에서 TodoItem 컴포넌트에 함수 onUpdate를 전달해야 합니다. TodoList 컴포넌트를 다음과 같이 수정합니다.

 

(...)
const TodoList = ({ todo, onUpdate }) => { ①
  (...)
  return (
    <div className="TodoList">
      (...)
      <div className="list_wrapper">
        {getSearchResult().map((it) => (
          <TodoItem key={it.id} {...it} onUpdate={onUpdate} /> ②
        ))}
      </div>
    </div>
  );
};
export default TodoList;

 

리액트 컴포넌트는 바로 한 단계 아래의 자식 컴포넌트에만 데이터를 전달할 수 있습니다. 따라서 한 단계 이상 떨어져 있는 자식 컴포넌트에 데이터를 전달하려면, 현재로서는 전달에 전달을 반복하는 수밖에 없습니다.
따라서 TodoList 자신은 해당 함수를 사용하지 않지만, TodoItem 컴포넌트에 함수 onUpdate를 전달해야 하므로 Props로 받아 다시 전달하는 일종의 매개 역할을 수행합니다.
 
이는 리액트에서 State와 Props를 사용할 때 흔히 발생하는 일입니다. 이런 상황을 “Props가 마치 땅을 파고 내려가는 것 같다”라고 하여 ‘Props Drilling’ 이라고 합니다. 

 

TodoItem 컴포넌트에서 아이템 수정 함수 호출

TodoItem을 다음과 같이 수정합니다.

import "./TodoItem.css";

const TodoItem = ({ id, content, isDone, createdDate, onUpdate }) => { ①
  const onChangeCheckbox = () => { ②
    onUpdate(id);
  };

  return (
    <div className="TodoItem">
      <div className="checkbox_col">
        <input onChange={onChangeCheckbox}  // ③
							 checked={isDone} type="checkbox" />
      </div>
      <div className="title_col">{content}</div>
      <div className="date_col">
        {new Date(createdDate).toLocaleDateString()}
      </div>
      <div className="btn_col">
        <button>삭제</button>
      </div>
    </div>
  );
};
export default TodoItem;

 

결과를 확인합니다. ‘React 공부하기’의 체크박스를 틱 했을 때 완료 여부를 표시하는 체크 표시가 나타나는지 확인합니다. 그리고 [Components]탭을 열고 TodoItem 컴포넌트에서 이 아이템의 isDone 프로퍼티가 true로 변경되는지도 확인합니다.

 

할 일 완료 여부를 확인하는 체크박스 틱하기

 


Delete: 할 일 삭제하기

기능 흐름 살펴보기

할 일 아이템의 삭제는 수정 기능과 유사한 흐름으로 진행됩니다. 사용자가 TodoItem의 <삭제> 버튼을 클릭하면 해당 할 일 아이템을 찾아 삭제하면 됩니다.

[할 일 관리] 앱의 Delete 기능 흐름
  1. 삭제하려는 할 일 아이템에서 <삭제> 버튼을 클릭합니다. 
  2. 할 일을 삭제하는 함수 onDelete를 호출합니다. 이 함수는 App의 State 값을 업데이트하므로 미리 App 컴포넌트에서 Props로 전달해야 합니다. 
  3. <삭제> 버튼을 클릭하면 삭제할 할 일 아이템만 빼고, 새 배열을 만들어 State 값을 업데이트합니다. 
  4. State 변수 todo가 업데이트되면, App가 TodoList 컴포넌트에 전달한 Props 의 값도 변경됩니다. 
  5. TodoList 컴포넌트는 Props의 값이 변경되면 리렌더됩니다. 이때 새로운 배 열 todo로 할 일 리스트 다시 렌더링합니다.

아이템 삭제 함수 만들기

  • App.js
(...)
function App() {
  (...)
  const onDelete = (targetId) => { ①
    setTodo(todo.filter((it) => it.id !== targetId));
  };

  return (
    <div className="App">
      <Header />
      <TodoEditor onCreate={onCreate} />
      <TodoList todo={todo} onUpdate={onUpdate} onDelete={onDelete} /> ②
    </div>
  );
}
export default App;
TodoItem의 <삭제> 버튼을 클릭 했을때 호출하는 함수 onDelete는 매개변수 targetId에 삭제 할 일기 아이템의 id를 저장합니다. 그리고 해당 id 요소를 뺀 새 배열로 todo를 업데이트 함으로써 대상 아이템을 삭제합니다. 
함수 onDelete는 TodoItem에서 <삭제> 버튼을 클릭할 때 호출합니다. 따라서 먼저 TodoList에 Props로 전달해야 합니다.
 
  • TodoList.js
(...)
const TodoList = ({ todo, onUpdate, onDelete }) => { ①
  (...)
  return (
    <div className="TodoList">
      (...)
      <div className="list_wrapper">
        {getSearchResult().map((it) => (
          <TodoItem
            key={it.id}
            {...it}
            onUpdate={onUpdate}
            onDelete={onDelete} ②
          />
        ))}
      </div>
    </div>
  );
};
export default TodoList;

삭제 함수 호출

  • TodoItem.js
import "./TodoItem.css";

const TodoItem = ({ id, content, isDone, createdDate, onUpdate, onDelete }) => { ①
  const onChangeCheckbox = () => {
    onUpdate(id);
  };
  const onClickDelete = () => { ②
    onDelete(id);
  };

  return (
    <div className="TodoItem">
      <div className="checkbox_col">
        <input onChange={onChangeCheckbox} checked={isDone} type="checkbox" />
      </div>
      <div className="title_col">{content}</div>
      <div className="date_col">
        {new Date(createdDate).toLocaleDateString()}
      </div>
      <div className="btn_col">
        <button onClick={onClickDelete}>삭제</button> ③
      </div>
    </div>
  );
};
export default TodoItem;

 

[할 일 관리] 앱의 삭제 기능 확인하기

 


Quiz

  1. CRUD 기능은 ( Create,Read,Update,Delete ) 를 나타낸다.
  2. 리액트에서 State와 Props를 사용할 때 흔히 발생하는 일로, Props로 받아 다시 전달하는 일종의 매개 역할을 수행하는 상황이 있다. 이런 상황을 “Props가 마치 땅을 파고 내려가는 것 같다”라고 하여 (  ‘Props Drilling’ )  이라고 한다.  
  3. 리액트에서 배열 데이터를 렌더링할 때 배열 메서드 ( map )을 이용하면 HTML 또는 컴포넌트를 순회하면서 매 요소를 반복하여 렌더링합니다.
  4. ( key )는 리스트에서 각각의 컴포넌트를 구분하기 위해 사용하는 값입니다. 리액트는 리스트에서 특정 컴포넌트를 수정, 추가, 삭제하는 경우, 이것으로 어떤 컴포넌트를 업데이트할지 결정합니다.
  5. ( toLowerCase() ) 메서드는 문자열에 있는 대문자를 모두 소문자로 바꿔 줍니다. 
  6. prj2에서 사용자가 TodoItem의 체크박스에 ( 틱(Tik, 체크 표시하는 것) )하면 할 일 아이템이 미완료 에서 완료, 완료에서 미완료 상태로 바뀌는 토글 기능이 동작합니다.
  7. 키를 설정하지 않으면, (  Each child in a list should have a unique "key" prop.  ) 경고 메시지가 콘솔에 출력된다.

 

코드 작성

1. prj 2의 Todo List.js 에서 검색 할 때 대소문자를 구별 하지 않도록 업그레이드 해보기

 

정답 :

더보기
(...)
const getSearchResult = () => {
    return search === ""
      ? todo
      : todo.filter((it) =>
          it.content.toLowerCase().includes(search.toLowerCase()) ①
        );
  };
(...)

 

2. 삼항연산자를 이용해 간단하게 수정해보기

function App() {
  (...)
  const onUpdate = (targetId) => { ①
    setTodo(
      todo.map((it) => { ②
        if (it.id === targetId) {
          return {
            ...it,
            isDone: !it.isDone,
          };
        } else {
          return it;
        }
      })
    );
  };

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

 

정답 :

더보기
(...)
const onUpdate = (targetId) => {
    setTodo(
      todo.map((it) =>
        it.id === targetId ? { ...it, isDone: !it.isDone } : it
      )
    );
};
(...)

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

Corner React.js 1

Editor: dalpaeng4

 
728x90

관련글 더보기