Memoization?
1. 정의?
- Memoization이란 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술
- useMemo, useCallback, React.memo는 모두 이 Memoization을 기반으로 작동
- 비용이 많이 드는 함수 호출의 결과를 저장하고 동일한 입력이 다시 발생할 때 캐시된 결과를 반환하여 컴퓨터 프로그램 속도를 높이는 데 주로 사용되는 최적화 기술
- 소프트웨어 시스템의 일부 측면을 보다 효율적으로 작동시키거나 더 적은 리소스를 사용하도록 수정하는 프로세스
2. 최적화와 메모이제이션
- 구성 요소의 수명 주기에서 React는 업데이트가 이루어질 때 구성 요소를 다시 렌더링함
- 웹 페이지 하나가 만들어질 때는 위와 같이, DOM Tree의 구성, 레이아웃 잡기, 페인팅하기 등의 다양한 작업이 이루어짐
- 리랜더링 시에, 레이아웃 및 페인팅 과정을 또 계산해야 할 수 있음
- React가 구성 요소의 변경 사항을 확인할 때 JavaScript가 동등성 및 얕은 비교(equality and shallow comparisons)를 처리하는 방식으로 인해 의도하지 않거나 예기치 않은 변경 사항을 감지할 수 있고 React 애플리케이션은 이러한 변경으로 인해 불필요하게 재렌더링될 수 있음
⇒ 비용이 많이 드는 작업은 시간, 메모리 또는 처리 비용이 많이 들 수 있어 성능저하가 발생할 수 있으므로 사용자 경험 또한 저하될 수 있음
⇒ React는 이를 개선하기 위해 메모 아이디어를 발표함
⇒ 쓸데없이 같은 계산을 반복하게 하지 않게 할 수 있는 방법은? 결과를 기억하는 것
useMemo
1. 이건 뭐야?
정의?
- 이전 값을 기억해두었다가 조건에 따라 재활용하여 성능을 최적화 하는 용도로 사용됨 (특정 value를 재사용)
- useMemo는 함수의 결과 값을 memoized하여 불필요한 연산을 관리
- 사용 형식
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
- useMemo의 특징은 일단 함수 호출 이후의 return 값이 memoized되며, 두 번째 파라미터인 배열의 요소가 변경될 때마다 첫 번째 파라미터의 callback 함수를 다시 생성하는 방식임
useRef와의 차이
useMemo는 deps가 변경되기 전까지 값을 기억하고, 실행후 값을 보관하는 역할로도 사용
- useMemo는 복잡한 함수의 return 값을 기억한다는 점에서 값만 기억하는 useRef와는 다름
- useRef는 특정 값을 기억하는 경우, useMemo는 복잡한 함수의 return값을 기억하는 경우에 사용됨
동작방식
- 초기 렌더링 중에 useMemo(compute,dependencies)계산을 호출하고 계산 결과를 메모한 다음 구성 요소로 반환함
- useMemo종속성 중 하나가 변경된 경우에만 메모된 값을 다시 계산하며, 이 최적화는 모든 렌더링에서 비용이 많이 드는 계산을 피하는 데 도움이 됨
- 다음 렌더링 중에 종속성이 변경되지 않으면 useMemo() 는 컴퓨팅을 호출하지 않고 메모된 값을 반환함
2. 어디에 써?
- 비용이 많이 드는 계산을 메모화하는 데 사용
- 여기서 비싸다는 의미는 메모리와 같은 리소스를 많이 사용한다는 것을 의미
3. 주의점은?
종속성 비교로 인한 계산 비용
- 내부적으로 React의 useMemo Hook은 값을 다시 계산해야 하는지 여부를 결정하기 위해 다시 렌더링할 때마다 종속성 배열의 종속성을 비교해야 하며, 종종 이 비교를 위한 계산은 단순히 값을 다시 계산하는 것보다 비용이 더 많이 들 수 있음 ⇒ useMemo애플리케이션에서 너무 자주 구현 하면 성능이 저하될 수 있음
- 프로파일링 도구를 사용하여 비용이 많이 드는 성능 문제를 식별할 수 있음
4. 예시를 살펴보자!
useMemo 사용 전
import { useState } from 'react';
export function MyComponent() {
const [number, setNumber] = useState(1);
const [inc, setInc] = useState(0);
const factorialResult = calculateFactorial(number);
const onChange = event => {
setNumber(Number(event.target.value));
};
const onClick = () => setInc(i => i + 1);
return (
<div>
Factorial of the following Number
<input type="number" value={number} onChange={onChange} />
is {factorialResult}
<button onClick={onClick}>Increment</button> <span>{inc}</span>
</div>
);
}
function calculateFactorial(number) {
console.log('calculateFactorial called!');
return number <= 0 ? 1 : number * calculateFactorial(number - 1);
}
- 입력 값을 변경할 때마다 factorialResult가 계산 'calculateFactorial(number) called!'되어 콘솔에 기록됨
- Increment 버튼을 클릭할 때마다 inc상태 값이 업데이트됩니다. 상태 값을 업데이트 하면 다시 렌더링 inc이 트리거됨
- <MyComponent /> 가 2.번의 이벤트로 인해, 재렌더링되는 동안 다시 calculateFactorial계산 되어 'calculateFactorial(n) called!'값이 콘솔에 기록됨
⇒ useMemo(()=> calculateFactorial(number), [number]) 으로 React는 계산값을 메모할 수 있음
useMemo 사용 후
import { useState, useMemo } from 'react';
export function MyComponent() {
const [number, setNumber] = useState(1);
const [inc, setInc] = useState(0);
const factorialResult = useMemo(() => calculateFactorial(number) , [number]);
const onChange = event => {
setNumber(Number(event.target.value));
};
const onClick = () => setInc(i => i + 1);
return (
<div>
Factorial of the following Number
<input type="number" value={number} onChange={onChange} />
is {factorialResult}
<button onClick={onClick}>Increment</button> <span>{inc}</span>
</div>
);
}
function calculateFactorial(number) {
console.log('calculateFactorial called!');
return number <= 0 ? 1 : number * calculateFactorial(number - 1);
}
- 입력(input) 값을 변경할 때마다 'calculateFactorial(n) called!'가 콘솔에 기록되지만, Increment 버튼을 클릭 하면 useMemo 에 의해, 메모된 계산값이 반환되기 때문에, 'calculateFactorial(n) called!' 은 콘솔에 기록되지 않음
useCallback
1. 이건 뭔데?
- useCallback은 리액트의 렌더링 성능을 위해서 제공되는 Hook이다.
- 부모컴포넌트에서 자식컴포넌트에 prop으로 넘겨주는 함수가 있을 때, 부모 컴포넌트가 렌더링 될 때마다 내부적으로 사용된 함수도 새로 생성되어, 자식 컴포넌트에 Prop으로 새로 생성된 함수가 넘겨지게 되면 불필요한 리렌더링이 일어날 수 있다.
- ⇒ 이 경우, 함수를 memoized하여 해결할 수 있음
- 메모리제이션된 함수를 반환하는 것이 핵심
- 사용형식
- const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], );
- useCallback을 사용하여 함수를 memoized 시키면, 종속성 배열의 종속성이 변경되는 경우에만 이 함수가 다시 정의됨
- useCallback의 특징
- useCallback은 function의 메모리 재할당을 막기위한 수단
- 여러곳에서 사용되는 컴포넌트가 불필요하게 같은 function을 메모리에 여러번 할당한다면, useCallback을 사용한 최적화가 필요
- useCallback은 함수의 결과를 메모리에 저장하는게 아니라, 메모리에 저장된 함수를 같은 컴포넌트들에서 공유하는 개념
2. 언제 써?
- 함수를 메모하기 위해 사용되며, 부모 구성 요소를 다시 렌더링할 때마다 함수가 다시 초기화되는 것에 대해 걱정하지 않고 다른 구성 요소에 함수를 전달할 때 이미 약간의 성능 향상이 있음
- useCallback은 React.Memo와 함께 사용할 때 특히 유용함
- 컴포넌트가 랜더링 될 때마다 내부에 선언되어 있던 표현식이 다시 선언되어 사용됨. 이 때, 컴포넌트 내부에 있는 함수는 변동이 없음에도 컴포넌트가 리랜더링 될 때마다 다시 선언됨. ⇒ 이런 경우에 useCallback을 import해서 사용하던 함수의 실행문을 넣어주면 랜더링 될 때마다 선언되는 것을 피할 수 있고 의존성 배열에 요소를 추가하면 해당 값이 변경될 때 재선언 가능.
- 또한, 상위컴포넌트의 함수가 매번 재선언되면, 내용이 같다고 하더라도 하위컴포넌트는 넘겨받는 함수가 달라졌다고 인식함. ⇒ 따라서 하위컴포넌트가 React.memo() 등으로 최적화 되어있고, 그 하위 컴포넌트에게 callback 함수를 props로 넘길 경우에, 상위컴포넌트에서 useCallback으로 선언하는 것이 최적화에 도움됨.
- React.memo()로 함수형 컴포넌트 자체를 감싸면 넘겨 받는 props가 변경되지 않았을 때는 상위 컴포넌트가 메모리제이션된 함수형 컴포넌트(이전에 렌더링된 결과)를 사용하게 됨.
3. 주의점은?
- React의 useCallback Hook은 함수를 재정의해야 하는지 여부를 결정하기 위해 다시 렌더링할 때마다 종속성 배열의 종속성을 비교해야 함 ⇒ 종종 이 비교를 위한 계산은 단순히 함수를 재정의하는 것보다 더 비쌀 수 있음 ⇒ 그렇기 때문에 프로파일러 API 를 사용하여 사용 여부를 확인하는 것이 좋습니다.
4. 사용 예시를 살펴보자
React.memo로 래핑된 컴포넌트가 callback을 받을 때
const MemoisedItem = React.memo(Item);
const List = () => {
**// this HAS TO be memoised, otherwise `React.memo` for the Item is useless**
const onClick = () => {console.log('click!')};
return <MemoisedItem onClick={onClick} country="Austria" />
}
- 함수 객체는 "일반" 객체와 동일한 비교 원칙을 따름 함수 객체는 오직 자신에게만 동일
- 몇가지 함수를 비교해보자.
function sumFactory() {
return (a, b) => a + b;
}
const sum1 = sumFactory();
const sum2 = sumFactory();
console.log(sum1 === sum2); // => false
console.log(sum1 === sum1); // => true
console.log(sum2 === sum2); // => true
- sumFactory()는 팩토리 함수이다. 이 함수는 2가지 숫자를 더해주는 화살표 함수를 반환
- 함수 sum1과 sum2는 팩토리에 의해 생성된 함수이고, 두 함수 모두 두 숫자를 더해주는 함수임. 그러나 sum1과 sum2는 다른 함수 객체임.
- 부모 컴퍼넌트가 자식 컴퍼넌트의 콜백 함수를 정의한다면, 새 함수가 암시적으로 생성될 수 있음.
- 예시를 통해 알아보자.
- Logout 컴퍼넌트는 콜백 prop인 onLogout을 갖는다.
function Logout({ username, onLogout }) {
return <div onClick={onLogout}>Logout {username}</div>;
}
const MemoizedLogout = React.memo(Logout);
- 함수의 동등성이란 함정 때문에, 메모이제이션을 적용할 때는 콜백을 받는 컴퍼넌트 관리에 주의해야함.
- 리렌더를 할 때 마다 부모 함수가 다른 콜백 함수의 인스턴스를 넘길 가능성이 있음.
function MyApp({ store, cookies }) {
return (
<div className="main">
<header>
<MemoizedLogout
username={store.username}
onLogout={() => cookies.clear()}
/>
</header>
{store.content}
</div>
);
}
- 동일한 username 값이 전달되더라고, MemoizedLogout은 새로운 onLogout 콜백 때문에 리렌더링을 하게 됨.
- ⇒ 메모이제이션이 중단되게 되는 것
- 이 문제를 해결하려면 onLogout prop의 값을 매번 동일한 콜백 인스턴스로 설정해야만 함. useCallback()을 이용해서 콜백 인스턴스를 보존시킬 수 있음.
const MemoizedLogout = React.memo(Logout);
function MyApp({ store, cookies }) {
const onLogout = useCallback(() => {
cookies.clear();
}, []);
return (
<div className="main">
<header>
<MemoizedLogout username={store.username} onLogout={onLogout} />
</header>
{store.content}
</div>
);
}
- useCallback(() => { cookies.clear() }, []) 는 항상 같은 함수 인스턴스를 반환하고, MemoizedLogout의 메모이제이션이 정상적으로 동작하도록 수정되었음
컴포넌트가 hooks(useMemo, useCallback or useEffect)에 dependency로 callback을 받을 때
const Item = ({ onClick }) => {
useEffect(() => {
// some heavy calculation here
const data = ...
onClick(data);
**// if onClick is not memoised, this will be triggered on every single render**
}, [onClick])
return <div>something</div>
}
const List = () => {
// this HAS TO be memoised, otherwise `useEffect` in Item above
// will be triggered on every single re-render
const onClick = () => {console.log('click!')};
return <Item onClick={onClick} country="Austria" />
}
나쁜 사용 사례
import { useCallback } from 'react';
function MyComponent() {
// Contrived use of `useCallback()`
const handleClick = useCallback(() => {
// handle the click event
}, []);
return <MyChild onClick={handleClick} />;
}
function MyChild ({ onClick }) {
return <button onClick={onClick}>I am a child</button>;
}
- 첫 번째 문제는 렌더링 useCallback()할 때마다 후크가 호출 된다는 것 : 그것은 이미 렌더링 성능을 감소시킴
- 두 번째 문제는 사용 useCallback()이 코드 복잡성을 증가시키는 것 : useCallback(..., deps) 의 deps와 memoized 콜백 내에서 사용 중인 것과 동기화 deps를 유지해야 함.
- useCallback()의미가 있을까? : <MyChild>구성 요소가 가볍고 다시 렌더링해도 성능 문제가 발생하지 않기 때문일 가능성이 높음
⇒ 결론적으로 최적화를 하지 않는 것보다 최적화 비용이 더 많이 듬
React.memo
1. 이게 뭔데?
- UI 성능을 증가시키기 위해, React는 고차 컴포넌트(Higher Order Component, HOC) React.memo()를 제공
- 고차 컴포넌트
- 고차 컴포넌트(HOC, Higher Order Component)는 컴포넌트 로직을 재사용하기 위한 React의 고급 기술
- 고차 컴포넌트(HOC)는 React API의 일부가 아니며, React의 구성적 특성에서 나오는 패턴
- 구체적으로, 고차 컴포넌트는 컴포넌트를 가져와 새 컴포넌트를 반환하는 함수
- 고차 컴포넌트
- 렌더링 결과를 메모이징(Memoizing)함으로써, 불필요한 리렌더링을 건너뜀
- 컴포넌트가 동일한 props로 동일한 결과를 렌더링해낸다면, React.memo를 호출하고 결과를 메모이징(Memoizing)하도록 래핑하여 경우에 따라 성능을 향상시킬 수 있음 ⇒ React.memo는 컴포넌트를 렌더링하지 않고 마지막으로 렌더링된 결과를 재사용함
- React.memo는 props 변화에만 영향을 주며, React.memo로 감싸진 함수 컴포넌트 구현에 useState, useReducer 또는 useContext 훅을 사용한다면, 여전히 state나 context가 변할 때 다시 렌더링됨
- 사용형태
const MyComponent = React.memo(function MyComponent(props) { /* props를 사용하여 렌더링 */ });
- (간단한 예) React.memo는 일반적으로 아래와 같이 사용됨
- React.memo는 Welcome의 결과를 Memoization해서 이후 props가 변경될때까지 현재 memoized된 내용을 그대로 사용하여 리렌더링을 막음
- ⇒ 이렇게 Memoized된 내용을 재사용하여 렌더링시 가상 DOM에서 달라진 부분을 확인하지 않아 성능상의 이점이 생기게 됨
const Welcome = ({ name }) => { return <h1>Hello { name }</h1>; }; export default React.memo(Welcome);
- props가 갖는 복잡한 객체에 대하여 얕은 비교만을 수행하는 것이 기본 동작이며, 다른 비교 동작을 원한다면, 두 번째 인자로 별도의 비교 함수를 제공하면 됨.
- 얕은 비교 : 원시 값의 경우는 같은 값을 갖는지 확인하고 객체나 배열과 같은 참조 값은 같은 주소 값을 갖고 있는지 확인
function MyComponent(props) {
/* props를 사용하여 렌더링 */
}
function areEqual(prevProps, nextProps) {
/*
nextProps가 prevProps와 동일한 값을 가지면 true를 반환하고, 그렇지 않다면 false를 반환
*/
}
export default React.memo(MyComponent, areEqual);
- (간단한 예) Movie의 props가 동일한지 수동으로 비교해보자.
- moviePropsAreEqual() 함수는 이전 props와 현재 props가 같다면 true를 반환할 것
function moviePropsAreEqual(prevMovie, nextMovie) {
return (
prevMovie.title === nextMovie.title &&
prevMovie.releaseDate === nextMovie.releaseDate
);
}
const MemoizedMovie2 = React.memo(Movie, moviePropsAreEqual);
2. 어디에 쓸까?
사용처
- Pure Functional Component(동일한 상태 및 props에 대해 동일한 출력을 렌더링하는 컴포넌트)에서 Rendering이 자주일어날 경우
- re-rendering이 되는 동안에도 계속 같은 props값이 전달될 경우
- UI element의 양이 많은 컴포넌트의 경우
- 동일한 props에 항상 같은 것을 랜더링하는데, 같은 props로 랜더링이 자주 일어날 때(즉, 일부 데이터를 가져오기 위해 네트워크 호출을 해야 하고 데이터가 동일하지 않을 가능성이 있는 경우 사용 지양)
결론
- 컴퍼넌트가 무겁고 비용이 큰 연산이 있는데 같은 props로 자주 렌더링되거나 같은 props로 리랜더링이 엄청 자주 일어난다면 React.memo()로 컴퍼넌트를 래핑하면 좋음
(단, 동일한 props로는 항상 같은 리랜더링 결과가 나올 때)
3. 주의점은?
일반적인 주의사항
- 이 메서드는 오직 **성능 최적화**를 위하여 사용되므로 렌더링을 “방지”하기 위하여 사용할 경우, 버그가 생성될 수 있음
- 최적화를 위한 연산이 불필요한 경우엔 비용만 발생시키기 때문에 무조건적인 사용은 지양
- React.memo는 Props의 변경 사항만 확인하므로, React.memo에 래핑된 함수 컴포넌트 요소에 useState , useReducer 또는 useContext Hook이 있는 경우 상태 또는 컨텍스트가 변경될 때 여전히 다시 렌더링됨
부모가 전달하는 callback 함수에 대한 주의사항
- useCallback과 함께 사용할 때의 예제
useCallback 사용 전
function CountButton({ onClick, count }) {
return <button onClick={onClick}>{count}</button>;
}
function DualCounter() {
const [count1, setCount1] = React.useState(0);
const increment1 = () => setCount1(c => c + 1);
const [count2, setCount2] = React.useState(0);
const increment2 = () => setCount2(c => c + 1);
return (
<>
<CountButton count={count1} onClick={increment1} /> // React.memo로 래핑되었다는 가정
<CountButton count={count2} onClick={increment2} /> // React.memo로 래핑되었다는 가정
</>
);
}
useCallback 사용 후
const CountButton = React.memo(function CountButton({ onClick, count }) {
return <button onClick={onClick}>{count}</button>;
});
function DualCounter() {
const [count1, setCount1] = React.useState(0);
const increment1 = React.useCallback(() => setCount1(c => c + 1), []);
const [count2, setCount2] = React.useState(0);
const increment2 = React.useCallback(() => setCount2(c => c + 1), []);
return (
<>
<CountButton count={count1} onClick={increment1} /> // React.memo로 래핑되었다는 가정
<CountButton count={count2} onClick={increment2} /> // React.memo로 래핑되었다는 가정
</>
);
}
- state count1이 변경되었을 때, state 변경이 없었던 count2를 참조하는 CountButton 컴포넌트는 리렌더리 되지 않아야 함(React.memo로 래핑되었다는 가정)
- 만약 increment2 함수에 useCallback이 없었다면, DualCounter 컴포넌트는 state의 변경으로 인해 re-rendering 될 것이고, increment1과 increment2 함수 모두 새로 생성되어 2개의 CountButton 컴포넌트는 모두 re-rendering 될 것
- 하지만 increment1, increment2 함수에 useCallback을 사용함으로써 두개의 함수는 재 생성이 되지 않고 (종속배열도 비어있음) 변경된 count1을 참조하는 CountButton만 re-rendering 되게 됨
4. 예시로 알아보자!
- 같은 props로 렌더링이 자주 일어나는 컴퍼넌트에 사용하기 좋음
- React.memo()를 사용하기 가장 좋은 케이스는 함수형 컴퍼넌트가 같은 props로 자주 렌더링 될거라 예상될 때이다.
- 일반적으로 부모 컴퍼넌트에 의해 하위 컴퍼넌트가 같은 props로 리렌더링 될 때가 있음
- Movie의 부모 컴퍼넌트인 실시간으로 업데이트되는 영화 조회수를 나타내는 MovieViewsRealtime 컴퍼넌트가 있다고 하자.
function MovieViewsRealtime({ title, releaseDate, views }) {
return (
<div>
<Movie title={title} releaseDate={releaseDate} />
Movie views: {views}
</div>
);
}
- 이 어플리케이션은 주기적(매초)으로 서버에서 데이터를 폴링(Polling)해서 MovieViewsRealtime 컴퍼넌트의 views를 업데이트함
// Initial render
<MovieViewsRealtime views={0} title="Forrest Gump" releaseDate="June 23, 1994"/>// After 1 second, views is 10
<MovieViewsRealtime views={10} title="Forrest Gump" releaseDate="June 23, 1994"/>// After 2 seconds, views is 25
<MovieViewsRealtime views={25} title="Forrest Gump" releaseDate="June 23, 1994"/>// etc
- views가 새로운 숫자가 업데이트 될 때 마다 MoviewViewsRealtime 컴퍼넌트 또한 리렌더링 되며, Movie 컴퍼넌트 또한 title이나 releaseData가 같음에도 불구하고 리렌더링 됨
- 이때가 Movie 컴퍼넌트에 메모이제이션을 적용할 적절한 케이스임
- MovieViewsRealtime에 메모이징된 컴퍼넌트인 MemoizedMovie를 대신 사용해 성능을 향상해보자.
function MovieViewsRealtime({ title, releaseDate, views }) {
return (
<div>
<MemoizedMovie title={title} releaseDate={releaseDate} />
Movie views: {views}
</div>
);
}
- title 혹은 releaseDate props가 같다면, React는 MemoizedMovie를 리렌더링 하지 않을 것이다. 이렇게 MovieViewsRealtime 컴퍼넌트의 성능을 향상할 수 있음
React.memo vs. useMemo vs. useCallback
1. 공통점
공통점
- React.memo, useMemo, useCallback은 모두 불필요한 렌더링 또는 연산을 제어하는 용도로 성능 최적화에 그 목적이 있음
- 재렌더링 사이의 메모이제이션임
- 전달하려는 항목이 새로운 참조여도 상관없다면, 사용하지 말아야 한다. 매번 새로운 참조여도 상관없는데, 새로운 참조라면 메모이제이션하는 것이 의미가 없음
useMemo와 useCallback을 사용해야 하는 경우
- 하위트리에 많은 Consumer가 있는 값을 Context Provider에 전달해야 하는 경우 useMemo를 사용하는 것이 좋음 <ProductContext.Provider value={{id, name}} >의 경우, 어떤 이유로든 해당 컴포넌트가 리렌더링 된다면 id name이 동일하더라도 매번 새로운 참조를 만들어 죄다 리렌더링 될 것
- 계산 비용이 많이 들고, 사용자의 입력 값이 렌더링 이후로도 참조적으로 동일할 가능성이 높은 경우, useMemo를 사용하는 것이 좋음
- 매우 큰 리액트 트리 구조 내에서, 부모가 리렌더링 되었을 때 이에 다른 렌더링 전파를 막고 싶을 때 사용하자. 자식 컴포넌트가 React.memo React.PureComponent일 경우, 메모이제이션된 props를 사용하게되면 딱 필요한 부분만 리렌더링 될 것
사용팁
React DevTools Profiler를 사용하면 컴포넌트의 리렌더링 속도가 느린 경우, 상태 변경이 일어났을 때 얼마나 렌더링 시간이 걸렸는지 조사할 수 있음
이렇게 하면 거대한 계단식 리렌더링을 방지하기 위해 React.memo를 사용할 위치를 찾을 수 있고, 필요한 경우 useCallback useMemo를 사용하여 상태변경을 더 효율적으로 만들 수 있음
2. 차이점
- React.memo는 HOC이고, useMemo와 useCallback은 hook
- React.memo는 HOC이기 때문에 클래스형 컴포넌트, 함수형 컴포넌트 모두 사용 가능하지만, useMemo는 hook이기 때문에 함수형 컴포넌트 안에서만 사용 가능
- useMemo는 함수의 연산량이 많을때 이전 결과값을 재사용하는 목적이고, useCallback은 함수가 재생성 되는것을 방지하기 위한 목적(React.memo와 useMemo의 차이는 어디에 활용되는가임)
- React.memo의 경우에는 컴포넌트를 받아 컴포넌트를 반환한다.
- useMemo의 경우에는 값을 계산하는 과정을 최적화해 값을 반환받음(컴포넌트도 값이기에 useMemo 안에 넣을 수 있음)
3. 주의점
- 일부 개발자가 흔히 저지르는 실수는 성능 문제를 방지하기 위해 필요하지 않은 경우에도 이러한 후크(및 기타 최적화 기술)를 사용하는 것임
- 이는 코드를 더 복잡하게 만들고(따라서 유지 관리하기 더 어렵게 만들고) 경우에 따라 성능이 더 나빠지기 때문에 권장되지 않음
- 성능 문제를 찾은 후 이러한 기술을 적용해야 함
- 원하는 만큼 빠르게 실행되지 않는 경우 병목 현상이 있는 부분을 조사하고 해당 부분을 최적화가 필요
- useCallback을 사용하여 접근하는 좋은 방법은 능동적이기보다는 반응적으로 접근하는 것임
- 즉, 구성 요소에 따라 조급한 성능 최적화가 아니라 분명히 필요할 때 사용하는 것이 중요함
- useCallback의 함수 본문 내부에 있는 모든 함수를 래핑하지 않도록 하자.
React Devtools Profile
React Devtools로 프로파일링
- 먼저 React Devtools 브라우저 확장 프로그램을 다운로드해야 함
- "Profiler" 탭을 선택
- 작은 기어 아이콘을 클릭하고 "프로파일링 중 각 구성 요소가 렌더링된 이유 기록" 옵션을 활성화함
- 일반적인 흐름은 다음과 같음
- 작은 파란색 "녹음" 원을 눌러 녹음 시작
- 애플리케이션에서 몇 가지 작업을 수행
- 녹음을 중지
- 기록된 스냅샷을 보고 무슨 일이 일어났는지 확인 가능
- 각 렌더링은 별도의 스냅샷으로 캡처되며 화살표를 사용하여 탐색할 수 있음
(참조) https://storage.googleapis.com/joshwcomeau/devtools-demo-v2.mp4
- 관심 있는 구성 요소를 클릭하면 특정 구성 요소가 다시 렌더링된 이유를 정확하게 확인할 수 있음.
- React 프로파일러에는 다시 렌더링하는 구성 요소를 강조 표시할 수 있는 옵션이 있음
이 설정을 사용하면 다시 렌더링하는 구성 요소 주위에 녹색 사각형이 깜박이는 것을 볼 수 있고 이를 통해 상태 업데이트가 얼마나 광범위한지 확인할 수 있고 일부 요소가 재렌더링을 성공적으로 피하는지 테스트할 수 있음
개인적으로 느낀 점
일전에 React를 사용하며, 부모 Component에서 자식 Component로 callback을 Prop으로 내려줬는데 의도치않게 너무 많은 랜더링이 일어나는 이슈를 겪은 적이 있다.
그 때, useCallback으로 해결한 경험이 있어서 그 이후 useCallback을 남발하게 되었던 것 같다.
메모이제이션이라는 것이 어딘가에 저장을 하는 만큼(메모리) 결코 공짜가 아니라는 생각이 들었고 과연 나는 useCallback을 효율적으로 사용하고 있는가라는 의문이 들었다.
관련하여 찾다보니, React의 다른 메모이제이션 훅, HOC에 대해서도 찾을 수 있었다.
역시, 메모이제이션 기능은 공짜가 아니었고 오히려 이러한 성급한 최적화 시도가 성능을 더 저하시킬 수 있다는 것을 확인하였다.
useCallback의 경우에도 꼭 필요한 경우(React.memo로 래핑한 자식 컴포넌트에 callback을 넘겨주는 경우, 자식 컴포넌트로 내려가는 callback으로 인해, 자식의 useEffect가 의도치 않게 계속 시행되는 경우 등)와 사용에 대한 근거 없이는 사용을 자제해야겠다는 생각을 했다.
이전에 너무 많은 랜더링이 일어난 상황이 현재는 잘 기억나진 않지만, 아마도 useEffect 종속성 문제와 겹치면서 일어난 참사이지 않았을까 생각이 든다.
당시 React.memo를 사용하는 상황은 아니었기 때문에 굳이 useCallback을 쓰지 않고 해결할 수 있는 방법도 있지 않았을까 생각이 들고 상황상 여의치 않다면 이전에 해결한 방법과 동일하게 useCallback을 결국 써야했을 것 같다.
부탁드리는 사항
혹시 잘못된 내용이나, 인용/차용 등에 있어 문제의 소지가 되는 내용이 있다면 언제든 알려주시면 큰 도움이 될 것 같습니다!
긴 글 읽어주셔서 감사합니다 :)
출처:
메모이제이션 정의
https://ko.wikipedia.org/wiki/메모이제이션
useMemo
https://www.digitalocean.com/community/tutorials/react-usememo
https://ko.reactjs.org/docs/react-api.html React.Memo
https://ui.toast.com/weekly-pick/ko_20190731
Pure Functional Component
https://blog.logrocket.com/what-are-react-pure-functional-components/
전반적 설명 및 예시
https://www.joshwcomeau.com/react/usememo-and-usecallback/
https://medium.com/geekculture/great-confusion-about-react-memoization-methods-react-memo-usememo-usecallback-a10ebdd3a316
전반적 역할 설명 및 비교
https://medium.com/hcleedev/web-최적화와-react-memo-usememo-알아보기-4324a237a039 https://velog.io/@sunkim/React.memo-useMemo-useCallback-역할-및-차이점 https://ssangq.netlify.app/posts/react-memo-useMemo-useCallback
https://www.developerway.com/posts/how-to-use-memo-use-callback
https://ddingg.tistory.com/119
useMemo vs. useEffect
https://stackoverflow.com/questions/56028913/usememo-vs-useeffect-usestate
useMemo와 useCallback 사용처
https://yceffort.kr/2022/04/best-practice-useCallback-useMemo
React Devtools Profile
https://www.joshwcomeau.com/react/why-react-re-renders/
useCallback 사용처와 주의사항
https://www.developerway.com/posts/how-to-write-performant-react-code
https://dmitripavlutin.com/react-usecallback/
useCallback, useMemo를 남용하면 안 되는 이유
https://kentcdodds.com/blog/usememo-and-usecallback
https://nicozerpa.com/when-to-use-usememo-and-usecallback-in-react/
https://amberwilson.co.uk/blog/how-and-when-to-use-react-usecallback/