useMemo 와 useCallback 은 불필요한 자식 요소의 재렌더링이나 계산을 억제하기 위해 사용합니다.
설명에 앞서 리액트의 컴포넌트의 재렌더링되는 시점을 보면
- props 값이나 state(상태값) 이 변경 되었을 때
- 컴포넌트에서 참조되는 Context 값이 변경 되었을 때
- 부모 컴포넌트가 렌더링 되었을 때
이렇게 크게 세가지로 인해 자식 컴포넌트에서 불필요한 재렌더링이 일어납니다.
React.memo
함수 컴포넌트를 메모이제이션할 수 있다. 부모의 컴포넌트가 제렌더링 되었을 때 자식 컴포넌트도 재랜더링이 일어나게 되어있습니다.
하지만 React.memo를 사용하면 props의 값이 변하지 않는 이상 자식 컴포넌트의 재렌더링을 방지 할 수 있습니다.
아래 예시를 보면 Child_1 컴포넌트는 메모이제이션을 하지 않았고 Child_2 컴포넌트는 메모이제이션을 사용하였다.
Parent의 count 값이 증가하면서 Child 컴포넌트들에 props 값이 변경되게 됩니다.
import { memo, useState } from "react";
/* Child 1 memo (X) */
interface Child_1Prop {
isChild: boolean;
}
const Child_1 = ({ isChild }: Child_1Prop) => {
console.log(`Child_1 이 다시 그려졌습니다. isChild: ${isChild}`);
return <div>{isChild ? "Child_1" : ""}</div>;
};
/* Child 2 memo (O) */
interface Child_2Prop {
isChild: boolean;
}
const Child_2 = memo(({ isChild }: Child_2Prop) => {
console.log(`Child_2 이 다시 그려졌습니다. isChild: ${isChild}`);
return <div>{isChild ? "Child_2" : ""}</div>;
});
/* Parent */
export const Parent = () => {
const [count, setCount] = useState<number>(1);
const isChild_1 = count % 3 === 0;
const isChild_2 = count % 5 === 0;
console.log(`Parent 가 그려졌습니다. count => ${count}`);
const handleClick = () => {
setCount((prev) => prev + 1);
};
return (
<div>
<button onClick={handleClick}>+1</button>
<p>현재 카운트 : {count}</p>
<div>
<Child_1 isChild={isChild_1} />
<Child_2 isChild={isChild_2} />
</div>
</div>
);
};
첫번째 렌더링시 모든 컴포넌트가 렌더링 됩니다.
+1 버튼을 눌러 count 의 state 값이 변경되면서 Parent 컴포넌트는 재렌더링이 일어나게 됩니다.
Child_1 컴포넌트는 부모가 재렌더링이 일어나면서 해당 컴포넌트도 재렌더링이 일어나게 됩니다.
Child_2 컴포넌트는 메모이제이션이 된 컴포넌트로 부모의 컴포넌트가 재렌더링이 일어나더라도 props로 넘어온 isChild값이 변경되지 않는다면 재렌더링이 일어나지 않습니다.
useCallback
useCallback 은 함수를 메모이제이션하기 위한 hook입니다.
Child_1 => memo (X), useCallback (X)
Child_1 컴포넌트는 memo를 사용하지 않는 컴포넌트로 부모 컴포넌트가 재렌더링 될 때마다 재렌더링이 일어납니다.
Child_2 => memo (O), useCallback (X)
Child_2 컴포넌트는 memo를 사용하였지만 부모 컴포넌트로부터 받은 onClick 함수가 업데이트 될 때마다 재렌더링이 일어납니다.
Child_3 => memo (X), useCallback (O)
Child_3 컴포넌트는 useCallback 를 사용하여 onClick 함수가 업데이트 되더라도 해당 함수를 업데이트하지 않아야하지만 memo를 사용하지 않는 컴포넌트이므로 부모 컴포넌트가 재렌더링 될 때마다 재렌더링이 일어납니다.
Child_3 => memo (O), useCallback (O)
Child_1 컴포넌트는 memo를 사용하여 부모 컴포넌트가 재렌더링 될 때 해당 컴포넌트는 재렌더링을 방지합니다.
또한 useCallback 를 사용한 onClick 함수를 props로 전달 받았으므로 부모 컴포넌트의 onClick 함수가 업데이트 되더라도 해당 컴포넌트는 처음에 받은 onClick 함수를 사용하므로 재렌더링이 일어나지 않습니다.
import { memo, useCallback, useState } from "react";
/* Child 1 memo (X), useCallback (X) */
interface Child_1Prop {
onClick: () => void;
}
const Child_1 = ({ onClick }: Child_1Prop) => {
console.log(`Child_1 이 다시 그려졌습니다`);
return <button onClick={onClick}>{"Child_2"}</button>;
};
/* Child 2 memo (O), useCallback (X) */
interface Child_2Prop {
onClick: () => void;
}
const Child_2 = memo(({ onClick }: Child_2Prop) => {
console.log(`Child_2 이 다시 그려졌습니다`);
return <button onClick={onClick}>{"Child_2"}</button>;
});
/* Child 3 memo (X), useCallback (O) */
interface Child_3Prop {
onClick: () => void;
}
const Child_3 = ({ onClick }: Child_3Prop) => {
console.log(`Child_3 이 다시 그려졌습니다`);
return <button onClick={onClick}>{"Child_3"}</button>;
};
/* Child 4 memo (O), useCallback (O) */
interface Child_4Prop {
onClick: () => void;
}
const Child_4 = memo(({ onClick }: Child_4Prop) => {
console.log(`Child_4 이 다시 그려졌습니다`);
return <button onClick={onClick}>{"Child_4"}</button>;
});
/* Parent */
export const Parent = () => {
const [count, setCount] = useState<number>(1);
console.log(
`------------------------- Parent 가 그려졌습니다. count => ${count} -------------------------`
);
const handleClickMemoization = useCallback(() => {
setCount((prev) => prev + 1);
}, []);
const handleClick = () => {
setCount((prev) => prev + 1);
};
return (
<div>
<p>현재 카운트 : {count}</p>
<div>
<Child_1 onClick={handleClick} />
<Child_2 onClick={handleClick} />
<Child_3 onClick={handleClickMemoization} />
<Child_4 onClick={handleClickMemoization} />
</div>
</div>
);
};
useMemo
useMemo는 값을 메모이제이션하기 위한 hook 입니다.
value_1 의 값은 그릴때 마다 호출되어 값을 변경합니다. items의 값이 변경되지 않은 상태에서도 계산이 일어나는건 성능저하가 일어날 수 있습니다.
value_2 의 값은 useMemo로 값을 메모이제이션한 값으로 items의 값이 변경이 일어 났을 때만 호출 되어 성능 저하 방지를 할 수 있습니다.
import { ChangeEvent, useMemo, useState } from "react";
export const UseMemoSample = () => {
const [text, setText] = useState<string>("");
const [items, setItems] = useState<string[]>([]);
const handleOnChangeInput = (e: ChangeEvent<HTMLInputElement>) => {
setText(e.target.value);
};
const handleOnClickBtn = () => {
setItems((prve) => {
return [...prve, text];
});
setText("");
};
const value_1 = items.reduce((sub, item) => sub + item.length, 0);
const value_2 = useMemo(() => {
return items.reduce((sub, item) => sub + item.length, 0);
}, [items]);
return (
<div>
<div>
<input value={text} onChange={handleOnChangeInput}></input>
<button onClick={handleOnClickBtn}>Add</button>
</div>
<div>
{items.map((item, idx) => {
return <p key={idx}>{item}</p>;
})}
</div>
<div>
<p>Total Value_1 : {value_1}</p>
<p>Total Value_2 : {value_2}</p>
</div>
</div>
);
};