useMemo와 React.memo을 알아보기 전 이해해야 할 개념은 Memoization 이다.
이미 계산해 본 연산 결과를 기억해두었다가 동일한 연산을 해야할 때 다시 연산하지 않고 기억해두었던 데이터를 반환시키는 방법.
마치 시험을 볼 때 이미 풀어본 문제는 다시 풀어보지 않아도 답을 알고 있는 것과 유사하다.
어떤 값을 리턴하는 어떤 함수에게 특정 값이 변화할 때만 리턴 연산을 수행할 것임을 명시해주고 싶을 때 사용 하는 것이 useMemo이다.
const cachedValue = useMemo(calculateValue, dependencies)
calculateValue
: 캐시(저장)하려는 값을 계산하는 함수.
dependencies
: 의존 인자.
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
위 코드의 경우 a, b 값이 변할 때만 useMemo의 첫번째 인자인 함수가 실행되어 재계산되고, 그렇지 않은 경우에는 메모이즈된 값을 리턴한다.
const getDiaryAnalysis = () => {
const goodCount = data.filter((elem) => elem.emotion >= 3).length;
const badCount = data.length - goodCount;
const goodRate = (goodCount / data.length) * 100;
return [goodCount, badCount, goodRate];
}
const [goodCount, badCount, goodRate] = useMemo(getDiaryAnalysis(), [data.length]);
return (
<div className='App'>
<h2>일기장</h2>
<DiaryEditor onCreate={onCreate} />
<div>
<p>좋은 감정 일기 개수 : {goodCount}개</p>
<p>나쁜 감정 일기 개수 : {badCount}개</p>
<p>좋은 감정 일기 비율 : {goodRate}%</p>
</div>
<DiaryList diaryList={data} onRemove={onRemove} onEdit={onEdit} />
</div>
)
컴포넌트에 동일한 props가 들어온다면 React.memo는 컴포넌트 렌더링 과정을 스킵하고 마지막에 렌더링된 결과를 재사용한다.
React.memo는 오직 props 가 변경됐는지 아닌지만 체크한다.
이때 props 를 받는 컴포넌트는 자식 컴포넌트로, 부모 컴포넌트로부터 props 를 받는다.
이때 문제는, 부모 컴포넌트가 리렌더링될 때 자식 컴포넌트도 함께 리렌더링된다는 것이다.
이러한 문제를 해결하기 위해 React.memo 를 사용할 수 있다.
아래 코드에서 MyComponent 가 이전과 동일한 props 를 받아 가장 마지막에 렌더링했던 결과를 다시 렌더링해야 한다면, React는 컴포넌트 렌더링 과정을 스킵하고 메모이제이션해두었던 렌더링된 값을 그대로 다시 리턴해준다.
const MyComponent = React.memo((props) => {
return (/* 컴포넌트 렌더링 코드 */)
});
다음과 같이 부모 컴포넌트인 OptimizeTest 가 2개의 자식 컴포넌트 CountView, TextView 에게 각각 text, count라는 props를 넘겨주고 있다.
이때, setCount() 가 실행되는 경우 count 값이 변화하면서 부모 컴포넌트인 OptimizeTest 가 리렌더링된다. 이때 자식 컴포넌트인 CountView, TextView 도 OptimizeTest 를 따라 리렌더링된다. 그러나 자식 컴포넌트 중 TextView 는 본인의 text props 에 아무런 변화가 없음에도 불필요하게 리렌더링을 하게 되는 문제가 발생한다.
import React, { useState, useEffect } from 'react';
const CountView = ({ count }) => {
useEffect(() => {
console.log(`count props 업데이트: ${count}`);
});
return <div>{count}</div>;
};
const TextView = ({ text }) => {
useEffect(() => {
console.log(`text props 업데이트: ${text}`);
});
return <div>{text}</div>;
};
const OptimizeTest = () => {
const [count, setCount] = useState(1);
const [text, setText] = useState('');
return (
<div>
<div>
<CountView count={count} />
<button onClick={() => setCount(count + 1)}>+</button>
</div>
<div>
<TextView text={text} />
<input
type='text'
value={text}
onChange={(e) => setText(e.target.value)} />
</div>
</div>
);
};
이러한 문제를 해결하기 위해 React.memo 를 이용할 수 있다.
다음과 같이 자식 컴포넌트 CountView, TextView 를 React.memo로 감싸주면 text 값이 변화할 때는 TextView만 리렌더링되고, count값이 변화할 때는 CountView 만 리렌더링된다.
import React, { useState, useEffect } from 'react';
const CountView = React.memo(({ count }) => {
useEffect(() => {
console.log(`count props 업데이트: ${count}`);
});
return <div>{count}</div>;
});
const TextView = React.memo(({ text }) => {
useEffect(() => {
console.log(`text props 업데이트: ${text}`);
});
return <div>{text}</div>;
});
const OptimizeTest = () => {
const [count, setCount] = useState(1);
const [text, setText] = useState('');
return (
<div>
<div>
<CountView count={count} />
<button onClick={() => setCount(count + 1)}>+</button>
</div>
<div>
<TextView text={text} />
<input
type='text'
value={text}
onChange={(e) => setText(e.target.value)} />
</div>
</div>
);
};
이때 주의할 점은 React.memo는 props의 타입이 object일 때는 얕은 비교(shallow compare)를 한다는 것이다.
즉, props가 number나 string과 같은 scarlar(단일) 값인 경우에는 값이 동일한지 여부를 비교하지만, 타입이 object인 경우 같은 값을 참조(reference)하고 있는지 비교한다.
얕은 비교란?
: 값에 의한 비교가 아닌 주소에 의한 비교
let a = { count: 1 };
let b = { count: 1 };
if (a === b) {
console.log("EQUAL");
} else {
console.log("NOT EQUAL");
}
console.log(a === b); // 결과: NOT EQUAL
위 코드에서 a와 b의 값은 둘 다 { count: 1 }
로 동일한데 왜 서로 다른 값으로 인식될까?
그 이유는 자바스크립트에서 객체, 함수, 배열 같은 비원시 타입의 자료형을 비교할 때 값에 의한 비교가 아닌 주소에 의한 비교를 하기 때문이다!
a와 b 변수에 각각 객체를 할당할 때 각각 고유한 메모리 주소를 가지게 된다.
얕은 비교는 해당 메모리 주소가 같은지 다른지를 가지고 비교를 한다.
import { useEffect, useState } from 'react';
const CounterB = React.memo(({ object }) => {
useEffect(() => {
console.log(`CounterB Update = count: ${object.count}`);
});
return <div>{object.count}</div>;
});
const OptimizeTest = () => {
const [object, setObject] = useState({ count: 1 });
return (
<div>
<h2>Counter B</h2>
<CounterB object={object} />
<button onClick={() => setObject({ count: object.count })}>B Button</button>
</div>
);
};
따라서 위 코드에서 B Button을 클릭할 때 setObject({ count: object.count })
가 실행되는데, 이때 object 값은 바뀌지 않는다.
하지만 object의 count에 object.count를 재할당하면서 object.count는 새로운 메모리 주소를 가지게 되어, 얕은 비교를 할 때 이전의 object값과 재할당된 object값이 서로 다른 값으로 인식된다.
이 때문에 props 값이 바뀌지 않는데도 CounterB 컴포넌트의 리렌더링이 발생한다.
객체 props에 대해 깊은 비교를 기준으로 리렌더링을 하고 싶다면, React.memo의 두번째 인자로 별도의 비교 함수를 제공하면 된다.
const CounterB = ({ obj }) => {
useEffect(() => {
console.log(`CounterB Update = count : ${obj.count}`);
});
return <div>{obj.count}</div>;
}
const areEqual = (prevProps, nextProps) => {
// 이전 props와 현재 props가 같다면 리렌더링을 일으켜라는 의미
return prevProps.obj.count === nextProps.obj.count;
}
const MemoizedCounterB = React.memo(CounterB, areEqual);
return (
<MemoizedCounterB obj={obj} />
);
React.memo는 전달받은 props 값이 변할 때, useMemo는 두번째 인자인 dependency가 변할 때 인자로 넘긴 함수가 실행되며, 그렇지 않으면 함수는 실행되지 않고 이전의 메모이즈된 결과를 반환한다.