React 개발자 도구(React dev tools)로 불필요한 렌더링을 없애본 경험
들어가며
React Deep dive를 스터디에서 공부하며 리액트 앱을 디버깅하고 검사할 때 보다 효율적으로 할 수 있는 도구인, React dev tools(React 개발자 도구)라는 것을 처음 접하게되었다. 이번 포스팅에서는 React 개발자 도구의 각종 기능에 대해 설명한 후, 이것을 통해 본인의 블로그의 페이지에서 본래 모르고 있었던 지속적으로 발생하고 있던 불필요한 렌더링을 없앤 경험도 공유하고자 한다.
React Dev tools란?
React 애플리케이션을 디버깅하고 최적화하는 데 도움을 주는 브라우저 확장 도구이다. 브라우저의 확장 프로그램으로써 설치하면 사용할 수 있다.
참고로 React app의 development모드에서만 원활하게 모든 기능을 사용할 수 있으며 production 모드로 빌드되었을시에는 profiler 기능은 비활성화된다.
만일 production 빌드시에는 이런식으로
이 도구를 사용할 수 있을 때는
이런식으로 색깔이 들어오게 된다.
그럼 React dev tools의 기능들인 Components, Profiler 탭을 보는 방법에 대해서 함께 알아보자
Components
Components 탭에서는 현재 리액트 애플리케이션의 컴포넌트 트리를 확인할 수 있으며, 단순히 구조 뿐만이 아니라, props와 내부 hooks 등 다양한 정보를 확인할 수 있다.
Components 탭의 기능
- 컴포넌트 트리의 전체적인 구조를 확인할 수 있다.
- 보통 state, props의 값을 브라우저에서 확인 하려면
console.log(...)이런식으로 로그를 출력하여 확인하는데 컴포넌트 탭에서는 특정 컴포넌트를 그 안에 있는 state, props의 값을 확인할 수 있다.
- 또한 단지 state, props 뿐만 아니라 컴포넌트에서 사용된 훅을 볼 수 있는 hooks 도 있는걸 볼 수 있다.
- state : useState
- Reducer: useReducer
- Context: useContext
- Callback: useCallback
- Ref: useRef
- id: useId
- LayoutEffect: useLayoutEffect
- Effect: useEffect
- 그외의 사용자 훅: use가 빠진채로 보여짐( ex. useCounter면 Counter로)
- 또한 rendered by를 통해 해당 컴포넌트를 렌더링한 부모컴포넌트까지 확인할 수 있다.
- 벌레 모양의 버튼을 클릭하면
log this component data to the console이라는 문자가 뜨는데 이걸 클릭하면 컴포넌트의 관한 데이터를 콘솔에 찍어볼 수 있다.
콘솔에 컴포넌트의 정보가 나타난다.
로 불필요한 렌더링을 없애본 경험/6.png)
Components탭의 대표적인 기능들만을 소개했지만, 이외에도 직접 에러를 발생시킨다거나, 컴포넌트의 state, props값을 임의로 바꿔본다든가 등의 기능 역시 사용할 수 있다.
Profiler
프로파일러는 리액트가 렌더링하는 과정에서 발생하는 상황을 확인하기 위한 도구다. 즉 리액트 애플리케이션이 렌더링되는 과정에서 어떤 컴포넌트가 렌더링됐는지, 또 몇 차례나 렌더링이 일어났으며 어떤 작업에서 오래 걸렸는지와 같은 것을 확인할 수 있다.
프로파일링 시작하기
처음 프로파일러 탭을 열고, 첫번째 버튼 "🔵" 버튼을 누르면 기록이 진행된다. 기록을 시작하면 렌더링 관련 정보들이 자동으로 수집되며, 🔴 버튼을 누르면 수집이 중단되고 결과가 나타난다.
두번째 버튼을 누르면 새로고침 후 동시에 프로파일링이 시작된다 마찬가지로 첫번째 버튼이 🔴로 바뀌며, 이를 누르면 프로파일링이 중단되고, 결과가 나타난다.
Flame Chart
Flame Chart는 특정 커밋에 대한 애플리케이션 상태를 보여준다.
차트의 각 막대는 컴포넌트들을 뜻하며 너비가 넓을 수록 해당 컴포넌트를 렌더링 하는데 오래 걸렸다는 것을 의미한다.
각 컴포넌트에 마우스를 가져다 대면 해당 컴포넌트의 렌더링과 관련된 정보를 확인할 수 있다.
또한 오른쪽위의 화살표 혹은 막대 그래프를 누르면 각 렌더 커밋별로 리액트에서 발생한 렌더링 정보와 발생한 횟수를 확인할 수 있어 의도한 횟수만큼 렌더링이 발생했는지도 알 수 있다.
Ranked
해당 커밋에서 렌더링하는데 오랜시간이 걸린 컴포넌트를 순서대로 나열한 그래프이다.
Flamegraph와의 차이점은 모든 컴포넌트가 아닌 단순히 렌더링이 발생한 컴포넌트만 보여준다는 것이다.
TimeLine
리액트 18이상의 환경에서만 확인할 수 있으며, 시간의 흐름에 따라 리액트가 작동하는 내용을 추적하는데 유용하다. 시간 단위로 프로파일링 기간동안 무슨 일이 있었는지 무엇이 렌더링됐고, 어느 시점에 렌더링됐는지 등에 대해 자세히 확인할 수 있다.
이를 직접 활용하여 불필요한 렌더링 줄여본 경험
문제 상황
내 블로그 페이지에는 글을 검색할 수 있는 SearchPage가 있다.
profiler 탭에서 프로파일링을 시작하고 검색어 입력창에 키워드를 입력하고, 프로파일링을 끝내 보았는데, 문제가 발견되었다.
검색어를 입력만 했는데 검색어에 따른 포스트들을 보여주는 PostItem을 제외하고 이와 관련없는 NavBar, Footer 등 페이지의 모든 요소가 함께 렌더링이 되고 있었다. Ranked로 보니까 시간 역시 제법 차지 하고 있었다.
또한 검색페이지 뿐만이 아니더라도 카테고리 선택 페이지에서도 마찬가지로 카테고리만 선택했는데도 이와 같은 현상으로 불필요하게 렌더링이 발생하고 있었다.
... 왜 이런지 Components 탭으로 Header와 Footer와 같은 컴포넌트를 포함하는 컴포넌트인 Layout 컴포넌트의 정보를 보니 다음과 같았다.
그 전에 참고: Layout 컴포넌트 코드 (모달창, 헤더, 푸터등을 관리한다.)
export default function Layout({ children }: React.PropsWithChildren) {
const [sidebar,setSideBar] = useState(false);
// 사이드바에 관한 useEffect 코드들 (생략)
return (
<Providers>
<main
className={cls(
notoSansKr.className,
opensans.variable,
kanit.variable,
'w-full relative flex flex-col items-center dark:bg-dark-primary dark:text-dark-primary transition-[background]',
)}
>
<Navbar setSideBar = {setSideBar}/>
<div className="w-full flex flex-col items-center">{children}</div>
{sidebar && (<SideBar setSideBar={setSideBar}/>)}
<Footer />
</main>
</Providers>
);
}
Layout 컴포넌트 정보
rendered by PostSearchPage... 참고로 PostSearchPage의 컴포넌트 정보는 다음과 같다.
1번째 State인 검색결과, 2번째 State인 검색 키워드 까지 여기서 관리되고 있었기에 당연히 검색을 할 때마다 PostSearchPage이 렌더링되고 이에 따라 자식 컴포넌트인 Layout역시 같이 렌더링이 되는 것 이었다.
PostSearchPage의 코드를 보면,
export default function PostSearchPage(props: {posts:Post[]}) {
const [posts,setPosts] = useState<Post[]>(props.posts);
const [keyword,setKeyword] = useState<string>('');
useEffect(()=>{
const filteredPosts:Post[] = searchPosts(props.posts,keyword);
setPosts(filteredPosts);
},[keyword, props.posts]);
return (
<Layout>
<div className='w-full mt-8 md:w-4/5 px-8'>
<SearchInput keyword={keyword} setKeyword = {setKeyword}/>
</div>
{/* 검색결과를 보여주는 레이아웃 */}
<PostLayout posts={posts} currentCategory={keyword} theme='search'/>
</Layout>
);
}
Layout이 자식으로 있는걸 볼 수 있다.
해결 방법
이에 대한 해결 방법으로 물론, Layout을 메모이제이션을 통해서 변경이 발생할 때만 렌더링 되도록 최적화 할 수도 있지만, Layout 컴포넌트가 모든 페이지에서 공통으로 사용되고, Layout 자체에서 각 페이지에 의존하는 상태가 없는 점을 고려하여 _app.tsx에서 이를 최상위 레이아웃으로 설정하는 것이 더 적절해 보였다.
_app.tsx
export default function App({ Component, pageProps }: AppProps) {
return (
<ThemeProvider attribute='class' >
<Layout>
<Component {...pageProps} />
</Layout>
</ThemeProvider>
);
}
변경된 PostSearchPage의 return문
return (
<>
<div className='w-full mt-8 md:w-4/5 px-8'>
<SearchInput keyword={keyword} setKeyword = {setKeyword}/>
</div>
{/* 검색결과를 보여주는 레이아웃 */}
<PostLayout posts={posts} currentCategory={keyword} theme='search'/>
</>
);
이와같이 변경하고 다시 프로파일링을 해보니
Layout이 더이상 렌더링이 되지않고, 당연히 그전에 불필요하게 렌더링 되었던 Navbar와 Footer도 렌더링이 되지않는다!
마치며
지금까지 React Devtools의 기능에 대해 알아보고, 필자가 이를 이용해서 본인의 블로그에서 발생되는 불필요한 렌더링을 없앤 경험까지 소개해보았다.
후후.. 지나가는 형님들은 이걸 보며 '뭐야 이 바보는 당연한거 아닌가' 라고 생각할 수도 있겠지만 지금까지 '구현만 되어라' 만 생각했던 구현충인 내가 나름 처음으로 한 최적화? 경험이기에 조금 뿌듯했다.
그리고 React Devtools를 사용하며 기능 자체는 좋았는데, 뭐가 문제인진 모르겠는데 자꾸 중간에 멈추고 먹통이 되어서 짜증났다. 해결되기 전까지는 chrome 개발자 도구 사용하련다.