상세 컨텐츠

본문 제목

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

23-24/React.js 3

by 롱롱😋 2023. 12. 22. 10:10

본문

728x90

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

CRUD의 두 번째 요소인 Read 기능을 이용하면 배열에 저장한 여러 할 일 아이템을 반복해서 페이지에 렌더링 가능하다.
 

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

1. App컴포넌트의 state변수 todo에 저장된 할 일 아이템들을 Props로 TodoList 컴포넌트에 전달한다.

//App.js
(...)
function App() {
  const [todo, setTodo] = useState(mockTodo);
  (...)
  return (
    <div className="App">
      <Header />
      <ToDoEditor onCreate={onCreate}/>
      <TodoList todo={todo} /> **
    </div>
  );
}

export default App;

 
2. TodoList 컴포넌트에서 todo를 리스트로 렌더링한다.
📌 리액트에서 배열 데이터를 렌더링하는 법
   => 배열 메서드 map을 이용하면 HTML 또는 컴포넌트를 순회하면서 매 요소를 반복하여 렌더링할 수 있다.
         
①  map을 이용한 HTML 반복하기

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

const TodoList = ({ todo }) => {  // props를 구조분해할당하기
    return (
    <div className="TodoList">
        <h4>Todo List🌱</h4>
        <input className="searchbar" placeholder="검색어를 입력하세요" />
        <div className="list_wrapper"> 
            {todo.map((it)=> (  // map 메서드를 이용해 배열 todo를 순회하며 HTML로 변환
                <div>{it.content}</div>
            ))}
        </div>
    </div>
    )

};

export default TodoList;

② map을 이용 컴포넌트 반복하기
여기서는 현재 순회 중인 배열 요소 it의 모든 프로퍼티를 스프레드 연산자를 이용해 Props로 전달한다.

//TodoList.js
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} /> // map 메서드의 콜백 함수가 TodoItem 컴포넌트 반환
            ))}
        </div>
    </div>
    )

};

export default TodoList;

 
3. TodoItem 컴포넌트에 전달된 Props를 사용하도록 수정한다.

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

const TodoItem = ({id, content, isDone, createdDate }) => { //props 전달받음
    return (
        <div className="TodoItem">
            <div className="Checkbox_col">
                <input checked={isDone} type="checkbox" /> // 체크 여부를 isDone으로 설정
            </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;

 
페이지에서는 '독서하기' 항목을 추가하였을 때 목 데이터의 할 일 아이템들과 새로 입력한 아이템을 잘 렌더링한 것을 볼 수 있다. 하지만 이때 개발자 도구 콘솔창에서 2가지 경고 메시지를 발견할 수 있다.
 
 리스트의 모든 자식 요소는 key라는 고유한 props를 반드시 가져야 한다.
② TodoItem 컴포넌트가 체크박스 입력폼에 onChange 이벤트 핸들러를 설정하지 않았다.
 
②의 경고사항은 나중에 추가할 것이니 지금은 무시하고 ①의 경고를 먼저 해결하도록 하자.
📌key 설정하기
key는 리스트에서 각각의 컴포넌트를 구분하기 위해 사용하는 값이다. 리액트는 리스트에서 특정 컴포넌트를 수정, 추가, 삭제하는 경우 key를 사용하여 어떤 컴포넌트를 업데이트할지 결정한다.
앞에서 우리는 목데이터를 설정할 때 각 아이템마다 고유한 id를 갖도록 모델링했기 때문에 이것을 key로 설정하면 된다.

//TodoList.js
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 key={it.id} {...it} /> //key로 id를 전달함
            ))}
        </div>
    </div>
    )

};

export default TodoList;

 
 

검색어에 따라 필터링하기

이번에는 TodoList 컴포넌트에 특정 할 일을 검색하는 기능을 만들어보겠다. 
 
1. 검색기능 만들기
사용자가 입력하는 검색어를 처리할 State 변수를 만들고, 검색 폼에서 사용자가 입력한 내용을 처리하는 기능을 만든다.

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

