참고
- debounce vs throttle로 최적화하기 with useMemo (lodash-es)
아래 코드를 실행해 보자.
import React, { useState } from "react";
const MySpan = ({ input }) => {
console.log("MySpan is Rendered...");
return <span>input 0 : {input}</span>;
};
const MemoTest = () => {
const [value0, setValue0] = useState("");
const [value1, setValue1] = useState("");
const [value2, setValue2] = useState("");
const [number, setNumber] = useState(0);
const handleChange0 = (e) => {
console.log("change 0");
let input = e.target.value;
setValue0(input);
};
const handleChange1 = (e) => {
console.log("change 1");
let input = e.target.value;
setValue1(input);
};
const handleChange2 = (e) => {
console.log("change 2");
let input = e.target.value;
setValue2(input);
};
const upperCase = (input) => {
console.log("upper case", input);
return input.toUpperCase();
};
const lowerCase = (input) => {
console.log("lower case", input);
return input.toLowerCase();
};
const upperInput = upperCase(value1);
const lowerInput = lowerCase(value2);
const plusNumber = () => {
console.log("plus number");
setNumber((prev) => prev + 1);
};
return (
<div
style={{
margin: 5,
display: "flex",
flexDirection: "column",
width: 200,
}}
>
<input
type="text"
value={value0}
onChange={handleChange0}
placeholder="value 0"
/>
<input
type="text"
value={value1}
onChange={handleChange1}
placeholder="value 1"
/>
<input
type="text"
value={value2}
onChange={handleChange2}
placeholder="value 2"
/>
<MySpan input={value0} />
<span>input 1 : {upperInput}</span>
<span>input 2 : {lowerInput}</span>
<button onClick={plusNumber}>+</button>
<span>number : {number}</span>
</div>
);
};
export default MemoTest;
위의 코드의 기능은 다음과 같다.
- value0에 값을 입력하면 MySpan에 value0이 그대로 출력된다. ( const MySpan = ({ input }) => { ... } )
- value1에 값을 입력하면 input 1 : 에 대문자로 출력된다. ( const upperInput = upperCase(value1); )
- value2에 값을 입력하면 input 2 : 에 소문자로 출력된다. ( const lowerInput = lowerCase(value2); )
- + 버튼을 누르면 number에 1을 더한다. ( const plusNumber = () => { setNumber((prev) => prev + 1); }; )
그러나 다음과 같이 렌더링에 의해 위의 기능과 관련된 로그가 모두 출력되는 것을 알 수 있다.
1) value0을 수정하면, change 0 로그가 출력되고,
수정하지 않은 value1, value2와 관련된 "upper / lower case" 로그가 출력된다.
2) value1, value2를 수정하면 change 1, 2가 출력되고, value0과 관련된 "My span is ..." 로그가 출력된다.
3) + 버튼을 누르면 "plus number"가 출력되고, "upper / lower case", "My span is ..." 로그가 출력된다.
리액트에서 useState의 set 메서드로 인해 값이 바뀌는 경우, 렌더링이 일어나게 된다.
그리고 이 렌더링에 의해 해당 상태를 사용하는 컴포넌트가 모두 다시 렌더링된다.
따라서 다른 값이 변경되지 않더라도 MySpan, upperInput, lowerInput의 렌더링으로 로그가 출력되고 있다.
React.memo
React.memo는 함수 컴포넌트를 메모이제이션 할 수 있다.
즉, 값의 변경이 없는 경우 이전에 렌더링된 결과를 재사용한다.
React.memo를 MySpan에 적용해 보자.
const MySpan = ({ input }) => {
console.log("MySpan is Rendered...");
return <span>input 0 : {input}</span>;
};
const MySpanMemo = React.memo(MySpan);
다음과 같이 컴포넌트를 변경한다.
<MySpanMemo input={value0}/>
이제 value0이 변경될 때만 "MySpan is Rendered..." 가 출력된다.
useMemo
useMemo도 마찬가지로 메모이제이션을 이용한다.
의존성 배열의 값이 변경되지 않으면, 이전 값을 사용하게 된다. (useEffect의 의존성 배열과 같다)
const upperInput = useMemo(() => upperCase(value1), [value1]);
value1이 변경될 때만, "upper case" 로그가 출력된다.
useMemo는 컴포넌트에도 사용할 수 있다.
예를 들어 React.memo로 MySpan을 최적화 한 예시를, useMemo를 이용하여 아래와 같이 처리할 수 있다.
const MySpanUseMemo = useMemo(() => <MySpan input={value0} />, [value0]);
의존성 배열에 value0이 있으므로, value0이 변경되지 않는다면, MySpanUseMemo는 이전 값을 사용한다.
{/* <MySpanMemo input={value0} /> */}
{MySpanUseMemo}
useCallback
useCallback은 인수로 넘겨받은 함수를 기억한다.
즉, useMemo로 함수를 반환하는 경우라면 useCallback을 사용하면 된다.
컴포넌트가 다시 렌더링이 될 때, 선언된 함수도 새롭게 렌더링 한다.
먼저 useState로 선언한 number를 로그에 추가해 보자.
const plusNumber = () => {
console.log("plus number", number);
setNumber((prev) => prev + 1);
};
디버깅을 위해 lowerCase의 로그는 임시로 지운다.
const lowerCase = (input) => {
// console.log("lower case", input);
return input.toLowerCase();
};
plus number에서 number가 계속 증가하는 것을 알 수 있다.
이제 useCallback으로 plusNumber를 선언해 보자.
의존성 배열( [ ] )이 비어 있으므로, 컴포넌트가 처음 렌더링 될 때만 함수를 만들게 된다.
const plusNumber = useCallback(() => {
console.log("plus number", number);
setNumber((prev) => prev + 1);
}, []);
최초 설정된 number 0만 출력되고 있다.
즉, 컴포넌트 렌더링이 발생해도, plusNumber가 다시 렌더링 되지 않았다.
전체 코드는 다음과 같다.
import React, { useCallback, useMemo, useState } from "react";
const MySpan = ({ input }) => {
console.log("MySpan is Rendered...");
return <span>input 0 : {input}</span>;
};
const MySpanMemo = React.memo(MySpan);
const MemoTest = () => {
const [value0, setValue0] = useState("");
const [value1, setValue1] = useState("");
const [value2, setValue2] = useState("");
const [number, setNumber] = useState(0);
const MySpanUseMemo = useMemo(() => <MySpan input={value0} />, [value0]);
const handleChange0 = (e) => {
console.log("change 0");
let input = e.target.value;
setValue0(input);
};
const handleChange1 = (e) => {
console.log("change 1");
let input = e.target.value;
setValue1(input);
};
const handleChange2 = (e) => {
console.log("change 2");
let input = e.target.value;
setValue2(input);
};
const upperCase = (input) => {
console.log("upper case", input);
return input.toUpperCase();
};
const lowerCase = (input) => {
console.log("lower case", input);
return input.toLowerCase();
};
const upperInput = useMemo(() => upperCase(value1), [value1]);
const lowerInput = lowerCase(value2);
const plusNumber = useCallback(() => {
console.log("plus number", number);
setNumber((prev) => prev + 1);
}, []);
return (
<div
style={{
margin: 5,
display: "flex",
flexDirection: "column",
width: 200,
}}
>
<input
type="text"
value={value0}
onChange={handleChange0}
placeholder="value 0"
/>
<input
type="text"
value={value1}
onChange={handleChange1}
placeholder="value 1"
/>
<input
type="text"
value={value2}
onChange={handleChange2}
placeholder="value 2"
/>
<MySpanMemo input={value0} />
{MySpanUseMemo}
<span>input 1 : {upperInput}</span>
<span>input 2 : {lowerInput}</span>
<button onClick={plusNumber}>+</button>
<span>number : {number}</span>
</div>
);
};
export default MemoTest;
React.memo, useMemo, useCallback 모두 렌더링 시, 불필요한 리렌더링을 방지하지만,
이를 위해 메모리에 이전 값을 저장하기 때문에 메모리 점유율이 높아지게 된다.
따라서 꼭 필요한 상황에 최적화를 하자.
'개발 > React' 카테고리의 다른 글
리액트 - Handsontable Context Menu 커스터마이징 (Fix Issue for Disappearing Comments) (0) | 2024.04.12 |
---|---|
리액트 - 게으른 초기화로 useState 초기화하기 (Lazy Initialization for useState) (0) | 2024.04.07 |
리액트 - classnames로 CSS 스타일 조건부 렌더링하기 (0) | 2024.04.07 |
리액트 - debounce vs throttle로 최적화하기 with useMemo (lodash-es) (0) | 2024.04.06 |
리액트, Node JS - Socket.IO로 Toast UI Editor 동시 편집하기 (0) | 2024.04.06 |
댓글