Puffin's DevLog

TIL: React에서의 성능 최적화

React 공식문서 (영문, 한글) 를 다시 읽어보면서 정리.

성능최적화...?

지금까지 개발을 공부하면서 성능최적화를 고민해본 적이 있나 생각해본다. 초급 개발자인 나는 일단 돌아가는 코드를 짜는 것이 우선이었다. 그리고 나서 성능과 속도를 생각했는데, 고려한 것은 아래와 같다.

  • 서버에 데이터를 여러 번 혹은 중복하여 요청하지는 않았는지 확인한다.

    • 최대한 한번의 요청으로 원하는 정보를 불러올 수 있는 방법을 고민한다. URLSearchParams 같은 것을 사용할 수 있다.
    • 한번에 데이터를 전부 다 불러오지 말고, 일단 화면에 표시되는 데이터만을 가져온다.
  • React라면 굳이 업데이트 하지 않아도 되는 상태는 위쪽으로 끌어올린다.

    • 상태가 바뀌면 아래 컴포넌트가 모두 리렌더링된다는 점을 고려한다.
    • 로그인 정보 같은 건 한번 저장되면 잘 바뀌지 않으니까 전체 페이지가 로드될 때 한번만 업데이트 되도록 한다.
  • state에서 값이 변경되는 경우(setState) 데이터는 immutable해야 한다. 그래야 추적 비용을 저렴하게 만든다. 새로운 객체를 만들기 때문에 객체에 대한 참조가 변경하였는지만 확인하면 된다.

  • Redux 같은 상태관리 라이브러리를 별도로 사용한다면 캐시 기법을 활용한다. 상태를 스토어에 저장해두었다가 사용한다.

그 외에는 사진 크기나 데이터를 조절하는 것 정도가 있었다. 이 정도만으로도 내가 만들었던 규모의 프로젝트에서는 큰 문제가 없었던 것 같다. 물론, 정식 서버에서 큰 데이터를 불러와본 적도 없고, 네트워크 탭을 확인해야 할만큼 문제가 있는 상황이 없었다.

그!런!데! 오늘 면접에서 리액트의 성능최적화와 관련한 질문을 받았다. 위에 한 이야기라도 잘 했으면 모를까, 이상한 말만 웅앵웅 쵸키포키... 했다. 멘탈이 털리고 후회되는 마음에 리액트 공식문서를 들어가니 질문에서 들었던 몇 가지 키워드가 딱! 하니 있다.

일단, 내가 알고 있는 것 -- 리액트에서 어떻게 DOM을 렌더링하는지를 정리해 본다.

React 비교조정

패캠 수업에서 강사님이 화면에 열심히 그려가면서 설명했던 그 개념이 바로 비교조정(Reconciliation)이다. 비교조정이란, React가 무엇을 어떻게 업데이트하는지를 결정하는 알고리즘이 채택한 개념이다. DOM 트리를 비교하는 것은 시간복잡도가 많이 필요한 일이기에, (간단하게 말하자면) 아래 두 가지 가정에 기반하여 어떤 요소/컴포넌트를 렌더링해야 하는지 결정한다.

  • 다른 타입을 가진 두 엘리먼트는 다른 트리를 만들어 낼 것이다.
  • key prop을 이용해서 변경되어야 할 렌더링과 변경되지 말아야 할 렌더링이 무엇인지를 알아낼 수 있을 것이다.

먼저, 타입이 다르면 (<a>에서 <div>로 바뀐 경우나 <Component />에서 <NewComponent />로 변경한 경우) 이전 트리를 완전히 버리고 새로운 트리를 그린다.

같은 타입의 엘리먼트라면 변경된 속성만을 갱신한다. 또, React에서는 map 등 배열을 순회해서 엘리먼트를 반환하는 코드를 많이 사용하게 되는데, 이때 key 같은 속성을 이용한다면 변경되지 않은 요소는 그대로 두되 변경된 key값을 가지는 요소만 변경/추가한다.

사실, React의 큰 장점이 바로 DOM을 직접 조정하지 않아도 필요에 따라서 화면을 그리고 또 업데이트해준다는 점이다. 하지만 동작원리를 어느 정도 파악해야, 원하는 대로 화면이 렌더링되지 않을 때 혹은 원하는 것보다 훨씬 더 많이 렌더링이 될 때 문제를 해결할 수 있다.

그래서, 비교조정을 어떻게 피한다고? (1): shouldComponentUpdate

그런데, 구조에 따라서 비교조정이 일어나야 할 상황이지만 UI를 다시 렌더링하지 않아야 하는 상황이 있을 수 있다. 가령, setState가 실행되었지만 state의 값은 변경되지 않았을 수도 있다. 그런 상황에서도 React 컴포넌트는 새로 화면을 그린다.

이런 경우에, React의 라이프사이클을 직접 건드려서 해결할 수 있다. 컴포넌트에서 다시 렌더링하는 프로세스가 시작하기 전에 트리거되는 함수인 shouldComponentUpdate를 사용할 수 있다.

shouldComponentUpdate는 변경 후 props와 state를 인자로 받으며, 기본으로는 true를 반환한다. 컴포넌트를 업데이트할 필요가 없는 경우에는 false를 반환하면 된다고 한다.

shouldComponentUpdate

그림을 보면, 부모 컴포넌트에서 shouldComponentUpdate에서 false를 반환하면 자식 컴포넌트들 또한 비교조정 및 리렌더링되지 않는다. (c2와 c4, c5) 부모 컴포넌트에서 shouldComponentUpdate가 true를 반환한 상황이라면, SCU가 true인지 false인지를 먼저 확인하고, 그 다음 DOM이 동일한지 아닌지를 확인한다. c3은 SCU가 true이고 DOM이 동일하지 않다. 자식 컴포넌트인 c6과 c8은 SCU가 true이지만 c6은 DOM이 동일하지 않기에 리렌더링되고, c8은 동일하기에 비교는 했지만 다시 렌더링이 되지는 않는다. c7은 SCU가 false를 반환했으므로 다시 업데이트되지 않는다.

또, 비교조정을 어떻게 피한다고? (2): PureComponent

shouldComponentUpdate를 매번 사용하는 것이 번거롭다면, React.PureComponent를 사용하는 방법도 있다. React.PureComponent는 흔히 사용되는 React.Component와 거의 비슷하지만 shouldComponentUpdate를 다루는 방식이 다르다. PureComponent의 경우는 prop과 state를 얕은비교를 통해서 비교한 뒤, 변경된 게 있을 때만 리렌더링한다. (얕은 비교란, 참조값을 비교한다는 의미이다.) 이 경우 shouldComponentUpdate를 굳이 신경쓰지 않아도 된다. 다만, depth가 깊은 객체나 배열이 있는 경우나 mutable한 객체가 있는 에는 문제가 발생하므로 사용할 수 없다.

Loading script...