사실 react에서 성능 최적화를 위한 훅으로 react.memo
와 useCallback
이 있다는 것을 안지는 생각보다 오래되었다. 그런데, 이 부분 항상 공부해보고 써봐야겠다고 생각하고 있었지만 미뤄두고 있었다. memo, callback
을 공부할 수록 어렵게 다가왔던 부분이 잘쓰기가 힘들다는 부분이었기 때문이다. 잘못된 사용은 오히려 이전의 상태와 현재를 비교하는 알고리즘만 작동되어 성능을 더 좋지 않게 만드는 부작용을 초래할 수 있다는 부분이 조금 무서웠다. 그런데, 얼마전 내가 진행하고 있는 블로그 프로젝트가 성능상에서 좋지않은 점수가 나오기도 했고 이 부분 꼭 한번 짚고 넘어가려고 했던 부분이라 이번에 공부하고 적용해보고자 한다.
공식문서에 따르면, React.memo
는 고차 컴포넌트이다. 고차 컴포넌트는, 컴포넌트를 가져와 새 컴포넌트를 반환하는 함수인데 간단히 forwardRef
를 생각하면 될 것 같다. (memo
도 이처럼 사용하는구나 ! 생각하고 넘어가자) 또, React.memo
를 사용함으로써 결과를 메모이징하도록 래핑해 성능의 향상을 누릴 수 있다고 한다. 즉, 이전에 렌더링한 결과와 현재가 같다면 새롭게 렌더링하지 않는다는 것이다.
다만, React.memo
는 props
변화에만 영향을 준다. React.memo
로 감싸진 함수 컴포넌트에서 상태나, context
가 있을 경우 상태와 context
가 변할때마다 렌더링 된다.
위의 React.memo
가 영향을 받는 조건을 다시 보자.
React.memo
는 props
변화에만 영향을 준다. props
가 같을 경우에만 - 달라진 부분이 없는 경우에만 - 불필요한 리렌더링을 건너 뛴다.이 두가지를 종합해보면 다음과 같지 않을까?
동일한
props
를 계속해서 받는 경우!
이는 위를 다시보자. 일단 계속해서 다른 props
를 받아야하는 경우에는 절대로 사용하지 말아야 할 것이다. (props
변화가 계속해서 이루어지므로 메모이제이션이 작동할 일이 없지 않나)
또, 상태변화 context
변화가 큰 부분에서도 memo
의 사용은 큰 의미가 없어보인다. 상태가 변할 경우, 또는 context
가 변할 경우에 리렌더링이 일어나는 것은 불가피하기 떄문이다.
계속해서 진행하고 있는 블로그 프로젝트에서 React.memo
를 적용할만한 부분을 찾았다. props
가 잘 변하지 않고, 메모이제이션이 충분히 가능할만한 부분을 말이다. 그나마, 여기였다.
일반적으로 사용하는 이 Header
부분이었는데 Header
부분은 constansts
에서 따로 props
를 불러와 렌더링된다. 살짝 아쉬운 부분은 왼쪽의 로고부분을 제외한 오른쪽 부분은 다크모드의 적용 및, 활성화 되어있는 탭을 표시해주기 위한 효과를 주려는 부분, 모바일 뷰 버전에서 dropdown
을 펼치는 부분 때문에 상태가 너무 많았다. 그래서 잘 적용이 될 지는 모르겠지만, 한번 해보자.
일단, 예상으로는 왼쪽의 OTTER-LOG
부분은 무조건 메모이제이션이 될 것같다. 오른쪽의 Nav
는 위와같은 이유로 조금 힘들 수 있을 것 같다.
리액트 프로파일러로 변한 부분을 확인해봤다.
실행 조건은 무조건 동일하게 메인페이지가 렌더링된 상태에서 블로그 버튼을 누르고, 블로그 페이지로 이동하는 과정에서의 리렌더링을 파악했다.
정확히 이렇게!
이는 memo
를 사용하지 않았을 때의 결과다. Header
컴포넌트만 보면, 0.6ms
가 걸렸고 아래 하위 항목을 보면, otter-log
로고와 navList
가 모두 리렌더링 된다.
조그맣게 /projects
로 보이는 부분이 projects
버튼이고 아래 버튼들이 search
버튼들이다. 사실 지금 props
를 전달받긴 하는 nav
부분이 이전과 똑같더라도 - projects 버톤과 otter-log 로고 등의 부분
- 이 모두 리렌더링 되는구나를 알 수 있다.
import Link from "next/link";
import React from "react";
import Nav from "./nav";
const Header = () => {
return (
<header className='fixed z-50 flex h-20 w-full items-center justify-between bg-white px-5 dark:bg-black md:max-w-[1080px]'>
<h1>
<Link href='/'>
<a className='text-3xl font-extrabold italic'>OTTER-LOG</a>
</Link>
</h1>
<Nav />
</header>
);
};
export default React.memo(Header);
Header.displayName = "Header";
이런 방식으로, Header
컴포넌트 전체에 memo
를 걸어버렸다.
그리고 렌더링이 어떻게 바뀌었는지 확인해보았다.
Header
부분과 Nav
부분은 메모이제이션에 성공해 리렌더링이 되지 않았다. 전체 레이아웃이 렌더링되는 속도를 체크해보면 memo
이전 6.1ms
에서 3.3ms
로 성능을 조금 더 높일 수 있었다. 3ms
라고 생각하면 아주 작지만 이런 티끌만한 것들이 모여서 아주 큰 것을 만들어 낸다는 걸 생각하자. 게다가, 비율로따지면 50%가 준거다. (이거 오류가 아닐까? 싶을정도로 내 생각보다 너무 크게 달라졌다.)
그런데, 위 부분 일단 뭐가 뭔지 모르니까 Header
컴포넌트 전체에 걸었다. 이제 조금 세세하게 들어가보자.
NavList
컴포넌트가 계속해서 re-render
되고 있는데 한번 걸어보면 어떨까?
Otter-Log
로고는 왜 자꾸 리렌더링 되는걸까? 이부분은 절대 달라지지 않는다!
이 두가지를 생각해보자.
우선, NavList 컴포넌트에도 memo
를 걸어보자.
import Link from "next/link";
import { useRouter } from "next/router";
import React from "react";
type props = {
query: string;
title: string;
};
const NavList: React.FC<props> = ({ query, title, onClickHandler }) => {
const router = useRouter();
return (
<li>
<Link href={query}>
<a
className={`
${
router.pathname.includes(query) &&
"text-blue-900 dark:text-yellow-200"
}
font-semibold hover:text-blue-500 dark:hover:text-yellow-300 md:text-xl`}
onClick={onClickHandler}
>
{title}
</a>
</Link>
</li>
);
};
export default React.memo(NavList);
NavList.displayName = "NavList";
위에, navList
들이 계속해서 리렌더링되는게 거슬렸던 나는 그냥 아무생각없이 이렇게 memo
를 걸었다. (참, 혹시 몰라 props로 받는 onClickHandler은 useCallback으로 wrapping 했다.)
Header, Nav
는 위의 상태와 같지만 놀랍게도 Layout
컴포넌트의 렌더링 시간이 증가해버렸다.
이 부분은 예상했던데로 상태를 계속해서 사용, 감지하고 있는 부분이여서 memo
가 생각처럼 먹히지 않았다. 그리고 무언가 좋은 예시처럼, 잘못 memo
를 걸었을 때 성능이 떨어지는 모습을 볼 수 있었다.
역시 함부로 쓰면 안돼… 😅
그러면, 두번째 문제로 들어가자. 위의 otter-log
는 왜 자꾸 리렌더링 되는거지?
저 부분은 달라지는 게 없는 부분인데 거슬린다.
import Link from "next/link";
import React, { memo } from "react";
import Logo from "./logo";
import Nav from "./nav";
const Header = () => {
return (
<header className='fixed z-50 flex h-20 w-full items-center justify-between bg-white px-5 dark:bg-black md:max-w-[1080px]'>
{/* <h1>
<Link href='/'>
<a className='text-3xl font-extrabold italic'>OTTER-LOG</a>
</Link>
</h1> */}
<Logo />
<Nav />
</header>
);
};
export default memo(Header);
Header.displayName = "Header";
사실 이문제 답을 알고 있었다. 얼마전, virtual DOM
에 깊숙이 들어가다 diff
알고리즘에 대해 공부한 적이 있었다. (요즘 그를 너무 과신하고 있다) 그런데, otter-log
부분이 계속해서 리렌더링 되고 있는 부분의 코드를 살펴보면 이는 컴포넌트로 만들어져 있지 않다. 컴포넌트로 만들어져 있지 않으므로, 이는 diff
알고리즘에 적용될 수 없다. 따라서, 이 부분은 원래 memo를 전혀 사용하지 않아도 알아서 memo가 되어야 하는 부분인데 계속해서 리렌더링 되고 있었던 것이다.
이게 컴포넌트를 작게 만드는 이유였다. (이거 얼마전에 고민하고 있었다)
단순히, 컴포넌트를 나눠주자 마자 저 부분은 아래처럼 리렌더링되지않고 메모이제이션 되었음을 확인할 수 있었다.
의미없이 반복되는 컴포넌트가 하나 더 있다. Footer
컴포넌트다.
위 부분을 말하는데, 위 부분은 다크모드가 아닌이상 절대 바뀌지 않는다. 어떤 props
을 받지도, 또 상태가 변경되지 않는다. 그런데 리렌더링 된다.
이 부분은 메인페이지에서 → Projects
를 눌렀을때 측정했다. (Blog페이지는 무한스크롤로 첫 렌더링때 이 부분이 보여지지 않아 다른 페이지로 측정했다. )
이 부분을 memo
로 수정해보자.
import { memo } from "react";
const Footer = () => {
return (
<footer className='mt-16 flex w-full flex-col items-center justify-center'>
<div>
<a href='https://github.com/otterp012'>
<svg
fill='black'
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 30 30'
width='30px'
height='30px'
className='dark:fill-white'
>
<path d='M15,3C8.373,3,3,8.373,3,15c0,5.623,3.872,10.328,9.092,11.63C12.036,26.468,12,26.28,12,26.047v-2.051 c-0.487,0-1.303,0-1.508,0c-0.821,0-1.551-0.353-1.905-1.009c-0.393-0.729-0.461-1.844-1.435-2.526 c-0.289-0.227-0.069-0.486,0.264-0.451c0.615,0.174,1.125,0.596,1.605,1.222c0.478,0.627,0.703,0.769,1.596,0.769 c0.433,0,1.081-0.025,1.691-0.121c0.328-0.833,0.895-1.6,1.588-1.962c-3.996-0.411-5.903-2.399-5.903-5.098 c0-1.162,0.495-2.286,1.336-3.233C9.053,10.647,8.706,8.73,9.435,8c1.798,0,2.885,1.166,3.146,1.481C13.477,9.174,14.461,9,15.495,9 c1.036,0,2.024,0.174,2.922,0.483C18.675,9.17,19.763,8,21.565,8c0.732,0.731,0.381,2.656,0.102,3.594 c0.836,0.945,1.328,2.066,1.328,3.226c0,2.697-1.904,4.684-5.894,5.097C18.199,20.49,19,22.1,19,23.313v2.734 c0,0.104-0.023,0.179-0.035,0.268C23.641,24.676,27,20.236,27,15C27,8.373,21.627,3,15,3z' />
</svg>
</a>
</div>
<div>
<span>Copyright ⓒ. All rights reserved</span>
</div>
</footer>
);
};
export default memo(Footer);
메모만 넣어주었을 뿐인데 바로 메모이제이션 되었다. 이부분은 아주 쉬운 부분이였기 때문에 바로 가능했던 것 같다. props, state, context
가 모두 없는 부분이니까!
(그런데, 개인적인 궁금증으로 이거 diff 알고리즘에 따라서 자동적으로 memo되어야 하는 부분아닌가..? 싶은데 모르겠다.)
진짜 좋은데… 이거 맨날맨날 쓰고 싶은데 조심할 필요가 있는 것 같다. 특히, 중간에 잘못 쓴 부분에 대해서 생각보다 막대한 피해를 입은 것 처럼 느껴졌다. (안써도 될 부분이었으니까) 다만, 이런식으로 실제로 시간을 재보기도 하고 어떤 상황에 좋을지도 고민해보니까 또 써볼만 한 것 같기도 하고 그렇다. 아니 이거 써야만 할 것 같다. 이렇게 조금만 신경써도 많이 좋아졌는데? 싶으니까
아쉬운점은 이번에 useCallBack 관련해선 할 수 있는게 따로 없었다. 흠 시간을 재 보거나 할 부분이 없었고 함수를 props로 받는 컴포넌트가 딱 두개 있었는데 하필 둘다 상태값도 같이 props로 받아왔다. callback으로 묶어주던지 묶어주지 않던지 메모이제이션이 되지 않았다. 좀 아쉬웠다. 이번에 callBack도 같이 적용해보려고 했는데..! callBack은 다음에 눈으로 내가 성능을 측정해서 바뀌는 부분이 있을때 새로운 포스트를 쓰는걸로 해야겠다.