React 메모제이션을 이용한 웹 페이지 성능개선(feat. lighthouse)

Doodream·2022년 1월 5일
2

ChawChaw - 프로젝트

목록 보기
8/8
post-thumbnail

ChawChaw 프로젝트 React 메모제이션 적용

리액트에는 메모제이션이라는 기능이 있다. React.memo ,useCallback, useMemo

세가지 기능으로 컴포넌트의 재실행을 방지 함으로서 여러번 실행되는 시간을 줄이고 리렌더링을 최소화 하는 것이다.

React.memo

이 기능은 상위 컴포넌트에 사용할 수록 좋다,
기본적으로 리액트는 상태변수라고 정의된 변수가 변경되면 컴포넌트가 재실행되며 변경된 상태를 컴포넌트에 반영한다.
이때 상위 컴포넌트에서 이러한 과정이 실행된다면 그아래 종속되어있는 하위 컴포넌트 모두가 재실행된다.

따라서 하위 컴포넌트에서는 상위컴포넌트에서 넘겨받는 props들이 변경이 일어나지 않는다면 굳이 재실행하지 않아도 된다.

이러한 개념에서 React.memo는 해당 컴포넌트를 감싼다. 그리고 부모로 부터 넘겨받는 props들이 변경되었는지 안되었는지
얕은 비교를 통해서 비교한다.

이러한 얕은 비교를 제대로 하기위해 useCallback, useMemo가 사용된다.
자세한 동작과정

이때는 사용하지 말자

위 두개의 코드에서 PostModal은 자식 컴포넌트에게 props를 거의 넘겨준다. 즉, 자식 컴포넌트 입장에서는 PostModal이 바뀌면 자식컴포넌트에 전달되는 props들이 매번 바뀌게 된다. 이렇게 되면 굳이 props들을 비교하는 의미가 없다. 의미 없는 얕은 비교연산을 수행하는것이다. 이럴때에는 사용할 필요가 없다.
PostModal.tsx

const PostModal: React.FC<PostModalProps> = (props) => {
  const now = new Date();
  const dateArr = props.regDate.substring(0, 10).split("-");
  const stDate = new Date(
    Number(dateArr[0]),
    Number(dateArr[1]),
    Number(dateArr[2])
  );
  const endDate = new Date(
    now.getFullYear(),
    now.getMonth() + 1,
    now.getDate()
  );
  const pastDays =
    (endDate.getTime() - stDate.getTime()) / (1000 * 60 * 60 * 24);
  const repCountry = props.repCountry;
  const repLanguage = LocaleLanguage[props.repLanguage] || "";
  const repHopeLanguage = LocaleLanguage[props.repHopeLanguage] || "";
  const country = props.country;
  const language = useMemo(() => {
    return props.language.map((item) => LocaleLanguage[item] || "");
  }, [props.language]);
  const hopeLanguage = useMemo(() => {
    return props.hopeLanguage.map((item) => LocaleLanguage[item] || "");
  }, [props.hopeLanguage]);

  return (
    <PostModalBox>
      <PostModalImage src={`${props.imageUrl}`} />
      <PostUserName>{props.name}</PostUserName>
      <PostModalActive id={props.id} isLike={props.isLike} />
      <PostModalInfoList
        title="I am from"
        values={country}
        mainValue={repCountry}
      />
      <PostModalInfoList
        title="I can speak"
        values={language}
        mainValue={repLanguage}
      />
      <PostModalInfoList
        title="I want to speak"
        values={hopeLanguage}
        mainValue={repHopeLanguage}
      />
      <PostModalContent content={props.content} />
      <PostModalSocialList
        title="Contact to me"
        faceBookUrl={props.facebookUrl}
        instagramUrl={props.instagramUrl}
      />
      <PostModalInfo
        regDate={String(pastDays)}
        views={props.views}
        likes={props.likes}
      />
    </PostModalBox>
  );
};

export default React.memo(PostModal);

이때 사용하자

LikeAlarm.tsx

const MLikeAlarm: React.FC = () => {
  const newLikes = useAppSelector((state) => state.chat.newLikes);

  const alarmlikeMessages = newLikes.map((item) => {
    return (
      <AlarmChatBox key={item.regDate + item.name}>
        <ChatBox
          imageUrl={`/Layout/heart.png`}
          regDate={item.regDate}
          sender={item.likeType}
          context={`${item.name}님이 ${item.likeType} 하셨습니다.`.substring(
            0,
            20
          )}
          type={LIKEALARM_TYPE}
        />
      </AlarmChatBox>
    );
  });

  return (
    <PushAlarmBox>
      {newLikes.length > 0 ? (
        alarmlikeMessages
      ) : (
        <EmptyAlarm title="No new likes Messages" />
      )}
    </PushAlarmBox>
  );
};

