원문: https://javascript.plainenglish.io/stop-using-usememo-now-e5d07d2bbf70
번역: https://velog.io/@lky5697/stop-using-usememo-now
을 읽고 재정리하였습니다.
useMemo 란
useMemo 는 알다시피 계산결과를 memorization (유사하게, 기억한다 라고 생각하자.) 한다.
const result = useMemo(() => {
// running....
return value
}, [deps])
코드가 위와 같을 때, deps 로 들어온 state 값에 따라 side effect 로 useMemo 안의 로직이 실행되고 그 결과값을 리턴하게 된다.
deps 의 값에 따라 리턴되는 value 값이 다를텐데, 이때 useMemo 는 어떤 deps 일 때 value를 리턴하는 지 memorization 한다.
즉, 예를들어 아래처럼 순차적으로 실행되었다고 생각해보자.
1. deps => 'abc' 👉 계산중 ... 👉 value => 2
2. deps => ab' 👉 계산중 ... 👉 value => 3
그 다음 deps 가 다시 'abc' 가 된다면, 이미 1번에서 수행한 이력이 있기 때문에, 굳이 계산결과를 거치지 않고 캐싱한 2 를 리턴하는 것이다.
우린 어떨 때 useMemo 를 사용했을까 ?
계산 결과를 기억한다 라는 사실만 보면, 정말 은혜로운 hook 으로 보인다. 리엑트로 개발하다보면 한번쯤 side effect 에 따른 리렌더링에 대한 고민을 했을 것이다. 정말 깊게 생각하지 않는다면, (혹은 useMemo 의 사용법을 자세히 알지 못한다면 ) 단순히 계산결과를 기억한다는 것에 꽂혀 최적화를 한다는 명목하에 여기저기 useMemo 를 남발하게 된다.
하지만, 이것은 오히려 메모리 사용량을 증가하게 하는 최악의 상황을 불러오게 된다.
참고로, useMemo 는 렌더링 도중 실행된다.
useMemo 가 최악의 성능을 보이는 경우는 언제인가 ?
// props 로 tabs, className, withExpander 받아온다.
export const NavTabs = ({ tabs, className, withExpander }) => {
const currentMainPath = useMemo(() => {
return pathname.split("/")[1];
}, [pathname]);
const isCurrentMainPath = useMemo(() => {
return currentMainPath === pathname.substr(1);
}, [pathname, currentMainPath]);
return (
<StyledWrapper>
<Span fontSize={18}>
{isCurrentMainPath ? (
t(currentMainPath)
) : (
<StyledLink to={`/${currentMainPath}`}>
{t(currentMainPath)}
</StyledLink>
)}
</Span>
</StyledWrapper>
);
};
위의 예시 코드가 있다. 우선 하나하나 쪼개서 기능을 이해해보자.
const currentMainPath = useMemo(() => {
return pathname.split("/")[1];
}, [pathname]);
props 로 받아온 pathname 을 '/' 로 split 하여 두번째 값 을 리턴한다.
const isCurrentMainPath = useMemo(() => {
return currentMainPath === pathname.substr(1);
}, [pathname, currentMainPath]);
위에서 계산한 currentMainPath 와 pathname를 substr(1) 한 값이 같은지를 체크한다.
return (
<StyledWrapper>
<Span fontSize={18}>
{isCurrentMainPath ? (
t(currentMainPath)
) : (
<StyledLink to={`/${currentMainPath}`}>
{t(currentMainPath)}
</StyledLink>
)}
</Span>
</StyledWrapper>
);
그리고 그 값에 따라 링크를 보여주는 것 같다.
지금 우리는 useMemo 을 두번 쓰고 있다. 이 두개의 로직에서 우리가 최적화하고 싶은건 무엇인지 생각해보자.
자세히 보면, split 와 === 와 같은 단순한 연산인데, useMemo 로 캐싱해야하는 건지도 불분명하다.
useMemo 를 걷어내보면 아래처럼 깔끔해지며 가독성을 챙길 수 있다.
export const NavTabs = ({ tabs, className, withExpander }) => {
return (
<Wrapper className={className} withExpander={withExpander}>
{tabs.map((tab) => (
<Item
key={tab.path}
to={tab.path}
withExpander={withExpander}
>
<StyledLabel>{tab.label}</StyledLabel>
</Item>
))}
</Wrapper>
);
아래와 같을 때 useMemo 사용은 피하자
- 최적화하려는 계산의 비용이 크지 않은 경우.
- 이러한 경우, useMemo를 사용할 때 발생하는 오버헤드가 이점보다 클 수 있다.
- 메모이제이션이 필요한지 확실하지 않은 경우.
- useMemo 없이 시작한 다음, 문제가 발생하면 코드에 점진적으로 최적화를 적용해보자.
- 메모하고 있는 값이 컴포넌트로 전달되지 않는 경우.
- 이 값이 JSX에서만 사용되고 컴포넌트 트리에 더 깊이 전달되지 않으면 대부분의 경우 최적화를 피할 수 있다. 다른 컴포넌트의 렌더링에 영향을 주지 않으므로 참조를 기억할 필요가 없다.
- 의존성 배열이 너무 자주 변경되는 경우.
- 이 경우 useMemo는 항상 재계산되므로 성능적인 이점을 제공하지 않는다.
'🌳Frontend > react' 카테고리의 다른 글
React 의 의존성 배열의 얕은 비교 (0) | 2023.05.17 |
---|---|
[React] 렌더링과 컴포넌트 렌더링 순서 (0) | 2023.04.20 |
React.memo vs useMemo vs useEffect (0) | 2023.03.24 |
React 에서 주의해야하는 Event 와 addEventListener (0) | 2023.03.24 |
[SWR] SWR 동작 시나리오 (0) | 2023.03.22 |