브라우저에 특정한 요소를 그려내는 것
⇒ 이 렌더링 과정을 잘 처리해주는 것이 바닐라 자바스크립트를 사용하지 않고, React 같은 UI 라이브러리 또는 프레임워크를 사용하는 이유
DOM 요소를 계산하고 그려내는 것
1. HTML을 파싱해서 DOM을 만든다.
2. CSS를 파싱해서 CSSOM을 만든다.
3. DOM과 CSSOM을 결합해서 Render Tree를 만든다.
4. Render Tree와 Viewport의 width를 통해서 각 요소들의 위치와 크기를 계산한다. (Layout)
5. 지금까지 계산된 정보를 이용해 Render Tree상의 요소들을 실제 Pixel로 그려낸다. (Paint)
6. DOM 또는 CSSOM이 수정될 때마다 4, 5단계가 반복된다.
이런 니즈에 맞춰서 React, Vue, Angular 등의 라이브러리, 프레임워크가 등장하게 되고 그 중에서 React가 현재는 가장 많이 사용되고 있는 것이다. 실제로 React 공식 문서를 보면 가장 첫번째 장점으로 “선언형”을 내세우고 있다.
이처럼 React는 선언형으로 실제 렌더링 과정은 React에서 대신 처리해주기 때문에, 개발자는 UI 설계하는 데만 집중할 수 있다. 하지만 React 내부에서 처리해주는 렌더링을 최적화 해야 되는 상황이 발생할 수 있다. 그러기 위해서 렌더링이 언제, 어떤 과정을 거쳐 이루어지는 지를 이해해야 한다.
state가 변할 때 리렌더링된다.
why ? UI와 상태(state)를 연동하기 위해서
UI는 어떤 데이터가 있고 그걸 보기 편한 형태로 표현한 것이다. 리액트는 이를 이해하고 UI와 연동되어야 하고, 변할 여지가 있는 데이터들을 state 형태로 사용할 수 있게 하는 것이다.
데이터가 변경되었을 때 UI도 변화시키기 위해 state를 변경시키는 방법을 setState로 제한시킨다. 이 함수가 호출될 때마다 리렌더링 되도록 설계하였다.
즉, 특정 컴포넌트의 state가 변한다면 해당 컴포넌트와 하위에 있는 컴포넌트들은 리렌더링이 발생한다.
1. 기존 컴포넌트의 UI를 재사용할 지 확인
2. 컴포넌트 함수를 호출(함수 컴포넌트) or render 메서드 호출(클래스 컴포넌트)
3. 2의 결과를 통해 새로운 VirtualDOM 생성
4. 이전 VirtualDOM 과 새로운 VirtualDOM을 비교해 변경된 부분만 DOM에 적용
4. Render Tree와 Viewport의 width를 통해서 각 요소들의 위치와 크기를 계산한다. (Layout)
5. 지금까지 계산된 정보를 이용해 Render Tree상의 요소들을 실제 Pixel로 그려낸다. (Paint)
6. DOM 또는 CSSOM이 수정될 때마다 4, 5단계가 반복된다.
이를 통해, 브라우저에서 수행되는 CRP의 빈도를 줄일 수 있어 4단계
이전 VirtualDOM 과 새로운 VirtualDOM을 비교해 변경된 부분만 DOM에 적용
에 해당되는 과정이 리액트가 수행하는 최적화이다.
그렇다면 개발자가 수행할 수 있는 최적화는 뭐가 있을까 ?
개발자가 할 수 있는 최적화는 1, 3단계가 있다.
1. 기존 컴포넌트의 UI를 재사용할지 확인
3. 2의 결과를 통해 새로운 VirtualDOM 생성
<div>
를 <span>
태그로 변환 시키는 것 보다는 <div className="block" />
을 <div className="inline">
으로 변환 시키는 것따라서 리액트에서는 개발자에게 이 컴포넌트가 리렌더링이 되어야 할지에 대한 여부를 표현할 수 있는 React.memo 함수를 제공하고 이를 통해 기존의 컴포넌트의 UI를 재사용할 지 판단한다.
const MyComponent = React.memo(function MyComponent(props) {
/* render using props */
});
React.memo는 HOC이다.
HOC(Higher Order Component)
컴포넌트를 인자로 받아서 컴포넌트를 리턴하는 컴포넌트function HOC(Component) { /* do something */ return <Component /> }
이때 중요하게 생각해야 할 것은 props를 비교하는 방식이다.
React.memo는 기본적으로 props의 변화를 이전 prop와 새로운 prop를 각각 shallow compare(얕은 비교)해서 판단한다. 만약 이 기본적인 비교 로직을 사용하지 않고 비교를 판단하는 로직을 직접 작성하고 싶을 경우를 대비해서 React.memo는 변화를 판단하는 함수를 인자로 받을 수 있도록 설정해둔다. (아래 코드 참고)
function MyComponent(props) {
/* render using props */
}
function areEqual(oldProps, newProps) {
/*
true를 return할 경우 이전 결과를 재사용
false를 return할 경우 리렌더링을 수행
*/
}
export default React.memo(MyComponent, areEqual);
예시코드) React.memo - CodeSandbox
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const [text, setText] = useState("");
const [_, setState] = useState(1);
const reRender = () => setState((prev) => prev + 1);
return (
<div className="App">
<h1>Memoization Test</h1>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button
style={{ display: "block", margin: "20px auto" }}
onClick={reRender}
>
re render
</button>
<ChildComponent name="memo X" value={text} />
<MemoizedComponent name="memo O" value={text} />
<ReturnFalseMemo name="return false" value={text} />
<ReturnTrueMemo name="return true" value={text} />
</div>
);
}
function ChildComponent({ name, value }) {
console.log(`${name} rendered`);
return (
<h3>
{name}: {value}
</h3>
);
}
const MemoizedComponent = React.memo(ChildComponent);
const ReturnFalseMemo = React.memo(ChildComponent, () => false);
const ReturnTrueMemo = React.memo(ChildComponent, () => true);
예시 코드) React.memo의 잘못된 활용 - CodeSandbox
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const [text, setText] = useState("");
const [_, setState] = useState(1);
const reRender = () => setState((prev) => prev + 1);
function hello() {
alert("Hello, World");
}
return (
<div className="App">
<h1>Memoization Test</h1>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button
style={{ display: "block", margin: "20px auto" }}
onClick={reRender}
>
re render
</button>
<MemoizedComponent name="memo O" value={text} hello={hello} />
</div>
);
}
function ChildComponent({ name, value, hello }) {
console.log(`${name} rendered`);
return (
<h3 onClick={hello}>
{name}: {value}
</h3>
);
}
const MemoizedComponent = React.memo(ChildComponent);
MemoizedComponent
를 memo 해도 hello라는 함수를 props로 전달하여, text 안바꾸고 그냥 렌더링해도 재사용되지 않고 렌더링된다.hello()
function을 만들어 이전 함수랑 메모리 주소값이 다르기 때문에, 매번 다른 props가 전달되기 때문에 리렌더링되는 것이다.import React, { useState } from "react";
import "./styles.css";
function hello() {
alert("Hello, World");
}
export default function App() {
// ...
const myHello = hello; // 함수를 담는 변수 추가
return (
<div className="App">
// ...
<MemoizedComponent name="memo O" value={text} hello={myHello} /> // 값 변경
</div>
);
}
//...
const MemoizedComponent = React.memo(ChildComponent);
특정한 값을 저장해뒀다가, 이후에 해당 값이 필요할 때 새롭게 계산해서 사용하는게 아니라 저장해둔 값을 활용하는 테크닉
리액트는 매 렌더링마다 함수 컴포넌트를 다시 호출한다. 함수는 기본적으로 이전 호출과 새로운 호출간에 값을 공유할 수 없다.
만약 특정한 함수 호출 내에서 만들어진 변수를 다음 함수 호출에도 사용하고 싶다면 그 값을 함수 외부의 특정한 공간에 저장해뒀다가 다음 호출 때 명시적으로 다시 꺼내와야 한다.
다행히 리액트에서는 함수 컴포넌트에서 값을 memoization 할 수 있도록 API를 제공해주고 있다.
리액트에서 값
을 memoization 할 수 있도록 해주는 함수
// useMemo(callbackFunction, deps]
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useMemo는 두 가지 인자를 받는다.
1. 콜백함수
- 이 함수에서 리턴하는 값이 메모된다.
2. 의존성 배열
- 이전의 결과를 그대로 활용해버리면 버그가 발생할 수 있음을 주의해야 한다.
- 만약a, b
라는 값이 변경되었는데 이전의 값을 그대로 활용해버리면 의도한 결과와 다른 결과가 나오게 될 것이다.
- 이런 상황을 방지하기 위해 의존성 배열을 인자로 받아, 값이 하나라도 이전 렌더링과 비교하여 변경되었다면 새로운 값을 다시 계산하게 된다.
const memorizedFunction = useMemo(() => () => console.log("Hello World"), []);
const memorizedFunction = useCallback(() => console.log("Hello World"), []);
메모이제이션은 무조건 사용하는것이 좋은게 아니라, 필요성을 분석하고 필요하다고 판단되는 순간에만 사용해야 한다.
새로운 값을 만드는 연산이 복잡하다.
함수 컴포넌트의 이전 호출과, 다음 호출 간 사용하는 값의 동일성을 보장하고 싶다.
⇒ 함수 컴포넌트의 호출 간 값들의 동일성을 보장하기 위해서
⇒ why? React.memo
와 연동하기 위해서
메모이제이션 된 객체는 새롭게 만들어진 것이 아니라 이전의 객체를 그대로 활용하는 것이기에 shallow compare에서 동일함을 보장 받을 수 있다.
import React, { useState, useCallback } from "react";
import "./styles.css";
export default function App() {
const [text, setText] = useState("");
const [_, setState] = useState(1);
const reRender = () => setState((prev) => prev + 1);
const memoizedHello = useCallback(() => alert("Hello, World"), []); // 변경코드
console.log("App rendered");
return (
<div className="App">
<h1>Memoization Test</h1>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button
style={{ display: "block", margin: "20px auto" }}
onClick={reRender}
>
re render
</button>
<MemoizedComponent name="memo O" value={text} hello={memoizedHello} />
</div>
);
}
function ChildComponent({ name, value }) {
console.log(`${name} rendered`);
return (
<h3>
{name}: {value}
</h3>
);
}
const MemoizedComponent = React.memo(ChildComponent);
위 내용은 원티드 프리온보딩 프론트엔드 리액트 최적화 강의를 정리한 내용입니다.