Frontend/ReactJS

컴포넌트 성능 최적화

findmypiece 2021. 3. 3. 11:19
728x90

컴포넌트 성능 저하는 일반적으로 불필요한 컴포넌트 리렌더링 때문에 발생하는데

리렌더링 되는 상황은 아래와 같다.

1. props 가 변경되었을 때

2. state 가 변경되었을 때

3. 부모 컴포넌트가 리렌더링 될 때

4. forceUpdate 함수가 실행될 때

6. useSelector 로 구독하는 값이 변경되었을 때

1, 2번은 변경된 상태에 따라 컴포넌트도 변화해야 하기 때문에 당연히 리렌더링 해야 하고

4번은 필요에 의해 강제로 리렌더링을 수행시키는 것이기 때문에 최대한 지양하면 된다.

그런데 3번을 보면 부모컨포넌트가 리렌더링 된다고 해도

어떤 자식 컴포넌트는 변경되는 것이 없이 때문에 리렌더링이 필요하지 않을 수 있다.

바로 부모 컴포넌트에서 받아오는 props 가 바뀌지 않은 경우가 그렇다.

6번의 역시 잘못 구현하면 값이 변경되지 않았는데도 리렌더링이 발생할 수 있다.

여기에서 말하는 불필요한 렌더링이 바로 이것들이며 우리는 이것을 방지하면 된다.

3번을 먼저 보면 방법은 간단하다.

아래와 같이 해당 자식 컴포넌트 export 부분을 React.memo()로 감싸주면 된다.

export default React.memo(Todos);

이렇게 하면 React는 최초 컴포넌트를 렌더링할때 그 결과를 메모라이징하고

다음 렌더링이 일어날 때 props가 같다면 메모라이징된 내용을 재사용하게 된다.

다만 React.memo 는 함수형 컴포넌트에서만 사용하는 것이 적절하고

클래스형 컴포넌트에는 그에 맞는 방법이 별도로 있으니 그걸 활용하면 된다.

또한 컴퍼넌트에 동일한 props가 자주 전달되거나

무겁고 비용이 큰 연산이 있는 경우에만 사용하는 것이 좋고

그 외에 경우에 잘못 사용하면 성능이 오히려 악화될 수 있다.

렌더링될 때 props가 다른 경우가 대부분인 컴포넌트의 경우

대부분 메모라이징된 내용이 사용되지 않을텐데

불필요하게 props 비교만 수행하게 될 것이기 때문이다.

하지만 여기에서 끝나는게 아니다.

부모 컴포넌트에서 props로 넘겨주는 값이 단순한 값이면 상관없는데

자식 컴포넌트에서 사용할 이벤트 함수를 부모 컴포넌트에서 생성해서 넘겨주는 상황이라면

props 로 넘겨주는 값이 함수 객체가 되는 셈인데

함수 객체일 경우 값이 아닌 인스턴스를 비교하게 된다.

기본적으로 부모 객체가 리렌더링 되는 경우 함수형 컴포넌트도 재수행 되기 때문에

그곳에 포함된 함수 객체 생성 로직도 다시 수행된다.

이에 동일한 내용의 함수라도 인스턴스가 바뀌게 되고

변경된 인스턴스 props 로 넘기게 되기 때문에

자식 컴포넌트에 React.memo 설정을 했더라도 리렌더링이 발생하게 된다.

이를 해결하기 위해서는 동일한 함수 인스턴스를 재사용 하게 하면 되는데

아래와 같이 함수객체 생성을 useCallback 함수로 감싸면 된다.

import {useCallback} from 'react';

const dispatch = useDispatch();
const onIncrease = useCallback(() => dispatch(increase()), [dispatch]);
const onDecrease = useCallback(() => dispatch(decrease()), [dispatch]);

useCallback 의 첫번째 인자는 생성하고 싶은 함수를 넣고

어떤 값이 변경되었을 때 함수를 새로 생성할지 배열로 명시한다.

두번째 인자는 [] 처럼 빈 배열을 선언할 수도 있는데

이 경우 초기 렌더링시에만 함수를 생성하고 계속해서 재사용하게 된다.

위에서는 리덕스를 사용하는 상황에서

자식 컴포넌트의 이벤트 함수에 디스패치 함수를 연결하는 예인데

두번째 인자로 지정한 [dispatch] 의 경우

리덕스 스토어가 변경되는 상황(앱이 재시작되거나 초기렌더링이 실행될 경우)이 아니라면

바뀌지 않을 것이고 결국 []로 지정하는 것과 동일할 것으로 생각된다.

주의할 점은 함수 안에서 state 값이 활용될 경우

함수 내에서 최신값을 참조한다는 보장이 없으므로

이때는 함수를 재사용하지 않아야 한다.

다르게 말하면 함수 내부에서 state 값이 활용될 경우

useCallback의 두번째 인자에 그 값을 반드시 명시해야 한다.

이 규칙은 useMemo에도 동일하게 적용된다.

6번에 말하는 useSelector 도 결국 함수나 객체는 인스턴스 비교라는 개념이 중요하게 작용한다.

useSelector는 리덕스를 사용할 경우 리덕스 연동전용 컴포넌트에서 현재 상태를 구해오는 Hook 함수이다.

해당 함수의 인자는 리덕스의 특정 상태값을 리턴하는 함수를 정의하게 되는데

이렇게 얻어오는 상태값이 변경되었다면 해당 함수가 포함된 컴포넌트가 리렌더링 된다.

반대로 상태가 변경되지 않았다면 리렌더링 되지 않아야 하는데

아래와 같이 정의할 경우 상태가 지정한 상태가 변경되지 않아도 항상 리렌더링 되게 된다.

const {input, todos} = useSelector(state => ({
  input: state.todos.input,
  todos: state.todos.todos
}));

위를 보면 함수에서 객체리터럴을 리턴하는 것을 볼 수 있다.

객체리터럴의 경우 생성될때마다 다른 인스턴스가 할당되기 때문에

매번 다른 값으로 인식되어 값 변경 유무와 상관없이 매번 리렌더링이 발생하게 되는것이다.

이에 아래와 같이 정의해야 한다.

이렇게 되면 실제 state.todos 데이터의 변화가 있을때에만 리렌더링을 수행하게 된다.

const {input, todos} = useSelector(state => state.todos);

그런데 state.todos 중에서도 input 값의 변화만 구독하고 싶을 경우는

아래와 같이 각각 나눠서 작성하는 방법 밖에는 없다.

const input = useSelector(state => state.todos.input);
const todos = useSelector(state => state.todos.todos);

또 다른 방법으로 아래와 같이 useSelector 함수의 두번째 인자로

shallowEqual 를 지정하는 경우도 있는데

이 경우 첫번째 함수의 리턴값의 값을 비교해서 리렌더링을 결정하게 된다.

하지만 문제는 1Depth 값만 비교한다.

const {input, todos} = useSelector(state => state.todos, shallowEqual);

 

728x90

'Frontend > ReactJS' 카테고리의 다른 글

리액트 프로젝트 디렉토리 구조  (0) 2021.03.03
mac+intellij 에서 리액트 시작하기...  (0) 2021.03.03
useRef  (0) 2021.03.03
컴포넌트 업데이트? 리렌더링?  (0) 2021.03.03
<React.StrictMode></React.StrictMode>  (0) 2021.03.03