7-11. 최적화 하기 (OPTIMIZATION)

2022. 8. 30. 11:33React/한입 크기로 잘라 먹는 리액트(React.js)

오늘은 프로젝트를 최적화 해보자!

컴포넌트와 연산간의 최적화를 해보자. 

자 그럼 어떤 부분이 연산을 낭비시키고 있는지 찾아보아야한다.

 

우선 정적분석, 동적분석으로 확인할 수 있는데 이에대해 먼저 알아보자.

 

정적분석, 동적분석이란?

출처 : https://isc9511.tistory.com/33

더보기

* 정적 분석 : 소프트웨어가 실행되지 않는 환경 하, 소스 코드 의미를 분석, 결함을 찾아내는 분석 기법

 

 

 

* 정적 분석 분류

분류 내용
소스 코드 분석 시큐어 코딩 가이드 기반 취약한 항목의 존재 확인
시멘틱 분석 인터페이스 & 함수 호출의 구조적 취약점을 분석
바이너리 구조 분석 역공학 분석 도구를 이용

 

 

 

* 동적 분석 : 소프트웨어가 실행중인 환경 하, 다양한 입/출력 데이터, 사용자 상호작용 변화들을 점검

 

 

 

* 동적 분석 분류

분류 내용
행위 탐지 기법 실행 시 취약/이상 현상을 탐지
샌드박스 기법 가상화 환경에서 직접 실행을 통해 이상 현상을 분석
모의 해킹 실제 화이트 해커가 직접 진단

 

 

 

* 정적 / 동적분석 비교

비교 정적 분석 동적 분석
점검 대상 프로그램 소스 코드 실제 애플리케이션
평가 기술 기존의 패턴 비교 HTTP 메세지의 변경 점검
점검 단계 애플리케이션 개발 시점 애플리케이션 운영 시점
결과 소스의 라인별 결과 표시 요청 / 응답에 따른 결과

- 보통 둘은 상호 보완적 차원에서 함께 많이 사용됨

 

그럼 우리는 먼저 동적분석적인 방법으로 한번 분석해보자.

아래와 같이 개발자모드의 Components에서 렌더링 될때 하이라이트 처리가 되도록 설정해주자.

그러면 현재 월만 바꿔도 아래와 같이 상단 탭들이 새로 렌더링 되는 것을 확인할 수 있다.

 

날짜를 변경시키는 컴포넌트는 Home.js 컴포넌트인데,

여기서는 MyHeader에서 leftChild와 rightChild의 상태변화함수로 날짜를 변경할 수 있도록 구성되어있다.

그래서 우리가 월을 변경하는 버튼을 클릭하면 Home컴포넌트의 state가 변경되어 이 state가 다시 렌더링된다는 것을 알 수 있다.

 

자 아래와 같이 그럼 필터와 버튼들을 가지고 있는 diaryList.js컴포넌트로 이동해보겠다.

Home컴포넌트가 렌더링 되면, Home컴포넌트의 자식요소인 diaryList는 당연 렌더링 되기는 한다!

근데, 필터링도 그의 자식요소이기 때문에  ControlMenu도 렌더링 된다.

그래서 우리가 해결해야하는 부분은 ControlMenu가 다시 렌더링 되는 부분인데!

그래서 우리는 이전에 배웠던 React.memo 기능을 사용하여 전달받은 prop이 변경되지 않으면 리렌더링 하지 않도록 memoization 하면된다.

그러면 ControlMenu 부분은 깜빡거림이 없어진 것을 찾을 수 있다.

이를 확실하게 알아내기 위해 useEffect로 렌더링 일어날때마다 콘솔에 출력될 수 있도록 한번 테스트 해보겠다.

다음 날짜로 클릭하여도 새로 렌더링 되지 않음을 확인할 수 있다.

근데 이상한 점이 있을 수 있는게, onChange함수는 useCallback과 같은 것들로 재사용하게 하지 않으면 부모 컴포넌트가 리렌더링 되면서 변경되어 React.memo가 정상적으로 동작되지 않는다고 배웠는데

