본문 바로가기

Study/React

Optimization | how does react work?

VDOM & RealDOM

https://medium.com/@gabrielrasdale/react-react-dom-eli5-db2101e614e5

 

  1. React | 핵심 기능은 컴포넌트를 만들고, props, state, context 등으로 그 컴포넌트를 관리(manage)하는 것이다. 컴포넌트가 업데이트 되어 함수를 재실행하고, 그 결과 re-rendering이 일어나야 한다면 리액트는 이것을 리액트 DOM에게 알린다. 
    • 이 때 리액트는, 렌더링하기로 결정한 components를 tree로 나타내는 Virtual DOM을 이용한다. 
    • => 화면에서 컴포넌트가 어떻게 보여야 할 것인지 결정하고, 이 정보를 스냅샷의 형태로 ReactDOM에 전달한다. 
  2. ReactDOM | 브라우저 화면에 컴포넌트를 출력한다. 
    • 실제 DOM 요소를 조작해서 전달받은 Components tree를 화면에 렌더링한다.  
  3. ReEvaluate vs. ReRendering 
    • 리액트가 컴포넌트 함수를 재실행 한다는 것은 jsx 코드를 재평가한다는 것이고, 이것이 곧 re-rendering을 의미하지는 않는다. 
    • 리액트 DOM은 이전에 전달받은 component tree와 현재 전달받은 것을 비교해서, 변화가 없는 부분은 그대로 두고, 업데이트가 필요한 부분만을 re-rendering 한다. => 그렇지 않을 때 보다 좋은 성능을 가진다. 

 

필요한 부분에만 업데이트가 일어난다

 

  1. App에서 button click state를 관리하고, 이를 자식 컴포넌트에 props으로 전달하고 있다. 이 때 자식 컴포넌트는 click state에 따라 화면에 출력할 내용을 변경한다.
  2. state가 변경되면, app과 자식 컴포넌트 함수가 실행된다(자식 컴포넌트도 App의 일부이기 때문). 즉 re-evaluation이 일어난다. 
  3. 재평가 결과에 따라, 실제 출력할 내용(RealDOM)에 변화가 생긴 자식 컴포넌트에서만 re-rendering이 일어난다. 
  4. => state에 변화가 생기면, 항상 그 컴포넌트(와 모든 자식 컴포넌트)가 재평가되며, 재평가가 곧 재출력을 의미하지는 않기 때문에 렌더링 성능을 보장한다. 그런데 매번 재평가가 일어나는 것과 성능 사이에는 아무런 관계가 없을까? 

optimization

React.memo | memorize 

how to use React.memo?

import react from 'react'; 

const Component = props => {
 // ... 
  return (/*...*/);
}

export default React.memo(Component);

 

  1. 컴포넌트를 export할 때 React.memo 함수로 감싸준다.
  2. 이 컴포넌트를 자식으로 가지고 있는 부모에서 re-evaluation이 일어나도, 자식 컴포넌트에 전달된 props의 값이 변경되지 않았다면 자식 컴포넌트는 재평가 하지 않는다. 즉 props이 변경될 때에만 재평가가 이루어진다. 
  3. 그렇다고 해서 모든 컴포넌트를 memo로 감싸지는 않는데, 이러한 최적화에는 cost가 따르기 때문이다. 
    • memo에 전달된 컴포넌트의 부모에서 re-evaluation이 발생한다.
    • 이 때 memo는 리액트에게 전달받은 컴포넌트의 props이 변경되었는지 확인할 것을 지시한다. 
    • 즉, 컴포넌트를 재평가하는 대신 props를 재평가하는 것이다. 
    • 따라서, 두 경우의 cost를 비교해서 적절한 때에 memo를 사용하는 것이 중요하다. 
      • props의 개수, component tree의 복잡성(자식 컴포넌트가 많음) 등을 고려한다. 
      • 예를 들어 state가 변경됨에 따라 props이 자주 변경된다면, memo를 사용하지 않는다(어차피 컴포넌트가 재평가 될텐데, 굳이 props을 재평가하는 과정을 추가할 필요가 없다). 
  4. 함수형 컴포넌트에서만 사용 가능하다.

primitive vs. refrence type

memo를 이용하면, 자식 컴포넌트에 props으로 전달된 값이 변경되었을 때에만 그 컴포넌트의 재평가가 이루어진다. 그러나 자바스크립트에서 함수는 객체이기 때문에, 함수를 실행하면 블럭 안에 작성된 모든 것이 다시 새롭게 만들어진다. 

 