const TodoList = ({ todo }) => {
    const [search, setSearch] = useState(""); //state변수 search 생성
    const onChangeSearch = (e) => { //검색폼의 이벤트 핸들러 onChangeSearch 만듦
        setSearch(e.target.value);
    };
    return (
        <div className="TodoList">
            (...)
            <input
                value={search}  //value로 state변수 search설정
                onChange={onChangeSearch} //검색폼의 onChange 이벤트 핸들러를 onChangeSearch로 만듦
                className="searchbar"
                placeholder="검색어를 입력하세요"
            />
           (...)
        </div>
    )
};
export default TodoList;

 
2. 필터링 기능 만들기
다음으로 검색어에 따라 할 일 아이템을 필터링하는 기능을 만든다.
여기서 대소문자를 구별하지 않도록 toLowerCase() 메서드를 사용해 코드를 수정하면 사용자가 더 쉽게 검색가능하도록 구현할 수 있다. 

//TodoList.js
(...)
const TodoList = ({ todo }) => {
    (...)
    const getSearchResult = () => {
        return search === "" 
            ? todo  //검색어인 search가 빈문자열이면 todo그대로 반환
            : todo.filter((it) => it.content.includes(search)); // 그렇지 않으면 search 내용과 일치하는 아이템만 필터링해 반환
	// : todo.filter((it) => it.content.toLowerCase().includes(search.toLowerCase()));  //대소물자 구별하지 않도록
}
    return (
        <div className="TodoList">
            (...)
            <div className="list_wrapper">
                {getSearchResult().map((it) => (  //getSearchResult의 결과를 map을 이용하여 리스트로 렌더링
                    <TodoItem key={it.id} {...it} />
                ))}
            </div>
        </div>
    )

};

export default TodoList;

 

 

Update : 할 일 수정하기

CRUD의 세 번째 기능은 Update로 [할 일 관리] 앱에서 수정 기능을 만들어보겠다.
 

기능 흐름 살펴보기

① 사용자가 TodoItem의 체크박스에 틱을 한다.
② TodoItem 컴포넌트는 함수 onUpdate를 호출하고 어떤 체크박스에서 틱이 발생했는지 해당 아이템의 id를 인수로 전달한다.
③ App 컴포넌트의 함수 onUpdate는 틱이 발생한 아이템의 상태를 업데이트한다.
④ App 컴포넌트의 State 값이 변경되면 TodoList에 전달하는 Props값도 변경된다.
⑤ TodoList는 변경된 State값을 다시 리스트에 렌더링한다.
 

아이템 수정 함수 만들기

수정을 위해 함수 onUpdate를 만들고 이 함수를 TodoItem 컴포넌트까지 전달해야 한다.
 
1. onUpdate 함수 생성 후 TodoList 컴포넌트에 Props로 전달하기
리액트 컴포넌트는 바로 한 단계 아래의 자식 컴포넌트에만 데이터를 전달할 수 있다. TodoList는 해당 함수를 사용하지 않지만 TodoItem 컴포넌트에 함수 onUpdate를 전달해야 하므로 일종의 매개 역할을 수행한다.
이것은 리액트에서 State와 Props를 사용할 때 흔히 발생하는 일이며, Props Drilling이라고 한다.

//App.js
(...)
function App() {
  (...)
	//수정함수 onUpdate만들기
  const onUpdate = (TargetId) => { // 틱이 발생한 할일 아이템의 id를 저장함
    setTodo( //todo값을 업데이트하기 위한 함수 setTodo 호출
      todo.map(
        (it) => {
          if (it.id === TargetId){  //targetId와 일치한 할일 아이템이 있으면
            return {  //isDone 프로퍼티 값을 토글한 새 배열을 만들어 인수로 전달
              ...it, 
              isDone: !it.isDone,
            }; 
          } else {
            return it;
          }
        }
      )
      /*
		todo.map(
          (it)=>  //삼항연산자를 이용한 표현
          it.id===TargetId ? { ...it, isDone: !it.isDone } : it 
        )
      */
    )
  }
  return (
    <div className="App">
      <Header />
      <ToDoEditor onCreate={onCreate}/>
      <TodoList todo={todo} onUpdate={onUpdate} /> // TodoList컴포넌트에 Props로 함수 onUpdate 전달
    </div>
  );
}

export default App;

 
2. TodoList에서 TodoItem 컴포넌트에 함수 onUpdate 전달하기

//TodoList.js
(...)
const TodoList = ({ todo, onUpdate }) => { // onUpdate 추가
    (...)
    return (
        <div className="TodoList">
           (...)
            <div className="list_wrapper">
                {getSearchResult().map((it) => (  
                    <TodoItem key={it.id} {...it} onUpdate={onUpdate} />  // TodoItem 컴포넌트에 함수 onUpdate를 props 전달
                ))}
            </div>
        </div>
    )

};

