티스토리 뷰

반응형

Material-UI는 리액트에서 사용되는 UI 컴포넌트 라이브러리로서 리액트에 Google Material Design을 손쉽게 적용 할 수 있게 도와줍니다.

리액트 개발자로서 수많은 프로젝트에 Material-UI를 사용한 경험이 있기 때문에 Material-UI가 훌륭한 라이브러리라는 것을 잘 알고 있습니다. Material-UI의 수많은 컴포넌트, 그리드 시스템, 일관된 UI를 바탕으로 매우 빠르게 앱을 개발할 수 있습니다.

몇년간 Material-UI를 만족하며 사용해왔지만 개인적인 오픈소스 프로젝트인 Ammo를 개발하면서 심각한 문제를 발견하였습니다.

Ammo에 대한 간단한 소개 : Ammo는 HTTP 요청을 캡쳐하여 네트워크 부하 테스트 엔진 스크립트로 변환하는 오픈소스 프로젝트입니다. (Gatling과 유사한 부하테스트 도구)

문제의 배경

Ammo 프로젝트는 아래 그림 처럼 단순한 리스트를 출력하는 앱입니다.

Ammo 앱 리스트

리스트의 각 항목은 클릭으로 펼치거나 접을 수 있도록 되어있습니다.

Ammo 앱 리스트를 펼쳤을 때

리스트 항목을 펼쳤을 때는 몇가지 정보를 출력합니다. 복잡하지도 않고 화려하지도 않습니다. 왼쪽에는 HTML Div 태그로 HTTP 패킷 정보를 보여줍니다. 오른쪽에는 관련된 코드 조각을 출력하고 있습니다. (React syntax highlighter를 통해 Formatting하여 출력합니다.)

이렇게 단순한 리스트로 구성했음에도 끔찍한 성능저하 현상이 발생했습니다. 도대체 왜 이러한 문제가 생기는 걸까요?

첫번째 조치

Syntax highlighter 라이브러리가 문제의 원인이라 생각하여, 직접 code-snippets-highlighting 기능을 작성하여 다시 테스트 해보았습니다. 그리고 Syntax highlighter 라이브러리가 성능 저하의 원인이 아니라는 것을 알 수 있었습니다.

얼마나 성능이 나빴는지 보여드리자면, 아래 그림은 리스트에 50개 항목을 추가했을 때 모습입니다. (setInterval을 통해 300ms 마다 1개 항목을 추가)

보다시피 단지 50개의 항목만으로도 고통스러울 정도의 성능저하를 보여줍니다.

단순히 HTML Div를 렌더링하는데 성능이 이렇게 저하되는 것은 도저히 이해하기 힘듭니다. 좀 더 구체적으로 문제를 찾아봅시다.

마녀사냥 : 리액트 에디션

제 직관으로는 리액트가 문제의 원인이라고 생각했었습니다. 지금까지 수많은 앱들이 불필요한 렌더링을 유발시키며 성능이 떨어지는 사례를 봐왔기 때문입니다. 그래서 처음으로 시도한 것은 리스트를 최적화하는 것이었고, 다음 2가지를 진행했습니다.

  1. 리스트의 항목에 Index가 아닌 유일한 키를 부여
  2. memo를 사용하여이미 렌더링된 항목이 다시 렌더링 되지 않게 하기

앱을 최적화하고 나서 앱을 프로파일 해보았습니다.

프로파일 결과

프로파일 결과 알 수 있는 것은

  1. 왼편 하단을 보면 memo가 잘 동작하고 있음을 알 수 있습니다. 한번 렌더링된 항목은 메모리에서 사라지지 않기 때문에 리렌더링으로 인한 성능저하를 막게됩니다.
  2. 오른편 하단을 보면 리스트에 새로운 항목이 추가될 때, 새 항목을 렌더링 하는데 많은 시간이 소요된다는 것을 알 수 있습니다.

프로파일 오른쪽 상단에 존재하는 그래프를 보면, 시간이 지날수록 렌더링 속도가 저하되는 것을 보여주고 있습니다. 처음에는 리스트 항목을 렌더링하는데 약 100ms가 소요되었다면, 새로운 항목이 추가될수록 렌더링 성능이 느려지게 되고 결국 500ms 가까운 시간을 사용하고 있습니다.
시간에 따른 렌더링 시간

