웹개발/React
useMemo란? useMemo의 개념 및 실습
harusari
2023. 6. 22. 20:18
useMemo 에 대하여
React hook중 최적화(optimization)를 위한 훅 두 가지가 있음
- useMemo
- useCallback
useMemo란?
- useMemo의 memo = memoization
메모이제이션(memoization)은 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술이다. (출처 : 위키백과)
- 동일한 값 리턴하는 함수를 반복적으로 호출해야 한다면, 맨 처음 값을 계산할 때 해당 값을 메모리에 저장해 필요할 때마다 다시 계산하지 않고 메모리에서 꺼내서 재사용하는 기법
- 즉, 자주 필요한 값을 맨 처음 계산 시 캐싱해두어 값이 필요할 때 다시 계산하지 않고 캐시에서 꺼내 사용
함수형 컴포넌트에서 왜 useMemo가 필요한가?
- 함수형 컴포넌트도 함수임
- 함수형 컴포넌트가 렌더링된다면 함수가 호출된다는 것
- 함수는 호출될 때마다 함수 내부에 있는 모든 변수가 초기화됨
function Component() {
const value = calculate();
return <div>{value}</div>;
}
function calculate() {
return 1;
}
- 현재 컴포넌트 안 value라는 변수 → calculate라는 함수로부터 값을 얻어오고 있음
- 대부분 리액트 컴포넌트는 state와 props의 변화로 수많은 렌더링을 거침
- 컴포넌트가 렌더링 될때마다 value 변수가 초기화되므로, calculate함수는 반복적으로 호출
- 만약 calculate함수가 무거운 함수라면 굉장히 비효율적일 것
- calculate함수는 무의미한 계산을 반복해 value라는 변수에 같은 값을 반복적으로 할당하기 때문
- useMemo를 사용하면 이런 상황을 간편히 해결할 수 있음
- useMemo는 처음 계산된 결과값을 메모리에 저장하여 컴포넌트가 반복적 렌더링되어도 계속 함수를 다시 호출하지 않고 이전에 계산된 결과값을 메모리에서 꺼내와 재사용할 수 있게 해줌
useMemo의 구조
//1. dependency에 빈 배열 전달
const value = useMemo(() => {
return function();
}, [])
//2. dependency에 요소 전달
const value = useMemo(() => {
return function();
}, [item])
- 두 개의 인자를 받음
- 첫 번째 인자 : 콜백함수
- 두 번째 인자 : dependency 배열
- 첫 번째 인자인 콜백함수는 메모이제이션해줄 값을 계산해 return해줄 함수
- 콜백함수가 리턴하는 값이 useMemo가 리턴하는 값
- 두 번째 인자인 배열은 의존성 배열이라고 불림
- useMemo는 배열 안의 요소값이 업데이트될 때만 콜백함수를 호출해 메모이제이션해둔 값을 업데이트해 다시 메모이제이션해줌
- 만약 빈 배열을 넘겨주면 맨 처음 컴포넌트가 마운트되었을 때만 값을 계산하고, 이후에는 항상 메모이제이션된 값을 꺼내와 사용
useMemo 사용 시 주의점
- useMemo는 값을 재활용하기 위해 따로 메모리를 소비해 저장한다는 의미
- 즉 불필요한 값까지 메모이제이션하면 성능이 악화됨
- 따라서, 필요시만 적절히 사용하는 게 중요함
useMemo의 사용 1) : 계산기
import { ChangeEvent, useState } from "react";
const hardCalculator = (n: number) => {
console.log("어려운 계산 🥵");
for (let i = 0; i < 999999999; i++) {}
return n + 10000;
};
export const UseMemo1: React.FC = () => {
const [hardNumber, setHardNumber] = useState<number>(0);
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setHardNumber(Number(event.target.value));
};
return (
<div>
<h1>짱 어려운 계산기</h1>
<input value={hardNumber} type="number" onChange={handleChange} />
<span>+10000 = {hardCalculator(hardNumber)}</span>
</div>
);
};
- 어떤 숫자에 10000을 더한 값을 화면에 출력해주는 계산기
- hardCalculate()라는 함수가 있음
- 어떤 숫자를 받고 숫자에 10000을 더한 값을 리턴해주는 함수
- 이는 어려운 계산이기 때문에 의미없는 for루프를 많이 돌려 오래 걸리게 만듦 → delay 만듦
- hardNumber라는 state를 생성함
- 컴포넌트 내부에는 number타입인 input태그, value로는 hardNumber
- 화면에 있는 숫자를 증가시키거나 감소시킬 때마다 onChange안의 setHardNumber 함수가 불려 hardNumber state를 업데이트 시켜줌
- 아래의 span태그는 hardSum값을 화면에 출력해 줌
- 이는 컴포넌트 안에 정의된 변수
- hardCalculate함수가 호출되면 이 함수가 리턴하는 값으로 할당이 됨
[렌더링시]
- 컴포넌트는 함수형 컴포넌트
- 컴포넌트가 렌더링된다는 것은 이 함수가 호출이 된다는 것
- 함수가 호출되면 함수 내부의 변수는 초기화됨
- 즉, 이 UseMemo1 컴포넌트가 반복해 렌더링된다면 이 안에 있는 hardSum 변수도 계속 초기화됨
- 이는 hardCalculate 함수가 반복적으로 불려서 이 hardSum 변수에 반복적으로 값을 할당해준다는 이야기
[상태 업데이트시]
- 브라우저에 있는 숫자 바꾸면 hardSum 스테이트가 업데이트되어 우리의 컴포넌트는 다시 렌더링됨
- 렌더링될 때마다 hardCalculate함수가 호출되는지 확인해보자
import { ChangeEvent, useState } from "react";
// delay 만들기 위해 CPU 많이 사용하는 함수 생성
const hardCalculator = (n: number) => {
console.log("어려운 계산 🥵");
for (let i = 0; i < 999999999; i++) {}
return n + 10000;
};
const easyCalculator = (n: number) => {
console.log("쉬운 계산 😄");
return n + 1;
};
export const UseMemo1: React.FC = () => {
const [hardNumber, setHardNumber] = useState<number>(0);
const [easyNumber, setEasyNumber] = useState<number>(0);
const handleHDChange = (event: ChangeEvent<HTMLInputElement>) => {
setHardNumber(Number(event.target.value));
};
const handleEZChange = (event: ChangeEvent<HTMLInputElement>) => {
setEasyNumber(Number(event.target.value));
};
return (
<div>
<div>
<h1>짱 어려운 계산기</h1>
<input value={hardNumber} type="number" onChange={handleHDChange} />
<span>+10000 = {hardCalculator(hardNumber)}</span>
</div>
<div>
<h1>짱 쉬운 계산기</h1>
<input value={easyNumber} type="number" onChange={handleEZChange} />
<span>+1 = {easyCalculator(hardNumber)}</span>
</div>
</div>
);
};
- 이번에는 쉬운 계산기를 만들어보기
- easyNumber라는 state 하나 더 만들기
- 이번에는 함수를 easyCalculate라고 하고 의미없는 for 루프를 지우고 인자로 받은 넘버에다가 1을 더해주는 함수로 만들어봄
- easySum 변수를 만들어 easyCalculate 함수로 값을 할당받고, 인자로 easyNumber 스테이트를 전달
- 어라? 근데 예상으로는 콘솔에 바로 찍힐 것 같았는데 hardCalculate처럼 1초 딜레이 이후에 콘솔에 찍히게 됨
이유 : UseMemo1 컴포넌트가 함수형 컴포넌트라는데에 있음
- 쉬운 계산기의 숫자를 증가시켜주면 easyNumber 스테이트가 바뀜
- 이는 곧 UseMemo1 컴포넌트가 다시 렌더링된다는 것
- UseMemo1 이라는 함수 안에 정의되어있는 하드썸과 이지섬 두 개의 변수 모두 초기화가 되어버림
- 그래서 hardC..함수도 불리기 때문에, 하드넘버를 바꾸건 이지넘버를 바꾸건 hardCalculator 함수 안에 있는 의미없는 for루프가 이만큼 돌아가게됨
- 너무 비효율적
- 그럼 이지넘버 스테이트 변경할때는 하드칼큘 함수가 불리지 않게 하는 방법은 없을까?
- 있다. useMemo를 사용하면 됨
- 이를 사용하면 어떤 조건이 만족되었을 때만 변수가 초기화되게 할 수 있음
- 만약 조건을 만족시키지 않았다면 앱 컴포넌트가 다시 렌더링되더라도 다시 초기화시키는 게 아니라 이전에 가지고 있던 값을 그대로 사용하게 해줌
- 이걸 다른말로 메모이제이션이라고 부름
const hardSum = useMemo<number>(() => {
return hardCalculator(hardNumber);
}, [hardNumber]);
- hardSum이라는 변수를 생성해 useMemo를 불러옴
- 첫 번째 인자인 콜백함수에 return값으로 메모할 값을 주면 됨
- 이 값은 hardCalculate함수를 호출해 리턴되는 값
- return안에 붙여넣음
- 두 번째 인자는 의존성 배열인데, 이 안에 값을 넣어주면 값이 바뀔때만 콜백 안의 하드칼큘함수를 다시 호출해 hardSum에 할당해 줌
- 즉 [] 안이 조건이고 조건을 만족시켜야만 초기화되게 만들어주는 것
- 여기에 hardNumber를 집어넣으면, 이 hardN이 변경되어야지만 hardC가 다시 불려 hardS을 초기화
- 만약 hardN가 변경되지 않았다면 그 전에 가지고 있던 hardSum의 값을 재사용하게 됨
- 저장해서 다시 확인해보면 어려운 계산기를 증가해보면 1초 딜레이가 있음
- 콘솔을 봐도 어려운 계산과 짱 쉬운 계산 → 두 함수가 모두 불림
- 쉬운 값을 변경해주면 클릭때마다 즉각적으로 delay없이 화면에 보여짐
- hardC 함수는 실행되지 않고, 오직 easyC 함수만 실행됨
- 왜냐하면, useMemo를 사용했으므로 hardN이 바뀌었을 때만 hardC를 부르기 때문에 쉬운 계산기를 증가시킬 때에는 hardN state에는 변화가 없어, 이미 메모된 값을 재사용하는 것
- 하지만 사실 리액트로 개발하며 1초 이상 걸리는 함수로 컴포넌트 내부 변수를 초기화줄 일은 그렇게 많지 않음
- useMemo가 빛을 발하는 상황은 따로 있음 ⇒ 아래 예시
예제 2 - useMemo의 진짜 사용 예시
import { ChangeEvent, useState } from "react";
export const UseMemo2: React.FC = () => {
const [number, setNumber] = useState<number>(0);
const [isKorea, setIsKorea] = useState<boolean>(true);
const handleNumberChange = (event: ChangeEvent<HTMLInputElement>) => {
setNumber(Number(event.target.value));
};
const toggleCountry = () => {
setIsKorea((prev) => !prev);
};
const location = isKorea ? "한국" : "외국";
return (
<div>
<h1>하루에 몇 끼나 먹나요? 🐷</h1>
<input type="number" value={number} onChange={handleNumberChange} />
<h1>어느 나라에 있나요? : {location}</h1>
<button onClick={toggleCountry}>비행기 타자! ✈️</button>
</div>
);
};
- 두 가지의 화면
- 하루에 몇끼? 숫자 입력 화면
- 어느 나라에 잇어요? 지금 나라 한국이고 비행기타자 버튼 누르면 외국 한국 이렇게 토글링되는 버튼이 있음
- 현재 컴포넌트의 구조는 두 가지 스테이트
- 첫 번째 스테이트는 number → 0으로 초기화되어있음
- 이는 input의 value로 쓰임
- 두 번째 스테이트는 isKorea 스테이트 → boolean값, 초기화는 true
- 밑의 비행기타자 버튼 누르면 isKorea가 true-false 토글링
- 첫 번째 스테이트는 number → 0으로 초기화되어있음
- 컴포넌트 내부 변수는 location 변수
- 만약 isKorea가 true라면 한국이라는 String을, false라면 외국이라는 String을 할당받음
useEffect(() => {
console.log("useEffect 실행!");
}, [location]);
- 이제 useEffect를 추가해 봄
- dependency에는 location을 전달
- 맨 처음 화면에 렌더링될때, location이 바뀌었을 때 실행
- 콜백함수에는 console.log로 ‘useEffect 호출’ 메시지
- 현재 number 스테이트 업데이트해주면 리렌더링이 되지 않음
- 아무리 렌더링 많이 되어도 location 값은 계속 초기화되긴 하지만, number 스테이트만 바꿔줘서 location에 들어있는 값은 변하지 않음
- useEffect가 불릴지 말리 판단할 때는 location의 값이 렌더링 이전과 이후에 차이가 있는지 확인
- 그래서 location의 값이 바뀌었을 때만 useEffect 호출
- 그래서 비행기타자를 클릭하면 location값이 외국으로 바뀌었으니까 useEffect가 실행
하지만, 만약 dependency로 전달해준 location이 String같은 원시타입이 아니라 object라면 이야기가 달라짐
const location = {
country: isKorea ? "한국" : "외국",
};
- 이제 location에 object 할당해보자
- country라는 key에 isKorea가 true라면 한국, false라면 외국
- p태그 안에는 location.country
- 비행기타자 버튼 누르면 아까와 동일하게 useEffect 호출
- 하루에 몇끼 먹어요 증가시켜도 똑같이 useEffect 호출됨
- location은 변경되지 않고 number만 변경되었는데 대체 useEffect 왜 불림?
자바스크립트 타입
- 변수는 어떤 값을 넣어둘 수 있는 상자
- 어떤 변수에 원시타입의 값을 할당하면 그 값은 상자에 바로 들어감
- 하지만 어떤 변수에 객체타입을 할당하면 객체는 너무 크기 때문에 변수라는 상자에 넣어지는 게 아닌, 어떤 메모리상의 공간에 할당되어 메모리 안에 보관
- 변수 안에는 객체가 담긴 메모리 주소가 할당
- input의 수를 증가시키면 number의 state가 바뀌어 UseMemo2라는 함수가 다시 호출
- 그럼 location이라는 변수도 object를 또 할당받음
- 이는 사실 다른 object → 이전 object랑은 다른 메모리상 공간에 저장
- location 변수는 또 생성된 object의 주소를 참조
- 그래서 react 관점에서는 location 변수의 주소가 바뀌었기 때문에 location이 변경되었다고 인식 (참조하는 주소가 바뀌었으므로)
- 해결하려면, 컴포넌트가 렌더링되었을 때 location 변수가 초기화되는 걸 막아주면 됨
- 이 location 변수를 isKorea 변수가 바뀌었을때만 초기화되게 해보자
- useMemo를 사용해 location을 메모이제이션 해줌
const location = useMemo(() => {
return {
country: isKorea ? "한국" : "외국",
};
}, [isKorea]);
// const location = {
// country: isKorea ? "한국" : "외국",
// };
- location에 useMemo를 추가
- 첫 번째 인자로 콜백함수
- 리턴할 값은 메모해줄 값 → object
- 두 번째 인자 dependency 배열에 isKorea 전달
- 이제 input을 증가시키면 숫자가 증가되지만(number의 state가 바뀌지만) useEffect 불리지 않음
- 이는 효과적으로 컴포넌트 최적화시킬 수 있음
- useEffect에서 만약 오래걸리는 작업을 해야한다면 꼭 필요할때만 호출되는 것이 좋음
(참조 : https://youtu.be/e-CnI8Q5RY4, 별코딩)