(상기 영상과 코드스테이츠 유어클래스 내용을 참고하여 공부 내용을 정리합니다.)
1. useMemo 개념 설명
리액트 컴포넌트의 성능 최적화를 위해 사용되는 대표적인 hook들인 useMemo와 useCallback에 대해 알아보자. (useCallback은 별도 문서에 정리)
Memoization
useMemo의 memo는 memoization을 뜻한다. 메모이제이션이란 어떠한 커다란 문제를 풀 경우에 문제를 중복되는 하위 문제가 있고, 이 하위 문제의 결과를 저장해서 상위의 문제를 해결할 수 있는 경우에 하위 문제의 결괏값을 저장해두어 상위문제를 풀 때 연산횟수를 줄일 수 있는 방법을 말한다. 자세한 내용은 이전에 작성한 동적 프로그래밍 정리 내용을 참고하자.
즉 메모이제이션은 동일한 값을 리턴하는 함수를 반복적으로 호출해야 한다면 맨 처음에 함수가 값을 계산할 때 해당 값을 메모리에 저장하여(캐싱해두어) 필요할 때 마다 다시 연산을 수행하지 않고 값을 재사용하는 것을 말하는 것이다. 따라서 리액트의 useMemo Hook은 캐싱해둔(메모이제이션) 값을 필요할 때 마다 재사용한다.
함수형 컴포넌트에서의 useMemo 훅 사용
브라우저가 컴포넌트를 렌더링하여 함수 컴포넌트가 호출되고 나면 해당 컴포넌트 내부의 일반 변수와 함수들은 전부 초기화되기 때문에 재렌더링이 일어나면 값이 유지되지 않는다. (useState의 state는 클로저이므로 값을 유지)
이러한 경우에 useMemo 훅을 사용하여 값을 메모이제이션 해두어 재렌더링 시에도 이전에 계산한 결괏값을 재사용할 수 있다.
useMemo의 구조
const value = useMemo(() => {
return calculate();
}, [item]);
useMemo 훅은 두가지 인수를 받는다. 첫번째는 콜백함수이고 두번째는 종속성 배열이다.
첫번째 인수인 콜백함수의 반환값이 useMemo의 반환값이 되며, 두번째 인수인 종속성 배열 안의 요소의 값이 업데이트 될 경우에만 메모이제이션 해둔 값을 업데이트하여 이를 다시 메모이제이션 한다. useEffect에서의 종속성 배열에 따라 훅이 실행되는 때와 동일하게 빈 배열일 경우 최초 렌더링 시 한번만, 생략 시 모든 렌더링의 경우, 배열의 요소가 있을 때: 요소의 값이 변경될 때 실행된다.
주의사항
값을 저장해두기 위해 따로 메모리를 소비하는 것이므로 불필요한 값들도 모두 메모이제이션 해버리는 경우 성능이 오히려 악화된다. 따라서 필요한 경우에만 사용해야 한다. 이는 아래 실제 구현 예제를 통해 자세히 알아보자.
2. useMemo 사용 예제
예제 1 - useMemo를 사용하지 않았을 경우의 문제점
import React, { useState } from 'react';
const hardCalculate = number => {
console.log('어려운 계산!');
for (let i = 0; i < 999999999; i++) {} // 무거운 연산의 간단한 예시용. for문을 엄청나게 돌리고 나서 반환문 실행.
return number + 10000; // 위의 연산 때문에 약 1초 후에 화면에 반영된다.
};
const easyCalculate = number => {
console.log('쉬운 계산!');
return number + 1;
};
function App() {
const [hardNumber, setHardNumber] = useState(1);
const [easyNumber, setEasyNumber] = useState(1);
const hardSum = hardCalculate(hardNumber);
const easySum = easyCalculate(easyNumber);
return (
<div>
<h3>어려운 계산기</h3>
<input
type="number"
value={hardNumber}
onChange={e => setHardNumber(parseInt(e.target.value))}
/>
<span> + 10000 = {hardSum} </span>
<h3>쉬운 계산기</h3>
<input
type="number"
value={easyNumber}
onChange={e => setEasyNumber(parseInt(e.target.value))}
/>
<span> + 1 = {easySum} </span>
</div>
);
}
export default App;
상기 코드에서 어려운 계산기는 함수가 의미없이 for문을 수없이 많이 실행하고 나서야 반환문을 실행하여 화면에서 state가 변경된걸 확인하는데 1초가량 딜레이가 생기는 무거운 연산을 수행한다.
또한 어려운 계산기 아래의 쉬운 계산기만 실행시켰을 때도 state가 변경되었기 때문에 App컴포넌트가 재렌더링되어 hardSum변수와 easySum변수의 함수 호출문도 다시 실행되기 때문에 쉬운 계산기만 실행해도 어려운 계산기의 함수에서 for문의 수많은 반복문 실행을 마치고 나서 쉬
예제 1 - useMemo를 사용해서 리팩토링하기
// ... 생략
const [hardNumber, setHardNumber] = useState(1);
const [easyNumber, setEasyNumber] = useState(1);
// const hardSum = hardCalculate(hardNumber);
const hardSum = useMemo(() => {
return hardCalculate(hardNumber); // hardNumber의 값이 변경이 있을 경우에만 hardSum 변수를 초기화.
}, [hardNumber]); // useMemo의 의존성 배열의 값이 변경이 있어야만 리턴문의 내용이 초기화된다.
const easySum = easyCalculate(easyNumber);
// ...
// jsx문 리턴부분 하단 생략
useMemo 훅을 사용해서 어떠한 조건이 만족될 경우에만 변수들을 초기화시키도록 할 수 있다.
useMemo에서 주어진 조건이 만족되지 않았다면 컴포넌트가 재렌더링되더라도 메모이제이션한 값이 초기화되지 않고 유지된다. 이 때의 주어진 조건이 만족될 경우란 의존성 배열의 값이 변경이 있을 경우를 말한다.
따라서, 처음의 useState와 일반 변수만 사용했던 코드에서 쉬운 계산기만 실행할 때 어려운 계산기의 코드가 실행되지 않게 하기 위해 useMemo Hook을 사용해서 hardSum 변수를 의존성 배열의 요소 hardNumber의 값이 변경될 경우에만 초기화시키는 방식으로 리팩토링할 수 있다.
예제 2 - 좀 더 실전적인 (리팩토링 전)
import React, { useState } from 'react';
function App() {
const [number, setNumber] = useState(0);
const [isKorea, setIsKorea] = useState(true);
const location = isKorea ? '한국' : '외국';
return (
<div>
<h2>하루에 몇끼 먹어요?</h2>
<input
type="number"
value={number}
onChange={e => setNumber(e.target.value)}
/>
<hr />
<h2>어느 나라에 있어요?</h2>
<p>나라: {location} </p>
<button onClick={() => setIsKorea(!isKorea)}>비행기 타자</button>
</div>
);
}
export default App;
예제 2 - useEffect의 종속성 배열 요소로 객체 사용시 문제점
import React, { useState, useEffect } from 'react';
function App() {
const [number, setNumber] = useState(0);
const [isKorea, setIsKorea] = useState(true);
// const location = isKorea ? '한국' : '외국';
const location = {
conuntry: isKorea ? '한국' : '외국',
};
useEffect(() => {
console.log('useEffect 호출');
}, [location]); // 종속성 배열 안의 요소가 객체인 경우 모든 렌더링 시 실행됨!!
return (
// ... 생략
- The 'location' object makes the dependencies of useEffect Hook (at line 14) change on every render. To fix this, wrap the initialization of 'location' in its own useMemo() Hook.
useEffect의 종속성 배열 안의 요소가 객체 인 경우 useEffect 콜백함수 내부의 코드는 모든 렌더링 시에 실행됨에 유의해야 한다. 종속성 배열 안의 요소가 원시값인 경우에만 해당 요소의 값이 바뀔 때 useEffect의 콜백함수 내부의 코드가 실행된다. 자세한 내용은 이전 정리내용을 참조하자. https://ryan-kim-dev.tistory.com/63
객체를 저장하고 있는 변수와 바인딩된 메모리 공간의 주소는 메모리 힙 주소를 담고 있고 원시값을 담은 변수에 바인딩 된 메모리의 주소는 값 자체가 담긴 메모리공간의 주소이다.
따라서, 같은 값의 원시타입끼리 동등 비교 시 true 이지만 같은 값의 객체(참조타입)끼리 비교 시 false가 된다.
그래서 useMemo로 객체인 값을 메모이제이션 해두어 useEffect의 종속성 배열 요소가 객체여도 모든 렌더링 시 마다 useEffect 안의 코드가 실행되는걸 막을 수 있다.
예제 2 - useMemo를 사용하여 useEffect의 모든 렌더링시 실행되는 문제 해결하기
function App() {
const [number, setNumber] = useState(0);
const [isKorea, setIsKorea] = useState(true);
// const location = {
// conuntry: isKorea ? '한국' : '외국',
// };
const location = useMemo(() => {
return {
conuntry: isKorea ? '한국' : '외국',
};
}, [isKorea]); // useMemo의 종속성 배열의 요소로 isKorea 상태값을 넣었다.
useEffect(() => {
console.log('useEffect 호출');
}, [location]);
return (
// ... 생략
유어클래스 문제
- add 함수가 이름을 입력하는 인풋창의 state가 변화될 때마다 불필요하게 실행되고 있는데, 이를 val1, val2의 state가 바뀔 경우에만 실행되도록 고치기.
// 기존 코드
export default function App() {
const [name, setName] = useState("");
const [val1, setVal1] = useState(0);
const [val2, setVal2] = useState(0);
const answer = add(val1, val2);
// useMemo 사용
export default function App() {
const [name, setName] = useState("");
const [val1, setVal1] = useState(0);
const [val2, setVal2] = useState(0);
const answer = useMemo(() => add(val1, val2), [val1, val2]); // <--
'개발 > React' 카테고리의 다른 글
[React] Redux 상태관리 (0) | 2022.07.09 |
---|---|
[React] JSX문법 (0) | 2022.03.23 |