Memo()를 하기 전에
1. production 빌드를 실행중인지 확인한다. (개발 빌드는 극단적인 경우 수십배까지 의도적으로 느려집니다.)
2. 상태를 트리에서 불필요하게 높은 곳에 두지 않았는지 확인합니다.ex) input의 상태를 중앙화된 스토어에 두는 것은 최선이 아닐수있음
3. React DevTools Profiler를 실행하여 리렌더링되는 부분을 확인하고 가장 복잡한 하위 트리를 memo()로 감쌉니다. (그리고 필요한 곳에 useMemo()를 추가합니다.)
import { useState } from 'react';
export default function App() {
let [color, setColor] = useState('red');
return (
<div>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<p style={{ color }}>Hello, world!</p>
<ExpensiveTree />
</div>
);
}
function ExpensiveTree() {
let now = performance.now();
while (performance.now() - now < 100) {
// 인위적인 지연 -- 100ms 동안 아무것도 하지 않음
}
return <p>저는 아주 느린 컴포넌트 트리입니다.</p>;
}
다음과 같이 렌더링 성능 문제가 있는 컴포넌트가 있습니다. App 내부에서 color가 변경될때, ExpensiveTree 컴포넌트를 리렌더링해서 성능의 문제가 발생할 수 있습니다. 우리는 이곳에서 memo()를 사용하기 전 취할수 있는 해결책을 두가지 알아보려 합니다.
Solution 1: Move State Down (상태를 아래로 내리기, 분리하기)
위의 코드를 살펴보면 실제로 color와 연관된 부분은 하이라이팅 된 부분만임을 알 수 있습니다.
따라서 그부분을 Form 컴포넌트로 추출하고 상태를 그안으로 내리면 color 변경시 관련있는 컴포넌트인 Form 만 리렌더링 됩니다.
export default function App() {
return (
<>
<Form />
<ExpensiveTree />
</>
);
}
function Form() {
let [color, setColor] = useState('red');
return (
<>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<p style={{ color }}>Hello, world!</p>
</>
);
}
Solution 2: Lift Content Up (내용물 끌어 올리기)
1번의 해결방법은 상태의 일부가 값 비싼 트리(the expensive tree) 위에서 사용되는 경우에는 소용이 없습니다.
export default function App() {
let [color, setColor] = useState('red');
return (
<div style={{ color }}>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<p>Hello, world!</p>
<ExpensiveTree />
</div>
);
}
부모 div에 color를 넘겨 받아 사용하는 컴포넌트는 어떻게 분리할수 있을까요
export default function App() {
return (
<ColorPicker>
<p>Hello, world!</p>
<ExpensiveTree />
</ColorPicker>
);
}
function ColorPicker({ children }) {
let [color, setColor] = useState("red");
return (
<div style={{ color }}>
<input value={color} onChange={(e) => setColor(e.target.value)} />
{children}
</div>
);
}
color 상태에 의존하는 부분을 color 상태변수를 포함해 ColorPicker로 분기하고, color와 상관없는 부분들은 children prop라고 알려진 jsx 콘텐츠로 ColorPicker에 전달합니다. color가 변경되면서 ColorPicker는 리렌더링 되나, prop로 전달되는 children 요소는 변경된 부분이 없기 때문에 하위트리는 리렌더링 되지 않습니다.
=> 정리
메모이제이션이라고 불리는 기법들을 적용하여 최적화를 하기 전에 변경되지 않는 부분과 변경되는 부분을 분기할 수 있는지 살펴보는 것이 좋습니다. 위에서 제시된 방법들은 일반적으로 컴포넌트를 분리하기 위해 children prop를 사용하여 데이터 흐름을 쉽게 확인할 수 있게 하고, 트리를 따라 아래로 내려오는 prop의 수를 줄일 수 있습니다. 동시에 성능개선까지 가능합니다.
예를 들면 서버 컴포넌트가 안정적이고 채택될 준비가 되면, 위 ColorPicker 컴포넌트는 children을 서버로 부터 받을 수 있습니다. 전체 <ExpensiveTree /> 컴포넌트 혹은 일부분이 서버에서 실행될 수 있으며, 최상위의 React 상태 업데이트조차도 클라이언트에서 해당 부분을 “건너뛸” 것입니다.
https://overreacted.io/ko/before-you-memo/
useMemo
- useMemo의 기본 아이디어는 랜더링 사이에서 계산된 값을 '기억' 하는 것.
- 리렌더링은 애플리케이션 상태를 기반으로 하여 주어진 순간에 애플리케이션의 UI가 어떻게 보여야 하는지에 대한 스냅샷
- 리렌더링을 위한 스냅샷을 만드는데 시간이 걸리고, 사용자가 작업을 수행 한 후 UI가 충분히 빨리 업데이트 되지 않는 등의 문제를 해결하기 위해
=> 주어진 렌더에서 수행해야할 작업의 양을 줄이거나 | 컴포넌트가 다시 렌더링 해야하는 횟수를 줄이기
하나의 복잡하고 시간이 많이 걸리는 상태가 있고, 하나의 컴포넌트 안에 두가지 상태를 가질때 상태변수가 하나 변경될 때마다 복잡하고 시간이 많이 걸리는 로직이 재실행되고, 만약 한가지 상태가 시간과 같이 주기적으로 변경되는 상태라면, 복잡한 상태를 계속 재생성 하는 문제가 생김. (사용자의 환경에 따라 문제가 심각해질 수 있음)
=> 계산을 건너뛰고 기존의 값을 사용하는 것! 바로 useMemo가 제공하는 것.
사례1: 무거운 계산
const allPrimes = React.useMemo(() => {
const result = [];
for (let counter = 2; counter < selectedNum; counter++) {
if (isPrime(counter)) {
result.push(counter);
}
}
return result;
}, [selectedNum]);
//useMemo는 두 개의 인수를 사용합니다.
//1. 함수로 래핑한 수행할 작업
//2. 종속성 목록
마운트하는 동안 컴포넌트가 처음으로 렌더링 될때 리액트는 함수를 호출하여 모든 소수를 계산하고, 반환되는 값을 모두 allPrimes 변수에 할당한다. 이후 리렌더링 시 리액트는 두가지 선택지 중 선택을 합니다.
1. 함수를 다시 호출하여 다시 계산
2. 마지막으로 수행했을때 가지고 있던 데이터를 재사용
종속성 목록을 확인하고, 렌더링 이후 변경사항이 있다면 1번 전략을 선택하고, 그렇지 않으면 이전 계산된 값을 재사용합니다.
useMemo는 본질적으로 작은 캐시와 같으며, 종속성은 캐시 무효화 전략입니다.
꼭 useMemo가 최선의 선택일까?
import React from "react";
import Clock from "./Clock";
import PrimeCalculator from "./PrimeCalculator";
function App() {
return (
<>
<Clock />
<PrimeCalculator />
</>
);
}
export default App;
시간과 상수의 상태값을 Clock과 PrimeCalculator 두가지 컴포넌트로 추출하고, 각각 자체적으로 상태를 관리한다면 나머지 한 컴포넌트에서 리렌더링이 발생하더라도 다른 컴포넌트에는 영향을 주지 않는다.
=> 만약 PrimeCalculator 컴포넌트에서 Clock에서 관리하는 time 변수가 필요하다면?
PrimeCalculator 컴포넌트를 순수 컴포넌트로 변경해 새로운 데이터 수신이나 내부상태 변경시에만 다시 렌더링
(동일한 입력이 주어지면 항상 동일한 출력, 변경사항 없을 시 리렌더링 생략)
const PurePrimeCalculator = React.memo(PrimeCalculator)
import React from "react";
import { getHours } from "date-fns";
import Clock from "./Clock";
import PrimeCalculator from "./PrimeCalculator";
// PrimeCalculator를 순수 컴포넌트로 변경
const PurePrimeCalculator = React.memo(PrimeCalculator);
function App() {
const time = useTime();
// 하루의 시간에 따라 적절한 배경색이 생성됩니다.
const backgroundColor = getBackgroundColorFromTime(time);
return (
<div style={{ backgroundColor }}>
<Clock time={time} />
<PurePrimeCalculator />
</div>
);
}
const getBackgroundColorFromTime = (time) => {
const hours = getHours(time);
if (hours < 12) {
// 아침에는 밝은 노란색
return "hsl(50deg 100% 90%)";
} else if (hours < 18) {
// 오후에는 칙칙한 파란색
return "hsl(220deg 60% 92%)";
} else {
// 밤에는 짙은 파란색
return "hsl(220deg 100% 80%)";
}
};
function useTime() {
const [time, setTime] = React.useState(new Date());
React.useEffect(() => {
const intervalId = window.setInterval(() => {
setTime(new Date());
}, 1000);
return () => {
window.clearInterval(intervalId);
};
}, []);
return time;
}
export default App;
import React from "react";
function PrimeCalculator() {
const [selectedNum, setSelectedNum] = React.useState(100);
const allPrimes = [];
for (let counter = 2; counter < selectedNum; counter++) {
if (isPrime(counter)) {
allPrimes.push(counter);
}
}
return (
<>
<form>
<label htmlFor="num">Your number:</label>
<input
type="number"
value={selectedNum}
onChange={(event) => {
// 컴퓨터가 과부화되는 것을 방지하기 위해, 최대 100k로 설정
let num = Math.min(100_000, Number(event.target.value));
setSelectedNum(num);
}}
/>
</form>
<p>
There are {allPrimes.length} prime(s) between 1 and {selectedNum}:{" "}
<span className="prime-list">{allPrimes.join(", ")}</span>
</p>
</>
);
}
function isPrime(n) {
const max = Math.ceil(Math.sqrt(n));
if (n === 2) {
return true;
}
for (let counter = 2; counter <= max; counter++) {
if (n % counter === 0) {
return false;
}
}
return true;
}
export default PrimeCalculator;
export default React.memo(PrimeCalculator) 와 같이 export 하는 곳에서 React.memo 를 적용
=> 지금까지 두가지 방법으로 메모이제이션을 했습니다. 처음에는 특정 계산 결과를 메모하여 소수를 구하였고, 후자에서는 전체 컴포넌트를 메모하는 방식으로 메모이제이션을 했습니다. 실제환경에서 순수 컴포넌트를 사용하려고 시도하는 경우 특이점을 발견할수 있는데요, 바로 순수컴포넌트는 변경되지 않은것처럼 보일 때에도 리렌더링이 종종 일어난다는 점입니다!
사례2: 보존된 참조
소스코드 보기
App.js
import React from "react";
import Boxes from "./Boxes";
function App() {
const [name, setName] = React.useState("");
const [boxWidth, setBoxWidth] = React.useState(1);
const id = React.useId();
// 이 값 중 일부를 변경해 보세요!
const boxes = [
{ flex: boxWidth, background: "hsl(345deg 100% 50%)" },
{ flex: 3, background: "hsl(260deg 100% 40%)" },
{ flex: 1, background: "hsl(50deg 100% 60%)" },
];
return (
<>
<Boxes boxes={boxes} />
<section>
<label htmlFor={`${id}-name`}>Name:</label>
<input
id={`${id}-name`}
type="text"
value={name}
onChange={(event) => {
setName(event.target.value);
}}
/>
<label htmlFor={`${id}-box-width`}>First box width:</label>
<input
id={`${id}-box-width`}
type="range"
min={1}
max={5}
step={0.01}
value={boxWidth}
onChange={(event) => {
setBoxWidth(Number(event.target.value));
}}
/>
</section>
</>
);
}
export default App;
Boxes.js
import React from "react";
function Boxes({ boxes }) {
return (
<div className="boxes-wrapper">
{boxes.map((boxStyles, index) => (
<div key={index} className="box" style={boxStyles} />
))}
</div>
);
}
export default React.memo(Boxes);
https://codesandbox.io/s/sandpack-project-forked-8cbg71?file=/index.js
sandpack-project (forked) - CodeSandbox
sandpack-project (forked) by jong6598 using react, react-dom, react-scripts
codesandbox.io
Boxes는 export default React.memo(Boxes) export default 구문을 감싸는 React.memo() 로 인해 순수 컴포넌트로 작동하지만, 사용자의 이름이 변경될 때마다 다시 렌더링 됩니다.
props로 boxes를 받는 Boxes 컴포넌트는 모든 렌더링에서 동일한 데이터를 제공받는 것처럼 보이고, boxes 배열에 영향을 주는 boxWidth 상태변수는 이름 입력시에는 변하지 않기 때문에 동일한 데이터를 제공받고 변경이 없는 것처럼 보입니다. 하지만 값만 봤을때는 동일하지만, 리액트가 리렌더링 시 새로운 배열을 생성하기 때문에 참조는 동일하지 않은 현상이 발생하는 것입니다. 다음과 같은 예시를 살펴본다면 그 의미를 쉽게 이해할 수 있습니다.
function getNumbers() {
return [1, 2, 3];
}
const firstResult = getNumbers();
const secondResult = getNumbers();
console.log(firstResult === secondResult);
firstResult가 secondResult와 같은가요? 둘은 값을 보면 동일해 보이지만, 콘솔의 결과창에서는 false가 출력될 것입니다. 동일한 내용을 가질 수는 있지만 동일한 배열이 아닌 것이지요. getNumbers 함수를 호출할 때마다 컴퓨터 메모리에 저장되는 고유한 배열로서 완전히 새로운 배열을 만듭니다. 여러 번 호출하면 이 배열의 여러 복사본을 메모리에 저장합니다.
위의 간단한 예시를 살펴보고 원래의 코드로 돌아가보면, name 상태가 변경될 때 마다 App 컴포넌트가 리렌더링 되고, 새로운 boxes 배열을 구성하여 새로운 배열을 Boxes 컴포넌트에 전달하는 것을 알 수 있습니다.
그리고 Boxes는 다시 렌더링됩니다. 새로운 배열을 제공했기 때문입니다! boxes 배열의 구조는 렌더링 간에 변경되지 않았지만 상관 없습니다. 리액트가 아는 것은 boxes prop이 전에 본 적이 없는 새로 생성된 배열을 받았다는 것뿐입니다. 이러한 문제를 해결하기 위해 우리는 제공하는 boxes 변수에 useMemo 훅을 사용할 수 있습니다.
const boxes = React.useMemo(() => {
return [
{ flex: boxWidth, background: "hsl(345deg 100% 50%)" },
{ flex: 3, background: "hsl(260deg 100% 40%)" },
{ flex: 1, background: "hsl(50deg 100% 60%)" },
];
}, [boxWidth]);
특정 배열에 대한 참조를 보존하고, 첫번째 박스의 크기가 변경될때 Boxes 컴포넌트의 렌더링을 위해 boxWidth를 종속성으로 나열합니다. 다음 스케치를 통해 다시 이해해 봅시다.
여러 렌더링에서 동일한 참조를 유지함으로써 순수 컴포넌트가 원하는 방식으로 작동하도록 하고 UI에 영향을 주지 않는 렌더링은 무시합니다.
useCallback은 useMemo와 같은 용도로 사용되지만 함수를 위해 특별히 제작되었고, useCallback으로 함수를 감싸서 해당 함수를 메모화하여 렌더링 간에 스레딩 합니다.
이 hook들은 언제 사용해야하나요?
원문 작성자의 개인적인 의견으로는, 훅을 단일개체/배열/함수에 모두 래핑하는 것은 시간 낭비이고, 리액트의 고도화된 최적화와 리렌더링 성능을 믿어야한다고 한다. 이 글의 서두부분에서 제공했던 Memo()를 하기 전에 의 원작자가 제시했던 방식을 적용하여 컴포넌트를 재구성하여 성능을 향상하는 방법을 적용해보고, 이후에도 성능개선이 필요하다면, React profiler를 이용하여 느린 렌더링을 추적하고 훅을 적용하는 방식으로 가는 것이 좋을 것 같다.
- 재사용 가능한 커스텀 훅을 만들 때, memo를 활용한다.
function useToggle(initialValue) {
const [value, setValue] = React.useState(initialValue);
const toggle = React.useCallback(() => {
setValue((v) => !v);
}, []);
return [value, toggle];
}
- 컨텍스트가 있는 애플리케이션에서 데이터를 공유할때, value 속성으로 개체를 전달 시 객체 memo 활용하기
컨텍스트를 사용하는 수십개의 순수 컴포넌트가 있을때, memo가 없다면 AuthProvider의 부모가 다시 렌더링 되는 경우 모든 컴포넌트가 강제 리렌더링 된다.
const AuthContext = React.createContext({});
function AuthProvider({ user, status, forgotPwLink, children }) {
const memoizedValue = React.useMemo(() => {
return {
user,
status,
forgotPwLink,
};
}, [user, status, forgotPwLink]);
return (
<AuthContext.Provider value={memoizedValue}>
{children}
</AuthContext.Provider>
);
}
리액트 팀은 코드컴파일 단계에서 코드를 "자동 메모화" 할 수 있는지 여부를 연구중이다. (React Forget 프로젝트)
참고자료
원문
Understanding useMemo and useCallback
What's the deal with these two hooks?! Lots of devs find them confusing, for a whole host of reasons. In this tutorial, we'll dig deep and understand what they do, why they're useful, and how to get the most out of them.
www.joshwcomeau.com
Before You memo()
Rendering optimizations that come naturally.
overreacted.io
번역본
[번역] useMemo 그리고 useCallback 이해하기
원문: https://www.joshwcomeau.com/react/usememo-and-usecallback/
medium.com
'자료 > article' 카테고리의 다른 글
It’s 2022, Please Don’t Just Use “console.log” Anymore (0) | 2022.09.11 |
---|---|
변경에 대해 의사소통하기 (0) | 2022.09.08 |
리엑트는 언제 리렌더링 하는가. (0) | 2022.08.31 |
과한 지연 로딩이 웹 성능에 미치는 영향 (0) | 2022.08.18 |