React.js 리액트 최적화(useMemo, useCallback)

강정우·2023년 1월 6일
0

react.js

목록 보기
20/45
post-thumbnail

리액트가 작동하는 방식

  • 리액트는 상태나 props, 컨텍스트, 컴포넌트에 변경이 발생하면 컴포넌트 함수가 재실행되어 리액트라 이를 재평가한다.
    하지만 이 재평가가 DOM을 다시 랜더링하는 것은 아님을 알아두어야 한다. 리액트에 의해 컴포넌트 함수가 재실행된다고 해서 실제 DOM의 각 부분들이 다시 랜더링된다던가 재평가되는 것은 아니다.

  • 즉, 우리는 컴포넌트 부분과 리액트 부분, 그리고 실제 DOM을 구분할 줄 알아야 한다.
    앞서 언급했듯 컴포넌트는 상태, props, 컨덱스트가 변경될 때 재평가된다.
    이러면 리액트는 컴포넌트 함수를 다시 실행한다.

  • 한편, 이에 반해 실제 DOM은 리액트가 구성한 컴포넌트의 이전 상태와 트리, 그리고 현재의 상태간의 차이점을 기반으로 변경이 필요할 때만 업데이트된다.

  • 즉, 실제 DOM은 필요한 경우에만 변경된다는 뜻이다.
    이 것은 성능 측면에서 매우 중요한데 이전과 현재의 상태를 가상으로 비교한다는 것은 간편하고, 작업이 메모리 안에서만 발생하기 때문에 자원도 적게 들기 때문이다.

  • 리액트는 가상 DOM을 통해 2개의 스뱁샷 간의 차이점을 알아내고 최종 스냅샷과 현재의 스냅샷을 실제 DOM에 전달하는 구조를 갖는다

  • 상태나 props, 또는 컨텍스트를 가지고 있고 이러한 것들이 변경되는 컴포넌트는 재실행, reEvaluate된다.

예시

  • 위와 같은 코드에서 child component로 보내는 prop의 값을 false라고 바꾸면 과연 어떻게 될까?
    정답은 child component도 전체 다시 재실행 된다는 것이다. 즉, 자식 컴포넌트들은 부모 컴포넌트의 일부분이기 때문에 부모 컴포넌트 함수가 재실행되면 자식 컴포넌트 함수들도 재실행된다는 것이다.

그렇다면 연결된 모든 컴포넌트 함수가 재실행되면 굉장히 많은 함수가 가상 비교가 된다는 것인데 성능에 영향을 미치지는 않을까?

  • 지금은 차이가 안 보이지만 프로젝트가 커지면 모든 자식 요소들이 재평가되기 때문에 무시할 수 없다.

React.memo()

  • React.memo는 인자로 들어간 해당 인수 컴포넌트에 어떤 props가 입력되는지 확인하고 입력되는 모든 props의 신규 값을 확인한 뒤 이를 기존의 props의 값과 비교하도록 리액트에게 전달한다.
    그리고 props의 값이 바뀐 경우에만 컴포넌트를 재실행 및 재평가하게 된다. 그리고 부모 컴포넌트가 변경되었지만 그 컴포넌트의 props 값이 바뀌지 않았다면 컴포넌트 실행은 건너뛰게 된다.

  • 함수형 컴포넌트만 최적화할 수 있다.

  • 즉 children2.js에 React.memo 함수를 걸어놓으면 children1이 변경되지 않는다면 children2.js의 재평가가 이루어지지 않는다는 듯이다. 약간 가지를 잘랐다고 표현해도 좋겠다.

  • 이렇게, 불필요한 재렌더링을 피하기 위해서 최적화가 이루어지고 있다.
    그렇다면 왜 이걸 모든 컴포넌트에 이렇게 최적화를 적용하지 않는 걸까?

  • 최적화에는 비용이 따르기 때문이다.
    무슨 비용이냐면 memo()를 실행할 때 리액트가 두 가지 작업을 실행한다.

  1. 기존의 props 값을 저장할 공간과 할당하는 작업
  2. 비교하는 작업
    이 각각의 작업은 개별적인 성능 비용이 필요하다
  • 따라서, 이 React.memo()의 성능 효율은 어떤 컴포넌트를 최적화하느냐에 따라 달라지게 된다.
    컴포넌트를 재평가하는 데에 필요한 성능 비용 vs props를 비교하는 성능 비용
    을 서로 맞바꾸는 것이다.

  • 그리고 이는 props의 개수컴포넌트의 복잡도, 그리고 자식 컴포넌트의 숫자에 따라 달라지므로
    또 어느 쪽의 비용이 더 높다고 딱 잘라 말하는 것은 불가능하다.

  • 그러니까 만약에 컴포넌트 트리의 상위에 위치해있다면 전체 컴포넌트 트리에 대한 쓸데없는 재렌더링을 막을 수 있으니까 좋겠지?

  • 이와는 반대로 부모 컴포넌트를 매 번 재평가할 때마다 컴포넌트의 변화가 있거나 props의 값이 변화할 수 있는 경우라면 이 React.memo는 크게 의미가 없다.
    왜냐하면 컴포넌트의 재렌더링이 어떻게든 필요하기 때문이다.

  • 이럴 때는 props 값의 추가적인 비교에 대한 비용을 아낄 수 있겠지만 오버헤드 코스트고 이는 가치가 없다.

  • 앱 크기React.memo()를 사용할지 말지 결정할 중요한요소이다.
    매우 작은 앱, 매우 작은 컴포넌트 트리의 경우에는 이런 과정을 추가하는 것이 필요가 없다.
    하지만 불필요한 재평가를 잘라내버릴 수 있는 큰 규모의 앱이라면 그럴 만한 가치가 있다.