const LikeAlarm = React.memo(MLikeAlarm);
export { LikeAlarm };

위 코드를 보면 props로 아무것도 받지 않는다. 즉, 상위 컴포넌트가 재실행되어도 굳이 재실행될 필요가 없다. 재실행되도 같은 실행이 반복되기 때문이다.
이럴경우 React.memo를 사용해서 얕은 비교를 통해 상위 컴포넌트가 재실행되어도 재실행되지 않도록 하자.

useCallback

useCallback 은 함수의 참조값을 저장한다. 기본적으로 자바스크립트는 함수또한 객체이기 때문에 참조값을 저장한다. 따라서 컴포넌트가 재실행되면 함수에 대한 참조값이 바뀌게 된다. 이 참조값을 메모한다음에 컴포넌트가 재실행될때 deps 배열의 값이 바뀌지 않는다면 새로 함수를 생성하지 않고 기존에 있던 참조값 불러와 함수를 실행시킨다.

이럴때 사용하자

위에서 말했듯이 React.memo는 얕은 비교를 한다. 만약 함수를 자식컴포넌트의 props로 넣게 되면 이 함수는 매번 재실행되어 새로운 참조값을 가지기 때문에 같은 함수이더라도 새로운 참조값을 가진 새로운 함수로서 자식컴포넌트에 전달된다. React.memo를 씌운 얕은 비교를 하는 자식 컴포넌트 입장에서는 변하지도 않는 함수때문에 재실행을 해야하는 것이다. 이때 useCallback 을 사용할 수 있다.

ChatRoom.tsx

const handleChangeInput: ChangeEventHandler<HTMLTextAreaElement> =
    useCallback((e) => {
      setMessage(e.target.value);
    }, []);

 const scrollToBottom = useCallback(() => {
    if (!chatMessageBox.current) return;
    chatMessageBox.current.scrollIntoView({
      behavior: "auto",
      block: "end",
      inline: "nearest",
    });
  }, []);

  useEffect(() => {
    scrollToBottom();
  }, [mainChatMessages.length, scrollToBottom]);
  return (
    <Outline>
      <Inner>
        <ChatRoomHeader
          selectLanguage={selectLanguage}
          setSelectLanguage={setSelectLanguage}
        />
        {isViewChatList ? (
          <ChatListWrapper>
            <ChatList />
          </ChatListWrapper>
        ) : (
          <>
            <MessageContainer>{messageBoxContent}</MessageContainer>
            <MessageInput
              onChange={handleChangeInput}
              sendMessage={sendMessage}
              value={message}
            />
          </>
        )}
      </Inner>
    </Outline>
  );

위 코드를 보면 MessageInputprops를 넘기는데 handleChangeInput을 넘긴다. 이 매번 재실행하는 것을 방지하기 위해서 MessageInput 컴포넌트에 함수 값을 저장하기 위해서 useCallback을 사용한다.

또한 useEffect() 에서도 마찬가지다. useEffect안에서 함수를 사용하면 deps에 함수를 넣어야 하는데, 굳이 함수가 매번같은 함수라면 함수값을 저장해서 useEffect 의 콜백함수가 쓸데없이 재실행되는 것을 막아야한다.

이런 비슷한 경우로 useCallback 로 묶은 함수안에 함수를 사용할때에도 마찬가지 이유로 안의 함수를 useCallback으로 묶는다.

이럴때 사용하지 말자

함수의 매개변수가 매번 바뀌는 경우 어짜피 매개변수 때문에 함수는 useCallback을 씌우더라도 재생성 되기 때문에 저장할 필요가 없다. 어짜피 재생성되어버리는 경우에는 사용하지 않아도 된다.

useMemo

객체는 참조값을 가지므로 매번 새로운 값을 가진다.
ChangeLanguageDropDown.tsx

const MChangeLanguageDropDown: React.FC<ChangeLanguageDropDownProps> = (
  props
) => {
  const { bodyBackgroundColor, bodyFontColor } = useContext(ThemeContext);
  const initialValue = LanguageLocale[props.selectLanguage[0]].toUpperCase();
  const options = useMemo(() => ["Korean", ...Object.keys(LanguageLocale)], []);

  return (
    <ChangeLanguageDropDownBox>
      <DropDown
        fontWeight="600"
        fontSize="1em"
        width="110px"
        height="30px"
        options={options}
        initialValue={initialValue}
        setValues={props.setSelectLanguage}
        value={initialValue}
        index={0}
        type={NOMAL_TYPE}
        backgroundColor={bodyBackgroundColor}
        color={bodyFontColor}
      >
        <ImEarth />
      </DropDown>
    </ChangeLanguageDropDownBox>
  );
};
const ChangeLanguageDropDown = React.memo(MChangeLanguageDropDown);

