기초. React 재랜더링 최적화하기

이 글은 모던 자바스크립트로 배우는 리액트 입문 책을 공부하면서 샘플을 만들고, 느낀점을 추가해 작성하였습니다. React 프레임워크 기반의 프론트엔드 개발에 필요한 전반적인 내용을 빠르게 다루기 때문에 React 입문 개발자분들께 이 책을 추천합니다.

복잡한 어플리케이션의 경우 화면의 성능을 개선해야 하는 경우가 있다. React에서 어떻게 재랜더링이 발생하고, 개선할 수 있는지 알아보자.

재랜더링이 일어나는 경우

  • state 값 변경
  • prop이 변경된 컴포넌트
  • 재랜더링된 컴포넌트 하위 모든 컴포넌트

개선 방법

React에서 제공하는 메모이제이션 방법을 통해서 성능 개선을 진행한다. 메모이제이션은 이전 처리
결과를 저장하여 처리 속도를 높이는 방법을 말한다.

예제 소스

  • Parent 컴포넌트가 Child 컴포넌트를 포함하고 있고, 재랜더링 내용은 브라우저에 콘솔 로그(console.log)로 확인하자.
  • “증가” 버튼을 클릭하면 Parent에 count 상태값이 변경되면서 Parent 재랜더링을 수행한다.
  • Parent 하위에 있는 모든 하위 컴포넌트가 재랜더링을 수행하는 것이 확인가능하다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    # Parent.jsx

    import { useState } from 'react';
    import { styled } from '@mui/material';

    export default function Parent() {
    console.log('Parent render');

    const [count, setCount] = useState(0);
    const onClickAddCount = () => {
    console.log('증가 버튼 클릭 !');
    setCount(count + 1);
    };

    return (
    <div>
    <div>{count}</div>
    <Button onClick={onClickAddCount}>증가</Button>
    <Child1 />
    <Child4 />
    </div>
    );
    }

    const Child1 = () => {
    console.log('Child1 render');

    return (
    <DivContainer color="red" margin="20px">
    <p>Child1</p>
    <Child2 />
    <Child3 />
    </DivContainer>
    );
    };

    const Child2 = () => {
    console.log('Child2 render');

    return (
    <DivContainer color="green" margin="10px">
    <p>Child2</p>
    </DivContainer>
    );
    };

    const Child3 = () => {
    console.log('Child3 render');

    return (
    <DivContainer color="blue" margin="10px">
    <p>Child3</p>
    </DivContainer>
    );
    };

    const Child4 = () => {
    console.log('Child4 render');

    return (
    <DivContainer margin="20px">
    <p>Child4</p>
    </DivContainer>
    );
    };

    const Button = styled('button')`
    border: 1px solid;
    `;

    const DivContainer = styled('div')`
    border: 1px solid
    ${(props) => {
    switch (props.color) {
    case 'red':
    return 'red';
    case 'blue':
    return 'blue';
    case 'green':
    return 'green';
    default:
    return 'black';
    }
    }};
    margin: ${(props) => props.margin};
    `;
    부모-자식 컴포넌트 재랜더링 예제

1. 컴포넌트 메모이제이션 (memo)

컴포넌트의 상태가 변경되는 경우, 재랜더링된 컴포넌트 하위의 모든 컴포넌트가 재랜더링 되어 성능을 저하시킨다.
하위 컴포넌트 중 화면이 변하지 않아서 재랜더링하지 않아도 된다면, 컴포넌트를 memo 함수로 감싸고, 해당 컴포넌트는 props이 변경되었을때만 재랜더링을 수행한다.

  • React.memo 함수로 기존 컴포넌트 함수를 감싸면 된다.
  • 화면이 복잡하고, 많은 컴포넌트를 사용하여 재렌더링 비용이 큰 경우 적합하다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    ...
    # 나머지 소스는 동일하다.
    const Child1 = memo(() => {
    console.log('Child1 render');

    return (
    <DivContainer color="red" margin="20px">
    <p>Child1</p>
    <Child2 />
    <Child3 />
    </DivContainer>
    );
    });

    const Child2 = memo(() => {...});
    const Child3 = memo(() => {...});
    const Child4 = memo(() => {...});
    ...
    1. memo 함수를 통한 컴포넌트 메모이제이션 - 모든 하위 컴포넌트가 재렌더링되지 않는다.

2. 함수 메모이제이션 (useCallback)

