안녕하세요. IT 엘도라도 에 오신 것을 환영합니다.
글을 쓰는 것은 귀찮지만 다시 찾아보는 것은 더 귀찮습니다.
완전한 나만의 것으로 만들기 위해 지식을 차곡차곡 저장해 보아요.   포스팅 둘러보기 ▼

리액트 (React)

[React] 컴포넌트 렌더링 성능 최적화 Tip (Hook을 사용할 때)

피그브라더 2021. 1. 26. 02:15

개인적으로 React 프로젝트를 진행하면서 각 컴포넌트의 렌더링 성능을 최적화하기 위해 사용했던 필자만의 Tip을 소개하려고 한다. 단 이는 필자만의 Tip이기 때문에 반드시 올바른 방법이라고는 보장할 수는 없다. 이 방법은 "실제로 측정해본 결과 성능이 좋게 나오더라"보다는 "개념적으로 생각했을 때 이렇게 하는 게 성능적으로 효율적이겠다"에 초점을 맞추었다. 또한 최대한 각 컴포넌트를 독립적인 하나의 부품으로 생각하여 적용할 수 있는, 그리고 최대한 다양한 유형의 컴포넌트들에게 적용할 수 있는 최소한의 일관된 규칙을 정립하는 것에 집중하였다.

 

* 만약 더 좋은 방법이나 개선할 만한 부분을 찾아서 댓글로 달아주신다면 매우 감사드리겠습니다.

 

1. 전제 조건

  • 함수형 컴포넌트와 Hook을 사용한다. 즉, 클래스형 컴포넌트는 다루지 않는다. 물론 shouldComponentUpdate 생명주기 메소드를 사용하는 방식으로 적절히 고쳐서 응용하면 아예 도움이 안 되지는 않을 것이다. 그러나 최근 트렌드 상 Hook이 절대적인 인기를 얻고 있기 때문에 가급적이면 Hook을 사용하는 것을 권장한다.
  • Redux를 사용한다면, 마찬가지로 connect 함수가 아닌 Hook을 사용하여 Store에 접근한다.

 

2. 렌더링 성능 최적화 Tip

2-1. 기본 (Default)

React.memo 함수를 이용하여 컴포넌트를 래핑 한다. 즉, 해당 컴포넌트가 전달받는 props 값들의 참조값이 변하지 않았다면 컴포넌트 함수를 다시 호출하지 않고 기존의 결과물을 그대로 사용하도록 한다. 클래스형 컴포넌트에서의 PureComponent와 동일한 역할을 수행하는 일종의 HOC이다.

 

2-2. props가 존재하지 않는 컴포넌트 (예외 ①)

React.memo 함수를 사용하되, 이 함수의 두 번째 인자로 넘기는 비교 함수로는 "() => true"를 사용한다. 이는 props 값들의 참조값 비교 결과가 항상 '같다'라는 것을 의미하여 언제나 기존의 결과물을 그대로 사용하도록 한다. props가 존재하지 않는다면 부모에 의해 리렌더링이 유발되더라도 렌더링 되는 내용이 바뀔 일이 없기 때문이다.

 

그런데 props는 없지만 해당 컴포넌트가 지역 상태를 가지거나(useState) 특정 Context를 참조하는(useContext) 경우는 어떠할까? 이 경우에도 마찬가지이다. React.memo 함수는 부모에 의해 해당 컴포넌트의 리렌더링이 유발되는 경우만을 다룰 뿐이기 때문이다. 어차피 지역 상태가 바뀌거나 특정 Context의 참조값이 바뀌는 경우에는 React.memo 함수와 무관하게 해당 컴포넌트의 리렌더링이 유발된다.

 

2-3. props 중 파라미터(props.match.params)만 렌더링에 영향을 주는 컴포넌트 (예외 ②)

React.memo 함수를 사용하되, 이 함수의 두 번째 인자로 넘기는 비교 함수로는 "comparePropsParams"를 사용한다. 여기서 comparePropsParams는 인자로 전달받는 두 개의 props 객체에 대하여 각각의 match.params가 동일한 값들을 가진 객체인지 비교하도록 직접 구현해야 하는 함수이다. 일반적으로 ID 값에 따라 URL의 파라미터 부분만 달라지는 상세 페이지 같은 곳에 사용하면 좋다.

 

2-4. props 중 쿼리 스트링(props.location.search)만 렌더링에 영향을 주는 컴포넌트 (예외 ③)