지금은 onChange 함수가 별도의 useCallback 처리를 하지 않았음에도 불구하고, rerendering이 발생하지 않는다는것을 알 수 있다.

 

그 이유는?

이전에 최적화 배울때에는 지금과 같은 setState함수가 아니였다!

현재

현재 ControlMenu에 전달한 onChange함수들은 useState가 반환하는 상태 변화 함수였던 것이었다.

 

그럼 만약 useState가 반환하는 상태변화 함수가 아니라

아래와 같이 중간에 handling할 수 있는 함수를 만들어 준 다음에,

이때 함수를 prop으로 전달하게 된다면

아래와 같이 날짜를 바꿀때 마다 Control Menu가 출력되면서, React.memo가 정상적으로 동작하지 않게 된다.

 

이것이 무슨 차이이냐면

이런식으로 생성 된 함수는 컴포넌트가 리렌더링 할때, 다시 생성이 되는 것이다.

그래서 React.memo할때 다른 prop이라 인지하여 리렌더링 하게 되는것이지!

 

그러나 아래와 같이 useState로 반환받은 상태변환 함수는 동일한 ID를 보장한다.

그래서 이것은 당연하게 useCallback처리가 되어 반환되는 함수라고 생각하면 되는 것이다.

 

그래서 아래의 함수들을 아래와 같이 힘들게 handler함수까지 만들 필요가 없는 경우에

useCallback까지해서 재사용하지 말고,

아래와 같이 당연히 재사용 되는 상태변화 함수 자체를 이용하면 더 편하게 컴포넌트를 최적화 할 수 있음을 알 수 있다.

 

그래서 필요없는 함수들을 지우고 아래와 같이 코드를 구성하면 된다.

DiaryList.js 코드

더보기
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import MyButton from "./MyButton";
import DiaryItem from "./DiaryItem";

const sortOptionList = [
  { value: "latest", name: "최신순" },
  { value: "oldest", name: "오래된 순" },
];

const filterOptionList = [
  { value: "all", name: "전부다" },
  { value: "good", name: "좋은 감정만" },
  { value: "bad", name: "안좋은 감정만" },
];

// value - select가 어떤것을 선택하고 있는지
// onChange - select가 변경했을 때 바꿀 함수
// optionList - select 안에 들어갈 option
const ControlMenu = React.memo(({ value, onChange, optionList }) => {
  useEffect(()=> {
    console.log("Control Menu");
  })
  return (
    <select
      className="ControlMenu"
      value={value}
      onChange={(e) => onChange(e.target.value)}
    >
      {optionList.map((it, idx) => (
        <option key={idx} value={it.value}>
          {it.name}
        </option>
      ))}
    </select>
  );
});

const DiaryList = ({ diaryList }) => {
  const navigate = useNavigate();
  // 정렬 (초기값 - latest)
  const [sortType, setSortType] = useState("latest");
  const [filter, setFilter] = useState("all");

  const getProcessedDiaryList = () => {
    const filterCallBack = (item) => {
      if (filter === "good") {
        return parseInt(item.emotion) <= 3;
      } else {
        return parseInt(item.emotion) > 3;
      }
    };

    const compare = (a, b) => {
      if (sortType === "latest") {
        return parseInt(b.date) - parseInt(a.date);
      } else {
        return parseInt(a.date) - parseInt(b.date);
      }
    };
    // 이는 diaryList를 JSON화 시켜서 문자화 시켜버린다!
    // 그래서 다시 parse를 하면 다시 복호화 시켜준다!
    // 이렇게 하고 copyList에 넣어주는 것이다
    // 그래서 값만 건드리고 원본은 건드리지 않고 수행했음을 알 수 있다
    const copyList = JSON.parse(JSON.stringify(diaryList));
    const filteredList =
      filter === "all" ? copyList : copyList.filter((it) => filterCallBack(it));

    const sortedList = filteredList.sort(compare);
    return sortedList;
  };

  return (
    <div className="DiaryList">
      <div className="menu_wrapper">
        <div className="left_col">
          <ControlMenu
            value={sortType}
            onChange={setSortType}
            optionList={sortOptionList}
          />
          <ControlMenu
            value={filter}
            onChange={setFilter}
            optionList={filterOptionList}
          />
        </div>
        <div className="right_col">
          <MyButton
            type={"positive"}
            text={"새 일기쓰기"}
            onClick={() => navigate("/new")}
          />
        </div>
      </div>

      {getProcessedDiaryList().map((it) => (
        <DiaryItem key={it.id} {...it} />
      ))}
    </div>
  );
};

