[Udemy] React 실전(감정일기장) - 최적화(Memoization, useCallback)

productuidev·2022년 6월 4일
0

React Study

목록 보기
46/52
post-thumbnail
post-custom-banner

React 실전 (Project)

Udemy - 한입크기로 잘라 먹는 리액트


📌 감정일기장 - 최적화(Memoization, useCallback)

☑️ 컴포넌트 연산 최적화

  • 프로젝트 완료 후 어떤 부분이 연산을 낭비시키고 있는가? 성능상의 문제 찾아 최적화하기
  • 컴포넌트 연산 최적화웹서비스의 성능을 결정할 수 있는 가장 중요한 작업

✔️ 작성한 코드가 정상적으로 잘 작동하는가 분석하는 방법

  • 정적 분석 : IDE에서 코드를 한 줄 한 줄씩 보면서 코드들의 상태만 보고 최적화가 안되어 있는지 판단하여 분석하는 과정
  • 동적 분석 : React Developer Tools, 크롬 개발자도구와 같은 도구의 힘을 빌려 어떤 부분이 낭비되고 있는지 문제되는 부분을 찾아 분석하는 과정
    ㄴ React Developer Tools의 Highlight updates render 기능을 활용해 컴포넌트의 state를 변화시켰을 때 어떤 컴포넌트들이 리렌더링되는지로 분석하기

☑️ 최적화할 부분

  • 동적 분석 후 리렌더링이 될 필요가 없는 부분 찾기
  • Home 컴포넌트
    날짜가 변경되도 필터(최신순,전부다)와 새일기쓰기 버튼이 다시 렌더링될 필요는 없음
    필터링이 되도 목록이 다시 렌더링 될 필요가 없음
  • Edit 컴포넌트
    일기 내용 수정 시 오늘의 감정아이템이 다시 렌더링될 필요는 없음

☑️ DiaryList 최적화

    <div>
      <MyHeader
        headText={headText}
        leftChild={<MyButton text={"<"} onClick={decreaseMonth} />}
        rightChild={<MyButton text={">"} onClick={increaseMonth} />}
      />
      <DiaryList diaryList={data} />
    </div>
  • MyHeader에서 이전/다음 버튼을 누르면 onclick 이벤트로 decreaseMonth/increaseMonth 함수를 수행하고 있고, 이 2개의 함수는 setCurDate state를 변화시킴
  • 이 두 버튼을 누르면 Home 컴포넌트의 state가 변화되어 다시 렌더링이 일어나고 DiaryList에 state가 변화하면서 data를 받아옴
  • DiaryList 컴포넌트는 Home 컴포넌트의 자식 요소이기 때문에 기본적으로 Home 컴포넌트의 state가 변화해서 렌더링이 일어나면 DiaryList 컴포넌트도 렌더링이 일어남. 또한 DiaryList의 자식 컴포넌트인 ControlMenu라는 컴포넌트도 하위 요소여서 Home 컴포넌트 state 변화 시 다시 렌더링됨

현재 성능상의 문제 : 하위 요소인 ControlMenu가 다시 렌더링 될 필요가 없어도 리렌더링되는 것

  • React.memo를 활용한 연산 최적화 : React.memo로 컴포넌트를 감싸서 인자로 전달하면 강화된 컴포넌트로 돌려주는 고차 컴포넌트(HOC) 돌려주는데, 전달받은 인자(prop)이 값이 바뀌지 않으면 리렌더링이 되지 않게 memoizaition해주는 기법

React.memo로 ControlMenu를 감싸고 이전/다음 버튼으로 월을 변경하면 ControlMenu의 깜빡임이 사라진 것을 확인할 수 있음

const ControlMenu = React.memo(({value, onChange, optionList}) => {
  // mount되었을 때 잘 되었는지 useEffect로 확인
   useEffect(()=>{
     console.log("Control Menu");
   });

  return ( 
    <select className="ControlMenu" value={value} onChange={(e)=>onChange(e.target.value)}>
      {optionList.map((it, index) => (
        <option key={index} value={it.value}>
          {it.name}
        </option>
      ))}
    </select>
  )
});
  • 최적화 시 useCallback을 활용해 함수를 재사용하지 않게 만들 수 있으나 이럴 경우 부모 컴포넌트가 리렌더링이 되면서 변경되면 React.memo가 정상적으로 동작하지 않을 수 있음
  • 현재 Control Menu에서 prop으로 전달받는 onChange 함수는 별도로 useCallback을 처리하지 않았음에도 리렌더링이 발생하지 않음
  • useState에서 반환받은 재사용되는 상태변화 함수 그 자체를 내려주면 그 자체로 최적화할 수 있어서 별도로 useCallback 처리를 하지 않아도 된다
