React 렌더링 성능 최적화 기법에 대하여

Chanho Yoon·2021년 5월 29일
1

React

목록 보기
5/6

렌더링 성능

리액트는 가장 많은 자원을 사용하는 것은 렌더링 이다.
리액트는 데이터와 컴포넌트 함수로 화면에 출력되게 되는데 그 과정에서 대부분의 연산은 컴포넌트 함수의 실행과 가상돔에서 발생한다.

React 렌더링 과정

component ➡ Virtual DOM 비교 ➡️ 실제 DOM 반영

컴포넌트를 렌더링하는데 있어 렌더링이 필요한지 판단하는 과정이 있는데 React.memo 입니다.
React.memo는 속성 값(props)이 변경될 때에만 렌더링이 되도록 한다.

1) Component 에서 최적화

component ➡ Virtual DOM 비교 ➡️ 실제 DOM 반영

1.1) React.memo 커스텀 속성값 비교 함수

React.memo의 두 번째 인자에 비교 함수를 직접 만들어 true,false를 반환할 수 있다.
이때 얕은비교를 실행한다.

function isEqual(prevProps, nextProps) {
  return true; // 이후 단계 생략 이전의 렌더링 재사용
  return false; // virtual DOM을 생성하고 비교후 변경된 부분만 DOM에 반영
}

export default React.memo(MyComponent, isEqual"속성 값 비교 함수");

1.2) 리액트를 불변 객체로 관리해야 하는이유

const prevProps = {
  todos: [
    {title: 'fix bug', priority: 'high'},
    {title: 'meeting with jone', priority: 'low'},
  ]
}

const nextProps = {
  todos: [
    { title: 'fix bug', priority: 'low'},
    { title: 'meeting with jone', priority: 'low'}
  ]
}

prevProps.todos === nextProps.todos

priority 값이 변경이 되면 그걸 감싸고 있는 객체의 레퍼런스가 바뀌게 되고 그럼 그 위의 부모인 todos의 배열의 레퍼런스 값도 바뀌게 됩니다.

이와 같이 어느 부분이 바뀌었는지 비교를 할 때에 모든 객체의 속성 값을 비교해야하지만
불변 객체로 관리했을 땐 어떠한 값이 바뀌게 됐을 때 전체 속성 값을 비교할 필요 없이
prevProps.todos === nextProps.todos와 같이 단순한 비교로 값이 바뀌었는지 알 수 있다.

1.3) 컴포넌트 내부


import React, { useState } from 'react';
import child from './child';

const Parent = () => {
   const [selectedFruit, setSelectedFruit] = useState('apple');
   const [count, setCount] = useState(0);
   return (
      <div>
         <p>{`count : ${count}`}</p>
         <button onClick={() => setCount(count => count + 1)}>증가</button>
         <child selected={selectedFruit} onChange={fruit => setSelectedFruit(fruit)} />
      </div>
   );
};

export default Parent;

child 컴포넌트에서 React.memo를 사용했다고 가정했을 때
위와 같이 selectedFruit 상태값과 상태값 변경함수인 setSelectedFruit를 자식인 child 컴포넌트로 props를 전달하게 되는데
이때 child 컴포넌트에서 React.memo를 사용했기 때문에 selectedFruit 상태값이 변하지 않으면 렌더링이 되지 않을거라고 생각하는데

onChange={fruit => setSelectedFruit(fruit)}

와 같이 사용한다면 렌더링이 될 때마다 계속해서 새로운 값이 입력되기 때문에 child 자식 컴포넌트는 렌더링이 된다.

1.3.1) 이벤트 핸들러에서 단순한 작업이라면

onChange={setSelectedFruit}

이와 같이 상태값 변경함수만 넘겨주면 된다.

1.3.2) 이벤트 핸들러에서 복잡한 작업이라면

useCallback을 사용하여 해당하는 함수가 필요할 때만 변경이 되도록 할 수 있다.
useCallback내에서 컴포넌트 상태값을 사용하게 되면 의존성 배열에 추가해줘야한다.

const onCahngeFruit = useCallback(fruit => {
  setSelectedFruit(fruit);
},[])

onChange = {onCahngeFruit}

1.3.3) 객체를 정의해서 자식 컴포넌트 props 전달

         <Child prodcut={[
            { name: 'mac', price: 5000 },
            { name: 'samsung', price: 8000 },
            { name: 'lg', price: 7000 }
         ]} />

이와 같이 컴포넌트 내부에서 객체를 생성해서 자식 컴포넌트로 props를 전달할 경우에는 저 속성값이 변하지 않는다고해도 부모 컴포넌트가 렌더링이 될 때마다 계속해서 새로운 객체를 생성하기 때문에 자식 컴포넌트 입장에서는 속성값이 바뀌었다고 인식합니다.

만약 바뀌지 않는 값이라면 컴포넌트 밖에서 상수 변수로 관리를 해야합니다.

1.3.4) 불변 객체로 상태값을 관리해야 하는 이유! (강조)

const Parent = () => {
   const [fruits, setFruits] = useState(['사과', '바나나', '망고']);
   const [newFruit, setNewFruit] = useState('');

   function addNewFruit() {
      fruits.push(newFruit);
      setNewFruit('');
   }
   
   return (
      <div>
         <Select options={fruits} />
         <input type="text" value={newFruit} onChange={e => setNewFruit(e.target.value)} />
         <button onClick={addNewFruit}>추가</button>
      </div>
   );
};

