[React] 렌더링 최적화 방법 (+ React.memo, useMemo, useCallback 알아보기)

GY·2021년 12월 21일
0

리액트

목록 보기
30/54
post-thumbnail
post-custom-banner

인스타그램을 리액트로 클론코딩해보는 것까지 마쳤으니,
최적화에 대해 더 알아보고 싶어졌다.
useMemo, useCallback, React.memo 에 대해서 정리하며 공부해보고, 어떻게 활용할 수 있는지 고민해보려고 한다.

2022.01.29 업데이트

의외인 부분.
이번에 알게 된 것은, 리액트나 자바스크립트에서는 사실 성능최적화가 크게 중요하지는 않다는 것이다.

그 이유는 ...

  1. 성능 최적화로 얻을 수 있는 효율은 최대 약 1초 정도에 불과하다.
    만약 1초의 차이가 큰 구매전환율을 가져오는 업계라면 중요하겠지만, 그게 아니라면 1초를 절약하기 위해 많은 공을 들여 성능 최적화에 힘쓰는 것이 효율적이지 않을 수 있다.
  2. 성능 최적화를 위한 코드가 오히려 가독성을 해칠 수 있다.

그러면, 성능최적화는 어디까지 고려해 사용하는 것이 적절할까?

물론, 프로젝트 규모가 커지거나 제대로 관리하지 않은 코드가 쌓여 불필요한 리렌더링이 많아질 경우 성능에 중요한 영향을 미칠 수 있다. 우리는 이를 방지하기 위해 성능을 해치는 나쁜 코드를 작성하지 않으면 된다.

불필요한 리렌더링을 발생시키지 않도록 코드를 관리하는 방법은 어떤 것들이 있을까?



👑 리액트 렌더링 최적화 방향

리액트는 단방향 하향식 데이터 흐름을 가지고 있고, 데이터들의 변화는 컴포넌트를 리렌더링 시킨다.
1. state와 props의 변경을 최소화
2. state와 props의 변경에 의한 불필요한 하위 컴포넌트 리렌더링을 최소화

이런 방향을 위해 어떤 방법들을 사용할 수 있을까?



👑 리액트 렌더링 최적화 방법

💎 state 선언은 그 state를 '사용'하는 최상위 컴포넌트에서

리액트는 특정 state가 변경되면 그 state가 선언된 컴포넌트와 그 하위 컴포넌트들을 모두 리렌더링한다.


💎 객체 타입의 state는 최대한 분할하여 선언

객체가 크고 복잡한 구조인 경우 분할할 수 있는 만큼 최대한 분할 하는 것이 좋다.
해당 state에서 일부의 프로퍼티만 사용하는 하위 컴포넌트가 있다면, 그 컴포넌트는 해당 프로퍼티가 변경될 때에만 리렌더링 되는 것이 좋다.

왜일까?

분할하지 않고 통째로 객체 state를 사용하면, 하위컴포넌트는 일부의 프로퍼티만 사용하지만 다른 프로퍼티 값이 업데이트 될 때에도 리렌더링이 발생하게 되기 때문이다.


💎 컴포넌트를 매핑할 때에는 key값으로 index를 사용하지 않는다.

중간에 다른 요소가 삽입되었을 때 그 중간 이후의 요소들은 전부 인덱스가 다시 변경된다.

따라서 key값이 변경됨에 따라 리마운트가 일어난다.
또한 데이터가 key와 매치가 되지 않아 서로 꼬이는 부작용도 발생한다.


💎 하위 컴포넌트의 props로 객체를 넘겨주는 경우 새 객체 생성 주의

하위 컴포넌트의 props 값으로 객체를 넘겨주는 경우, 컴포넌트 안에서 생성자 함수나 객체 리터럴 등으로 새로 객체를 생성해 넘겨주는 것을 주의해야 한다.

선언된 props나 state에 참조하지 않고 아예 새로운 객체가 하위 컴포넌트로 전달된다면, 메모이제이션이 불가능하다. 새로 생성된 객체는 이전 객체와 다른 참조 주소를 가진 객체이기 때문이다.

따라서 state를 그대로 하위 컴포넌트에 넘겨준 다음 필요한 데이터 가공을 그 하위 컴포넌트에서 해주는 것이 좋다.



Memoization
이전 값을 메모리에 저장해 동일한 계산의 반복을 제거해 빠른 처리를 가능하게 하는 기술
앞으로 공부할 세가지는 이 Memoization을 기반으로 작동한다.




props값이 변하지 않으면 컴포넌트 리렌더링 방지

💎 React.memo

Highter-Order Components (HOC)로,
컴포넌트를 인자로 받아 새로운 컴포넌트를 반환하는 함수

컴포넌트가 같은 props를 받을 때 같은 결과를 렌더링한다면 React.memo를 사용해 불필요한 컴포넌트 렌더링을 방지할 수 있다.
이 때 리액트는 마지막에 렌더링된 결과를 재사용한다.

  • props가 변경됐는지 아닌지만 체크하기 때문에 React.memo가 감싼 함수형 컴포넌트가 함수 내부에서 useState나 useContext같은 훅을 사용하고 있다면 state나 context가 변경될때마다 리렌더링된다.