const Component = props => {
 const [clickState, setClickState] = useState(false);
 const clickHandler = () => {
   setClickState(prev => !prev);
 }

 return (
   <div>
     <Button onClick={clickHandler}>Click</Button>
     <Para isClicked={false} />
   </div>
 )
export default Component;

 

  1. button을 클릭하면 clickHandler에서 state를 변경한다.
  2. Component 함수가 실행된다.
  3. 함수 블럭 안에서는 다양한 변수에 값을 할당하고 있는데, 이 변수는 함수가 실행될 때 마다 매번 다시 만들어진다. 즉 똑같은 값을 가지는 변수가 매번 다시 만들어 진다.
    • clickState와 clickHandler 뿐만 아니라 Button과 Para에 전달한 props의 값도 매번 다시 만들어진다. 
  4. 즉, props은 항상 바뀐다.
  5. 그럼에도 불구하고, Para를 memo에 전달하면 App이 재실행 되어도 Para는 재평가되지 않는다. 반면 Button은 memo에 전달하더라도 항상 재평가 된다. 왜일까? 
    1. Para의 props으로 전달된 isClicked는 boolean, 즉 primitive type의 데이터를 저장하고 있다. 
      • primitive type의 데이터를 변수에 저장하면, 그 변수에는 값 자체가 저장된다.
      • 따라서 리액트가 두 변수에 저장된 데이터를 비교하면, false === false 이므로 props에 변화가 없다고 간주한다. 
    2. Button의 props으로 전달된 onClick은 함수(객체), 즉 reference type의 데이터를 저장하고 있다.
      •  reference type의 데이터에는 메모리 주소가 저장되어 있다(하나의 메모리 셀에 함수를 저장하기에는 그 값이 너무 무겁기 때문에, 다른 여러 메모리 셀에 값을 저장하고 이 셀의 주소를 저장한다).
      • 모든 데이터는 고유한 메모리 주소를 가지고 있으므로, 두 변수에 저장된 데이터는 당연히 같지 않다. 따라서 리액트는 props에 변화가 있다고 간주한다(리액트에 국한된 것이 아니라, 자바스크립트 자체의 작동 방식때문이다). 

useCallback

useCallback 훅을 이용하면, 함수(reference type의 데이터)를 같은 메모리 주소에 저장함으로써(덮어쓰기), 객체 props을 전달받은 컴포넌트에서도 memo를 정상적으로 이용할 수 있다. 

 

import React, {useCallback} from 'react';

function App() {
  // ...
  
  const clickHandler = useCallback(() => {
    setClickState(prev => !prev)
  }, [clilckState]);
  
  //... 
  return (
    <div>
      <Button onClick={clickHandler}>click</Button>
    </div>
  );
}

// Button component
const Button = props => {
 // ... 
  return (/*...*/);
}

export default React.memo(Button);

 

 

  1. useCallback에 인자로 전달된 콜백함수는 같은 메모리 주소에 저장된다.
  2. useCallback은 그 주소에 저장되어있는 함수를 반환한다. 
  3. useEffect에서와 같이 두번째 인자로 dependencies를 전달할 수 있다. 텅 빈 배열을 전달하면, 우리가 저장한 콜백함수(의 메모리 주소)는 절대 변하지 않는다. 

dependencies

그렇다면 dependencies는 언제 전달할까?

  1. 자바스크립트에서모든 함수는 클로저를 생성한다. | it means that function always have reference to it's surrounding state. => ex) inner function has access to an outer function's scope. 
  2. useCallback에 전달한 함수가 정의될 때, 그 함수와 함수에서 사용되고 있는 data(surrounding state)를 전부 변하지 않는 값으로 고정해서 저장한다(모든 함수는 클로저이기 때문).
  3. 함수에서 사용하고 있는 data가 컴포넌트의 state일지라도, 콜백 함수에는 그 변화가 반영되지 않는다.
  4. 이 때 해당 state 데이터를 dependencies로 전달하면, 그 state에 변화가 생겼을 때 콜백 함수를 재생성해서 사용한다.

useMemo | memoize 

memorize vs. memoize | Memorization is the act of committing something to memory or memorizing. Memoization is (computer science) a technique in which partial results are recorded (forming a memo) and then can be re-used later without having to recompute them.

 

함수를 재생성하지 않고 고정하는 useCallback 처럼, 데이터를 재생성하지 않고 고정할 수 있는 useMemo 훅이 있다. 예를 들어, props으로 어떤 배열 데이터를 받고, 그 배열을 sorting 하는 작업이 있다고 해보자. sorting은 성능집약적(perfomance intensive)인 일이기 때문에, 컴포넌트가 재평가 될 때 마다 해당 작업을 반복하는 것은 좋지 않다. 

 

import {useMemo} from 'react';

const List = (props) => {
  // const sorted = props.items.sort((a, b)=> a-b); high perfomance
  const { items } = props; // de-struccturing
  const sorted = useMemo( () => {
    return items.sort((a, b)=> a-b); 
  }, [items])
  
  return (
    {sorted.map(item => /*...*/)}
  );
}

// App

function App() {
 const items = useMemo(() => {
   return [1, 9, 4, 2, 5]; 
 }, [])
 
 return (
   <div>
     <List itmes={items}/>
   </div>
 )
}

 

  1. useMemo에 전달할 콜백함수는 저장하고 싶은 계산 결과를 반환해야 한다.
  2. dependencies에 변화가 생길 때에만 콜백함수를 재실행한다.
  3. 이 때 items는 여전히 참조타입 데이터로, App이 재실행 될 때 마다 새로운 값이 된다. => 배열 데이터를 Memo의 콜백함수로 반환한다. 
  4. 모든 데이터에 useMemo를 사용하는 것이 아니라, 계산의 과정이 performance intensive할 때에만 그 결과를 memo 한다. 

'Study > React' 카테고리의 다른 글

custom Hooks  (0) 2022.06.24
HTTP Request  (0) 2022.06.23
etc | Image, Icon, Input  (0) 2022.06.21
Rules of Hooks  (0) 2022.06.20
useContext  (0) 2022.06.20