
React는 특정 값이 변경되면 화면을 다시 렌더링 하여 변경된 화면을 그려줍니다. 그런데 이 부분이 때로는 원치 않는 랜더링으로 성능의 저하를 가져올 수 있습니다.
예를 들어 두 개의 input 창이 있을 때 React는 두 개의 input 중 한 개의 input 창에서 값의 변화가 일어나도 랜더링이 발생됩니다. 이렇게 state 값이 변할 때 컴포넌트의 리렌더링을 방지하기 위해 값이 변경되는 것에만 메모이제이션값을 반환하는 코드가 memo(React.memo, useMemo)입니다.
위에서 설명한것을 예제로 실습해볼려고 합니다. input 테그를 두개를 만들어 사용해봅니다.
const OptimizerTest = () => {
const [val1, setVal1] = useState(0)
const [val2, setVal2] = useState(0)
const handleAdd1 = () => {
setVal1(prev => prev + 1)
}
const handleAdd2 = () => {
setVal2(prev => prev + 1)
}
const computedVal = val1 * val1
console.log('computedValue', computedVal)
return (
<>
<div>val1: {val1}</div>
<div>val2: {val2}</div>
<div>val3: {computedVal}</div>
<br />
<button type="button" onClick={handleAdd1}>
Add val1
</button>
<button type="button" onClick={handleAdd2}>
Add val2
</button>
</>
)
}
val1 버튼을 누르나, val2 버튼을 누르나 console의 진행상황은 계속 적히게 됩니다.

하지만 val2버튼은 computedVal 객체에는 아무런 영향을 주지 않습니다. React가 Component의 변화를 감지하고 화면을 전체 re-reder하기 때문에 생기는 문제 입니다.
아래 코드처럼 변화를 줍시다.
/*
const computedVal = val1 * val1
console.log('computedValue', computedVal)
*/
// useMemo는 hook이므로 함수형 컴포넌트에서만 사용기 가능하기 때문에 아래와 같이 변화를 줍니다.
const computedVal = useMemo(() =>
{
console.log('computedValue', val1 * val1);
return val1 * val1;
},[val1]);

다음과 같이 val2를 눌러도 console은 반응을 안하는것을 알 수 있습니다. val1의 값에 변화가 생긴게 아니라 computeVal의 랜더를 진행하지 않은 것입니다.
React.memo라고 함수형뿐만 아니라 전체 컴포넌트를 memo할때사용하는 기능입니다.
아래 예시를 보시죠.
const OptimizerChild = () => {
const [val3, setVal3] = useState(0)
const [val4, setVal4] = useState(0)
const handleAdd1 = () => {
setVal3(prev => prev + 1)
}
const handleAdd2 = () => {
setVal4(prev => prev + 1)
}
const computedVal2 = val3 * val3
console.log('Child', computedVal2)
// const computedVal = useMemo(() =>
// {
// console.log('computedValue', val1 * val1);
// return val1 * val1;
// },[val1]);
return (
<>
<div>ChildVal1: {val3}</div>
<div>ChildVal2: {val4}</div>
<div>ChildVal3: {computedVal2}</div>
<br />
<button type="button" onClick={handleAdd1}>
Add val1
</button>
<button type="button" onClick={handleAdd2}>
Add val2
</button>
</>
);
}
export default OptimizerChild;
return (
<>
<div>val1: {val1}</div>
<div>val2: {val2}</div>
<div>val3: {computedVal}</div>
<br />
<button type="button" onClick={handleAdd1}>
Add val1
</button>
<button type="button" onClick={handleAdd2}>
Add val2
</button>
<OptimizerChild /> // 요기 추가하세요.
</>
)
그럼 결과는 다음과 같습니다.

빨간색쪽이 자식이니 부모쪽에는 반응이 없습니다만, 부모쪽 버튼은 자식까지 반응해 버리네요. 결과는 4로 변함없는데 말이죠. 이럴때 React.memo를 사용하시면 됩니다.
export default React.memo(OptimizerChild);

그럼 다음과 같이 부모버튼을 눌러도 자식의 컴포넌트에 변화가 없다면 re-render하지 않습니다.
위에서 설명하였던 memo기능들은 의존성을 참조하는 객체의 동일성을 갖고있다는 가정에서 기능이 작동합니다. 단, 배열이나 java Map, javascript object를 사용해보신 분들은 아시겠지만, 그 안에 값이 동일하더라도 동일성을 갖지는 않습니다.
let A = {obj : 1};
let B = {obj : 1};
A === B //false
그래서 useCallback을 이용해 함수를 특정 조건이 변경되지 않는 이상 재생성하지 못하게 제한하여 함수 동등성을 보장할 수 있습니다.
function Profile({ id }) {
const [data, setData] = useState(null)
const fetchData = () =>
fetch(`https://test-api.com/data/${id}`)
.then(response => response.json())
.then(({ data }) => data)
useEffect(() => {
fetchData().then(data => setData(data))
}, [fetchData])
언뜻 보면 페이지가 마운트 되었을 때 데이터 가져오는 fetchData 함수를 호출해 데이터를 잘 가져오는 듯 보이지만, fetchData는 함수이기 때문에 id 값에 관계없이 컴포넌트가 렌더링 될 때마다 새로운 참조값으로 변경이 됩니다. 함수를 참조하는 useEffect가 실행되어 re-render되고 무한루프에 진입하게 됩니다.
이 문제를 해결하기 위해서 다음고 같이 useCallback을 사용하여 id를 참조하게 만들면 됩니다.
const fetchData = useCallback(
() =>
fetch(`https://test-api.com/data/${id}`)
.then(response => response.json())
.then(({ data }) => data),
[id],
)
만약 callback을 참조하는 객체가 setter 되는 객체와 동일할 경우 배열을 비워놓고 함수형식으로 setter를 제작하셔도 알아서 작동합니다.
//해당 컴포넌트는...
const onCreate = useCallback((author, content, emotion) => {
const create_dt = new Date().getTime();
const newItem = {
author,
content,
emotion,
create_dt,
id: dataId.current
};
dataId.current += 1;
setData([newItem, ...data]);
},[data]);
// 아래와 같이 수정가능합니다.
const onCreate = useCallback((author, content, emotion) => {
const create_dt = new Date().getTime();
const newItem = {
author,
content,
emotion,
create_dt,
id: dataId.current
};
dataId.current += 1;
setData((data) => {[newItem, ...data]}); // 변경된 지점
},[]);
연습을 해보다가 발견했습니다. 다음과 같이 작동이 안됩니다.
//작동 안됨
const newDiaryList = data.filter( (it) => it.id !== targetId );
setData(newDiaryList);
//작동 됨
setData(data.filter( (it) => it.id !== targetId ));
어찌보면 useEffect로 값을 참조하여 useMemo와 같은 기능을 구사하는게 가능합니다.
단, useEffect는 값의 변화를 보고 함수를 실행한다는 것, useMemo는 값이 바뀐 유무를 보고 실행하지 않는다는 것을 명심하며, useMemo 는 랜더링 과정 중에, useEffect 는 랜더링이 끝나고 나서 발동된다고 합니다.
이점을 이해하시고 더 효과적인 코드를 작성하시면 될 겁니다.
참고자료들___
(참고) Web: 최적화와 React.memo, useMemo 알아보기
(참고) useMemo와 useCallback는 왜, 언제 사용할까?
(참고) React의 컴포넌트(Component) - 함수형, 클래스형 컴포넌트
(참고) React.memo와 useMemo 차이점
(참고) useMemo 와 useEffect 비교