문제발생 일기 하나를 삭제하면 모든 일기가 rerender되는 심각한 문제가 발생!

→ 심한 메모리 낭비가 발생

기존의 DiaryItem.js

import { useRef, useState } from 'react';
const DiaryItem = ({ onEdit, onDelete, author, content, created_date, emotion, id }) => {

    const [isEdit, setIsEdit] = useState(false); // 현재 수정중인지 수정중이 아닌지 확인용
    const  toggleIsEdit = () => setIsEdit(!isEdit);  // true, false를 toggle 시키기

    const [localContent, setLocalContent] = useState(content);
    const localContentInput = useRef();

    const handleRemove = ()=>{
        if(window.confirm(`${id}번째 일기를 정말 삭제하시겠습니까?`)){
            onDelete(id);
        }
    }

    const handleQuitEdit = () =>{
        setIsEdit(false);
        setLocalContent(content);
    };

    const handleEdit = () => {
        if(localContent.length < 5){
            localContentInput.current.focus();
            return;
        }

        if(window.confirm(`${id}번 째 일기를 수정하시겠습니까?`)){
            onEdit(id,localContent)
            toggleIsEdit();
        }
    }

    return <div className="DiaryItem">
        <div className="info">
            <span>
               작성자 : {author} | 감정점수 : {emotion}
            </span>
            <br />
            <span className="date">{new Date(created_date).toLocaleDateString()}</span>
        </div>
        <div className="content">
            {isEdit ? (
                <>
                    <textarea ref={localContentInput} 
                    value={localContent} onChange={(e)=>setLocalContent(e.target.value)}>

                    </textarea>
                </>
             ):(
                <>{content}</>
             )}
        </div>
        {isEdit ? (
                <>
                    <button onClick={handleQuitEdit}>수정 취소</button>
                    <button onClick={handleEdit}>수정 완료</button>
                </>
             ):(
                <>
                    <button onClick={handleRemove}>삭제하기</button>
                    <button onClick={toggleIsEdit}>수정하기</button>
                </>
             )}
    </div>
};

export default DiaryItem;

최적화 진행!

DiaryEditor 에 React.memo 설정

App.js에 DiaryItem에서 받는 비정형화 prop에 대해서 useCallback 으로 감싸준다.

→ 이때 주의할 사항은 setState 해주는 부분은 무조건 data를 인자로 받고 return을 인자로 넘겨주도록 콜백함수로 제작 dependency는 빈배열

DiaryItem.js

import React, { useEffect, useRef, useState } from "react";
const DiaryItem = ({
  onEdit,
  onDelete,
  author,
  content,
  created_date,
  emotion,
  id,
}) => {
  useEffect(() => {
    console.log(`${id}번 째 아이템 렌더!`);
  });
  const [isEdit, setIsEdit] = useState(false); // 현재 수정중인지 수정중이 아닌지 확인용
  const toggleIsEdit = () => setIsEdit(!isEdit); // true, false를 toggle 시키기

  const [localContent, setLocalContent] = useState(content);
  const localContentInput = useRef();

  const handleRemove = () => {
    if (window.confirm(`${id}번째 일기를 정말 삭제하시겠습니까?`)) {
      onDelete(id);
    }
  };

  const handleQuitEdit = () => {
    setIsEdit(false);
    setLocalContent(content);
  };

  const handleEdit = () => {
    if (localContent.length < 5) {
      localContentInput.current.focus();
      return;
    }

    if (window.confirm(`${id}번 째 일기를 수정하시겠습니까?`)) {
      onEdit(id, localContent);
      toggleIsEdit();
    }
  };

  return (
    <div className="DiaryItem">
      <div className="info">
        <span>
          작성자 : {author} | 감정점수 : {emotion}
        </span>
        <br />
        <span className="date">
          {new Date(created_date).toLocaleDateString()}
        </span>
      </div>
      <div className="content">
        {isEdit ? (
          <>
            <textarea
              ref={localContentInput}
              value={localContent}
              onChange={(e) => setLocalContent(e.target.value)}
            ></textarea>
          </>
        ) : (
          <>{content}</>
        )}
      </div>
      {isEdit ? (
        <>
          <button onClick={handleQuitEdit}>수정 취소</button>
          <button onClick={handleEdit}>수정 완료</button>
        </>
      ) : (
        <>
          <button onClick={handleRemove}>삭제하기</button>
          <button onClick={toggleIsEdit}>수정하기</button>
        </>
      )}
    </div>
  );
};

export default React.memo(DiaryItem);

App.js

import { useCallback, useMemo, useEffect, useRef, useState } from "react";
import "./App.css";
import DiaryEditor from "./DiaryEditor";
import DiaryList from "./DiaryList";
import LifeCycle from "./Lifecycle";
// import OptimizeTest from "./OptimizeTest";

function App() {
  const [data, setData] = useState([]);

  const dataId = useRef(0);

  const getData = async () => {
    const res = await fetch(
      "<https://jsonplaceholder.typicode.com/comments>"
    ).then((res) => res.json());

    const initData = res.slice(0, 20).map((it) => {
      return {
        author: it.email,
        content: it.body,
        emotion: Math.floor(Math.random() * 5) + 1,
        created_date: new Date().getTime(),
        id: dataId.current++,
      };
    });

    setData(initData);
  };

  useEffect(() => {
    getData();
  }, []);

  const onCreate = useCallback((author, content, emotion) => {
    const created_date = new Date().getTime();
    const newItem = {
      author,
      content,
      emotion,
      created_date,
      id: dataId.current,
    };
    dataId.current += 1;
    // useCallback의 딜레마에 벗어나기 위해 함수에 인자로 함수를 전달하는 함수형 업데이트 이용
    // newItem을 추가한 data를 return해주는 것을 전달한 것
    // 이렇게 사용하면 dependecy 인자에 data를 안넣고 비울 수 있음
    // -> 항상 최신의 state를 인자를 통해서 참고할 수 있음
    setData((data) => [newItem, ...data]); //원래 데이터 를 가져오고 새로운 item은 최상단에 나오도록 함
  }, []);

  const onDelete = useCallback((targetId) => {
    // filter 이용해서 삭제할 Id에 대한 일기 빼고 해당 객체 리스트 가져옴
    // const newDiaryList = data.filter((it) => it.id !== targetId);

    // 항상 인자부분에 data사용하고 return부분에 data를 사용해야 최신 data 사용가능
    setData((data) => data.filter((it) => it.id !== targetId));
  }, []);

  const onEdit = useCallback((targetId, newContent) => {
    setData((data) =>
      data.map((it) =>
        it.id === targetId ? { ...it, content: newContent } : it
      )
    );
  }, []);

  const getDiaryAnalysis = useMemo(() => {
    const goodCount = data.filter((it) => it.emotion >= 3).length;
    const badCount = data.length - goodCount;
    const goodRatio = (goodCount / data.length) * 100;
    return { goodCount, badCount, goodRatio };
  }, [data.length]);

  const { goodCount, badCount, goodRatio } = getDiaryAnalysis;

  return (
    <div className="App">
      {/* <LifeCycle /> */}
      {/* <OptimizeTest /> */}
      <DiaryEditor onCreate={onCreate} />
      <div>전체 일기 : {data.length}</div>
      <div>기분 좋은 일기 개수 : {goodCount}</div>
      <div>기분 나쁜 일기 개수 : {badCount}</div>
      <div>기분 좋은 일기 비율 : {goodRatio}</div>
      <DiaryList onEdit={onEdit} onDelete={onDelete} diaryList={data} />
    </div>
  );
}

export default App;