VDOM & RealDOM
- React | 핵심 기능은 컴포넌트를 만들고, props, state, context 등으로 그 컴포넌트를 관리(manage)하는 것이다. 컴포넌트가 업데이트 되어 함수를 재실행하고, 그 결과 re-rendering이 일어나야 한다면 리액트는 이것을 리액트 DOM에게 알린다.
- 이 때 리액트는, 렌더링하기로 결정한 components를 tree로 나타내는 Virtual DOM을 이용한다.
- => 화면에서 컴포넌트가 어떻게 보여야 할 것인지 결정하고, 이 정보를 스냅샷의 형태로 ReactDOM에 전달한다.
- ReactDOM | 브라우저 화면에 컴포넌트를 출력한다.
- 실제 DOM 요소를 조작해서 전달받은 Components tree를 화면에 렌더링한다.
- ReEvaluate vs. ReRendering
- 리액트가 컴포넌트 함수를 재실행 한다는 것은 jsx 코드를 재평가한다는 것이고, 이것이 곧 re-rendering을 의미하지는 않는다.
- 리액트 DOM은 이전에 전달받은 component tree와 현재 전달받은 것을 비교해서, 변화가 없는 부분은 그대로 두고, 업데이트가 필요한 부분만을 re-rendering 한다. => 그렇지 않을 때 보다 좋은 성능을 가진다.
- App에서 button click state를 관리하고, 이를 자식 컴포넌트에 props으로 전달하고 있다. 이 때 자식 컴포넌트는 click state에 따라 화면에 출력할 내용을 변경한다.
- state가 변경되면, app과 자식 컴포넌트 함수가 실행된다(자식 컴포넌트도 App의 일부이기 때문). 즉 re-evaluation이 일어난다.
- 재평가 결과에 따라, 실제 출력할 내용(RealDOM)에 변화가 생긴 자식 컴포넌트에서만 re-rendering이 일어난다.
- => state에 변화가 생기면, 항상 그 컴포넌트(와 모든 자식 컴포넌트)가 재평가되며, 재평가가 곧 재출력을 의미하지는 않기 때문에 렌더링 성능을 보장한다. 그런데 매번 재평가가 일어나는 것과 성능 사이에는 아무런 관계가 없을까?
optimization
React.memo | memorize
how to use React.memo?
import react from 'react';
const Component = props => {
// ...
return (/*...*/);
}
export default React.memo(Component);
- 컴포넌트를 export할 때 React.memo 함수로 감싸준다.
- 이 컴포넌트를 자식으로 가지고 있는 부모에서 re-evaluation이 일어나도, 자식 컴포넌트에 전달된 props의 값이 변경되지 않았다면 자식 컴포넌트는 재평가 하지 않는다. 즉 props이 변경될 때에만 재평가가 이루어진다.
- 그렇다고 해서 모든 컴포넌트를 memo로 감싸지는 않는데, 이러한 최적화에는 cost가 따르기 때문이다.
- memo에 전달된 컴포넌트의 부모에서 re-evaluation이 발생한다.
- 이 때 memo는 리액트에게 전달받은 컴포넌트의 props이 변경되었는지 확인할 것을 지시한다.
- 즉, 컴포넌트를 재평가하는 대신 props를 재평가하는 것이다.
- 따라서, 두 경우의 cost를 비교해서 적절한 때에 memo를 사용하는 것이 중요하다.
- props의 개수, component tree의 복잡성(자식 컴포넌트가 많음) 등을 고려한다.
- 예를 들어 state가 변경됨에 따라 props이 자주 변경된다면, memo를 사용하지 않는다(어차피 컴포넌트가 재평가 될텐데, 굳이 props을 재평가하는 과정을 추가할 필요가 없다).
- 함수형 컴포넌트에서만 사용 가능하다.
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;
- button을 클릭하면 clickHandler에서 state를 변경한다.
- Component 함수가 실행된다.
- 함수 블럭 안에서는 다양한 변수에 값을 할당하고 있는데, 이 변수는 함수가 실행될 때 마다 매번 다시 만들어진다. 즉 똑같은 값을 가지는 변수가 매번 다시 만들어 진다.
- clickState와 clickHandler 뿐만 아니라 Button과 Para에 전달한 props의 값도 매번 다시 만들어진다.
- 즉, props은 항상 바뀐다.
- 그럼에도 불구하고, Para를 memo에 전달하면 App이 재실행 되어도 Para는 재평가되지 않는다. 반면 Button은 memo에 전달하더라도 항상 재평가 된다. 왜일까?
- Para의 props으로 전달된 isClicked는 boolean, 즉 primitive type의 데이터를 저장하고 있다.
- primitive type의 데이터를 변수에 저장하면, 그 변수에는 값 자체가 저장된다.
- 따라서 리액트가 두 변수에 저장된 데이터를 비교하면, false === false 이므로 props에 변화가 없다고 간주한다.
- Button의 props으로 전달된 onClick은 함수(객체), 즉 reference type의 데이터를 저장하고 있다.
- reference type의 데이터에는 메모리 주소가 저장되어 있다(하나의 메모리 셀에 함수를 저장하기에는 그 값이 너무 무겁기 때문에, 다른 여러 메모리 셀에 값을 저장하고 이 셀의 주소를 저장한다).
- 모든 데이터는 고유한 메모리 주소를 가지고 있으므로, 두 변수에 저장된 데이터는 당연히 같지 않다. 따라서 리액트는 props에 변화가 있다고 간주한다(리액트에 국한된 것이 아니라, 자바스크립트 자체의 작동 방식때문이다).
- Para의 props으로 전달된 isClicked는 boolean, 즉 primitive type의 데이터를 저장하고 있다.
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);
- useCallback에 인자로 전달된 콜백함수는 같은 메모리 주소에 저장된다.
- useCallback은 그 주소에 저장되어있는 함수를 반환한다.
- useEffect에서와 같이 두번째 인자로 dependencies를 전달할 수 있다. 텅 빈 배열을 전달하면, 우리가 저장한 콜백함수(의 메모리 주소)는 절대 변하지 않는다.
dependencies
그렇다면 dependencies는 언제 전달할까?
- 자바스크립트에서모든 함수는 클로저를 생성한다. | it means that function always have reference to it's surrounding state. => ex) inner function has access to an outer function's scope.
- useCallback에 전달한 함수가 정의될 때, 그 함수와 함수에서 사용되고 있는 data(surrounding state)를 전부 변하지 않는 값으로 고정해서 저장한다(모든 함수는 클로저이기 때문).
- 함수에서 사용하고 있는 data가 컴포넌트의 state일지라도, 콜백 함수에는 그 변화가 반영되지 않는다.
- 이 때 해당 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>
)
}
- useMemo에 전달할 콜백함수는 저장하고 싶은 계산 결과를 반환해야 한다.
- dependencies에 변화가 생길 때에만 콜백함수를 재실행한다.
- 이 때 items는 여전히 참조타입 데이터로, App이 재실행 될 때 마다 새로운 값이 된다. => 배열 데이터를 Memo의 콜백함수로 반환한다.
- 모든 데이터에 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 |