const Select = React.memo(( { options } ) => (
   <div>
      {options.map(data => (
         <div>{data}</div>
      ))}
   </div>
));

export default Parent;

현재 이 코드에서 문제점은 fruits.push()fruits state상태값에 새로운 값을 추가 해도 렌더링이 되지 않는 것이다.

💡 왜 렌더링이 되지 않을까?? 분명히 새로운 값을 넣었고 state값이 변경이 되었는데??
그것은 단순히 배열에 추가만 했을 뿐 해당 배열이 가지고 있는 레퍼런스 값은 바뀌지 않았다.

그래서 불변 객체로 상태값을 관리해야 하는 이유이다.
우선 기존 배열에 새로운 값이 들어온다고 한들 리액트 입장에선 배열의 레퍼런스 값이 바뀌지 않았으니 "변경이 되지 않았네?" 하는 것이다.

그래서 추가된 배열의 레퍼런스 값을 재생성 해줘야 리액트 입장에서도 "레퍼런스 값이 바뀌었으니 변화가 일어났네 렌더링 시작해" 라고 하는 것

이것을 불변객체로 관리하는 방법은 전개 연산자를 이용해 기존의 값들과 새로운 값으로 객체를 재생성 해주는 방법이다.

// 불변 객체 관리 X
fruits.push(newFruit);
// 불변 객체로
setFurits([...fruits, newFruit]

2) Virtual DOM ➡️ DOM 최적화

component ➡ Virtual DOM 비교 ➡️ 실제 DOM 반영

2.1) 요소의 부모 태그 (타입, 속성)

if (toggle) {
      return (
         <div>
            <Child />
            <p>맥북</p>
            <p>아이패드</p>
         </div>
      );
   } else {
      return (
         <span>
            <Child />
            <p>맥북</p>
            <p>아이패드</p>
         </span>
      )
   }

2.1.1) 부모 태그의 타입이 바뀌면

toggle 상태값의 true, false 값에 따라 다르게 렌더링을 하는데 다른점은 부모 요소인 <div><span>가 바뀌었습니다.
이럴때 리액트는 부모의 타입이 변경이 되면 그 아래에 있는 자식요소들도 모두 변경이 된다.
그렇다는 말은 자식 컴포넌트인 <Child /> 컴포넌트의 state(상태값)도 부모의 태그의 타입이 바뀔때마다 초기화가 된다.

부모 태그의 타입을 변경했을 때

부모 태그의 타입이 그대로일 때

2.1.2) 부모 태그의 속성이 바뀌면

타입이 바뀌었을 때와는 다르게 바뀐 속성 값만 수정이 됩니다. 자식 컴포넌트에도 아무런 영향을 주지 않는다.

2.2) 요소 추가

2.2.1) 중간에 요소를 추가하거나 삭제

if (flag) {
  return (
    <div>
      <Counter />
      <p>맥북프로</p>
      <p>아이맥 프로</p>
    </div>
  );
} else {
  return (
    <div>
      <Counter />
      <p>맥북프로</p>
      <p>아이패드 프로!</p>
      <p>아이맥 프로</p>
    </div>
  );
}

위와 같이 있을때 <p>아이패드 프로!</p>는 중간에 삽입이 되는데 리액트는 중간에 요소를 추가하거나 삭제하게 되면 그 뒤에 있는 요소가 변경되지 않았다는 것을 모른다.

위와 같이 중간에 <p>아이패드 프로!</p> 삽입이 되면 두 개의 요소를 다시 그리게 된다.

💡 그렇다면 바뀐 부분만 넣을 순 없을까??
있다! key 값을 활용하면 된다!!

리액트는 key 속성값을 이용해서 같은 key를 갖는 요소끼리만을 비교합니다.
즉 변경된 요소에서 같은 key 값이 존재한다는 것은 변경되지 않았다는 것이기 때문에 요소를 다시 그리지 않게 됩니다.

❗️ 하지만

{arr.map(( data, index ) => {
  <span key={index}>dd</span>;
})}

이와 같이 index (0,1,2,3, ....) 값을 사용하는 것은 정말 좋지 못하다. 이러한 경우가 효율적으로 렌더링 되는 경우는 끝에서만 삭제하거나 추가할 때이고, 그 이외 중간에서 삭제하거나 추가할 때는 무의미하므로

{arr.map(( data, index ) => {
  <span key={data.id}>dd</span>;
})}

이와 같이 data의 고유 값인 data.id 값을 넣어줘야한다.

✨ 이렇게 리액트에서 자동으로 렌더링을 효율적으로 할 수 있도록 필요한 key 값이 왜 필요한지 자세히는 몰랐다. 다만 리액트에서 최적화 하는데 필요한 옵션이라고 하니 생각없이 고유id값이 아닌 index(0,1,2,3,....)값을 넣어 사용했는데 이제는 잘 알게 되었다.
어떠한 data(배열)를 map과 같은 반복문으로 화면을 그리는데 key 속성값으로 고유한 값을 왜 넣어야 하는지!!


참고
실전 리액트 프로그래밍

1개의 댓글

comment-user-thumbnail
2021년 5월 30일

오 렌더링 성능 최적화 저도 해봐야지!

답글 달기