리액트는 컴포넌트를 활용하는 라이브러리이다. 컴포넌트 탐색하고 변화한 부분을 찾아, 해당 컴포넌트만 변화시키는 것이 가능하다. 리액트에서는 자체적으로 앱의 성능을 향상시킬 수 있는 몇가지 기능을 가지고 있는데, 렌더링을 미리 메모라이징 한 후, 불필요한 렌더링을 회피하는 것이 해당 기능들의 키 포인트 이다.
리액트는 이전과 렌더링 결과를 비교하여 컴포넌트의 갱신을 결정한다. React.memo()
를 통해 랩핑 된 컴포넌트는 미리 메모라이징 되며 이후 앱이 시작할때 미리 렌더링 되었던 이전과 컴포넌트가 동일하다면 해당 내용을 지속적으로 재사용한다.
그렇다면 모든 컴포넌트를 미리 다 메모라이징 하는 것이 좋지 않나? 그것은 아니다. 컴포넌트를 메모라이징하는 것은 상당한 비용적인 측면이 뒤따른다. 자주 데이터가 갱신되는 컴포넌트는 지속적인 리렌더링이 필요할 것이다. 자주 갱신되는 컴포넌트를 미리 메모라이징 하는 것은 오히려 성능을 악화시키는 요인으로 작용할 것이다.
따라서 React.memo
가 필요한 컴포넌트를 잘 파악하여 적절하게 사용해야한다.
React.memo()
는 기존의 라이프 사이클 메서드 중shouldComponentUpdate()
함수형 컴포넌트에서 사용하도록 만든 리액트 전용 HOC(High-Order-Component)
이다.
// app.js
...
...
return (
<div className="app">
<h1>Hi there!</h1>
{showParagraph && <p>This is new!</p>}
// 일부러 고정된 데이터만 보내고 있다.
<DemoOutput show={false}></DemoOutput>
<Button onClick={allowToggleHandler}>Allow Toggle!</Button>
<Button onClick={toggleParagraphHandler}>Toggle Paragraph!</Button>
</div>
);
// DemoOutput.js
import React from "react";
import MyParagraph from "./MyParagraph";
const DemoOutput = (props) => {
console.log("DemoOupput RUNNING");
return <MyParagraph>{props.show ? "This is new!" : ""}</MyParagraph>;
};
export default React.memo(DemoOutput);
useCallback() 을 이해하기 위해서는 원시형과 참조형의 데이터 차이를 먼저 알고 있어야 한다. 아래의 컴포넌트가 새롭게 갱신되었을때 갱신된 toggleParagraphHandler()
와 toggleParagraphHandler()
를 프로그래밍 언어는 두 함수를 동일한 함수로 보지 않는다. 메모리에 저장되어 주소값이 서로 다르기 때문이다.
따라서 컴포넌트 재평가 마다 메모리에 새로운 toggleParagraphHandler()
함수가 저장이 될테고, 결국 비효율적인 메모리 낭비를 하게 되는 것 입니다.
function App() {
const [showParagraph, setShowParagraph] = useState(false);
const [allowToggle, setAllowToggle] = useState(false);
console.log("APP RUNNING");
const toggleParagraphHandler = () => {
if (allowToggle) {
setShowParagraph((prevShowParagraph) => !prevShowParagraph);
}
};
const allowToggleHandler = () => {
setAllowToggle(true);
};
return (
<div className="app">
<h1>Hi there!</h1>
{showParagraph && <p>This is new!</p>}
<DemoOutput show={false}></DemoOutput>
<Button onClick={allowToggleHandler}>Allow Toggle!</Button>
<Button onClick={toggleParagraphHandler}>Toggle Paragraph!</Button>
</div>
);
}
해당 문제를 해결하는 방법은 필요한 함수에 useCallback()
함수를 적용해주면 된다. 사용방법은 useEffect()
와 같이 두번째 인자로 의존성 인자값을 넣어주면 된다. 이제부터 해당 함수는 의존성 인자값이 변화하지 않는 이상 기존에 한번 저장한 주소값을 통해 저장한 함수를 가져와 사용합니다.
const toggleParagraphHandler = useCallback(() => {
if (allowToggle) {
setShowParagraph((prevShowParagraph) => !prevShowParagraph);
}
},[allowToggle]);
useCallback()
특정 함수를 새로 만들지 않고 재사용하기 위해서 사용한다면, useMemo()
는 결과 값 을 재사용할때 사용한다. 여기 동일한 결과값을 나타내는 함수 a()
를 예로 들어 이야기해보자.
const app = () => {
const value = a();
return <div>{value}</div>
}
function a() {
return 1;
}
컴포넌트가 새롭게 갱신될때마다, 동일한 함수 a()
를 메모리에 계속해서 저장하는 불필요한 작업이 이루어진다. 또한 함수가 의존성이 적고, 동일한 결과를 반환하는 함수라면, 함수 자체를 저장하기 보다는 값을 아예 저장하여 연산과정마저도 생략하게 만들 수 있다.
useMemo()
역시 useCallback()
동일하게 의존성 배열 인자를 받으며, 해당 인자가 변화하는 경우에만 값을 새롭게 메모라이징 한다.
const app = () => {
const value = useMemo(()=> a(), [user]);
return <div>{value}</div>
}
우선 공통적으로 3가지 기능 모두, 필요 요소를 메모라이징 하여, 리액트 앱의 성능을 한층 더 높이는데 쓰일 수 있다.
서로간의 차이점은, React.memo()
컴포넌트 전체를 담당하는 HOC
의 영역이고, useCallback()
및 useMemo()
은 이름처럼 React Hooks
에 속한다.
useCallback()
은 함수를 새롭게 생성하는 것을 방지하며 의존성 인자가 변화하지 않는 이상 동일한 참조값에서 함수를 재사용하도록 해준다. useMemo()
는 동일한 결과 값을 반복하여 연산하는 것을 방지한다.
useCallback()
은 함수 생성에 필요한 데이터가 많은 경우 적합하고, useMemo()
값이 일정하면서도 복잡한 연산이 필요한 경우 매우 유용하다.