React.memo 함수를 사용하되, 이 함수의 두 번째 인자로 넘기는 비교 함수로는 "comparePropsSearch"를 사용한다. 여기서 comparePropsSearch는 인자로 전달받는 두 개의 props 객체에 대하여 각각의 location.search가 동일한 값을 가지는지 비교하도록 직접 구현해야 하는 함수이다. 일반적으로 검색 결과에 따라 URL의 쿼리 스트링 부분만 달라지는 리스트 페이지 같은 곳에 사용하면 좋다.

 

2-5. props 중 특정 값의 깊은 비교가 필요한 경우 (예외 ④)

React.memo 함수를 사용하되, 이 함수의 두 번째 인자로 넘기는 비교 함수로는 "깊은 비교 함수"를 사용한다. 여기서 깊은 비교 함수란 인자로 전달받는 두 개의 객체에 대해 깊은 비교를 수행하는 함수를 말한다. 필자의 경우 react-fast-compare 라이브러리에서 제공하는 isEqual 함수를 사용하였다. 일반적으로 전달받는 객체의 참조값은 변하지만 객체의 내용 자체는 변하지 않는 경우에 불필요한 리렌더링이 유발되는 것을 막기 위해 사용하면 좋다.

 

2-6. 자식 엘리먼트를 렌더링에 사용하는 경우 (예외 ⑤)

React.memo 함수를 사용하지 않는 것이 더 효율적이다. 자식 엘리먼트는 React 엘리먼트의 일종으로 매번 참조값이 달라지는 객체이기 때문에 어차피 항상 리렌더링이 유발되어야 한다. 따라서 불필요한 비교 연산으로 시간을 낭비할 필요가 없다.

 

2-7. 인라인 함수/객체를 렌더링에 사용하는 경우 (예외 ⑥)

React.memo 함수를 사용하지 않는 것이 더 효율적이다. 예외 ⑤와 동일한 이유이다. 인라인 함수/객체는 매번 참조값이 달라지기 때문에 결과가 정해진 비교 연산을 매번 수행하는 것은 시간을 낭비할 뿐이다. 이 경우에는 해당 인라인 함수/객체를 전달해주는 컴포넌트 쪽에서 그 함수/객체를 인라인이 아닌 방식으로 넘겨주도록 수정해주는 것을 고민해봐야 한다. 예를 들어 인라인 함수의 경우 useCallback 함수를 잘 활용하면 필요한 경우에만 참조값이 바뀌는 함수로 만들어줄 수 있다.

 

3. 참고) useState 함수와 useContext 함수

앞서 Tip을 소개하면서 잠깐 언급하긴 했지만, 컴포넌트 렌더링 성능 최적화의 측면에서 이 둘을 정리해 보자. 다시 한 번 강조하자면, React.memo 함수는 오직 부모에 의해 해당 컴포넌트의 리렌더링이 유발되는 경우만 다루는 것이다. 즉, 오직 부모에 의해 리렌더링이 유발되려 할 때 props의 변화만 검사하는 역할이라고 보면 된다. 그리고 useState 함수와 useContext 함수는 이와 완전히 별개로 리렌더링을 유발한다. 다시 말해서, React.memo 함수와는 무관하게 지역 상태가 바뀌거나 참조하는 Context의 참조값이 바뀌면 어차피 리렌더링을 유발하는 것이다. 따라서 React.memo 함수를 사용할 때는 오로지 부모에 의해 리렌더링이 유발되는 순간의 props의 변화만 생각하면 된다. 참고로, Redux에서 제공하는 useSelector, useParams, useLocation 함수는 모두 useContext 기반이기 때문에 useContext와 마찬가지로 생각하면 된다.

 

4. 참고) Redux 사용 패턴에 따른 렌더링 성능 최적화 Tip

4-1. connect 함수를 이용할 때

  • Redux의 Store에서 전달받는 props도 결국은 부모로부터 받는 props이다.
  • connect 함수에 의해 생성되는 Wrapper 컴포넌트가 넘겨주는 것이기 때문이다.
  • 즉, 모든 props는 똑같이 부모로부터 전달받은 것으로 생각해도 무방하다.
  • 따라서 성능 최적화 시 Redux가 전달하는 props를 따로 고려할 필요는 없다.

 

4-2. Hook을 이용할 때 (EX. useSelector 함수, useDispatch 함수)

  • useSelector 함수로 Redux의 Store에서 값을 직접 가져와서 사용한다.
  • 즉, 그 값들은 connect 함수를 사용할 때와 달리 props로 전달받는 것이 아니다.
  • 따라서 렌더링 성능 최적화 시 useSelector 함수로 가져오는 값들을 따로 고려할 필요는 없다.
  • 어차피 그 값들 중 하나라도 변한다면 Hook에 의해 리렌더링이 강제로 유발되기 때문이다.