React-hook Memoization을 통한 렌더링 성능올리기

신동민·2022년 3월 27일
0

react

목록 보기
1/1
post-thumbnail

잘하는 엔지니어가 되고 싶습니다!! 그러한 엔지니어가 되려면 정말 많은 능력이 필요할 것 같습니다!

뷰적으로는, 디자이너의 시안을 그대로 구현할 수 있는 능력, 더 나아가 디자이너와 함께 UX를 고민하고 서비스가 원하는 방향으로 유저들을 이끌어줄 수 있는 능력이 필요한 것 같고..

기능적으로는, 서버에서 받은 데이터를 빠르고 효율적이게 보여주는.. 여기서 효율적이라는 말은 최소한의 렌더링, 적절한 메모리 사용을 통한 빠른 반응이 중요하다고 생각합니다.

이번편에서는, 기능적인 관점에서 어떻게 하면 효율적인 렌더링을 할 수 있을까에 대한 요인들을 살펴보고자 합니다. 그 중에서도, hook을 사용하면서 Memoization(Memorization으로 헷갈리면 안돼요)을 바탕으로 최적화를 위해 사용하는 React.memo , React.useCallback, React.useMemo을 사용하면서 주의해야할 점을 살펴보겠습니다.

Why memoization

What is Memoization?
이전 값을 메모리에 저장해 동일한 계산의 반복을 제거하여 빠른 처리를 가능하게 하는 기술

한 페이지에 버튼이 두 개 이상있는 화면은 굉장히 흔합니다! 간단히 아래와 같이 두 개의 버튼이 있는 counting 코드를 작성해보았습니다.

위 코드의 문제점을 뜯어보기 전에, 우리가 원하는 flow를 생각해봅시다.
버튼1을 클릭하면, 그에 해당하는 count state가 바뀌고, 그것을 뷰에 보여주기 위해 버튼1을 리렌더링합니다.
제대로 flow대로 되는지 console창을 열어 확인해봅시다! 버튼1을 클릭하면 console에 버튼1 만 render되었다고 나오나요? 반대로 버튼2를 클릭하면 버튼2만 render되었다고 나오나요?

아니죠! 1,2가 동시에 나오는걸요! 버튼1을 눌렀는데 속성이 바뀌지 않는 버튼2도 다시 렌더링 되는 것을 볼 수 있습니다. 불필요한 리랜더링이 발생하군요!
그래서 React에서는 렌더링 최적화를 위하여 React.memo 를 제공합니다. 위와 같은 상황에서 컴포넌트의 렌더링한 결과를 저장하여, props가 바뀌지 않았으면 리렌더링을 하지 않고 기존 것을 보여주는 것이죠! 그럼 한 번 해볼까요? 잘되나요?

실패... 왜 실패했을까요.. 버튼1을 누를 때 버튼2에 대한 props가 바뀐 것이 있나...?
있습니다! 바로 onClickBtn 함수입니다. 버튼1을 눌러 App컴포넌트가 리랜더링이 되면, onClickBtn1뿐만 아니라, onClickBtn2가 다시 정의되면서 새로운 메모리로 할당됩니다. (기존함수는 garbage collection) 즉, 함수는 동일하지만 메모리에 새로 할당되어 주소가 바뀌었기때문에 CountBtn에서 props를 확인할 때 새로운 함수가 들어왔다고 판단을 내려 버튼2도 재랜더링 하는 것입니다! 그러면 이제 우리가 처리해줘야할 것은 onClickBtn함수가 재정의되는 것을 막으면 되겠네요!

React는 이것을 React.useCallback이라는 것으로 해결하게해줍니다. onClickBtn함수에 useCallback으로 감싸, deps(dependency lists)에 변화가 있을 때만 재정의하라~ 라는 의미입니다. 그럼 한 번 해볼까요?

deps에 아무것도 넣지 않을 때와 해당하는 count state를 넣을 때 둘 다 해보세요. 재밌는 현상이 나옵니다.

성공하셨나요? deps에 아무것도 넣지 않을 때는 어떤가요?
이에 대한 해석은 여러분들에게 맡기겠습니다 :)

그럼 항상 Memoization을 사용해야 성능이 좋은가요?

