6.1 useMemo 개요6.1.1 메모이제이션(Memoization)이란?6.1.2 함수형 컴포넌트6.2 useMemo의 기본 구조
6.2.1 useEffect 와 useMemo의 차이6.3 useMemo 사용해보기6.3.1 (실습1) useMemo 사용해보기6.3.2 (실습2) useMemo 사용해보기
6.1 useMemo 개요
useMemo는 컴포넌트의 성능 최적화를 위하여 사용되는 대표적인 훅입니다. useMemo에서 Memo는 memoization을 뜻하며 이어지는 챕터에서 메모이제이션과 useMemo를 사용하는 이유가 무엇인지 살펴보겠습니다.
6.1.1 메모이제이션(Memoization)이란?
메모이제이션 기법은 연산의 결괏값을 메모리에 저장해 두고 이전 렌더링에서 계산한 값과 현재 렌더링에서의 결과가 같은 경우, 중복 연산을 할 필요 없이 저장해 둔 값을 재사용 할 수 있으므로 성능을 최적화할 수 있습니다. 예를 들어 A라는 함수의 전체 실행시간이 10초라고 가정해본다면 A 함수가 실행될 때마다 10초라는 시간이 걸리게 됩니다. 그러나 메모이제이션을 통해 값을 저장하고 함수가 실행될 때 결괏값만 재사용한다면 그만큼 연산을 줄일 수 있으므로 프로그램의 실행 속도를 올릴 수 있게 됩니다.
6.1.2 함수형 컴포넌트
아래 예제에서 함수형 컴포넌트는 MyComponent를 말하며, MyComponent를 호출하는 것은 함수형 컴포넌트가 렌더링이 되는 것을 의미하며 함수가 호출될 때마다 함수 내부의 모든 변수를 초기화합니다.
function calc(a, b) { return a + b } // 함수형 컴포넌트 const MyComponent() { const result = calc(3,5) return <p>{result}</p> }
기본적으로 컴포넌트는 state가 변경되거나, props가 변경되었을 때마다 리렌더링이 되는데, 리렌더링이 될 때마다 MyComponent를 호출하게 되고 변수 result는 초기화되므로 매번 calc 함수를 실행합니다. 위 예제와 같이 간단한 계산만 하고 끝내는 함수라면 상관없겠지만, 만약 calc 함수가 실행될 때마다 약 10초 가량의 연산을 한다고 가정해보면 어떨까요?
리렌더링이 세 번만 일어나도 30초 가량의 연산이 수행되므로 매우 비효율적입니다. 이러한 현상을 해결하기 위해 우리는 useMemo를 사용하여 부하가 걸리는 함수의 결괏값을 메모리에 저장한 뒤, 리렌더링이 될 때 그 결괏값만 가져와서 재사용해 줌으로써 성능을 최적화할 수 있습니다.
6.2 useMemo의 기본 구조
// 빈 배열이 들어간 경우 const result = useMemo(() => calc(a, b), []); // 요소가 들어간 경우 const result = useMemo(() => calc(a, b), [item);
useMemo의 기본적인 구조는 위와 같습니다. useMemo는 두 개의 인자를 받는데 첫 번째 인자는 콜백 함수, 두 번째 인자는 의존성 배열(Array dependencies)이라고 불리는 배열을 받습니다. 첫 번째 인자에 들어가는 콜백 함수의 결괏값은 useMemo의 리턴값으로 재사용하는 결괏값이 됩니다.
두 번째 인자의 의존성 배열에 요소가 들어갈 때에는 의존성 배열의 전달 값이 변경될 될 때만 콜백 함수가 실행되는데, 이때 렌더링 과정에서 배열 안의 요소 item의 값이 변경되었는지를 확인하고 값이 변경된 경우에만 콜백 함수를 동작시켜서 메모이제이션(Memoization)된 값만 다시 계산합니다.
따라서 useMemo는 의존성 배열에서 전달하는 값의 변경 여부에 따라 중복 연산을 최소화할 수 있으므로 컴포넌트의 성능을 최적화할 수 있습니다.
6.2.1 useEffect 와 useMemo의 차이
useMemo는 이전 챕터 3에서 다뤘던 useEffect와 비슷하게 의존성 배열(Array dependencies)에 전달된 값의 변경 여부에 따라 콜백 함수가 실행됩니다.
이 두 가지 Hook의 가장 큰 차이점은 렌더링 과정 중의 동작 여부입니다.
useEffect는 기본적으로 모든 렌더링이 완료된 이후에 실행되며, 렌더링 후 상태가 업데이트 되었을 때를 감지하여 동작하기 때문에 리렌더링을 방지하지 못합니다.
이와 달리 useMemo는 렌더링 과정 중에 실행됩니다. 렌더링 과정 중에 의존성 배열의 값이 변경 되었는지 확인한 후, 값이 변경 되었다면 이전에 저장한 값과 비교하여 값이 다른 경우에만 리렌더링 해줍니다. 그렇기 때문에 useMemo는 메모이제이션 기법을 통해 불필요한 계산을 최소화하고, 리렌더링을 막을 수 있으므로 렌더링 성능을 최적화할 때 useMemo Hook을 사용합니다.
6.3 useMemo 사용해보기
6.3.1 (실습1) useMemo 사용해보기
이번 챕터에서는 예제를 통해
useMemo
를 사용해보겠습니다. 해당 예제는 무거운 연산을 실행하면서 숫자와 텍스트를 변경하는 코드입니다. App.jsx에서는 화면이 렌더링 될 때마다 heavyCalc
함수를 실행하며 number와 text를 바꿔주는 함수 핸들러, 그리고 바뀐 number와 text값을 보여주는 ShowState 컴포넌트가 있습니다.import React, { useState,useMemo } from 'react'; import ShowState from './ShowState'; export default function App() { const [number, setNumber] = useState(0); const [text, setText] = useState(''); // 무거운 연산을 실행하는 함수 function heavyCalc() { let s = 0; for (let i = 0; i < 1000000000; i++) { s += i; } return s; } let calc = heavyCalc(); // number 값 바꿔주는 함수 const increaseNum = () => { setNumber((prev) => prev + 1); }; const decreaseNum = () => { setNumber((prev) => prev - 1); }; // text값 바꿔주는 함수 const handleText = (e) => { setText(e.target.value); }; return ( <> <h4> ✨ 엄청난 연산값</h4> <p>{calc}</p> <h4> ✨ 숫자바꾸기</h4> <button onClick={increaseNum}>+</button> <button onClick={decreaseNum}>-</button> <h4> ✨ 글자바꾸기</h4> <input type="text" onChange={handleText} /> <ShowState number={number} text={text} /> </> ); }
ShowState
컴포넌트에서는 App.jsx에서 바꾼 값들을 출력해주고 값이 바뀔 때 마다 '숫자가 변경되었습니다.' 와 '글자가 변경되었습니다.' 라는 문구를 콘솔에 출력합니다.import React, { useMemo } from 'react'; export default function ShowState({ number, text }) { const consoleNumber = (number) => { console.log('숫자가 변경되었습니다.'); return number; }; const consoleText = (text) => { console.log('글자가 변경되었습니다.'); return text; }; const showNum = consoleNumber(number); const showText = consoleText(text); return ( <div> <p> 숫자 : {showNum}</p> <p> 글자 : {showText}</p> </div> ); }

위 예제를 실행하면 이런 모습입니다.
+
와 -
버튼을 누르면 숫자 값이 바뀌고, input창에 글자를 입력하면 입력하는대로 글자가 출력되는 것을 알 수 있습니다. 이를 실행해보면 매우 느리게 실행되는데, 이는 숫자나 글자를 바꿈으로써 화면이 리렌더링될 때마다 heavyCalc 함수가 실행되기 때문입니다.하지만
heavyCalc
함수의 결과값인 calc는 계속 같은 값을 가지기 때문에 매번 새로 연산을 할 필요가 없습니다. 이 때 쓸 수 있는 것이 이전에 이미 계산한 값을 재사용할 수 있도록 도와주는 훅이 useMemo
입니다.App.jsx의
heavyCalc
함수 실행문을 이렇게 useMemo
를 이용하여 감싼다면 어떻게 될까요?const calc = useMemo(() => { return heavyCalc(); }, []);
useMemo
를 통해 이전에 계산했던 499999999067109000 라는 값을 계속 재사용하여 화면에 출력해주기 때문에 같은 계산을 위해 부하를 일으키지 않는 것을 확인할 수 있습니다.이처럼
useMemo
는 렌더링이 발생했을 때, 이전 렌더링과 현재 렌더링 사이에 값이 동일하다면 다시 함수를 호출을 하여 결과를 구하는 대신, 기존에 메모리에 저장해두었던 값을 그대로 사용하도록 도와줍니다.이제 불필요한 계산을 하지 않게 되었으니 다시 값을 변경해볼까요?

increaseNum
함수를 두번 실행하여 숫자가 2가 되었습니다. 하지만 콘솔창을 확인하니 increaseNum
함수가 실행될 때마다 Text값을 바꾸지 않았음에도 '글자가 변경되었습니다.' 라는 문구가 같이 출력된 것을 확인할 수 있습니다.변경하고자 하는 state에 해당되지 않는 함수가 실행될 필요는 없기 때문에
useMemo
를 사용하여 숫자가 변경될 경우와 글자가 변경될 경우에만 각각 그에 맞는 console 출력을 하도록 해보겠습니다.const showNum = useMemo(() => consoleNumber(number), [number]); const showText = useMemo(() => consoleText(text), [text]);
showNum과 showText 값을 이렇게 변경하여 number 값이 변했을때만
consoleNumber
함수가, text값이 변했을때만 consoleText
함수가 실행되도록 합니다.
이전처럼
increasNum
을 두 번 실행했을 때 text의 값을 변경하지 않았으므로 consoleNumber
함수만 두 번 실행되는 것을 확인할 수 있습니다.
이 상태에서 text의 값을 다섯 번 변경하면 consoleText함수도 다섯 번 실행되는 것을 알 수 있습니다.
즉, 숫자가 변경될 때는 ‘숫자가 변경되었습니다.’ 라는 console만 출력되고, 글자가 변경될 때는 '글자가 변경되었습니다.' 라는 console만 출력됩니다.
이처럼
useMemo
를 사용하면 메모이제이션 기법을 이용하여 이전에 저장되었던 결과를 다시 가져와 재사용할 수 있기 때문에 코드 최적화에 도움을 줄 수 있습니다.6.3.2 (실습2) useMemo 사용해보기
이번 예제에서는 useMemo를 사용하여 useEffect의 의존성 배열이 객체 타입일 때 생기는 참조 동일성 문제를 해결해 보겠습니다. 아래 예제는 number와 answer를 변경할 수 있고, 그중 answer가 바뀔 때만 콘솔이 찍히도록 작성해놓은 코드입니다.
import React, { useEffect, useState, useMemo } from "react"; function App() { const [number, setNumber] = useState(0); const [isTrue, setIsTrue] = useState(true); const answer = isTrue ? "true" : "false"; // answer가 바뀔 때만 콘솔이 찍히도록 작성 useEffect(() => { console.log("answer이 변경되었습니다."); }, [answer]); return ( <div> <p>✨ number</p> <input type="number" value={number} onChange={(e) => setNumber(e.target.value)} /> <hr /> <p>✨ answer</p> <p>T or F ? {answer}</p> <button onClick={() => setIsTrue(!isTrue)}>Click</button> </div> ); } export default App;
아래와 같이 number를 10으로, answer를 false로 변경해보면 answer를 변경했을 때만 콘솔이 찍히는 것을 볼 수 있습니다.

그렇다면 이번에는 원시 타입인 answer를 아래처럼 객체 타입으로 만들어 주고 이전과 같이 number를 변경해보겠습니다.
import React, { useEffect, useState, useMemo } from "react"; function App() { const [number, setNumber] = useState(0); const [isTrue, setIsTrue] = useState(true); const answer = { bool: isTrue ? "true" : "false" }; useEffect(() => { console.log("answer이 변경되었습니다."); }, [answer]); return ( <div> <p>✨ number</p> <input type="number" value={number} onChange={(e) => setNumber(e.target.value)} /> <hr /> <p>✨ answer</p> <p>T or F ? {answer.bool}</p> <button onClick={() => setIsTrue(!isTrue)}>Click</button> </div> ); } export default App;
아래에 실행된 것을 보면 number만 변경했음에도 콘솔이 출력되었고 이는 곧 answer가 변경되고 있다는 것을 의미합니다.

이렇게 동작하는 이유는 데이터 타입과 관련이 있습니다.
number를 변경해서 컴포넌트가 리렌더링 되면, 함수형 컴포넌트인 App 안에 있는 변수들이 모두 초기화되어 다시 할당되기 때문에 객체 타입인 answer가 다시 할당됩니다.
그러나 객체 타입은 그 값이 변수에 직접 저장되는 게 아니라, 메모리 주솟값이 할당되어 값을 참조하게 됩니다. 따라서 컴포넌트가 리렌더링되면 객체는 다른 메모리에 할당되고, 변수에는 그에 맞는 달라진 주솟값이 할당됩니다. useEffect는 렌더링 전과 후의 answer의 주솟값이 다르다고 판단하여 콘솔을 출력하게 되는 것입니다.
const answer = useMemo(() => { return { bool: isTrue ? "true" : "false" }; }, [isTrue]);
이러한 문제를 useMemo를 통해 해결해보겠습니다. 위 코드와 같이 useMemo를 사용하여 코드를 변경하면 최초로 렌더링 될 때 객체의 주솟값을 answer가 메모이제이션합니다. 이후 리렌더링부터는 메모이제이션 된 주솟값을 재사용하게 됩니다. 따라서 반복적으로 리렌더링 되더라도 콘솔은 찍히지 않습니다.

이처럼 useMemo는 메모이제이션으로 수행한 연산의 결괏값을 기억함으로써 불필요한 계산을 최소화하고, 리렌더링을 막을 수 있습니다.