위 코드를 보면 매번 options는 정해진 값을 갖는다. 따로 다른 값에 의존해서 변하는 값도 아니므로 매번 새로운 참조값을 생성해 낼 필요는 없다.
따라서 객체를 useMemo로 감싸 참조값을 저정한다. 그러면 자식 컴포넌트에서 받을때 React.memo 에 의해 재실행을 방지 할 수 있다.

LightHouse를 통한 웹 성능 향상

Performance

리덕스와 메모제이션을 적용하면서 성능향상을 측정하고자 크롬의 LightHouse를 사용했다. 이부분중에서 Performance는 6가지의 다양한 기준으로 측정을 한다.
길게 말하기 보단 함축적으로 정의해서 사용자에게 보여지는 렌더링 시간을 측정한다. 만약 내가 메모제이션을 잘 적용했다면 성능향상으로 이부분에서 측정이 될 것이다.

Accessibility

이부분에서 많은 개선을 했다.

시각장애인들이 웹페이지를 사용할때 페이지 리더를 사용한다. 이때 해당 태그들에 대한 설명을 리더가 제대로 수행할 수 없다면 접근성의 저하로 나타난다.

input, textarea tag 에 label 추가

li, ul 태그 연계

button tag 안에 text가 없다면 aria-label 혹은 text 추가

등등...

❗️ 배경색과 텍스트의 명암비를 강조하는데 이부분은 신뢰적이지 않은 것 같다. 주황생이랑 흰색이면 엄청 구분잘되는데...

Best Practices

error console 모두 제거

@font-face: font-display swap 추가

  • 폰트파일을 가져오기전에 사용자가 보여지는 폰트로 바꾸라는 지시

    ❗️ unload 이벤트를 사용한적이 없는데 왜자꾸 바꾸라는 건지 모르겠다.

SEO

title, description, opengraph, twitter, ...

_app.tsx

 <DefaultSeo
            title={"ChawChaw 언어를 교환합시다.🗣"}
            description={"대학내 교환학생 언어교환 채팅 어플리케이션입니다."}
            canonical="https://www.chawchaw.vercel.app"
            openGraph={{
              type: "website",
              locale: "en_IE",
              title: "ChawChaw 언어를 교환합시다.🗣",
              description: "대학내 교환학생 언어교환 채팅 어플리케이션입니다.",
              images: [
                {
                  url: "https://i.ibb.co/m0NY7yQ/image.jpg",
                  width: 800,
                  height: 600,
                  alt: "ChawChaw 소개 이미지",
                },
              ],
              url: "https://www.chawchaw.vercel.app",
              site_name: "ChawChaw",
            }}
            twitter={{
              handle: "@chawchawTwitter",
              site: "chawchaw.vercel.app",
              cardType: "summary",
            }}
            additionalLinkTags={[
              {
                type: "image/png",
                sizes: "32x32",
                href: "/Layout/chaw.png",
                rel: "icon",
              },
            ]}
            additionalMetaTags={[
              {
                name: "viewport",
                content:
                  "viewport-fit=cover, width=device-width, initial-scale=1",
              },
            ]}
          />
          <Component {...pageProps} />

_document.tsx

<Html lang="kr">
        <Head>
          <link
            href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@200;300;400;600;700;900&display=swap"
            rel="stylesheet"
          />
          <link rel="canonical" href="https://www.chawchaw.vercel.app" />
        </Head>
        <body>
          <div id="root">
            <Main />
            <NextScript />
          </div>
          <div id="notification"></div>
        </body>
      </Html>

LightHouse 측정 결과


  • /home

    Before

    After

  • /account/login

    Before

    After

  • /account/signup

    Before

    After

  • /account/signup/webMailAuth

    Before

    After

  • /account/profile

    Before

    After

  • /account/setting

    Before

    After

  • /chat

    Before

    After

  • /post

    Before

    After

  • /manage/users

    Before

    After

  • /manage/users/detail

    Before

    After

  • /manage/statistics

    Before

    After

즉, 성능향상 전과 후를 비교하면 모든 점수를 더 한후 향상치를 %화 해본다면...!!!
두구구둑두구두구구

4026 -> 4282 평균 6.36% 상승!

기대했던 Performance의 성능향상은

  • /home : 96 -> 100
  • /manage/users : 97 -> 100
  • /manage/users/detail: 92->93
  • /manage/statistics : 97 -> 100

와 같다.

profile
일상을 기록하는 삶을 사는 개발자 ✒️ #front_end 💻

0개의 댓글