export default TodoList;

 

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

TodoItem 컴포넌트에서 이벤트가 발생하면 함수 onUpdate를 호출하도록 코드를 수정하겠다.

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

const TodoItem = ({id, content, isDone, createdDate, onUpdate }) => { // onUpdate추가
    const onChangeCheckbox = () => { // 체크박스 틱했을때 호출할 함수 onChangeCheckbox 생성
        onUpdate(id);
    };
    return (
        <div className="TodoItem">
            <div className="Checkbox_col">
                <input onChange={onChangeCheckbox} checked={isDone} type="checkbox" /> // onChange 이벤트 핸들러 설
            </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;

 

Delete : 할 일 삭제하기 

마지막으로 CRUD의 Delete 기능을 구현하여 할 일 아이템을 삭제해 보겠다.
 

기능 흐름 살펴보기

삭제는 수정 기능과 유사한 흐름으로 진행이 된다.

① 사용자가 삭제하려는 할 일 아이템에서 삭제 버튼을 클릭한다.
② 할 일을 삭제하는 함수 onDelete를 호출한다. 이 함수는 App의 State 값을 업데이트하므로 미리 App 컴포넌트에서 Props로 전달해야 한다.
③ 삭제 버튼을 클릭하면 삭제할 아이템만 빼고, 새 배열을 만들어 State 값을 업데이트한다.
④ State 변수 todo가 업데이트되면, App이 TodoList 컴포넌트에 전달한 Props 값도 변경된다.
⑤ TodoList는 Props가 변경되면 리렌더된다. 이때 새로운 배열 todo로 할 일 리스트를 다시 렌더링한다.
 

아이템 삭제 함수 만들기

수정과 마찬가지로 삭제를 위해 함수 onDelete를 만들고 이 함수를 TodoItem 컴포넌트까지 전달해야 한다.

 

1. onDelete 함수 생성 후 TodoList 컴포넌트에 Props로 전달하기

//App.js
(..)
function App() {
  (...) 
  const onDelete = (TargetId) => { // 삭제 버튼을 클릭했을 때 호출하는 함수 onDelete 생성
    setTodo(todo.filter((it)=> it.id !== TargetId));
  };

  return (
    <div className="App">
      <Header />
      <ToDoEditor onCreate={onCreate}/>
      <TodoList todo={todo} onUpdate={onUpdate} onDelete={onDelete} /> // 먼저 TodoList에 props로 전달
    </div>
  );
}

export default App;

 

2. TodoList에서 TodoItem 컴포넌트에 함수 onDelete 전달하기

//TodoList.js
const TodoList = ({ todo, onUpdate, onDelete }) => { // onDelete 추가
    (...)
    return (
        <div className="TodoList">
           (...)
            <div className="list_wrapper">
                {getSearchResult().map((it) => (  
                    <TodoItem key={it.id} {...it} 
			onUpdate={onUpdate}
			onDelete={onDelete} /> // TodoItem에 props로 전달
              	  ))}
            </div>
        </div>
    )

};

export default TodoList;

 
TodoItem 컴포넌트에서 삭제 함수 호출하기

TodoItem에서 삭제 버튼을 클릭하면 함수 onDelete를 호출하도록 구현한다. 
여기까지 구현하면 삭제버튼을 클릭했을 때 페이지에서 바로 삭제되는 것을 확인할 수 있다. 

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

const TodoItem = ({id, content, isDone, createdDate, onUpdate, onDelete }) => { // onDelete 추가
    (...)
    const onClickDelete = () => { // 삭제 함수 onClickDelete 생성
        onDelete(id); // 인수로 id 전달
    };

    return (
        <div className="TodoItem">
          (...)
	    <div className="btn_col">
                <button onClick={onClickDelete}>삭제</button> // onClick 이벤트 핸들러로 onClickDelete 설정
            </div>
        </div>
    )
}
export default TodoItem;

 
 
 

Quiz 

1. CRUD의 기능에는 (   Create   ) , (    Read    ) , (    Update    ), (    Delete   )가 있다.
2. 리엑트에서 배열 메서드 (    map    )을 이용하면 HTML 또는 컴포넌트를 순회하면서 매 요소를 반복하여 렌더링할 수 있다.
3. 리엑트에서 리스트의 모든 자식 요소는 (    key    )라는 고유한 props를 반드시 가져야 한다.
4. (O/X 문제) 리액트 컴포넌트는 바로 한 단계 아래의 자식 컴포넌트에만 데이터를 전달할 수 있다.  (  O  )
5. 한 단계 이상 떨어져 있는 자식 컴포넌트에 데이트를 전달하기 위해, 전달에 전달을 반복하는 현상을 (  Props Drilling  )이라고 한다.
6. (   toLowerCase()   ) 메서드는 문자열에 있는 대문자를 모두 소문자로 바꿔준다. 대소문자를 구별하고 싶지 않을 때 사용한다.
7. [할 일 관리] 앱에서 배열에 저장한 여러 할 일 아이템을 반복해서 페이지에 렌더링하는 것은 CRUD 중 (   Read   )  기능이다.
 

프로그래밍 문제 

이번 프로그래밍 문제에서는 검색 필터링 기능을 구현해 보도록 할 것이다. 
 
1. 먼저 사용자가 입력하는 검색어를 처리할 State 변수를 만들고, 검색 폼에서 사용자가 입력한 내용을 처리하는 기능을 만들어라. (주석문을 코드로 작성해라)

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

const TodoList = ({ todo }) => {
    //state변수 search 생성
    //검색폼의 이벤트 핸들러 onChangeSearch 만듦
    return (
        <div className="TodoList">
            (...)
            <input
                //value로 state변수 search설정
                //검색폼의 onChange 이벤트 핸들러를 onChangeSearch로 만듦
                className="searchbar"
                placeholder="검색어를 입력하세요"
            />
           (...)
        </div>
    )
};
export default TodoList;

 
 
2. 다음으로 검색어에 따라 할 일 아이템을 필터링하는 기능인 getSearchResult를 완성해라. 이때 대소문자를 구별하지 않도록 코드를 구현하여라. (주석문을 코드로 작성해라)

//TodoList.js
(...)
const TodoList = ({ todo }) => {
    (...)
    const getSearchResult = () => {
        // 검색어인 search가 빈문자열이면 todo그대로 반환
        // 그렇지 않으면 search 내용과 일치하는 아이템만 필터링해 반환 (대소물자 구별하지 않도록)
}
    return (
        <div className="TodoList">
            (...)
            <div className="list_wrapper">
                {getSearchResult().map((it) => (  //getSearchResult의 결과를 map을 이용하여 리스트로 렌더링
                    <TodoItem key={it.id} {...it} />
                ))}
            </div>
        </div>
    )

};

export default TodoList;

 
 
 
 


정답
1. 

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

const TodoList = ({ todo }) => {
    const [search, setSearch] = useState(""); //state변수 search 생성
    const onChangeSearch = (e) => { //검색폼의 이벤트 핸들러 onChangeSearch 만듦
        setSearch(e.target.value);
    };
    return (
        <div className="TodoList">
            (...)
            <input
                value={search}  //value로 state변수 search설정
                onChange={onChangeSearch} //검색폼의 onChange 이벤트 핸들러를 onChangeSearch로 만듦
                className="searchbar"
                placeholder="검색어를 입력하세요"
            />
           (...)
        </div>
    )
};
export default TodoList;

 
 
2.

//TodoList.js
(...)
const TodoList = ({ todo }) => {
    (...)
    const getSearchResult = () => {
        return search === "" 
            ? todo  //검색어인 search가 빈문자열이면 todo그대로 반환 
	    : todo.filter((it) => it.content.toLowerCase().includes(search.toLowerCase()));  // 그렇지 않으면 search 내용과 일치하는 아이템만 필터링해 반환 (대소물자 구별하지 않도록)
}
    return (
        <div className="TodoList">
            (...)
            <div className="list_wrapper">
                {getSearchResult().map((it) => (  //getSearchResult의 결과를 map을 이용하여 리스트로 렌더링
                    <TodoItem key={it.id} {...it} />
                ))}
            </div>
        </div>
    )

};

export default TodoList;

 
 
 
출처 : 이정환, 『한 입 크기로 잘라 먹는 리액트』, 인사이트, p326-342
Corner React.js 3 
Editor:  via

728x90

관련글 더보기