-
Week02 리액트/Next.js 기초 3.5 React Hooks (2) - 메모이제이션 훅카테고리 없음 2023. 12. 31. 18:37
3.5.1 useCallback과 useMemo - 메모이제이션 훅
메모이제이션이란?
어떤 함수의 계산 결과를 저장하고, 같은 호출이 발생하면 새 연산을 하지 않고 저장한 값을 반환해 재사용하는 최적화 방법이다.
React에서 메모이제이션이 필요한 이유는 같은 값을 재사용함에도 화면을 다시 그리는 경우 일 것이다.
React에서 화면을 그리는 경우는 다음과 같다.
- Props나 내부 상태가 업데이트 됐을 때
- 컴포넌트가 참조하는 Context 값이 업데이트 됐을 때
- 부모 컴포넌트가 다시 그려졌을 때
Props, 내부상태, Context의 변화에 따른 컴포넌트의 재렌더링은 당연하지만, 내부 값의 변화가 없는데 부모가 다시 그려질때의 리렌더링은 불필요하다. 이를 메모이제이션 훅을 사용해 방지할 수 있다.
우선, React에서 기본 제공하는 memo 함수를 사용해 감싸서 방지할 수 있다.
//src/components/Parent.tsx import React, { memo, useState } from "react"; type FizzProps = { isFizz: boolean; }; const Fizz = (props: FizzProps) => { const { isFizz } = props; console.log(`Fizz가 다시 그려졌습니다. isFizz=${isFizz}`); return <span>{isFizz ? "Fizz" : ""}</span>; }; type BuzzProps = { isBuzz: boolean; }; //BuzzProps의 값이 변경되지않으면 Buzz는 다시 그려지지 않음. const Buzz = memo<BuzzProps>((props) => { const { isBuzz } = props; console.log(`Buzz가 다시 그려졌습니다. isBuzz=${isBuzz}`); return <span>{isBuzz ? `Buzz` : ``}</span>; }); //isFizz는 3의배수일때, isBuzz는 5의 배수일때 값이 변경됨. export const Parent = () => { const [count, setCount] = useState(1); const isFizz = count % 3 === 0; const isBuzz = count % 5 === 0; console.log(`Parent가 다시 그려졌습니다. count=${count}`); return ( <div> <button onClick={() => setCount((c) => c + 1)}>+1</button> <p>{`현재 카운트 : ${count}`}</p> <p> <Fizz isFizz={isFizz} /> <Buzz isBuzz={isBuzz} /> </p> </div> ); }; //index.tsx import React from "react"; import ReactDOM from "react-dom/client"; import "./index.css"; import { Parent } from "./components/Parent"; import reportWebVitals from "./reportWebVitals"; const root = ReactDOM.createRoot( document.getElementById("root") as HTMLElement ); root.render(<Parent />); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals();
실행한 콘솔로그는 다음과 같다.
count가 3->4로 변경될때는 Fizz가 그려져야 하지만 4->5로 변경될때는 Fizz가 그려질 필요가 없지만 재렌더링된다.
반면 Buzz는 기존의 값에서 변경될때만 재렌더링 되는 것을 확인할 수 있다.
그러면 memo를 쓰면 될 것 같은데 왜 memoization hook을 사용할까?
메모이제이션 컴포넌트에 변수가 아닌 함수나 객체를 전달하면 부모의 화면이 다시 그려질 때 컴포넌트도 다시 그려진다.
//src/components/Parent.tsx import React, { memo, useState } from "react"; type FizzProps = { isFizz: boolean; }; const Fizz = (props: FizzProps) => { const { isFizz } = props; console.log(`Fizz가 다시 그려졌습니다. isFizz=${isFizz}`); return <span>{isFizz ? "Fizz" : ""}</span>; }; type BuzzProps = { isBuzz: boolean; //props에 onClick 추가 onClick: () => void; }; const Buzz = memo<BuzzProps>((props) => { const { isBuzz, onClick } = props; console.log(`Buzz가 다시 그려졌습니다. isBuzz=${isBuzz}`); return <span onClick={onClick}>{isBuzz ? `Buzz` : ``}</span>; }); export const Parent = () => { const [count, setCount] = useState(1); const isFizz = count % 3 === 0; const isBuzz = count % 5 === 0; //onBuzzClick 함수는 parent가 다시 그려질 때마다 작성된다. const onBuzzClick = () => { console.log(`Buzz가 클릭됐습니다.isBuzz=${isBuzz}`); }; console.log(`Parent가 다시 그려졌습니다. count=${count}`); return ( <div> <button onClick={() => setCount((c) => c + 1)}>+1</button> <p>{`현재 카운트 : ${count}`}</p> <p> <Fizz isFizz={isFizz} /> <Buzz isBuzz={isBuzz} onClick={onBuzzClick} /> </p> </div> ); };
이 경우에는 Buzz에 전달되는 함수가 새로 작성되기 때문에 Buzz는 계속 렌더링 된다.
이렇게 전달되는 값이 함수나 객체일때도 메모이제이션 될 수 있도록 useCallback과 useMemo 훅을 사용한다.
3.5.2 - 1 useCallback
useCallback은 함수를 메모이제이션 하기 위한 훅이다.
//src/components/Parent.tsx import { useCallback, useState, memo } from "react"; type ButtonProps = { onClick: () => void; }; const IncrementButton = memo((props: ButtonProps) => { const { onClick } = props; console.log(`IncrementButton이 다시 그려졌습니다.`); return <button onClick={onClick}>Increment</button>; }); const DoubleButton = memo((props: ButtonProps) => { const { onClick } = props; console.log(`DoubleButton이 다시 그려졌습니다.`); return <button onClick={onClick}>Double</button>; }); export const Parent = () => { const [count, setCount] = useState(1); const increment = () => { setCount((c) => { return c + 1; }); }; const double = useCallback(() => { setCount((c) => { return c * 2; }); }, []); console.log(`Parent가 다시 그려졌습니다. count=${count}`); return ( <div> <p>{`현재 카운트 : ${count}`}</p> <p> <IncrementButton onClick={increment} /> <DoubleButton onClick={double} /> </p> </div> ); };
Increment button은 React의 memo함수를 사용했지만 Parent가 다시 그려지며 함수가 재정의되는 과정에서 재랜더링 된다. 하지만 Double button은 함수를 감싸 memoization함으로써 Parent가 다시 그려지더라도 함수가 같아 재렌더링 되지않는다.
이렇게 더블버튼을 클릭하더라도 함수가 변하지 않았기 때문에 DoubleButton의 재렌더링을 막아 최적화 할 수 있다.
UseCallback의 두번째 인수는 의존배열이다. 함수가 화면을 다시 그릴 때, 의존 배열안의 값이 이전과 같을때는 메모이제이션 함수를 반환해 렌더링을 막고 다른 값이 있다면 현재의 첫번째 인수 함수를 저장하고 새로운 함수를 반환한다. 지금은 의존 배열이 비어있으므로 항상 같은 함수를 반환한다.
To do : 의존 배열이 필요한 경우가 이해가 잘안된다.. chatgpt도 답이없는.. 이부분 추가해야함
3.5.2 - 2 useMemo
useMemo는 값을 메모이제이션한다. 첫 번째 인수는 값을 생성하는 함수, 두 번째 인수에는 의존배열을 전달한다.
useMemo는 useCallback과 마찬가지로 컴포넌트를 그릴 때 의존 배열을 비교하고 의존 배열이 다르면 함수를 실행하고 그 값을 메모로 저장한다. 의존 배열이 같으면 함수를 실행하지 않고, 메모된 값을 반환한다.
import React, { useState, useMemo } from "react"; const UseMemoSample = () => { const [text, setText] = useState(""); const [items, setItems] = useState<string[]>([]); const onChangeInput = (e: React.ChangeEvent<HTMLInputElement>) => { setText(e.target.value); }; const onClickButton = () => { setItems((prevItems) => { return [...prevItems, text]; }); setText(""); }; const numberOfCharacters1 = items.reduce((sub, item) => { const result = sub + item.length; console.log("Recalculating numberOfCharacters1:", result); return result; }, 0); const numberOfCharacters2 = useMemo(() => { return items.reduce((sub, item) => sub + item.length, 0); }, [items]); return ( <div> <p>UseMemoSample</p> <div> <input value={text} onChange={onChangeInput} /> <button onClick={onClickButton}>Add</button> </div> <div> {items.map((item, index) => ( <p key={index}>{item}</p> ))} </div> <div> <p>Total Number of Characters 1: {numberOfCharacters1}</p> <p>Total Number of Characters 2: {numberOfCharacters2}</p> </div> </div> ); }; export default UseMemoSample;
numberOfCharacters2는 items의 배열이 변경될때만 실행되지만 numberOfCharacters1은 text의 input이 변경될때마다 Add버튼을 누르기 전에도 불필요한 연산을 계속해서 사용하는 것을 확인할 수 있다.
이처럼 메모이제이션을 활용해 자식 요소의 그리기를 억제하거나 불필요한 계산을 억제해 웹앱을 최적화 시킬 수 있다.
혹시 틀린 부분이 있다면 지적해주시면 감사하겠습니다. :)