원티드 프론트엔드 프리온보딩 (22.12.19 월 ~ 23.01.20 금)
1. 렌더링
📍렌더링이란?
브라우저에서의 렌더링 : DOM 요소를 계산하고 그려내는 것
- JavaScript DOM API 가 담당
React의 렌더링
- 실제 렌더링 과정은 React에서 대신 처리해주고, 개발자는 UI 를 설계하는데 집중 (선언형 프로그래밍)
- React 내부에서 렌더링이 어떻게 실행되는지 이해하고, 이를 최적화할 필요가 있다
📍React에서 리렌더링이 발생하는 시점
⭐state가 변했을 때
⭐특정 컴포넌트의 state가 변하면, 해당 컴포넌트와 하위 컴포넌트가 모두 리렌더링됨
state를 사용하는 이유
- state(변경될 수 있는 데이터)를 UI와 연동하기 위해서
📍React의 렌더링 과정
1️⃣기존 컴포넌트의 UI를 재사용할지 확인
2️⃣(함수형 컴포넌트) : 컴포넌트 함수 호출
(클래스형 컴포넌트) : render 메서드 호출
3️⃣새로운 VIrtual DOM 생성
4️⃣이전의 Virtual DOM과 새로운 Virtual DOM을 비교해서 실제 변경된 부분만 DOM에 적용
CRP (Critical Rendering Path)
- 브라우저가 화면을 보여주기 위해 HTML, CSS, JS를 다운로드받고, 이를 처리하여 픽셀 형태로 그려내는 과정
1️⃣HTML 파싱 -> DOM 생성
2️⃣CSS 파싱 -> CSSOM 생성
3️⃣DOM과 CSSOM을 결합해서 Render Tree를 생성
4️⃣Render Tree와 Viewport의 width를 통해 각 element의 위치와 크기 계산 (Layout)
5️⃣지금까지 계산된 정보를 이용해 Render Tree 상의 요소들을 실제 Pixel로 그림 (Paint)
Layout과 Paint는 많은 연산이 필요한 과정이며, 이 부분을 최소화하는 것이 성능 개선의 핵심
(React가 Virtual DOM을 사용하는 이유)
📍React에서 사용할 수 있는 최적화 아이디어
1️⃣새롭게 컴포넌트 함수를 호출하지 않기
- 이전의 결과값을 그대로 사용하도록 함
2️⃣새로 만드는 Virtual DOM이 기존 형태와 가급적 차이가 없게 하기
2. React.memo
📍사용 목적
⭐컴포넌트를 Memoization
state(props)가 변한 컴포넌트라면 당연히 리렌더링이 필요하지만, 하위 컴포넌트까지 불필요한 리렌더링을 할 필요가 없다
- 이런 경우에는 새롭게 컴포넌트 함수를 호출하지 않고, 이전 값(memoization)을 그대로 사용하는 것이 효율적이다
React.memo 는 props 변화에만 영향을 주며, useState, useReducer, useContext 훅이 사용되면, 여전히 state나 context가 변할 때 다시 렌더링된다
📍사용 방법
React.memo는 고차 컴포넌트이며, 컴포넌트(함수)를 인수로 받아 컴포넌트를 반환한다 (고차 함수처럼 동작)
const MyComponent = React.memo(function MyComponent(props) {
/* render using props */
});
⭐React.memo로 감쌀 컴포넌트는 화살표 함수 대신 함수 선언문을 사용한다 (타입스크립트에서도 수월)
React.memo로 감싸진 컴포넌트의 경우, 부모 컴포넌트가 리렌더링된다고 무조건 리렌더링되는 것이 아니라, props가 바뀌는 경우에만 리렌더링 수행
- 이를 통해 불필요한 리렌더링을 막을 수 있음
props를 비교하는 방법
- 기본값 : shallow compare (모든 props를 비교하며 하나라도 다르면 다른 것으로 인식)
- 하지만, 메모리 주소가 바뀌면 다르다고 판단
- 비교하는 로직을 바꾸고 싶으면 compare 함수를 직접 작성할 수 있음
shallow compare의 한계
- props에 함수를 넣었을 경우, 함수는 기본적으로 객체이므로 리렌더링마다 메모리 주소가 달라진다- 따라서 shallow compare로는 같은 코드의 함수라도 다른 메모리 주소라서 다른 값이라고 판정
⭐compare 함수의 반환값이 true이면? 같은 컴포넌트이므로 재사용
⭐compare 함수의 반환값이 false이면? 리렌더링
function MyComponent(props) {
/* render using props */
}
function areEqual(prevProps, nextProps) {
/*
true를 return할 경우 이전 결과를 재사용
false를 return할 경우 리렌더링을 수행
*/
}
export default React.memo(MyComponent, areEqual);
예시)
⭐re render 버튼을 클릭해도 React.memo로 감싼 (memoization된) 컴포넌트는 리렌더링되지 않음
⭐그러나, compare 함수가 false를 반환하면, 무조건 리렌더링 (React.memo로 감싸는 경우에도)
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); // state를 바꿔 리렌더링 유발하는 함수
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); // 재사용
// 만약, 이 컴포넌트에 props로 함수를 전달했다면?
// shallow compare로는 다르다고 판정 (메모리 주소 바뀜)
// compare 함수가 항상 false를 반환하므로, 무조건 React.memo를 사용해도 무조건 리렌더링
const ReturnFalseMemo = React.memo(ChildComponent, () => false);
// compare 함수가 true를 반환하므로 어떠한 경우에도 리렌더링 X
const ReturnTrueMemo = React.memo(ChildComponent, () => true);
3. Advanced Hook (Memoization 관련)
📍useMemo
React.memo가 컴포넌트를 memoize한다면, useMemo는 값을 memoize한다
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
인수1️⃣ memo할 값을 리턴하는 콜백 함수
- 콜백 함수의 인수들은 의존성 배열의 원소로 추가되어야 함
인수2️⃣ 의존성 배열 (배열의 원소 중 하나라도 변경되면 새로운 값을 재계산)
- 의존성 배열이 없다면 렌더링마다 재계산
📍useCallback
useCallback은 메모이제이션된 콜백 함수를 반환
- useCallback(fn, deps)은 useMemo(() => fn, deps)와 같다
- 따라서 useMemo를 편하게 쓰는 방법이라 할 수 있다
const memorizedFunction = useMemo(() => () => console.log("Hello World"), []);
const memorizedFunction = useCallback(() => console.log("Hello World"), []);
⭐특정 원시값 하나를 메모하는 경우가 아니면 대 useCallback을 사용하면 된다
⭐shallow compare의 문제 해결 가능!
- 만약 props에 함수를 넣는다면, useCallback으로 그 함수를 메모이제이션해주고 넣으면, 메모리 주소가 바뀌지 않아서 shallow compare에서 같다고 판단
📍언제 Memoization을 해야할까?
1️⃣새로운 값을 만드는 연산이 복잡할 때
- 예) 1만개의 원소를 가진 배열을 생성해야 할 때
2️⃣컴포넌트의 렌더링 전후 값의 동일성을 보장하고 싶을 때
- 예) shallow compare의 메모리 주소 바뀌면 다르다고 판단하는 문제를 useCallback으로 해결
최적화가 무조건 좋은 것은 아님을 명심
- 최적화로 인해 프로그램 복잡도 증대로 유지보수 비용 증가
- 성능 이슈가 확실하게 예상될 경우에만 시도