다들 React를 처음 공부할 때 여러가지 훅의 쓰임새와 활용방법등을 공부하면서 useMemo와 useCallback이라는 훅을 접해봤을 것이다.
처음 내가 useMemo와 useCallback을 공부했을 때 사용이유는 다음과 같았다.
- useMemo는 계산 비용이 높은 연산의 결과를 기억하고, 의존성 배열에 넣어준 deps가 변경되지 않으면 계산한 값을 재사용한다.
- useCallback은 함수를 메모이제이션하고, 의존성 배열에 넣어준 deps가 변경되지 않으면 이전에 생성한 함수를 재사용한다.
두 훅 모두 React를 사용하면서 렌더링 성능 최적화를 위해 사용하는 훅인데, 사실 사용하지 않아도 개발자 경험에는 큰 영향을 주지않는다.
사실 큰 영향을 주지않는다기보다 개인적으로 아직 내 실력과 경험이 부족해서 영향력을 체감하지 못하는 것일 수도 있다.
한 번 사용해볼까하고 여러 블로그에서 useMemo와 useCallback에 대한 포스팅을 살펴보면 "useMemo와 useCallback을 무분별하게 사용하면 메모리의 낭비가 발생하기 때문에 적절하게 사용해야한다" 라는 문구를 심심치않게 볼 수 있다.
시간이 무한대로 많은 개발자라면 모든 로직마다 useMemo와 useCallback의 사용유무에 따른 성능 비교를 통해 메모리 낭비없이 적절하게 사용하는 것이 가능할지도 모르지만 일반적으로는 매번 로직을 작성할 때마다 성능 비교를 하는 것은 불가능하다. 따라서 useMemo와 useCallback을 사용하는 개인적인 원칙을 세우는것이 바람직하다고 생각한다.
그럼 가장 먼저, 원칙을 세우기 전에 몇가지 중요한 개념에 대해서 짚고 넘어가보도록 하자.
React 불변성 개념
React에서 상태는 가장 중요한 요소 중 하나이다. 상태는 컴포넌트의 동적인 데이터를 나타내고, React를 사용한 애플리케이션은 이 상태가 변경될 때마다 리렌더링이 발생하고 화면이 업데이트된다. 여기서 불변성이라는 개념이 등장한다. React의 상태 변화 감지 기준은 콜 스택의 주소 값이다.
변수에 저장된 값이 원시 타입인 경우 해당 변수의 값을 변경한다면 콜 스택의 새로운 주소에 새로운 값을 저장하게된다. 따라서 React는 콜 스택의 주소 값이 바뀌었기 때문에 상태 변화가 일어났음을 감지한다.
하지만 변수에 저장된 값이 배열, 객체와 같은 참조 타입인 경우 해당 객체나 배열에 새로운 값을 추가하거나 삭제한다면 메모리 힙에 있는 데이터가 변경이되고 콜 스택의 주소값은 동일하기 때문에 상태 변화를 감지할 수 없다.
따라서 참조 타입의 경우 새로운 객체나 배열을 만들어 콜 스택의 주소 값을 교체하는 방식으로 상태 변화를 감지할 수 있게 해야한다.
전체적인 손해를 따져보자
일단 useMemo와 useCallback은 위에서 언급했듯이 성능 최적화를 위한 훅이다. 오로지 성능때문이라면 useMemo와 useCallback을 사용하든 사용하지 않든 동작의 차이는 존재하면 안된다. 만약 useMemo와 useCallback을 무조건 사용하거나, 무조건 사용하지 않아야하는 선택지 중에 하나를 골라야한다고 가정해보자.
굉장비 비싼 연산을 하거나 새로운 함수를 반복적으로 생성해야하는 경우가 있을 때 useMemo와 useCallback을 사용한다면 확실하게 성능을 끌어올릴 수 있도록 보장해준다. 작은 연산인 경우에도 엄청나게 큰 손해는 아닐 수 있다. 하지만 굉장히 비싼 연산을 하거나 새로운 함수를 반복적으로 생성해야하는 경우에도 useMemo와 useCallback을 사용하지 않는다면 새로운 값과 함수가 재생성됨으로써 props로 전달되는 경우 위에서 언급한 불변성을 유지하지 못하기 때문에 문제가 증폭될 수 있다. 따라서 결과적으로 두 가지 극단적인 선택지가 존재한다면 무조건 사용하는 편이 무조건 사용하지 않는 편보다 좋은 선택지일 것이다.
손해의 범위를 따져보자
useMemo와 useCallback을 사용했을 때의 손해를 대강 한 번 예측해보자면, 이전에 계산된 값이나 함수를 저장하는데 사용되는 메모리 + deps에 넣어준 값들과 이전 값의 비교 연산으로 예측해볼 수 있다. 하지만 useMemo와 useCallback을 사용하지 않았을 때의 손해를 예측해보면, 매번 새로운 연산과 함수 생성 그리고 그로인해 불변성이 유지되지 않아 하위 컴포넌트들에서 발생하는 사이드 이펙트가 존재한다.
각각 위의 손해를 통해서 알 수 있는 점이 무엇일까? 바로 손해의 범위이다.
useMemo와 useCallback을 사용했을 때는 내가 지금 보고있는 코드 범위 내에서 손해가 발생한다. 하지만 useMemo와 useCallback을 사용하지 않았을 때는 하위 컴포넌트, 하위 컴포넌트의 하위 컴포넌트로 영향을 미치는 범위가 광범위해진다. 만약 하위 컴포넌트가 많으면 많을수록 미치는 영향도 증폭될 것이다.
원시 타입의 경우 굳이 사용하지 말자
위에서 이야기한대로라면 연산의 값과 함수는 모두 useMemo와 useCallback을 사용해야하는 것처럼 들릴지도 모른다. 여기서 React의 불변성 개념을 설명한 이유가 등장한다. useMemo또는 useCallback을 사용하는 이유는 참조 타입의 불변성 유지를 위해서 사용하는 것이다. 원시 타입의 경우에는 useMemo와 useCallback을 사용하든 사용하지 않든 같은 값이라면 콜 스택의 주소는 동일하다. 따라서 원시타입의 경우 현재 내가 보고있는 코드 범위 밖에서의 영향을 미치지 않는다.
useMemo와 useCallback을 어떤 부분에 적용해야할지 갈피를 못잡거나, "무분별하게 사용하면 메모리 낭비가 발생한다" 라는 설명 때문에 쉽사리 적용하지 못하고 있던 개발자들이 있었을 것이라고 생각한다. (내가 그랬다 ㅎㅎ) 조금 더 어렵지 않게 접근하고 사용하기 위해 생각을 한 번 정리해봤는데 useMemo와 useCallback을 사용하는데 두려움이 조금 없어진 것 같다.
'개발 지식 정리' 카테고리의 다른 글
렌더링 방식(CSR, SSR, SSG) (2) | 2023.10.17 |
---|---|
주소창에 URL을 입력하면 무슨 일이 일어날까? (2) | 2023.09.04 |
nextjs에서 recoil 세팅하기 (0) | 2023.05.06 |
외부 이미지 가져오기 (feat. nextjs) (0) | 2023.04.30 |
CORS 그만 마주치자! (0) | 2023.03.19 |