Child1.jsx 컴포넌트에 “초기화” 버튼 추가하고, “초기화”버튼 클릭시 수행할 핸들러를 부모 컴포넌트(Parent.jsx)에서 prop으로 전달 받는다.

  • 함수를 props으로 전달할 때, 컴포넌트 메모이제이션을 하더라도 재랜더링이 되면서 함수가 다시 생성되기 때문에, 하위 컴포넌트에서는 재랜더링이 발생한다.
  • prop 값(함수 재선언) 변경에 따른 컴포넌트 재렌더링
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    ...
    # 나머지는 기존 소스와 동일
    export default function Parent() {
    console.log('Parent render');

    const [count, setCount] = useState(0);
    const onClickAddCount = () => {
    console.log('증가 버튼 클릭 !');
    setCount(count + 1);
    };

    const onClickReset = () => {
    console.log('초기화 버튼 클릭 !');
    setCount(0);
    }

    return (
    <div>
    <div>{count}</div>
    <Button onClick={onClickAddCount}>증가</Button>
    <Child1 onClickReset={onClickReset} />
    <Child4 />
    </div>
    );
    }

    const Child1 = memo(({ onClickReset }) => {
    console.log('Child1 render');

    return (
    <DivContainer color="red" margin="20px">
    <p>Child1</p>
    <Button onClick={onClickReset}>초기화</Button>
    <Child2 />
    <Child3 />
    </DivContainer>
    );
    });
    ...

화면 시나리오

  1. “증가” 버튼 클릭 시, Parent.jsx 재렌더링 -> onClickReset 함수 재선언
  2. Child1.jsx 컴포넌트에 prop 값(onClickReset 함수) 변경에 따른 Child1.jsx 컴포넌트 재랜더링
  3. “초기화” 버튼 클릭 시, Parent.jsx 재랜더링
  4. Child1.jsx 컴포넌트 props 값(onClickReset 함수) 변경에 따른 Child1.jsx 컴포넌트 재랜더링
    prop 값 변경(onClickReset 함수 재선언)에 따른 컴포넌트 재랜더링

useCallback 함수를 사용하여 함수 메모이제이션이 가능하다.

함수 안에서 사용하는 변수를 의존성 배열안에서 설정해야 한다.

  • 의존성 배열이 비어 있는 경우: 컴포넌트가 최초 렌더링 되는 시점에 선언된 것을 재사용한다.
  • 의존성 배열에 값이 있는 경우: 값이 변경되는 시점에 다시 작성된다.
    1
    2
    3
    # useCallback 함수 형식: 첫번째 인수에 함수, 두번째 인수에 의존성 배열

    const onClickReset = useCallback(() => setCount(0), []);
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    # 나머지는 기존 소스와 동일
    # onClickReset 함수에 useCallback 함수로 감싸서 함수 메모이제이션 수행

    export default function Parent() {
    console.log('Parent render');

    const [count, setCount] = useState(0);
    const onClickAddCount = () => {
    console.log('증가 버튼 클릭 !');
    setCount(count + 1);
    };

    const onClickReset = useCallback(() => {
    console.log('초기화 버튼 클릭 !');
    setCount(0);
    }, []);

    return (
    <div>
    <div>{count}</div>
    <Button onClick={onClickAddCount}>증가</Button>
    <Child1 onClickReset={onClickReset} />
    <Child4 />
    </div>
    );
    }
    2. useCallback 함수를 통한 함수 메모이제이션 - Child1 컴포넌트가 재렌더링 되지 않는다.

3. 변수 메모이제이션 (useMemo)

React.useMemo를 사용하여 변수 메모이제이션을 통해서, 불필요한 재렌더링을 해결할 수 있다.

화면 시나리오

  1. Parent 컴포넌트가 재랜더링이 수행되면 profileName 선언이 다시 된다.
  2. profileName을 prop으로 사용하는 하위 컴포넌트가 재렌더링 된다.
    이런 경우에 변수 메모이제이션 통해서 profileName 변수를 사용하는 컴포넌트의 불필요한 재렌더링이 발생한다.
  • 로그인한 사용자의 세션 정보로부터 프로필명을 조회한다. -> 프로필명 = ‘이름 (부서명)’
  • 프로필명의 경우 사용자 로그인 이후에 바뀌지 않을 정보이다.
    1
    2
    3
    4
    5
    6
    7
    const sessionUser = getSessionUser();

    # useMemo 함수 형식: 첫번째 인수에 변수가 값을 리턴하는 함수, 두번째 인수에 의존성 배열
    const profileName = useMemo(() => {
    const { name, department } = sessionUser;
    return `${name} (${department})`;
    }, [sessionUser]);

마무리

React에서 메모이제이션 방법을 통해서 불필요한 재랜더링을 막아서 성능을 개선할 수 있는 방법을 알아보았다.

  • memo 함수를 통한 컴포넌트 메모이제이션
  • useCallback 함수를 통한 함수 메모이제이션
  • useMemo 함수를 통한 변수 메모이제이션

그렇다면 어디까지 메모이제이션을 통한 최적화를 진행해야 할까?

  • 작은 컴포넌트나 시스템이 작은 경우에는 고려하지 않고, 성능이 문제가 되는 시점에 개선한다.
  • Atomic Design 관점에서 atom(원자), molecules(분자) 해당하는 공통 컴포넌트의 경우 미리 고민한다.
  • 컴포넌트에 데이터가 많을 것으로 예상되는 복잡한 컴포넌트 경우, 미리 고민하여 최적화 진행한다.

참고

Share