리액트에서 렌더링 속도를 올리기 위한 성능 최적화

렌더링 속도를 올리기 위한 성능 최적화 방법

속성값(props)이나 상탯값(state)이 변경되면 리액트가 자동으로 컴포넌트 함수를 이용해서 화면을 다시 그린다. 리렌더링은 다음과 같은 과정을 거친다.

  1. 이전 렌더링 결과를 재사용할지 판단한다.
  2. 컴포넌트 함수를 호출한다.
  3. 가상 돔끼리 비교해서 변경된 부분만 실제 돔에 반영한다.

1번의 단계에서는 propsstate의 이전 값과 이후 값을 비교하고 이후 단계를 생략할 수 있다. 클래스 컴포넌트에서는 shouldComponentUpdate 메서드가 이 역할을 하고, 함수 컴포넌트에서는 React.memo를 이용해서 구현할 수 있다.

2번 단계에서는 1번 단계에서 렌더링이 필요하다고 판단되면 컴포넌트 함수를 호출한다. 컴포넌트 함수를 호출해서 새로운 가상 돔을 만들고 이전에 만들었던 가상 돔과 비교해서 변경점을 찾는다. 그리고나서 변경된 부분만 실제 돔에 반영한다.

평상시에는 성능 최적화를 고민하지말고 코딩한 후에, 성능 이슈가 생기면 그때 고민해서 적용하는게 좋다.

React.memo로 렌더링 결과 재사용하기

React.memo 함수로 감싼 컴포넌트는 속성값 비교 함수가 호출되서 참, 거짓을 반환한다. 참이면 렌더링을 멈추고, 거짓이면 리렌더링을 진행한다.

렌더링 성능이 중요한 상황에서는 컴포넌트를 React.memo 함수로 감싸서 컴포넌트 함수의 실행과 가상 돔의 계산을 생략할 수 있다.

- React.memouseMemo훅의 차이점

React.memo는 Higher-Order Components(HOC)이다. 즉, 컴포넌트를 인자로 받아 새로운 컴포넌트로 return해주는 함수이다. 일반 컴포넌트는 props를 UI에 활용하는 반면, HOC는 인자로 받은 컴포넌트로 새로운 컴포넌트로 만든다. React.memo도 컴포넌트를 인자로 받아서 같은 props를 받을 때 같은 결과를 렌더링 하는지를 비교하여 불필요한 컴포넌트 렌더링을 방지한다. 다만, 비교 시 오직 props만 체크한다. 컴포넌트 자체를 재사용할 수 있게 해준다.

useMemo는 메모이즈된 값을 return하는 훅이다. 인자로 함수와 의존성 배열을 받는다. useMemo는 의존성 배열 인자 중에 하나라도 변경되면 값을 재 계산한다. 이를 통해 매 렌더링 마다 소요되는 불필요한 계산을 피할 수 있다. 값만 재사용할 수 있게 해준다.

const memoizedValue = useMemo(() => computeValue(a,b), [a,b])

메모이제이션 기법은 초기에는 적용하지 않는게 좋고 성능 이슈가 발생하면 그때 적용하는게 좋다. 메모이제이션이 너무 많이 되면 오히려 성능이 떨어지는 이슈가 있다.

둘다 props가 변하지 않으면 인자로 넘긴 함수는 재 실행되지 않고 이전에 저장된 결과를 반환한다는 점이 동일하다.

속성값과 상탯값을 불변 변수로 관리하는 방법

  • 함수의 값이 변하지 않도록 관리하기

    • useStateuseReducer의 상탯값 변경 함수는 변하지 않기 때문에 onChange와 같은 속성에 상탯값 변경함수를 입력하면 매번 함수를 새로 생성하지 않는다. onChange={setCount}
    • 이벤트 처리 함수에서 상탯값 변경 외에 다른 처리도 필요하다면 useCallback()을 사용할 수 있다.
    • 의존성 배열에 빈 배열을 입력하면 항상 고정된 값을 가진다.
  • 객체의 값이 변하지 않도록 관리하기

    • 컴포넌트 내부에서 객체를 정의해서 자식 컴포넌트의 속성으로 사용할 경우, 자식 컴포넌트는 객체의 내용이 변경되지 않더라도 속성 값이 변경되었다고 인식한다.
    • 렌더링과 무관한 항상 같은 값을 가지는 객체는 컴포넌트 밖에서 상수 변수로 관리하는 게 좋다.
    • 만약 상탯값을 이용해서 속성값을 계산해야 하는 상황이라면 useMemo를 사용해서 필요한 경우에만 속성 값이 변경되도록 할 수 있다.
// useMemo 적용 전
<Select options={FRUITS.filter(item => item.price <= maxPrice)} />

// 적용 후
const fruits = useMemo(() => FRUITS.filter(item => item.price <= maxPrice), [
  maxPrice,
])
// ...
<Select options={fruits} />

가상 돔에서의 성능 최적화

  • 요소의 타입 또는 속성을 변경하는 경우

    • 요소의 타입을 변경하면 해당 요소의 모든 자식 요소도 같이 변경된다. 자식 요소가 변경되지 않아도 실제 돔에서 삭제되고 다시 추가되기 때문에 비효율적이다.
    • 요소가 삭제되고 다시 추가되는 과정에서 상탯값도 초기화된다.
    • 요소의 속성값만 변경하면 해당하는 속성만 실제 돔에 반영한다.
  • 요소를 추가하거나 삭제하는 경우

    • 일반적으로 새로운 요소를 추가하거나 삭제하면 해당 요소만 실제 돔에 추가 또는 삭제되고 기존 요소는 건드리지 않는다.
    • key 속성 값을 이용하면 같은 키를 가지는 요소끼리만 비교해서 변경점을 찾는다. 리액트는 key속성을 통해 렌더링을 효율적으로 진행한다.

정리

무조건 useMemo, useCallback, React.memo를 사용하는 것은 좋지 않다. 성능을 최적화하는 코드는 가독성이 안 좋고 유지보수 비용을 증가시킨다. 성능 이슈가 발생한 부분만 최적화 하자.


출처

  1. 실전 리액트 프로그래밍

Written by@[Ykss]
고이게 두지 않고 흘려보내는 개발자가 되자.

GitHubInstagramLinkedIn