함수형 컴포넌트는 함수다.
따라서 리렌더링 될 때마다 재호출이된다.
이 때, 함수컴포넌트의 스코프 내부에 정의된 모든 값(변수, 함수)들은 재정의(초기화)된다.
이를 방지하기위해 리액트는 useCallback
, useMemo
와 같은 hook을 제공한다.
그러면 함수내에서 변하지 않는 상수나 함수를 매번 useCallback
, useMemo
을 사용해서 메모이제이션해두는게 좋지 않을까?
나 또한 전에는 메모이제이션 hook을 최대한 사용하는게 좋다고 생각했다.
리렌더링 될 때마다 변하지 않는 값들이 매번 재정의 되는것이 비효율적이고 이치에 맞지 않는다고 생각했다. 그래서 남발했다.
그러나 지나친 메모이제이션의 사용은 성능적인 측면에서 오히려 독이 될 수 있다.
왜 그런지 이유에 대해서 알아보고, 언제 메모이제이션 훅을 사용해야하는지 정리해본다.
결론부터 말하면,
메모이제이션을위해 소요되는 비용이 값이 재정의되는 비용보다 클 수 있기때문에 지양해야한다.
함수컴포넌트가 리렌더링(재실행)될 때, 메모이제이션된 부분의 코드수행이 skip되는것이 아니다.
변수선언문 코드가 재실행 되는 것과 마찬가지로, 메모이제이션 코드들도 똑같이 재실행된다.
단, 메모이제이션 hook이 실행되면 내부적으로,
- dependency의 값이 변경되었는지를 체크
- 변경이 없다면, 리렌더링되기 전에 들고있던 참조값을 할당
하는 작업을 수행한다.
그래서 개발자와 React는 값이 변하지 않았다고 인식하게된다.
더 나아가 개발자는 ‘해당 구문이 수행되지않는구나’ 라는 착각에까지 빠질 수 있다. (그게바로 나..)
오히려 앞서말한 depdency를 확인하고, 이전 참조값을 찾아 할당해주는 메모이제이션 hook의 내부로직이, 단순 값을 재할당하는 것보다 더 큰 리소스를 필요로할 수 있다.
따라서 메모이제이션을 남발하는 것은 더 큰 비용을 소요시키며, 가독성또한 해칠 수 있다.
반드시 필요한 상황이 아니라면, 사용을 지양하는 것이 좋다.
useCallback
과 useMemo
을 적절히 사용하면 컴포넌트의 리렌더링 횟수를 최소화 시킬 수 있다.
자식컴포넌트에 props로 넘겨주는 값들은 메모이제이션을 해주자.
이유를 알아보자면, 리액트에서 리렌더링이 되는 조건은 다음과 같다.
- 부모컴포넌트가 리렌더링 되었을 때
- props가 변경되었을 때
- state가 변경되었을 때
- key가 변경되었을 때
props로 내려주는 데이터의 타입이 primitive할 경우, 이전과 값만 동일하다면 데이터가 변하지 않는걸로 인식한다. 따라서 재정의가 되더라도 큰 문제가 없다.
그러나 나머지 데이터타입은 값이 동일하다한들 immutable 하기 때문에, 재정의될때마다 다른참조값을 갖게되고, 자식컴포넌트는 이를 props가 변경된것으로 인지하고 리렌더링한다.
따라서 이런 케이스에서 메모이제이션훅을 이용하면 부모컴포넌트가 리렌더링되더라도, 이전참조값을 유지시키기 때문에, props가 변경되는 횟수를 줄임으로써, 리렌더링의 횟수를 줄일 수 있다.
앞서 말했듯이, 메모이제이션의 무분별한 사용은 지양하는것이 좋다.
메모이제이션을 사용하지않고 리렌더링 횟수를 줄이는 방법이 있는데, 바로 컴포넌트를 잘게 분리하는 것이다.
변하는 부분과 변하지 않는 부분을 잘 나눠놓는다면, 대부분의 렌더링 횟수 문제는 메모이제이션 없이 해결이된다.
리렌더링 횟수의 최소화 말고도, 값을 메모이제이션해주는 useMemo
hook을 이용하면 불필요한 연산의 횟수를 줄일 수 있다.
만약 값을 구하는데 20초가 걸리는 연산작업이 있다고 하자.
메모이제이션을 하지 않는다면, 함수형 컴포넌트가 리렌더링 될 때마다, 20초의 연산작업을 매번 수행한다.
그러나 메모이제이션을 해둔다면, dependency가 바뀔때만 연산을 수행한다.