const CounterButton = ({ color }) => {
	const [count, setCount] = useState(1);  

	return (
    <button
      color={color}
      onClick={() => setCount(state => state.count + 1)}>
      Count: {count}
    </button>
  );
}

export default React.memo(CounterButton);

이런 경우를 생각해보자.
Modal 컴포넌트에서 props로 전달된 title이 변경되면 해당 컴포넌트는 리렌더링된다.
이 때 하위 컴포넌트인 CoutnerButton 컴포넌트도 함께 리렌더링된다.
Counterbutton에 전달되는 color props는 바뀌지 않기 때문에, props값이 변한 것이 아니라면 CounterButton 컴포넌트도 함께 리렌더링 되지 않도록 React.memo를 사용해주었다.

const Modal = ({ title }) => {
	const [btnColor, setBtnColor] = useState('yellow');  

	return (
    <div>
			<p>{title}</>
			<button onClick={() => { setBtnColor('black') }}>Change Color!</button>
			<CounterButton color={btnColor} />
		</div>
  );
}

props가 변경됐는지 어떻게 체크하길래 불필요한 렌더링이 발생할까?

state가 변경되거나 새로운 컴포넌트가 렌더링 되는 시점에서 shallow copy를 통해 같은 값인지 판단하고 렌더링 여부를 결정한다.

따라서 같은 값의 props라도 컴포넌트의 state가 변경되면 shallow copy에 의해 새로운 값으로 인식한다.
props가 함수나 객체, 배열같은 reference type이라면 같은 참조가 아니라면 새로운 값으로 판단한다. 이럴 경우 불필요하게 리렌더링이 발생하는 것이다.

React.memo, 언제 써야 할까?

  1. Pure Functional omponent
  2. Rendering이 자주 일어날 경우
  3. re-rendering이 되는 동안에도 계속 같은 props값이 전달될 경우
    4 UI element의 양이 많은 컴포넌트의 경우

react dev tools로 렌더링 상황을 보면서 적용여부를 결정하는 것이 좋겠다.


React.memo의 한계점

하위 컴포넌트로 함수 props를 전달해줄때를 생각해보자.
함수의 내용이 같더라도 참조값이 다르다면 리렌더링 될때마다 새로운 참조값을 갖게 된다.
앞서말한 이유로 참조값이 다르다면 React.memo를 사용했더라도 이것을 다른 props로 인식해 리렌더링한다.
즉, memoization을 했지만 리렌더링은 그대로 발생해 메모리를 낭비할 뿐 본래의 목적은 달성하지 못한다.

React.PureComponent

클래스형 컴포넌트에서는 React.memo와 같은 역할을 수행하는 것이 React.PureComponent이다.
얕은 비교를 통해 props와 state의 참조값이 같다면 리렌더링을 방지해준다.

class CounterButton extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = { count: 1 };
  }

  render() {

함수 메모이제이션

💎 useMemo

함수의 결과 값을 memoize하여 불필요한 연산을 없앤다.
함수 호출 이후 리턴 값을 memoize하며, 두번째 파라미터인 의존성 배열의 요소가 변경 될 때마다 첫번째 파라미터의 callback 함수를 다시 생성한다.

왜 사용할까?

특정 상황에서만 동작되어야 하는 함수가, 컴포넌트의 렌더링 조건에 따라 지속적으로 실행되는 경우 이것을 막기 위해 사용한다.

useMemo는 메모이즈된 값을 리턴하는 훅이다.
인자로 함수와 의존 값을 받는데, 의존 인자중 하나라도 변경되면 값을 재 계산한다.
의존인자에 아무것도 전달되지 않는다면 렌더 시마다 항상 값을 새롭게 계산하여 리턴한다.

function NameTag(props) {
  return useMemo(
    () => <div>{props.name}</div>
  ,
    [props.name]
  )
}

💎 useCallback

사용 전
const increment1 = () => setCount1(c => c + 1);
사용 후
const increment1 = React.useCallback(() => setCount1(c => c + 1), []);
useMemo는 특정 값을 재사용하기 위한 훅이었다면,
useCallback은 특정 함수를 재사용하기 위한 훅이다.

자식 컴포넌트에 함수를 props로 줄 때는 useCallback을 사용하여 리렌더링이 안되도록 해주는 것이 좋다.

useCallback 함수를 통해 callback 함수를 동일한 callback 인스턴스로 설정해주면 위에서 언급한 문제가 해결된다.
이것은 항상 같은 함수 인스턴스를 반환하기 때문에 React.memo가 정상 기능을 수행하게 된다.



Reference

profile
Why?에서 시작해 How를 찾는 과정을 좋아합니다. 그 고민과 성장의 과정을 꾸준히 기록하고자 합니다.
post-custom-banner

0개의 댓글