결론부터 말하자면, Memoization을 사용하면 그만큼 라인에 추가되는 코드로 인한 비용이 들기 때문에 남용해서는 안됩니다.
간단하게 onClickBtn함수를 다시 살펴봅시다.

// Memoization 사용 전
const onClickBtn1 = () => setCount1(count1 + 1);

// Memoization 사용 후
const onClickBtn1 = () => setCount1(count1 + 1);
const onClickBtnCallback = useCallback(onClickBtn1,[count1]);

코드만 봐도 알겠지만 Memoization을 사용하게 되면, 사용하기 전인 onClickBtn1만 사용하는 것보다 더 많은 일을 합니다. useCallback에 대한 추가적인 처리, deps([count1])에 대한 정의 등으로 인하여 메모리에 추가적인 함수를 정의하게 되기 때문이죠.

더 나아가, 새로 렌더링이 될 때, Memoization을 사용하기전에는 기존 함수는 garbage collection으로 처리가 되면서 새로운 함수가 정의되지만, Memoization을 사용하면 새로운 함수가 생성되는 것은 동일하되, 기존 함수를 그대로 사용하기 위해 garbage collection이 처리되지 않기 때문에 메모리 사용 측면에서 비효율적입니다. 그리고 deps가 바뀌었는지 체크하기 위해서 deps 배열안의 값들을 계속 가지고 있어야하기까지 합니다. 또한 새로 프로젝트를 보는 개발자의 입장에서 진입장벽이 하나 더 생기는 것이고, deps를 잘못설정하는 문제로 유지보수가 더 어려워질 수 있습니다.

성능 개선을 위해 추가적인 리소스가 들기 때문에 공짜라고 생각하면 절대 안됩니다. 즉, 최적화작업을 할 때는 최적화를 했을 때 나오는 성능개선과 그것을 적용시키기 위해 필요한 리소스 사이의 trade-off를 잘 따져 판단해야합니다.

그럼 언제 Memoization을 사용해야 할까요?

  • 3D, 실시간 data graph, chart 등 극단적인 렌더링 퍼포먼스가 필요할 때
  • UI element 양이 많은 컴포넌트일 때
  • Pure Functional Component에서 같은 props가 전달됨에도 불구하고 불필요한 리렌더링이 일어날 때
  • 복잡한 계산과정(피보나치 계산, 소수 계산 등)이 빈번히 일어날 때 (React.useMemo : useCallback이 함수를 재활용한다면, useMemo는 return value를 재활용)

일단 개발자 협업에 있어서 구현을 해놓고, 느린 지점이 명확하게 밝혀지면 어디가 느리고 그 느린속도는 어디서 초래하는지를 파악한 후 최적화 작업을 했을 때 얼마만큼의 속도개선이 되는지에 대한 분석이 완료되었을 때 하는 것이 성능 최적화라고 합니다. 사용자가 느끼기에 똑같은 뷰를 최적화 하는데에 시간을 쏟는 것은 리소스 낭비이고 자기만족일 수 있다는 생각이 들기도 합니다.

여러 글을 읽어보니 사실 React framework 자체에서 최적화를 많이 녹여내서 뷰적으로 극단적인 delay가 있는 것이 아니면 framework 바깥에서의 최적화가 큰 의미가 없다고 합니다.( 직접 react 내부 소스를 분석할 거 아니면.. )
오히려 이렇게 React가 발전하면서 여러 지원기술들이 생겼는데, 이 각각의 기술들을 완벽히 이해하고 적재적소에 쓰임에 맞게 사용하는 것이 더 중요한 문제라고 판단되는 시대인 것 같습니다.

참고문헌

https://student513.tistory.com/76
https://ssangq.netlify.app/posts/react-memo-useMemo-useCallback
https://zereight.tistory.com/960
https://kentcdodds.com/blog/usememo-and-usecallback
https://stackoverflow.com/questions/51038490/reactjs-counter-of-renders
https://www.debugcn.com/ko/article/53490407.html
https://k-developer.gitbook.io/dev/react/react-hook/usecallback

profile
세상에 존재하는 수 많은 비즈니스 문제를 IT 서비스로 풀어내고 있습니다.

0개의 댓글