사실 위 사진의 최상단 네모를 App.js라고 보았을 대 2번째 줄의 빨강 동그라미는 옳지 않는 표시이다. 왜일까?

  • 저 App 컴포넌트는 어쨌건 함수이기 때문에 마치 일반적인 자바스크립트 함수처럼 재실행된다.
    왜냐하면, 결국 이 것은 상태가 바뀌게 되면 일반적인 자바 함수와 같으니까.

  • 여기서 우리가 인지해야할 것은 이 함수는 사용자가 아닌 리액트에 의해 호출된다는 것이다.
    즉, 모든 코드가 다시 실행된다는 의미이다.

  • 우리가 상수로 설정했기에 App 함수의 모든 렌더링, 또는 모든 실행 사이클에서 완전히 새로운 함수이다. 여기에 있는 모든 코드가 다시 실행되므로 당연히, 새로운 함수가 만들어진다.

  • 자 여기서 React.memo()<사용자지정컴포넌트 props={proptype}/>에 의하여 실행될지 말지 결정되는 것이다.
    JAVA를 공부했다면 금방 이해할 수 있는 개념인데
    primitive 와 reference의 차이이다.

  • 그렇다면 reference type을 가진 props이 붙은 사용자지정 컴포넌트라면 무조껀 재평가가 될수밖에 없을까? 다행히 그렇지는 않다.

useCallback()

  • useCallback 훅은 기본적으로 컴포넌트 실행 전반에 걸쳐 함수를 저장할 수 있게 하는 훅이다.

  • 리액트 메모리에 원하는 함수를 저장하고 매번 실행때마다 이 함수를 재생성할 필요가 없다는 것을 알려준다. 이렇게 되면 동일한 함수 객체가 메모리의 동일한 위치에 저장되므로 이를 통해 비교 작업을 할 수 있다.

우리가 선택한 함수를 리액트의 내부 저장 공간에 저장해서 함수 객체가 실행될 때마다 이를 재사용할 수 있게 해준다는 뜻이다.

  • 그렇다면 dependency는 useEffect 와 마찬가지로 useCallback함수 내의 요소로 선언한다.
    즉, props에서 가져온 값이 있다면 최신값을 참조하기위해 넣어줘야한다.

closure를 이용하여 callback함수 응용

  • 혹시나 closure가 헷갈린다면 ㄱㄱ
    매우 1차원적으로 설명하자면 함수 내의 함수가 2중, 3중으로 있을 때 가장 내부에 있는 함수에서 부모함수 props에 접근하여 값을 가져올 수 있다는 것이다.
  • 토글단락트리거 함수에서 closure를 이용하여 해당 블록함수 밖, allotToggle값에 접근하였고, 이 값이 변해야만 react에게 해당 블록함수를 다시 정의하겠다고 알려준 것이다.
profile
智(지)! 德(덕)! 體(체)!

0개의 댓글