React에서 함수 컴포넌트가 도입된 이후 v16.8부터 다양한 hook들이 소개되고 있습니다.
그 중에서 상태를 관리하는 useState, useReducer와 같은 상태를 다루기 위해서 꼭 사용해야 하는 훅도 있지만 useCallback, useMemo, memo와 같이 겉보기 동작에는 전혀 영향을 주지 않는 훅들도 있습니다.
메모이제이션은 무엇일까?
사용하는 것과 사용하지 않는 것에 동작의 차이가 없다면 이 훅은 어떤 목적에서 사용되는 걸까요?
이미 알고 계시겠지만 React에서는 메모이제이션(Memoization)을 위해 사용하고 있습니다.
메모이제이션의 대한 위키를 찾아보면 '컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술이다. 라고 설명하고 있습니다.
리액트에서도 비슷한 목적으로 사용되고 있습니다. 컴포넌트 내부에 함수가 존재할 때 컴포넌트가 리렌더링 되면 함수를 새롭게 생성하게 될 것입니다. 이는 항상 최신 동작을 유지할 수 있기 때문에 나쁘다고만 볼 수는 없으나, 만약 함수의 의존성이 변하지 않았음에도 계속해서 리렌더링이 될 때마다 함수를 새롭게 생성한다면 불필요하게 생성이 된다고 볼 수 있습니다.
바로, 이러한 경우에 우리는 메모이제이션을 활용하여 함수가 의존성을 가진 state가 변경될때만 함수를 새롭게 생성하도록 만듭니다. 또한 기존에는 함수가 새로 생성되었기 때문에 리렌더링이 일어났을 때 JSX에서 해당 함수를 사용하는 element에 대해 리액트는 값이 변경되었다고 판단하여 Virtual DOM에서 비교를 진행하고 Real DOM에 반영을 하게 되는데 이를 메모이제이션으로 방지할 수 있습니다.
사실 생각해보면 의존성이 변할때만 함수를 새롭게 생성하는게 당연하다고 볼 수 있습니다.
메모이제이션을 무조건 사용하면 될까?
물론 그렇지는 않습니다.
메모이제이션은 값이나 함수를 메모리에 올려놓기 때문에(캐싱을 해놓기 때문에) 그 자체로 비용이 들기 때문입니다.
컴포넌트가 유저의 인터랙션에 따라서 항상 state가 변하는 경우라면 그리고 함수가 이 state에 의존성을 갖고 있다면 이러한 경우에는 메모이제이션을 하는게 의미가 없습니다. 왜냐하면 리렌더링이 될 때마다 계속해서 함수를 생성하면서 메모리에는 계속 올려두고 있기 때문입니다. 메모리에 올려놓는 비용이 들기 때문에 오히려 사용하지 않는 게 좋습니다.
그러면 언제 사용할까?
컴포넌트 내부의 함수가 순수 함수(pure function)이거나 혹은 컴포넌트 내부의 모든 state가 아닌 일부 state에 대해서만 의존성을 갖는 경우에 메모이제이션을 사용하는 것을 고려할 수 있습니다.
혹시 최적화와 관련된 글을 읽어보셨다면 이러한 내용을 보신적이 있으실 것 같습니다.
'최적화는 섣불리 하지 마라.', '실제로 느려지기 전까지 먼저 최적화를 하지 마라.'
리액트에서 메모이제이션을 할 때도 이와 같은 원칙을 적용할 수 있을까요?
저는 리액트에서 적용하기에는 조금 다른 얘기라고 생각합니다.
실제로 메모이제이션을 하는 데 드는 코드 비용(코드를 작성하는 관점)이 거의 없다고 볼 수 있고 메모이제이션을 했다고 해서 그 자체로 성능이 크게 느려진다거나 하지는 않기 때문입니다.
오히려 대규모 서비스라고 하면, 그리고 여러 개발자들이 협업하고 있는 프로젝트라고 하면 리렌더링과 관련된 성능 이슈를 미리 예방할 수 있다는 관점에서 의미있는 동작이라고 생각합니다.
아래는 한 개발자분이 useCallback에 대한 생각을 말씀해주신 걸 간단하게 정리한 내용입니다.
useCallback과 같은 메모이제이션 기법은 성능 최적화를 위한 훅이다.
성능 최적화는 실제로 성능이 느려졌을 때 하는게 이상적이라는 것에는 동의한다.
하지만 실제로 규모가 큰 프로덕트를 개발함에 있어서 리렌더링 이슈는 성능에 큰 영향을 줄 수도 있다.
그리고 내가 작성한 모듈을 다른 사람이 사용한다고 생각했을 때 만약 메모이제이션이 되어 있지 않다면, 연산 비용이 비싼 컴포넌트에 사용되었을 때 큰 문제가 생길 수 있다.
그리고 이러한 문제는 실 서비스에서 발생한다면 매우 큰 손실로 돌아온다.
useCallback을 섣불리 쓰면 오히려 캐싱하고 있어야 하기 때문에 성능에는 더 안좋을 수도 있지만, 미래에 일어날 수 있는 문제들에 대해 방어를 하기 위해 당장의 성능에 조금의 손해를 보더라도 큰 이슈를 방지하는 것이 더 옳다고 볼 수 있지 않을까?
저도 위와 같이 동의를 하고 있고 메모이제이션을 해서 의미가 없는 경우가 아니라면, 해주는 것이 좋다고 생각하고 있습니다.
리액트 공식문서에서는?
리액트 공식문서에서는 우선 커스텀 훅의 경우에는 내부 함수를 useCallback으로 래핑해서 사용할 것을 권장하고 있습니다.
이에 대한 이유도 위에서 설명했던 내용과 비슷하게 얘기하고 있습니다.
다만 일반적으로 사용할 때는 2가지 경우에 유용하다고 합니다.
- memo로 래핑되어 있는 컴포넌트에 prop을 전달하는 경우
- useEffect 내부에서 함수에 대한 의존성이 있을때
첫 번째의 경우에는 상위에서 메모이제이션을 해주지 않고 자식 컴포넌트에 prop으로 내려주면 자식 컴포넌트에서 memo로 래핑해준 의미가 없기 때문에 해당됩니다.
두 번째의 경우에는 만약 useEffect 내부에서 컴포넌트 내부의 함수를 사용한다면 리렌더링이 될 때마다 함수는 계속 새롭게 생성되고 useEffect는 다시 실행될테니 무한 루프에 빠질 수 있습니다. 이 경우에는 useCallback으로 함수를 감싸주거나, useEffect 내부로 옮겨주면 됩니다.
정리
- React에서는 메모이제이션을 위해 사용되는 useCallback, useMemo, memo와 같은 훅들이 있다.
- 메모이제이션이 항상 필요한 건 아니다, 오히려 성능에 좋지 않은 경우도 있다.
- 적절한 메모이제이션은 코드의 안정성을 높일 수 있다.
- 메모이제이션을 사용하는 나만의 명확한 기준이 필요하다.
출처
'TIL > 개발' 카테고리의 다른 글
요즘 개발자 베타리딩 - 2주차 (0) | 2023.11.09 |
---|---|
프론트엔드에서의 비즈니스 로직은 어떻게 분리할 수 있을까요? (0) | 2023.11.09 |
BFF(Backend For Frontend)는 어떤 문제를 해결하나? (0) | 2023.11.07 |
[Astro] Astro 3.0에서 달라진 것들 (0) | 2023.09.01 |
테스트코드와 SPA 환경을 만들어보며 배우는 모던 자바스크립트 입문 - 1주차 (0) | 2023.04.03 |