// List
const DiaryList = ({diaryList}) => {
  // sort
  const [sortType, setSortType] = useState("latest");
  // filter
  const [filter, setFilter] = useState("all");

...

  return (
          <ControlMenu value={sortType} onChange={setSortType} optionList={sortOptionList} /> 
          <ControlMenu value={filter} onChange={setFilter} optionList={filterOptionList} />
  • 이전 달/다음 달로 이동해도 console에 control menu가 다시 출력되지 않는 것 확인 후 useEffect 삭제

☑️ DiaryItem 최적화

  • DiaryList의 자식 컴포넌트 (개별 일기 목록)
  • 최신순/오래된순 변경 시 sortType, filter state가 변화하게 되어 DiaryItem 컴포넌트가 리렌더링됨
  • 그런데 목록에 텍스트가 많거나 이미지나 동영상 등 어떤 컨텐츠가 올지 모르기 때문에 페이지가 굉장히 버벅일 수 있음 그렇기 때문에 DiaryItem 컴포넌트에 React.memo를 적용
import React from "react";

export default React.memo(DiaryItem);
  • 정상적으로 리렌더링이 일어나지 않는지 확인하기 위해 필터를 변경해도 Highlight가 표시를 통해 아이템 자체가 다시 렌더링이되지 않는 것을 확읺라 수 있음

☑️ EmotionItem 최적화

  • Edit.js 수정하기 페이지에서는 여러 개의 상태를 컨트롤할 수 있는 기능들이 있음
  • 오늘의 일기의 textarea에 내용을 수정하면 오늘의 감정의 Content State가 변화하게 되어 자식 컴포넌트인 EmotionItem까지 리렌더링되고 있음
  • DiaryEditor로 이동하여 오늘의 감정이 어떻게 코드화 되어 있는지 확인하면 EmotionItem을 렌더링하고 있는 것을 확인할 수 있음.
const DiaryEditor = ({isEdit, originData}) => {
  const [emotion, setEmotion] = useState(3);
  const [date, setDate] = useState(getStringDate(new Date()));
  const [content, setContent] = useState(); // state 변화
  
  ...
  
        <section>
          <h4>오늘의 감정</h4>
          <div className="inputBox emotionListWrapper">
            {emotionList.map((it)=>(
              <EmotionItem key={it.emotion_id} {...it} onClick={handleClickEmote} isSelected={it.emotion_id === emotion} />
              // <div key={it.emotion_id}>{it.emotion_descript}</div>
            ))}
          </div>
        </section>
  • 현재 코드 상으로 DiaryEditor가 갖고있는 state가 변화하면 content의 state도 계속 변화하게 됨
  • 그렇기 때문에 EmotionItem 컴포넌트에 React.memo를 적용하여 EmotionItem이 리렌더링되지 않도록 변경하여 최적화
import React from "react";

const EmotionItem = ({
  emotion_id,
  emotion_img,
  emotion_descript,
  onClick,
  isSelected
}) => {
  return (
    <div
      className={[
        "EmotionItem",
        isSelected ? `EmotionItem_on${emotion_id}` : `EmotionItem_off`].join(" ")}
      onClick={()=>onClick(emotion_id)}
    >
      <img src={emotion_img} alt={emotion_descript} />
      <span>{emotion_descript}</span>
    </div>
  );
};

export default React.memo(EmotionItem);
  • 다시 똑같이 리렌더링이 되고 있다면...

useState를 통해서 전달받은 상태 변화 함수가 아니거나 useCallback으로 묶어놓은 함수가 아니라면 기본적으로 컴포넌트가 렌더링될 때 다시 생성되어 React.memo의 강화된 컴포넌트(HOC)가 되어도 렌더링을 발생시킨다

  • DiaryEditor 컴포넌트로 돌아가서 EmotionItem에게 prop으로 전달하고 있는 onClick 함수를 useCallback으로 사용할 수 있도록 만들어줘서 문제를 해결한다.
        <section>
          <h4>오늘의 감정</h4>
          <div className="inputBox emotionListWrapper">
            {emotionList.map((it)=>(
              <EmotionItem key={it.emotion_id} {...it} onClick={handleClickEmote} isSelected={it.emotion_id === emotion} />
              // <div key={it.emotion_id}>{it.emotion_descript}</div>
            ))}
          </div>
        </section>
  • onClick 이벤트에 매핑이 된 handleClickEmote 함수를 수정해야 함
  • handleClickEmote 함수를 재사용하기 위해 useCallback을 적용해 memoization 적용
  // memoization useCallback
  const handleClickEmote = useCallback((emotion) => {
    setEmotion(emotion);
  },[]);
  
  • 일기를 수정해도 EmotionItem이 리렌더링되지 않아 최적화 완료

💬 밀린 포스팅 주말에 몰아쓰기

profile
필요한 내용을 공부하고 저장합니다.
post-custom-banner

0개의 댓글