DiaryList.defaultProps = {
  diaryList: [],
};

export default DiaryList;

 

 

다음으로는 아래의 필터를 변경할때,

아이템의 위치만 바꾸면 되는데, 항목들 모두가 리렌더링 되고 있는 것을 확인할 수 있다.

이를 해결하기 위해 DiaryItem 컴포넌트를 한번 분석해 보자

DiaryItem 컴포넌트는 DiaryList컴포넌트의 자식이다.

DiaryList에서 필터값을 변경해주면, DiaryList의 state가 변경되기 때문에 업데이트가 발생하고,

자식컴포넌트인 DiaryItem 컴포넌트도 리렌더링 되는 것이다.

사실 Item의 rerendering은 굉장히 위험하다. 왜냐하면 이미지를 가지고 있는 요소이기 때문에.

이미지, 동영상, 많은 텍스트를 가진 경우에는 페이지가 버벅일지 모르기 때문이다!

그래서 DiaryItem 에 React.memo를 한번 적용해 보자.

그러면 필터는 잘 적용이 되지만, 아이템 자체가 렌더링 되지 않는 것을 확인할 수 있다.

 

자 그러면 List자체의 최적화는 완료가 되었고,

상태 페이지에서는 State를 변화시키는 부분이 전혀 없기 때문에 

수정하기 페이지로 한번 가보자!

 

다음과 같이 일기를 계속 수정하게 된다면, 아이템들이 모두 리렌더링 되고 있는 것을 확인할 수 있다.

자 그럼 우리가 DiaryEditor로 이동하여

오늘의 감정 부분에서 EmotionItem을 렌더링 하고 있는 것을 확인할 수 있다.

우리가 오늘의 일기를 수정하게 된다면, content state가 계속 변화하게 된다.

DiaryEditor가 가지고 있는 state가 계속 변화하게 되니까 자식 컴포넌트인 EmotionItem도 rerendering되는 것이다.

그렇기 때문에 EmotionItem로 이동하여 React.memo로 재생성하지 않도록 설정을 해보자.

근데 그런데도 똑같이 문제가 발생하고 있다.

이 이유는 전달받는 prop중에 함수가 있다!

함수는

useState를 통해 전달받은 상태변화함수가 아니거나

useCallback으로 묶어놓은 함수가 아니라면

기본적으로 컴포넌트 렌더링 될 때 다시 생성되어서 

React.memo에 강화 된 컴포넌트에도 렌더링을 발생시킨다.

 

그래서 우리는 DiaryEditor 컴포넌트로 돌아가서

onClick함수도 useCallback으로 재사용할 수 있도록 만들어 보자.

 

그럼 onClick에 맵핑되는 handleClickEmote로 이동해보자

우선 재사용을 위해 useCallback으로 memoization하기 위해 Callback함수를 전달해보자.

그런데 이 setEmotion함수에서 emotion을 받기 때문에 여기서는 가장 최신의 state를 참조할 필요가 없다.

그래서 우리는 함수의 update를 전달할 필요가 없다.

그래서 여기서 마무리를 하고, 오늘의 일기를 작성해도 오늘의 감정부분을 보면 리렌더링 되지 않고 있는 것을 확인할 수 있다.