React.memo, useCallback, useMemo 차이
by 담담이담useMemo
- 비용이 큰 연산 결과를 저장하고, 이 저장된 값을 반환하는 훅
- 첫 인수로는 어떤 값을 반환하는 생성함수, 두 번째 인수로는 해당함수가 의존하는 값의 배열을 전달
- 렌더링 시 의존성 배열의 값이 변경되지 않았으면, 함수를 재 실행하지 않고 기억해둔 해당 값을 반환
- 의존성 배열의 값이 변경되었다면, 첫 인수의 함수를 실행하고 그 값을 반환한 다음 또 그 값을 기억
- 메모이제이션은 값 뿐만 아니라 함수도 가능
- 물론 컴포넌트도 useMemo로 감쌀 수 있지만, React.memo를 쓰는 것이 현명
useCallback
- useMemo가 값을 기억했다면, useCallback은 인수로 넘겨받은 콜백 자체를 기억
- 새로 특정함수를 만들지 않고, 재 사용한다는 말
- 첫 번쨰 인수로 함수를 두 번째 인수로 의존성 배열을 집어넣음
- 함수의 재생성을 막아 불필요한 리소스 또는 리렌더링을 방지하고 싶을 때 사용
- useMemo와 useCallback의 유일한 차이는 메모이제이션을 하는 대상이 변수냐 함수냐일 뿐
예시로 살펴보기
import React, { useReducer } from 'react';
import personReducer from './reducer/person-reducer';
export default function AppMentorsButton() {
const [person, dispatch] = useReducer(personReducer, initialPerson);
const handleUpdate = () => {
const prev = prompt(`누구의 이름을 바꾸고 싶은가요?`);
const current = prompt(`이름을 무엇으로 바꾸고 싶은가요?`);
dispatch({ type: 'updated', prev, current });
};
const handleAdd = () => {
const name = prompt(`멘토의 이름은?`);
const title = prompt(`멘토의 직함은?`);
dispatch({ type: 'added', name, title });
};
const handleDelete = () => {
const name = prompt(`누구를 삭제하고 싶은가요?`);
dispatch({ type: 'deleted', name });
};
return (
<div>
<h1>
{person.name}는 {person.title}
</h1>
<p>{person.name}의 멘토는:</p>
<ul>
{person.mentors.map((mentor, index) => (
<li key={index}>
{mentor.name} ({mentor.title})
</li>
))}
</ul>
<Button text="멘토의 이름을 바꾸기" onClick={handleUpdate} />
<Button text="멘토 추가하기" onClick={handleAdd} />
<Button text="멘토 삭제하기" onClick={handleDelete} />
</div>
);
}
function Button({ text, onClick }) {
console.log('Button', text, 're-rendering 😜');
const result = calculateSomething();
return (
<button
onClick={onClick}
style={{
backgroundColor: 'black',
color: 'white',
borderRadius: '20px',
margin: '0.4rem',
}}
>
{text} {result}
</button>
);
}
function calculateSomething() {
for (let i = 0; i < 10000; i++) {
console.log('🥹');
}
return 10;
}
const initialPerson = {
name: '엘리',
title: '개발자',
mentors: [
{
name: '밥',
title: '시니어개발자',
},
{
name: '제임스',
title: '시니어개발자',
},
],
};
- 멘토를 추가, 삭제, 변경할 때마다 Button 컴포넌트들이 리렌더링됨
- 왜? 상위 컴포넌트인 AppMentorsButton의 state가 변경되기에 하위 컴포넌트인 Button도 리렌더링됨
- 또한, AppMentorsButton가 리렌더링 되면서, handleUpdate, handleAdd, handleDelete라는 함수가 전부 다시 만들어짐
- 뿐만 아니라, Button에 프롭으로 보내주는 text도 새로운 값으로 만들어짐
- (변수에 새롭게 할당된 문자열이 text라는 prop으로 전달되는 것과 같음
- 만약 Button이라는 컴포넌트 안에 다른 컴포넌트들이 존재한다면 그것들까지 다 재렌더링이 될 거임
- 하지만 그렇게 걱정할 일은 아니긴 함
- 왜? 리액트의 가상돔이 실제로 변경이 일어난 요소만 화면에 바뀌어서 보여지기 때문(렌더링 O, 커밋 X)
- Button이 수많은 자식 컴포넌트를 가지고 있거나,
- 무거운 계산이나 API 콜을 하는 게 아니라면 성능에 영향이 없음
하지만,
- Button 컴포넌트 내에서 위처럼 for문을 돌며 무거운 연산을 해야한다면 버튼 하나를 리렌더링하는데에 시간이 오래걸릴 거임
- 즉, 가상돔을 만들고 어떤 걸 업데이트 해야하는지 결정하는데까지도 시간이 걸려버림
- 처음에만 계산해야한다면, useEffect를
- 또는 useMemo를 사용하면 됨
function Button({ text, onClick }) {
console.log('Button', text, 're-rendering 😜');
const result = useMemo(() => calculateSomething());
return (
<button
onClick={onClick}
style={{
backgroundColor: 'black',
color: 'white',
borderRadius: '20px',
margin: '0.4rem',
}}
>
{text} {result}
</button>
);
}
- 하지만 이렇게 바꾸더라도, 상위 컴포넌트가 리랜더링 되면서 props로 들어오는 text와 onClick이 새롭게 만들어진 값으로 변경되기 때문에 Button이 리렌더링이 됨
- <Button text="멘토의 이름을 바꾸기" onClick={handleUpdate} />
- 이렇게 프롭을 전달하게 되면, 컴포넌트를 호출할 때마다 Props라는 새로운 객체가 만들어짐
- 새로운 props라는 객체가 만들어지더라도, 이 안의 값이 동일한 값이라면 리렌더링 하지마! 라고 해주는게 memo임
const Button = memo(({ text, onClick }) => {
console.log('Button', text, 're-rendering 😜');
const result = useMemo(() => calculateSomething(), []);
return (
<button
onClick={onClick}
style={{
backgroundColor: 'black',
color: 'white',
borderRadius: '20px',
margin: '0.4rem',
}}
>
{`${text} ${result}`}
</button>
);
});
Button.displayName = 'Button';
- 여기서 질문..
- 너가 text도 새로 만들어져서 들어온다며 그럼 걔도 메모이제이션 해줘야하는 거 아님? useCallback으로 onClick으로 들어올 함수만 메모이제이션 해주면 됨?
- 음.. text는 메모이제이션 안해줘도 됨 근데 onClick으로 들어오는 함수는 메모이제이션 해줘야함
- 왜?
- memo 함수는 컴포넌트의 props가 변경되지 않으면 리렌더링이 되지 않게 막아줌
- 하지만 props의 얕은 비교를 수행하므로, props 내부의 객체나 함수와 같이 참조가 변경되지 않으면 변경이 없다고 판단함
- text는 문자열이므로 원시 데이터 타입으로 취급되며, onClick은 함수이기 때문에 참조가 변경되지 않는 한 리렌더링이 발생하지 않음 따라서 memo가 효과적으로 작동하여 Button의 불필요한 리렌더링을 방지함
'React' 카테고리의 다른 글
데이터를 가져온 뒤 옵셔널 체이닝과 null 병합 연산자 (0) | 2024.01.09 |
---|---|
useEffect의 실행 시점과 클린업 함수의 실행 시점 (1) | 2024.01.08 |
React : 데이터 로딩 및 에러 처리하기 (1) | 2023.10.24 |
React : 사이드 이펙트와 useEffect (0) | 2023.10.24 |
React : ref와 useRef (0) | 2023.10.24 |
블로그의 정보
유명한 담벼락
담담이담