#3 To-Do 앱 최적화 하기 - 6

김민성·2023년 5월 7일
0
post-thumbnail

React.memo 를 이용한 컴포넌트 렌더링 최적화

현재 Todo 앱의 문제점!

현재 Todo 앱에서 App , Lists, List, Form 컴포넌트로 나눠져 있다. 이렇게 나눠준 이유는 재사용성을 위해서이기도 하지만 각 컴포넌트의 렌더링의 최적화를 위해서 이기도 하다. 예를 들어, Form에서 글을 타이핑을 할 때 원래는 Form 컴포넌트와 그 State 값을 가지고 있는 App 컴포넌트만 렌더링이 되어야 하는데 현재는 어떻게 되고 있는지 살펴보자.

모든 컴포넌트에 console.log 출력

<App.js>

export default function App() {
  console.log('App Component');

<Lists.js>

export default function Lists({ todoData, setTodoData }) {

    console.log('Lists Component');

<List.js>


}) => {

    console.log('List Component');

<Form.js>

export default function Form({handleSubmit, value, setValue}) {

    console.log('Form Component');

한 글자 입력 시마다 props가 바뀌지 않아서 렌더링 하지 않아도 되는 Lists 컴포넌트와 List 컴포넌트까지 다시 렌더링 되는 걸 볼 수 있다.

React.memo 적용으로 문제 해결

React.memo 적용은 간단하게 원하는 컴포넌트를 React.memo로 감싸면 된다.

<List.js>

import React from 'react'

const List = React.memo(({ //'React.memo('로 감싸준다.

    ...
    
}) //여기에 마지막으로 감싸준다.
export default List;

일단 Lists.js를 화살표형으로 다시 바꿔주자.
rafce+Enter을 하면 화살표형으로 자동으로 나온다.

<Lists.js>


import React from 'react';
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
import List from './List';


const Lists = ({todoData, setTodoData}) => {

    console.log('Lists Component');

    const handleEnd = (result) => {
        //result 매개변수에는 source 항목 및 대상 위치와 같은 드래그 이벤트에 대한 정보가 포함됩니다.
        console.log(result)

        //목적지가 없으면(이벤트 취소) 이 함수를 종료합니다.
        if (!result.destination) return;
        // 리액트 불변성을 지켜주기 위해 새로운 todoData 생성
        const newTodoData = todoData;


        //1. 변경시키는 아이템을 배열에서 지워준다.
        //2. return 값으로 지워진 아이템을 잡아준다.
        const [reorderedItem] = newTodoData.splice(result.source.index, 1);

        //원하는 자리에 redorderItem을 insert 해준다.
        newTodoData.splice(result.destination.index, 0, reorderedItem);
        setTodoData(newTodoData);
    } 

  return (
    <div>
        <DragDropContext onDragEnd={handleEnd}> 
        <Droppable droppableId="todo">
            {(provided) => (
            <div {...provided.droppableProps} ref={provided.innerRef}>
                {todoData.map((data, index) => (
                <Draggable
                    key={data.id}
                    draggableId={data.id.toString()}
                    index={index}
                >
                    {(provided, snapshot) => (
                        <List 
                            key={data.id}
                            id={data.id}
                            title={data.title}
                            completed={data.completed}
                            todoData={todoData}
                            setTodoData={setTodoData}
                            provided={provided}
                            snapshot={snapshot}
                        />
                    
                    )}
                </Draggable>
                ))}
                {provided.placeholder}
            </div>
            )}
            </Droppable>
        </DragDropContext>
        </div>
  )
}

export default Lists

이제 마찬가지로 React.memo로 감싸주자.

<Lists.js>


import React from 'react';
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
import List from './List';


const Lists = React.memo(({todoData, setTodoData}) => {

    console.log('Lists Component');

    const handleEnd = (result) => {
        //result 매개변수에는 source 항목 및 대상 위치와 같은 드래그 이벤트에 대한 정보가 포함됩니다.
        console.log(result)

        //목적지가 없으면(이벤트 취소) 이 함수를 종료합니다.
        if (!result.destination) return;
        // 리액트 불변성을 지켜주기 위해 새로운 todoData 생성
        const newTodoData = todoData;


        //1. 변경시키는 아이템을 배열에서 지워준다.
        //2. return 값으로 지워진 아이템을 잡아준다.
        const [reorderedItem] = newTodoData.splice(result.source.index, 1);

        //원하는 자리에 redorderItem을 insert 해준다.
        newTodoData.splice(result.destination.index, 0, reorderedItem);
        setTodoData(newTodoData);
    } 

  return (
    <div>
        <DragDropContext onDragEnd={handleEnd}> 
        <Droppable droppableId="todo">
            {(provided) => (
            <div {...provided.droppableProps} ref={provided.innerRef}>
                {todoData.map((data, index) => (
                <Draggable
                    key={data.id}
                    draggableId={data.id.toString()}
                    index={index}
                >
                    {(provided, snapshot) => (
                        <List 
                            key={data.id}
                            id={data.id}
                            title={data.title}
                            completed={data.completed}
                            todoData={todoData}
                            setTodoData={setTodoData}
                            provided={provided}
                            snapshot={snapshot}
                        />
                    
                    )}
                </Draggable>
                ))}
                {provided.placeholder}
            </div>
            )}
            </Droppable>
        </DragDropContext>
        </div>
  )
})

export default Lists

다음과 같이 이제는 렌더링이 필요치 않은 Lists, List 컴포넌트가 렌더링이 되지 않는 것을 볼 수 있다.

useCallback 을 이용한 함수 최적화

원래 컴포넌트가 렌더링 될 때 그 안에 있는 함수도 다시 만들게 된다. 하지만 똑같은 함수를 컴포넌트가 리렌더링 된다고 해서 계속 다시 만드는 것은 좋은 현상은 아니다. 그리고 이렇게 컴포넌트가 리렌더링 될 때마다 함수를 계속 다시 만든다고 하면, 만약 이 함수가 자식컴포넌트에 props로 내려준다면 함수를 포함하고 있는 컴포넌트가 리렌더링 될 때마다 자식컴포넌트도 함수가 새롭게 만들어지니 계속 리렌더링 하게 된다.

이러한 것을 막기 위해 useCallback Hooks를 이용한다.

삭제 버튼 함수 App 컴포넌트로 이동

List.js에 있는 다음 코드를 최상위 컴포넌트인 App.js로 옮긴다.

const handleClick = (id) => {
        let newTodoData = todoData.filter(data => data.id !== id);
        console.log('newTodoData', newTodoData);
        setTodoData(newTodoData);
    };

props로 함수 넘겨주기

하지만 List.js에서 handleClick을 사용해야 하니까 App.js에서 handleClick을 Lists 내에 props를 내려준다.

 <Lists handleClick={handleClick} todoData={todoData} setTodoData={setTodoData} />

그리고 Lists.js에서 props로 가져온다.

const Lists = React.memo(({todoData, setTodoData, handleClick}) => {

그리고 이를 Lists.js의 List 내에 props를 내려준다.

<List 
                            handleClick={handleClick} //이렇게 말이다.
                            key={data.id}
                            id={data.id}
                            title={data.title}
                            completed={data.completed}
                            todoData={todoData}
                            setTodoData={setTodoData}
                            provided={provided}
                            snapshot={snapshot}
                        />

그리고 List.js에서 props로 가져온다.

const List = React.memo(({
    id,
    title,
    completed,
    todoData,
    setTodoData,
    provided,
    snapshot,
    handleClick //이렇게 말이다.

}) => {

input에 입력...


원래는 React.memo로 감싸줘서 리렌더링 되지 않던 컴포넌트들이 한 글자 입력 시마다 Lists 컴포넌트와 List 컴포넌트까지 다시 리렌더링 되는 것을 확인할 수 있다.

이를 통해 React를 사용하는 개발자의 공감점(?)인 무한 props에 대해 공감할 수 있었다..

React.useCallback 적용으로 문제 해결

이렇게 Lists 컴포넌트와 List 컴포넌트가 리렌더링이 되는 것을 해결하기 위해 useCallback을 사용하는 것이다.

어떻게 사용하느냐?
useCallback 적용은 useCallback 안에 콜백함수와 의존성 배열을 순서대로 넣어주면 된다.

일단 useCallback을 React 라이브러리에서 가져온다.

import React, {useState, useCallback} from "react";

그리고 handleClick 함수를 다음과 같이 수정해준다.

  const handleClick = useCallback(
    (id) => {
    let newTodoData = todoData.filter(data => data.id !== id);
    setTodoData(newTodoData);
  },
  [todoData]
  );

함수 내에서 참조하는 state, props가 있다면 의존성 배열에 추가해주면 된다.
useCallback으로 인해 todoData가 변하지 않는다면, 함수는 새로 생성되지 않는다.
새로 생성되지 않기 때문에 메모리에 새로 할당되지 않고 동일참조값을 사용하게 된다.
의존성 배열에 아무것도 없다면 컴포넌트가 최초 렌더링 시에만 함수가 생성되며 그 이후에는 동일한 참조값을 사용하는 함수가 된다.

적용 후 다시 타이핑...


이제는 렌더링이 필요치 않은 Lists, List 컴포넌트가 없는 것을 볼 수 있다.

useMemo를 이용한 결과 값 최적화

Memoization 이란?

Memoization은 비용이 많이 드는 함수 호출의 결과를 저장하고 동일한 입력이 다시 발생할 때 캐시된 결과를 반환해 컴퓨터 프로그램의 속도를 높이는 데에 주로 사용되는 최적화 기술아다.

Component 내의 compute 함수가 만약 복잡한 연산을 수행하면 결과값을 리턴하는데 오랜 시간이 걸리게 된다. 이럴 때, 컴포넌트가 계속 리렌더링 된다면 연산을 계속 수행하는데에 오랜 시간이 걸려 성능에 안좋은 영향을 미치게 되며, UI 지연 현상도 일어나게 된다.

이러한 현상을 해결해주기 위해서 사용하는 것이 useMemo이다.
compute 함수에 넘겨주는 a, b의 값이 이전과 동일하다면 컴포넌트가 리렌더링 되더라도 연산을 다시 하지 않고 이전 렌더링 때 저장해두었던 값을 재활용하게 된다.

useMemo 적용하기

useMemo로 감싸준 후에, 첫번째 인수의 의존성 배열에다가 compute 함수에서 사용하는 값을 넣어준다.

이전에 배운 useCallback과 유사한데, 이들 모두 최적화를 위해 사용되는 것이다.

리액트 확장 프로그램 추가하기

Download

https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi? hl=ko&

익스텐션을 이용해서 렌더링 되는 부분 표시하기

Components 탭에서 Highligh updates 부분을 체크해주면 쉽게 컴포넌트가 렌더링 되는 것을 볼 수 있다.


이렇게 렌더링 되는 부분에 하이라이트가 씌워져서 렌더링 되는 부분을 알 수 있다.

할 일 리스트 모두 지우기 버튼 생성

다음과 같이 Delete All 버튼 UI를 생성해준다.

<div className="flex justify-between mb-3">
          <h1>할 일 목록</h1>
          <button onClick={handleRemoveClick}>Delete All</button> // 이렇게 말이다.
        </div>

그리고 handleRemoveClick 함수를 만들어주는데, setTodoData를 빈 배열로 주면 모두 지우게끔 구현이 가능하다.

  const handleRemoveClick = () => {
    setTodoData([]); // setTodoData를 빈 배열로 주면 된다.
  }

할 일 목록을 수정하는 기능 추가하기


이렇게 save, edit 버튼을 추가해 수정하는 기능을 구현할 것이다.

다른 UI 제공을 위한 State 생성

editing을 할 때 UI를 다르게 주기 위해서 State들을 만들 것이다.

 const [isEditing, setIsEditing] = useState(false);
 const [editedTitle,setEditedTitle] = useState(title);

Edit 버튼 추가 & 클릭 시 isEditing State 변경

x버튼 아래에 edit 버튼을 추가해준다.

<button
                    className="px-4 py-2 float-right"
                    onClick={() => setIsEditing(true)}
                    >
                    edit
                    </button>

조건에 따른 UI 렌더링

 if(isEditing) {
        return (
        <div>editing..</div>
        )
    }else { //아래에 return을 else 안에 모두 넣어준다.
        return (
            <div 
                key={id} 
                {...provided.draggableProps} 
                ref={provided.innerRef} 
                {...provided.dragHandleProps}
                className={`${
                snapshot.isDragging ? "bg-gray-400": "bg-gray-100"
                } flex items-center justify-between w-full px-4 py-1 my-2 text-gray-600 border rounded`}
                >
                    <div className='items-center'>
                        <input
                        type="checkbox"
                        defaultChecked={completed}
                        onChange={() => handleCompleteChange(id)}
                        />
                        {" "}
                        <span className={completed && "line-through"}>{title}</span>
                    </div>
                    <div>
                        <button
                        className="px-4 py-2 float-right"
                        onClick={() => handleClick(id)}
                        >
                        x
                        </button>
                        <button
                        className="px-4 py-2 float-right"
                        onClick={() => setIsEditing(true)}
                        >
                        edit
                        </button>
                    </div>
                        
                </div>
        )
    }

editing 시 UI 작성

return 안에 있는 것을 모두 if(isEditing)인 경우의 조건 안에 가져온다. 그리고 drag and drop 부분을 지워준다.

그리고 form 태그로 input 태그 부분을 감싸준다. 그리고 input 태그 내부의 모든 것과 바로 다음의 '{" "}'를 지워준다. 그리고 input 태그 내부에 다음 코드를 추가해준다.

value={editedTitle}
className="w-full px-3 py-2 mr-4 text-gray-500 rounded"

이제 save와 x버튼도 구현해주어야 한다.


    if(isEditing) {
        return (
            <div 
            className={`flex items-center justify-between w-full px-4 py-1 my-2 bg-gray-100 text-gray-600 border rounded`}
            >
                <div className='items-center'>
                    <form>
                        <input
                        value={editedTitle}
                        className="w-full px-3 py-2 mr-4 text-gray-500 rounded"
                        />
                    </form>
                </div>
                <div>
                    <button
                    className="px-4 py-2 float-right"
                    onClick={() => setIsEditing(false)}
                    >
                    x
                    </button>
                    <button
                    className="px-4 py-2 float-right"
                    type="submit"
                    >
                    save
                    </button>
                </div>
                    
            </div>
        )
    }else {
        return (
            <div 
                key={id} 
                {...provided.draggableProps} 
                ref={provided.innerRef} 
                {...provided.dragHandleProps}
                className={`${
                snapshot.isDragging ? "bg-gray-400": "bg-gray-100"
                } flex items-center justify-between w-full px-4 py-1 my-2 text-gray-600 border rounded`}
                >
                    <div className='items-center'>
                        <input
                        type="checkbox"
                        defaultChecked={completed}
                        onChange={() => handleCompleteChange(id)}
                        />
                        {" "}
                        <span className={completed && "line-through"}>{title}</span>
                    </div>
                    <div>
                        <button
                        className="px-4 py-2 float-right"
                        onClick={() => handleClick(id)}
                        >
                        x
                        </button>
                        <button
                        className="px-4 py-2 float-right"
                        onClick={() => setIsEditing(true)}
                        >
                        edit
                        </button>
                    </div>
                        
                </div>
        )
    }


저기서 x버튼을 눌러주면

아직은 typing을 해도 바뀌지 않는다.

editing 입력할 때 editedTitle State 변경

input 태그 안에 handleEditChange 함수를 call 해주고,

<input
                        value={editedTitle}
                        onChange={handleEditChange} // 이렇게.
                        className="w-full px-3 py-2 mr-4 text-gray-500 rounded"
                        />

그리고 handleEditChange 함수를 작성해준다.

const handleEditChange = (event) => {
        setEditedTitle(event.target.value);
    }

아직은 save가 되진 않는다.

editing 입력 후 Save

다음과 같이 onclick을 적용해준다.

 <form onSubmit={handleSubmit}>
<button onClick={handleSubmit}
className="px-4 py-2 float-right"
type="submit"
>

그리고 handleSubmit 함수를 추가해준다.

const handleSubmit = (event) => {
        event.preventDefault();

        let newTodoData = todoData.map(data => {
            if(data.id === id) {
                data.title = editedTitle
            }
            return data;
        })
        setTodoData(newTodoData)
        setIsEditing(false);
    }


그러면 이렇게 save를 할 수 있게 된다.

localStorage에 todoData 값 담기

하는 이유는,
브라우저 안의 localStorage에 todoData 값을 담아서 페이지를 refresh해도 todoData가 계속 남아 있을 수 있게 해주기 위해서이다.

local storage 를 사용해서 데이터를 저장하기

https://developer.mozilla.org/ko/docs/Web/API/Window/localStorage

여기서 확인해보자!

setTodoData 를 이용해서 todoData State를 바꿔줄 때 localStorage에도 같이 바꿔주기

객체나 배열을 저장해줄시에는 JSON.stringfy를 이용해 텍스트로 변환해준 후에 저장을 해주면 된다.

텍스트를 변환해주지 않고 저장을 하게 되면 다음과 같이 [object, object] 이렇게 나오게 된다.

다음은 콘솔창에 입력했을 때의 결과이다.
업로드중..

이제 setTodoData가 붙어있는 곳을 다 찾아서 모두 적용시켜주자.

<App.js>

import React, {useState, useCallback} from "react";
import "./App.css"
import Lists from "./components/Lists";
import Form from "./components/Form";


export default function App() {
  console.log('App Component');
  const [todoData, setTodoData] = useState([]);
  const [value, setValue] = useState("");

  
  const handleClick = useCallback(
    (id) => {
    let newTodoData = todoData.filter(data => data.id !== id);
    setTodoData(newTodoData);
    localStorage.setItem('todoData',JSON.stringify(newTodoData))
  },
  [todoData]
  );

  const handleSubmit = (e) => {
    e.preventDefault();
    let newTodo = {
      id: Date.now(),
      title: value,
      completed: false
    }
    setTodoData(prev => [...prev, newTodo]);
    localStorage.setItem('todoData',JSON.stringify(...todoData, newTodo))
    setValue("");
  }

  const handleRemoveClick = () => {
    setTodoData([]); // setTodoData를 빈 배열로 주면 된다.
    localStorage.setItem('todoData',JSON.stringify([]))
  }

  return(
    <div className="flex items-center justify-center first-letter:w-screen h-screen bg-blue-100" >
      <div className = "w-full p-6 m-4 bg-white rounded shadow lg:w-3/4 md:max-w-lg lg:W-3/4 lg:max-w-lg">
        <div className="flex justify-between mb-3">
          <h1>할 일 목록</h1>
          <button onClick={handleRemoveClick}>Delete All</button>
        </div>
        <Lists handleClick={handleClick} todoData={todoData} setTodoData={setTodoData} />
        <Form handleSubmit={handleSubmit} value={value} setValue={setValue} />
      </div>
    </div>
  )
}

<Lists.js>


import React from 'react';
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
import List from './List';


const Lists = React.memo(({todoData, setTodoData, handleClick}) => {

    console.log('Lists Component');

    const handleEnd = (result) => {
        //result 매개변수에는 source 항목 및 대상 위치와 같은 드래그 이벤트에 대한 정보가 포함됩니다.
        console.log(result)

        //목적지가 없으면(이벤트 취소) 이 함수를 종료합니다.
        if (!result.destination) return;
        // 리액트 불변성을 지켜주기 위해 새로운 todoData 생성
        const newTodoData = todoData;


        //1. 변경시키는 아이템을 배열에서 지워준다.
        //2. return 값으로 지워진 아이템을 잡아준다.
        const [reorderedItem] = newTodoData.splice(result.source.index, 1);

        //원하는 자리에 redorderItem을 insert 해준다.
        newTodoData.splice(result.destination.index, 0, reorderedItem);
        setTodoData(newTodoData);
        localStorage.setItem('todoData',JSON.stringify(newTodoData))
    } 

  return (
    <div>
        <DragDropContext onDragEnd={handleEnd}> 
        <Droppable droppableId="todo">
            {(provided) => (
            <div {...provided.droppableProps} ref={provided.innerRef}>
                {todoData.map((data, index) => (
                <Draggable
                    key={data.id}
                    draggableId={data.id.toString()}
                    index={index}
                >
                    {(provided, snapshot) => (
                        <List 
                            handleClick={handleClick}
                            key={data.id}
                            id={data.id}
                            title={data.title}
                            completed={data.completed}
                            todoData={todoData}
                            setTodoData={setTodoData}
                            provided={provided}
                            snapshot={snapshot}
                        />
                    
                    )}
                </Draggable>
                ))}
                {provided.placeholder}
            </div>
            )}
            </Droppable>
        </DragDropContext>
        </div>
  )
})

export default Lists

<List.js>

import React, { useState } from 'react'

const List = React.memo(({
    id,
    title,
    completed,
    todoData,
    setTodoData,
    provided,
    snapshot,
    handleClick

}) => {

    console.log('List Component');

    const [isEditing, setIsEditing] = useState(false);
    const [editedTitle,setEditedTitle] = useState(title);

    const handleCompleteChange = (id) => {
        let newTodoData = todoData.map(data => {
        if(data.id === id) {
            completed = !completed;
        }
        return data;
        });
        setTodoData(newTodoData);
        localStorage.setItem('todoData',JSON.stringify(newTodoData))

    };

    const handleEditChange = (event) => {
        setEditedTitle(event.target.value);
    }

    const handleSubmit = (event) => {
        event.preventDefault();

        let newTodoData = todoData.map(data => {
            if(data.id === id) {
                data.title = editedTitle
            }
            return data;
        })
        setTodoData(newTodoData)
        localStorage.setItem('todoData',JSON.stringify(newTodoData))
        setIsEditing(false);
    }

    if(isEditing) {
        return (
            <div 
            className={`flex items-center justify-between w-full px-4 py-1 my-2 bg-gray-100 text-gray-600 border rounded`}
            >
                <div className='items-center'>
                    <form onSubmit={handleSubmit}>
                        <input
                        value={editedTitle}
                        onChange={handleEditChange}
                        className="w-full px-3 py-2 mr-4 text-gray-500 rounded"
                        />
                    </form>
                </div>
                <div>
                    <button 
                    className="px-4 py-2 float-right"
                    onClick={() => setIsEditing(false)}
                    >
                    x
                    </button>
                    <button onClick={handleSubmit}
                    className="px-4 py-2 float-right"
                    type="submit"
                    >
                    save
                    </button>
                </div>
                    
            </div>
        )
    }else {
        return (
            <div 
                key={id} 
                {...provided.draggableProps} 
                ref={provided.innerRef} 
                {...provided.dragHandleProps}
                className={`${
                snapshot.isDragging ? "bg-gray-400": "bg-gray-100"
                } flex items-center justify-between w-full px-4 py-1 my-2 text-gray-600 border rounded`}
                >
                    <div className='items-center'>
                        <input
                        type="checkbox"
                        defaultChecked={completed}
                        onChange={() => handleCompleteChange(id)}
                        />
                        {" "}
                        <span className={completed && "line-through"}>{title}</span>
                    </div>
                    <div>
                        <button
                        className="px-4 py-2 float-right"
                        onClick={() => handleClick(id)}
                        >
                        x
                        </button>
                        <button
                        className="px-4 py-2 float-right"
                        onClick={() => setIsEditing(true)}
                        >
                        edit
                        </button>
                    </div>
                        
                </div>
        )
    }
})
export default List;

localStorage에 저장된 todoData 활용하기

지금까지 저장만 해줬지 가져와서 사용을 하지 않았다.
localStorage에 저장된 것을 가져와서 사용을 해야한다.
그래서 다음과 같이 해주면 된다.

initialTodoData를 가져올 때, localStorage 안에 todoData의 key로 저장된 값이 있으면 그걸 가져오는 것이다. JSON.parse를 해서 가져오고, 없으면 빈 배열을 todoData로 해주면 된다.
업로드중..
이렇게 refresh를 해도 변하지 않는 것을 확인할 수 있다.

전체 코드

<App.js>

import "./App.css";
import { useState } from "react";
import Lists from "./components/Lists";
import Form from "./components/Form";

const initialTodoData = localStorage.getItem("todoData")
  ? JSON.parse(localStorage.getItem("todoData"))
  : [];

function App() {
  const [todoData, setTodoData] = useState(initialTodoData);

  const [value, setValue] = useState("");

  const handleSubmit = (e) => {
    // form 안에 input을 전송할 때 페이지 리로드 되는 걸 막아줌
    e.preventDefault();

    // 새로운 할 일 데이터
    let newTodo = {
      id: Date.now(),
      title: value,
      completed: false,
    };

    // 원래 있던 할 일에 새로운 할 일 더해주기
    setTodoData((prev) => [...prev, newTodo]);
    localStorage.setItem("todoData", JSON.stringify([...todoData, newTodo]));

    // 입력란에 있던 글씨 지워주기
    setValue("");
  };

  const handleRemoveClick = () => {
    setTodoData([]);
    localStorage.setItem("todoData", JSON.stringify([]));
  };

  return (
    <div className="flex items-center justify-center w-screen h-screen bg-blue-100">
      <div className="w-full p-6 m-4 bg-white rounded shadow md:w-3/4 md:max-w-lg lg:w-3/4 lg:max-w-lg">
        <div className="flex justify-between mb-3">
          <h1>할 일 목록</h1>
          <button onClick={handleRemoveClick}>Delete All</button>
        </div>
        <Lists todoData={todoData} setTodoData={setTodoData} />

        <Form handleSubmit={handleSubmit} value={value} setValue={setValue} />
      </div>
    </div>
  );
}

export default App;

<Form.js>

import React from "react";

export default function Form({ handleSubmit, value, setValue }) {
  const handleChange = (e) => {
    setValue(e.target.value);
  };

  return (
    <form onSubmit={handleSubmit} className="flex pt-2">
      <input
        type="text"
        name="value"
        className="w-full px-3 py-2 mr-4 text-gray-500 border rounded shadow"
        placeholder="해야 할 일을 입력하세요."
        value={value}
        onChange={handleChange}
      />
      <input value="입력" type="submit"
        className="p-2 text-blue-400 border-2 border-blue-400 rounded hover:text-white hover:bg-blue-200" />
    </form>
  );
}

<Lists.js>

import React from "react";
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";
import List from "./List";

export default function Lists({ todoData, setTodoData }) {

  const handleEnd = (result) => {
    // result 매개변수에는 source 항목 및 대상 위치와 같은 드래그 이벤트에 대한 정보가 포함됩니다.
    console.log(result);
    // 목적지가 없으면(이벤트 취소) 이 함수를 종료합니다.
    if (!result.destination) return;

    // 리액트 불변성을 지켜주기 위해 새로운 todoData 생성 
    const newTodoData = todoData;

    // 1. 변경시키는 아이템을 배열에서 지워줍니다.
    // 2. return 값으로 지워진 아이템을 잡아줍니다.
    const [reorderedItem] = newTodoData.splice(result.source.index, 1);

    // 원하는 자리에 reorderedItem을 insert 해줍니다.
    newTodoData.splice(result.destination.index, 0, reorderedItem);
    setTodoData(newTodoData);
    localStorage.setItem("todoData", JSON.stringify(newTodoData));
  };

  return (
    <div>
      <DragDropContext onDragEnd={handleEnd}>

        <Droppable droppableId="to-dos">
          {(provided) => (
            <div {...provided.droppableProps} ref={provided.innerRef}>
              {todoData.map((data, index) => (
                <Draggable
                  key={data.id}
                  draggableId={data.id.toString()}
                  index={index}
                >
                  {(provided, snapshot) => (
                    <List
                      key={data.id}
                      id={data.id}
                      title={data.title}
                      completed={data.completed}
                      todoData={todoData}
                      setTodoData={setTodoData}
                      provided={provided}
                      snapshot={snapshot}
                    />
                  )}
                </Draggable>
              ))}
              {provided.placeholder}
            </div>
          )}
        </Droppable>

      </DragDropContext>
    </div>
  );
}

<List.js>

import React, { useState } from 'react'

const List = ({ id, title, completed, todoData, setTodoData, provided, snapshot }) => {
    const [isEditing, setIsEditing] = useState(false);
    const [editedTitle, setEditedTitle] = useState(title);

    const handleClick = (id) => {
        let newTodoData = todoData.filter((data) => data.id !== id);
        setTodoData(newTodoData);
        localStorage.setItem("todoData", JSON.stringify(newTodoData));
    };

    const handleCompleteChange = (id) => {
        let newTodoData = todoData.map((data) => {
            if (data.id === id) {
                data.completed = !data.completed;
            }
            return data;
        });
        setTodoData(newTodoData);
        localStorage.setItem("todoData", JSON.stringify(newTodoData));
    };

    const handleEditChange = (e) => {
        setEditedTitle(e.target.value);
    };

    const handleSubmit = () => {
        let newTodoData = todoData.map((data) => {
            if (data.id === id) {
                data.title = editedTitle;
            }
            return data;
        });
        setTodoData(newTodoData);
        localStorage.setItem("todoData", JSON.stringify(newTodoData));
        setIsEditing(false);
    };

    if (isEditing) {
        return (
            <div className="flex items-center justify-between w-full px-4 py-1 my-1 text-gray-600 bg-gray-100 border rounded row">
                <form onSubmit={handleSubmit}>
                    <input
                        className="w-full px-3 py-2 mr-4 text-gray-500 appearance-none"
                        value={editedTitle}
                        onChange={handleEditChange}
                        autoFocus
                    />
                </form>
                <div className="items-center">
                    <button
                        class="px-4 py-2 float-right"
                        onClick={() => setIsEditing(false)}
                        type="button"
                    >
                        x
                    </button>
                    <button onClick={handleSubmit} class="px-4 py-2 float-right"  type="submit">
                        save
                    </button>
                </div>
            </div>
        )
    } else {
        return (
            <div
                key={id}
                {...provided.draggableProps}
                ref={provided.innerRef}
                {...provided.dragHandleProps}
                className={`${snapshot.isDragging ? "bg-gray-400" : "bg-gray-100"} flex items-center justify-between w-full px-4 py-1 my-2 text-gray-600 bg-gray-100 border rounded`}>
                <div className="items-center">
                    <input
                        type="checkbox"
                        onChange={() => handleCompleteChange(id)}
                        defaultChecked={completed}
                    />{" "}
                    <span className={completed ? "line-through" : undefined}>{title}</span>
                </div>
                <div className="items-center">
                    <button className="float-right px-4 py-2" onClick={() => handleClick(id)}>
                        x
                    </button>
                    <button className="float-right px-4 py-2" onClick={() => setIsEditing(true)}>
                        edit
                    </button>
                </div>
            </div>
        )
    }

}

export default List

profile
다양한 활동을 통해 인사이트를 얻는 것을 즐깁니다. 저 또한 인사이트를 주는 사람이 되고자 합니다.

0개의 댓글

관련 채용 정보