memo()
(1) What is "memo()"?
memo() 는 훅이 아닙니다. 하지만 우리가 배울 useCallback 이나 useMemo 사용하기 위해서는 반드시 알아야할 개념입니다.
Memo 라는 함수는 컴포넌트의 불필요한 리렌더링을 하지 않도록 해주는 함수입니다.
불필요한 리렌더링이란, 화면에서 변경되는 부분이 없음에도 불구하고 아래의 이유로 화면이 다시 렌더링 되는 것을 말합니다.
불필요한 렌더링을 줄이는 것으로 리액트 프로젝트의 부하를 줄이고, 퍼포먼스 능력을 향상시킬 수 있습니다.
*컴포넌트가 Re-Rendering될 조건
1. 자신의 상태가 변경될 때(state 변경)
2. 자신이 전달받은 props가 변경될 때(props)
3. 부모 컴포넌트가 리렌더링 될 때
4. forceUpdate 함수가 실행될 때
(2) 컴포넌트의 리렌더링 확인하는 법
컴포넌트가 리렌더링 된다는 것을 어떻게 확인할 수 있는지 아래 예시를 보겠습니다. App.js 와 Button.js 두개의 컴포넌트가 있습니다.
//App.jsx
import React, { useState } from "react";
import Button from "./components/Button";
const App = () => {
const [value, setValue] = useState("");
const onChangeHandler = (e) => {
setValue(e.target.value);
};
return (
<div>
<input type="text" value={value} onChange={onChangeHandler} />
<Button />
</div>
);
};
export default App;
// components/Button.js
import React from "react";
const Button = () => {
console.log("리렌더링되고 있어요.");
return <button>버튼</button>;
};
export default Button;
input 에 값을 넣으면 Button.js 에 있는 리렌더링되고 있어요. 라는 텍스트가 콘솔에 찍히는 것을 볼 수 있습니다. 이 텍스트가 콘솔에 찍히는 것이 Button.js 컴포넌트가 리렌더링을 하고 있다는 것을 의미합니다.
“컴포넌트가 렌더링된다는 것 → 컴포넌트 함수가 실행된다는 것 → 리렌더링 된다는 것 → 컴포넌트 함수가 계속 실행된다는 것” 이기 때문이죠.
(3) 불필요한 리렌더링을 막는 방법
App.js가 리렌더링 되는 이유는 1번 이유 입니다. value 라는 state가 onChange 될때마다 setState 되고 있기 때문에 리렌더링됩니다. 하지만 이것은 불필요한 리렌더링이 아닙니다. 왜냐하면 input에 값을 입력할때마다 입력한 값이 화면에 보여야하는게 당연하기때문이죠. 여기서 불필요한 리렌더링은 Button.js 에서 발생하고 있습니다. input value의 변화와는 아무 관련이 없는 Button이 3번의 이유로 계속 렌더링 되고 있습니다. 이럴 때 memo() 를 사용합니다. export default memo(Button) 으로 Button.js를 감싸주면 3번의 이유가 발생하더라도 Button.js는 리렌더링을 하지 않습니다.
import React, { memo } from "react";
const Button = () => {
console.log("리렌더링되고 있어요.");
return <button>버튼</button>;
};
export default memo(Button);
이제 Button.js는 리렌더링 하지 않습니다. 최초에 콘솔이 찍힌이후로 input에 아무리 값을 넣어도 텍스트가 보여지지 않는 것이 그것을 보여주는 것이죠.
근데 만약 우리가 Button.js를 부모에서 동작하게 하고자 어떤 함수를 props로 넘겨주면 다시 Button.js는 리렌더링을 하기 시작합니다. 이것의 이유는 무엇이며, 어떻게 해결해야 할까요?
useCallback
(1) Button.js 가 다시 리렌더링을 하게 된 이유
// src/components/Button.js
import React, { memo } from "react";
const Button = ({ onClick }) => {
console.log("리렌더링되고 있어요.");
return <button onClick={onClick}>버튼</button>;
};
export default memo(Button);
memo() 를 사용했음에도 불구하고, Button.js가 다시 리렌더링을 하게된 원인은 함수를 props로 전달 받았기 때문입니다. 그리고 그렇게 전달받은 props가 “2번 부모로부터 전달받은 props 값이 변경된 경우” 에 해당되도록 원인을 제공합니다.
여러분들은 이해가 되지 않을 수도 있습니다. 부모 컴포넌트에서 보내준 onClick 이라는 함수가 대체 어떻게 변경되고 있다는 것인지요.
// src/App.jsx
import React, { useState } from "react";
import Button from "./components/Button";
const App = () => {
// App.js가 리렌더링 될때마다 재생성됨
const [value, setValue] = useState("");
// App.js가 리렌더링 될때마다 재생성됨
const onChangeHandler = (e) => {
setValue(e.target.value);
};
// App.js가 리렌더링 될때마다 재생성됨
const onClickHandler = () => {
console.log("hello button!");
};
return (
<div>
<input type="text" value={value} onChange={onChangeHandler} />
{/* 매번 재생성되는 함수를 props로 넘겨줌 -> Button.js 리렌더링 유발 */}
<Button onClick={onClickHandler} />
</div>
);
};
export default App;
App.js에서부터 Button.js 까지 input에 값을 넣을 때 마다 어떤식으로 컴포넌트들이 동작하고 있는지 아래에 순서대로 나열해보겠습니다.
- input에 값을 입력
- onChangeHandler가 실행되고, setValue가 실행 → value라는 state가 변경됨
- state 가 변경됨에 따라 App.js가 리렌더링 됨
- App.js 가 리렌더링되면, App.js 안에 있는 useState 및 함수들이 다시 생성됨
- 함수란? → onChangeHandler, onClickHandler
- onClickHandler 이 재선언되었기 때문에 Button.js 입장에서는 새로운 값으로 판단함
- Button.js은 onClickHandler 함수를 새로운 props 로 인식하고 리렌더링 함
- Button.js이 리렌더링되면서 console.log("리렌더링되고 있어요."); 이 실행됨
(2) Button.js의 리렌더링을 막으려면?
Button.js가 리렌더링을 하는 이유는 App.js가 리렌더링을 함에 따라 props로 전달하는 함수를 매번 재생성하고 있기 때문입니다. 즉, Button.js 의 리렌더링을 막으려면, App.js가 리렌더링한다고 해도 함수를 매번 재생성하지 않게 하면 됩니다. 리액트에서는 이러한 상황에서 사용할 수 있는 훅을 제공합니다.
(3) useCallback 이란?
컴포넌트가 리렌더링되더라도 생성된 함수를 새로 만들지 않고, 재사용하고자 할때 사용하는 훅입니다. memo()를 사용했음에도 Button.js가 리렌더링되고 있는 원인을 해결할 수 있는 훅이죠.
// 예시 코드
import React, { useCallback, useState } from "react";
import Button from "./components/Button";
const App = () => {
const onClickHandler = useCallback(() => {
console.log("hello button!");
}, []);
return (
<div>
<Button onClick={onClickHandler} />
</div>
);
};
export default App;
(4) useCallback 사용해보기
기본적인 사용방법은 위와 같습니다. 마치 useEffect와 굉장히 비슷하죠? useCallback() 안에서 첫번째 매개변수로 구현하고자 하는 함수가 들어오고, 두번째 매개변수자리에는 의존성 배열이 들어옵니다. 의존성 배열의 역할은 useEffect 와 비슷합니다.
useCallback을 사용하면, 함수가 생성되고 나서 재생성되지 않습니다. 하지만 상황에 따라서는 이 함수를 재생성해줘야 할 때가 있습니다. 그러한 경우 의존성배열에 값을 넣어주면 해당 값이 변경되었을 때 이 함수도 같이 재생성됩니다.
이제 useCallback을 사용해서 memo()가 다시 작동할 수 있도록 개선해보겠습니다. Button.js에 props로 보내고자하는 함수를 useCallback으로 감싸고 넘겨주었습니다.
// src/App.jsx
import React, { useCallback, useState } from "react";
import Button from "./components/Button";
const App = () => {
const [value, setValue] = useState("");
const onChangeHandler = (e) => {
setValue(e.target.value);
};
const onClickHandler = useCallback(() => {
console.log("hello button!");
}, []);
return (
<div>
<input type="text" value={value} onChange={onChangeHandler} />
<Button onClick={onClickHandler} />
</div>
);
};
export default App;
useCallback으로 인해서 함수를 전달했음에도 불구하고 Button.js가 리렌더하지 않고 있음을 알 수 있습니다.
useMemo
(1) useMemo란?
useMemo는 useCallback과 똑같은 기능을 하는 훅 입니다. 다만, 대상이 함수가 아니라 배열이나 객체와 같은 값일 때 사용합니다. 사용원리와 방법은 모두 useCallback과 같습니다.
(2) useMemo 사용해보기
// src/App.jsx
import React, { useState } from "react";
import List from "./components/List";
const App = () => {
const [value, setValue] = useState("");
const onChangeHandler = (e) => {
setValue(e.target.value);
};
const data = [
{
id: 1,
title: "react",
},
];
return (
<div>
<input type="text" value={value} onChange={onChangeHandler} />
<List data={data} />
</div>
);
};
export default App;
App.js 에서 data라는 배열을 List라는 자식 컴포넌트에 넘겨주고 있습니다. List가 memo를 사용하고 있음에도 계속 리렌더링이 될텐데요. 그 원인은 props로 받은 배열이 App.js가 리렌더링될때마다 재생성되고 있기 때문입니다.
useCallback에서 했던 것과 마찬가지로 data 라는 배열을 useMemo를 통해서 재성생되지 않도록 해줍니다. 아래의 변경코드처럼 useMemo를 사용하면 App.js가 리렌더링을 하더라도 재생성되지 않기 때문에 Button.js도 리렌더링을 하지 않습니다.
import React, { useState } from "react";
import { useMemo } from "react";
import List from "./components/List";
const App = () => {
const [value, setValue] = useState("");
const onChangeHandler = (e) => {
setValue(e.target.value);
};
const data = useMemo(() => {
return [
{
id: 1,
title: "react",
},
];
}, []);
return (
<div>
<input type="text" value={value} onChange={onChangeHandler} />
<List data={data} />
</div>
);
};
export default App;
주의사항
memo(), useCallback, useMemo 의 무분별한 사용은 오히려 퍼포먼스 성능에 악영향이 될 수 있습니다. 반드시 리렌더링이 필요한 컴포넌트나 또는 값 (함수, 배열, 객체)에는 memo(), useCallback, useMemo 를 사용하는 것이 오히려 좋지 않습니다. 왜냐하면 memo(), useCallback, useMemo 를 사용하는 것은 리액트에게 “렌더링 이후의 값과 전의 값을 비교해서 같으면 재생성하지마!” 라고 주문을 하는거에요. 근데 반드시 바뀌어야하는 값에 저 기능을 사용하면 굳이 비교하지 않아도 될 것들에도 리액트가 비교를 하기 때문입니다. 그렇기 때문에 저 기능을 사용하기에 앞서, 불필요한 리렌더링이 맞는지, 개선할 수 있는 부분인지 확인하고 사용하도록 하세요.
최종정리
- memo() 를 사용하면 컴포넌트의 불필요한 리렌더링을 막을 수 있다. 다만, 컴포넌트의 props가 있는 경우 useCallback과 useMemo를 적절하게 사용하여 리렌더링의 원인이 되는 것을 모두 제거해줘야 정상적으로 작동한다.
- useCallback의 대상은 함수, useMemo의 대상은 객체나 배열과 같은 값이다.
원시타입 데이터는 useMemo를 사용하지 않아도 리렌더링하지 않습니다. → useMemo 안써도 됩니다.
'내일배움캠프[4기_Reac트랙] > [원격]React_심화' 카테고리의 다른 글
Custom hook[심화_7] (0) | 2023.01.08 |
---|---|
Thunk(2)[심화_5] (0) | 2023.01.06 |
Thunk(1)[심화_4] (0) | 2023.01.06 |
Axios[심화_3] (0) | 2023.01.06 |
json-server[심화_2] (0) | 2022.12.22 |