반응형
Notice
Recent Posts
Recent Comments
Link
관리 메뉴

짧은코딩

최적화3-컴포넌트 최적화 본문

인프런, 유데미/한입 크기로 잘라 먹는 리액트

최적화3-컴포넌트 최적화

5_hyun 2022. 5. 29. 19:06
반응형

-DiaryEditor.js

import React, { useEffect, useRef, useState } from "react";

const DiaryEditor = ({ onCreate }) => {
  useEffect(() => {
    console.log("DiaryEditor 렌더");
  });

  const authorInput = useRef();
  const contentInput = useRef();

  const [state, setState] = useState({
    author: "",
    content: "",
    emotion: 1,
  });

  const handleChangeState = (e) => {
    setState({
      ...state,
      [e.target.name]: e.target.value,
    });
  };

  const handleSubmit = () => {
    if (state.author.length < 1) {
      authorInput.current.focus();
      return;
    }

    if (state.content.length < 5) {
      contentInput.current.focus();
      return;
    }

    onCreate(state.author, state.content, state.emotion);
    alert("저장 성공!");
    setState({
      author: "",
      content: "",
      emotion: 1,
    });
  };

  return (
    <div className="DiaryEditor">
      <h2>오늘의 일기</h2>
      <div>
        <input
          ref={authorInput}
          value={state.author}
          onChange={handleChangeState}
          name="author"
          placeholder="작성자"
          type="text"
        />
      </div>
      <div>
        <textarea
          ref={contentInput}
          value={state.content}
          onChange={handleChangeState}
          name="content"
          placeholder="일기"
          type="text"
        />
      </div>
      <div>
        <span>오늘의 감정점수 : </span>
        <select
          name="emotion"
          value={state.emotion}
          onChange={handleChangeState}
        >
          <option value={1}>1</option>
          <option value={2}>2</option>
          <option value={3}>3</option>
          <option value={4}>4</option>
          <option value={5}>5</option>
        </select>
      </div>
      <div>
        <button onClick={handleSubmit}>일기 저장하기</button>
      </div>
    </div>
  );
};
export default React.memo(DiaryEditor);

최적화를 위해 마지막에 React.memo를 하고 결과를 확인하기 위해 useEffect로 콘솔을 찍었다.

=> 콘솔에 2번 찍힌다.

 

-App.js

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

const 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 = (author, content, emotion) => {
    const created_date = new Date().getTime();
    const newItem = {
      author,
      content,
      emotion,
      created_date,
      id: dataId.current,
    };
    dataId.current += 1;
    setData([newItem, ...data]);
  };

  const onRemove = (targetId) => {
    const newDiaryList = data.filter((it) => it.id !== targetId);
    setData(newDiaryList);
  };

  const onEdit = (targetId, newContent) => {
    setData(
      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">
      <DiaryEditor onCreate={onCreate} />
      <div>전체 일기 : {data.length}</div>
      <div>기분 좋은 일기 개수 : {goodCount}</div>
      <div>기분 나쁜 일기 개수 : {badCount}</div>
      <div>기분 좋은 일기 비율 : {goodRatio}</div>
      <DiaryList onEdit={onEdit} onRemove={onRemove} diaryList={data} />
    </div>
  );
};

export default App;

const [data, setData] = useState([]);에서 빈 배열이라서 렌더링이 한 번 일어난다.

그리고 getData에서 업데이트되어서 렌더링이 한번 더 일어난다.

=> 따라서 App은 Mount되자마자 2번의 렌더링이 일어나고, DiaryEditor.js는 onCreate로 App에서 데이터를 전달받아서 2번 렌더링이 일어난다. 왜냐하면 객체는 얕은 비교를 하기 때문이다. 결론적으로 onCreate 때문에 렌더링이 또 일어난다.

 

-해결법

useMemo는 결론적으로 함수를 반환하는 것이 아닌 값을 반환하는 것이라서 사용하면 안된다. 왜냐면 우리의 목적은 onCreate를 원문 그대로 DiaryEditor에 보내주는 것이 목표이기 때문이다.

 

https://ko.reactjs.org/docs/hooks-reference.html#usecallback

 

Hooks API Reference – React

A JavaScript library for building user interfaces

ko.reactjs.org

따라서 usecallback을 사용한다.

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

usecallback은 두번째 인자 배열의 값이 변화하지 않으면 첫번째 인자인 콜백 함수를 재사용할 수 있도록 해주는 hook이다.

이것을 이용해서 onCreate가 다시 생성되지 않게 하는 것이다.

 

-App.js

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;
    setData((data) => [newItem, ...data]);
  }, []);

onCreate를 이렇게 수정한다.

함수형 업데이트를 활용했다. setData에 함수를 전달해도 된다. setData에 data를 받아서 data에 newItem을 추가한 데이터를 리턴하는 콜백 함수를 전달한다.

이러면 두번째 인자인 dependency array를 비워도 항상 최신의 state를 업데이트 가능하다.

반응형
Comments