useCallback, React.memo, useMemo 모두 렌더 페이즈에 관여하여 불필요한 리렌더링이 발생하지 않도록 하는 것이 주요 목적
import { useState, useMemo } from "react";
function ExpensiveComponent() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
// 연산량이 큰 함수 (useMemo 사용 전)
const expensiveCalculation = () => {
console.log("연산 수행...");
return count * 1000;
};
const result = expensiveCalculation(); // 매번 렌더링마다 실행됨
return (
<div>
<p>결과: {result}</p>
<button onClick={() => setCount(count + 1)}>카운트 증가</button>
<input onChange={(e) => setText(e.target.value)} placeholder="입력하세요" />
</div>
);
}
const result = useMemo(() => {
console.log("연산 수행...");
return count * 1000;
}, [count]); // count가 변경될 때만 연산 실행
import { useState, useMemo } from "react";
function ExpensiveComponent() {
const [count, setCount] = useState(0);
const [text, setText] = useState("");
// count가 변경될 때만 실행됨
const result = useMemo(() => {
console.log("연산 수행...");
return count * 1000;
}, [count]);
return (
<div>
<p>결과: {result}</p>
<button onClick={() => setCount(count + 1)}>카운트 증가</button>
<input onChange={(e) => setText(e.target.value)} placeholder="입력하세요" />
</div>
);
}
import { useState } from "react";
function Parent() {
const [count, setCount] = useState(0);
// 매번 렌더링될 때마다 새로운 함수가 생성됨
const handleClick = () => {
console.log("클릭!");
};
return <Child onClick={handleClick} />;
}
function Child({ onClick }) {
console.log("🔄 자식 컴포넌트 렌더링");
return <button onClick={onClick}>클릭</button>;
}
export default Parent;
import { useState, useCallback } from "react";
function Parent() {
const [count, setCount] = useState(0);
// count가 변경되지 않는 한, 같은 함수가 유지됨
const handleClick = useCallback(() => {
console.log("클릭!");
}, []);
return <Child onClick={handleClick} />;
}
function Child({ onClick }) {
console.log("🔄 자식 컴포넌트 렌더링");
return <button onClick={onClick}>클릭</button>;
}
export default Parent;
import { useState } from "react";
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>카운트 증가</button>
<Child name="React" />
</div>
);
}
function Child({ name }) {
console.log("🔄 자식 컴포넌트 렌더링");
return <p>안녕하세요, {name}!</p>;
}
export default Parent;
import { memo, useState } from "react";
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>카운트 증가</button>
<MemoizedChild name="React" />
</div>
);
}
// React.memo를 사용하여 같은 props면 리렌더링 방지
const MemoizedChild = memo(function Child({ name }) {
console.log("🔄 자식 컴포넌트 렌더링");
return <p>안녕하세요, {name}!</p>;
});
export default Parent;
Hello, world!를 100번 출력하는 코드
//렌더링을 여러 번 하는 컴포넌트
const ManyRendering = ({onClick}) => {
return (
<>
{Array.from({length: 100}, (_, i) => (
if(i === 99) {
console.log('렌더링 마지막 아이템: ', i)
}
return (
<div key={i} onClick={onClick}>
Hello, world!
</div>
)
))}
</>
)
}
export default ManyRendering
function App() {
const [state, setState] = useState(0)
//함수나 값은 매번 App()이 실행될 때마다 새로 생성된다,
const onClick = () => {}
useEffect(() => {
setTimeout(() => {
setTimeout(1)
console.log('업데이트')
}, [1000])
}, [])
}
return (
<div>
<ManyRendering onClick={onClick} />{/*그러므로 props가 변경*/}
</div>
)
해당 코드는 먼저 ManyRendering를 100번 렌더링을 한다.
그리고 App 컴포넌트의 state가 변경될 때마다 ManyRendering가 100번 또 리렌더링이 된다.
그러므로 불필요한 렌더링이 많아진다.
props가 일정하지 않다.
App 컴포넌트의 const onClick = () => {}
을 보면 변경된게 없지만 훅이 사용되지 않고 선언된 함수나 값은 컴포넌트가 다시 실행될 때마다 새로 할당된다.
즉, 매번 렌더링이 될 때마다 다른 친구다.
그러므로 <ManyRendering onClick={onClick} />
의 props가 변경된 것으로 된다.
해결법: props를 변경해지 않아야 한다.
메모이제이션한 useCallback
을 쓴다.
//const onClick = () => {} //이전코드
const memoizationCallback = useCallback(() => {
onClick()
}, [])
return (
<div>
<ManyRendering onClick={onClick} />{/*그러므로 props가 변경*/}
</div>
)
1번 해결법으로도 렌더링이 발생한다.
리액트는 렌더링 과정에서 2가지 페이즈를 거친다.
1. 렌더 페이즈
2. 커밋 페이즈: 최적화를 할 수 있음
현재 렌더 페이즈는 반영되지 않은 상태다.
//export default ManyRendering 이전 코드
export default React.memo(ManyRendering)
이제 ManyRendering
의 props로 전달받은 값이 바뀌지 않는 한 ManyRendering
도 렌더링을 할 필요가 없이 캐싱이 된다.function App() {
const [state, setState] = useState(0)
/*함수나 값은 매번 App()이 실행될 때마다 새로 생성된다.
그러므로 props가 변경되는데 때론 이게 성능에 좋지 않다.*/
const onClick = () => {}
const value = {a:1}
const memoizationValue = useMemo(() => {
return value
}, [])
}
return (
<div>
<ManyRendering onClick={memoizationValue} />
</div>
)
props로 전달하는 value
가 있다고 했을 때 이전과 마찬가지로 재선언이 여러 번 되면서 렌더링 횟수가 기하급수적으로 늘어난다.
useMemo
를 사용한다.상황에 따라 다르므로 정답이 없다. 그리고 메모리가 사용되는 것이기 때문에 공짜가 아니다.
무분별하게 훅으로 걸어놓으면 너무 오버한 개발이 될 수도 있다.
정답이 정해진 경우
이전까지의 경우처럼 렌더링이 100번, 1000번 되는 경우는 당연히 메모이제이션을 해주는게 좋다.
자식 컴포넌트에게 props로 함수를 전달하고자 할 때 써야한다.
하지만 갯수가 적으면 굳이 메모이제이션을 해줄 필요가 없다.
실제로 리액트의 dev tool인 profiler
를 통해서 속도 등을 측정해보면 도리어 useCallback
을 잡아줬을 때 성능이 하락하는 경우도 분명히 있다.