리스트 항목의 개수가 새 항목의 렌더링 시간에 영향을 주는 이유가 무엇일까요? 단순한 Div 태그들을 렌더링하는데 500ms나 사용한다니 말도 안됩니다!

하나의 항목을 렌더링하는 데 무슨 일이 일어나는지 프로파일을 상세히 살펴보도록 합시다.

새 항목을 렌더링하는 것에 관한 프로파일 결과

위 이미지에서 우리가 주목해야할 부분은 2가지 입니다.

  1. 오른쪽 아래에 React-syntax-highlighter를 볼 수 있습니다. 처음에는 이게 성능저하의 원인이 아닌가 의심했었죠. 프로파일 결과를 살펴보면 렌더링 성능에 큰 영향을 주지 않는 다는 것을 알 수 있습니다. 상당히 짧은 시간에 렌더링이 완료됩니다.
  2. 왼쪽 아래를 살펴보면 "Headers"를 렌더링 하는데 많은 시간을 쓴다는 것을 알 수 있습니다.

헤더가 무엇이길래 시간을 이렇게 쓰는 것일까요. Ammo 앱에서 헤더는 아래 그림과 같은 단순한 엘리먼트입니다.

Ammo Header

헤더는 단지 2개의 인라인 텍스트 일 뿐입니다. <Header> 컴포넌트 코드는 다음과 같이 단순하게 구성되어 있습니다.

<Box className={classes.values}>
    <Typography variant="subtitle2">
        <span>{key}</span> :
        <span className={classes.headerValue}>
            "{value}"
        </span>
    </Typography>
</Box>

몇 개의 단순한 Div와 Span을 렌더링하는 것 뿐인데, 도대체 무슨 일이 생긴 걸까요?

모든 문제의 원인은 Material UI

절망적이게도 이 문제를 밝혀내기 위해 많은 시간을 사용하였습니다. 다양한 것들을 시도해보았고, 여러 포럼을 둘러보기도하고, 어떻게 리액트가 이렇게 심각한 문제를 일으킬 수 있는지 원인을 찾아보고자 노력하였습니다. 그러다가 문득 아무 생각 없이 <Box> 컴포넌트를 <div>로 변경해 보았습니다.

<div className={classes.values}>
    <Typography variant="subtitle2">
        <span>{key}</span> :
        <span className={classes.headerValue}>
            "{value}"
        </span>
    </Typography>
</div>

놀랍게도 성능이 개선되는 것을 발견하였습니다. <Box>, <Typography>와 같은 나머지 Material-UI 컴포넌트도 가능한 제거하였고 그 결과는 다음과 같습니다.

Material-UI 제거 후 프로파일 결과

다음과 같은 몇가지를 알 수 있습니다.

  1. 500개의 항목(~1000번의 렌더)로 테스트하였습니다.
  2. 가장 긴 렌더링 시간은 ~100ms 입니다. (500ms를 사용했던 것에서 큰 성능향상이 있습니다)
  3. 리스트 항목들을 렌더링하는 시간에 차이가 발생하지 않습니다. 리스트에 항목을 추가하더라도 렌더링 시간이 늘어나지 않습니다.

이제 리스트에 10배나 많은 항목을 추가하였음에도 상당히 부드럽게 동작합니다. 성능이 들쭉 날쭉 하지도 않구요.

결론

Material-UI는 훌륭한 라이브러리이지만, 심각한 성능 저하 문제를 일으킬 수 있습니다. 단순한 폼이나 일반적인 웹사이트는 큰 문제가 없을지 모릅니다만, Material-UI를 사용하다 성능저하 현상이 발생하면 먼저 Material-UI를 의심해보세요.

Material-UI의 성능 문제는 처음 지적되는 것이 아닙니다. 이미 많은 사람들이 Material-UI의 성능을 지적해왔습니다. 다행 스럽게도 Material-UI를 대신할 수 있는 UI 컴포넌트 라이브러리들이 존재합니다. 특히 무거운 페이지를 개발한다면 Material-UI 대신 다른 UI 프레임워크의 사용을 고려하는 것도 좋은 방법입니다.

References

댓글