본문 바로가기
공식문서/리액트

리액트의 작동 원리 와 최적화 정리

by Integer Essence 2023. 7. 11.

 

 

최적화 실패시 시나리오 

 

 

# 리액트 랜더링 작동원리?

 

리액트에서 스냅샷으로 가상돔 비교 => 다른게 있다면 랜더링 

 

실제 돔은 리액트가 구성한 이전 상태와 차이점을 기반으로 변경이 필요할때만 업데이트 되기때문에 필요한 경우에만 변경이 된다. 

 

이는 성능 측면에서 아주 중요한데,  이전과 현재 상태를 가상으로 비교한다는것은 메모리안에서만 발생하기때문에 간편하고 자원도 적게 든다. 

 

 

# 리액트 컴포넌트 작동원리 ? 

state 나 props 가 변경 될때마다 함수 전체가 재생성됨. 

 

기본적으로 리액트 컴포넌트는 함수로 생성되는 형태이기때문에 마찬가지로 안에서 선언된 모든 함수와 자식 컴포넌트도 재평가 재실행된다. (물론 클래스도 되지만 이 글에선 함수형 을 전제로 함)

 

 

# 리랜더링 !== 재평가 

공부하다가 새롭게 알게 된것은 리랜더링 === 재평가 가 아니라는것이다.

나는 컴포넌트가 재평가, 재실행되면 리랜더링이 무조건 일어난다고 생각했으나 실제 돔에서 리랜더링 되는건 가상돔에서 비교해서 바뀐 부분만 리랜더링 되는것이고 재평가 재실행은 말 그대로 함수의 재평가 재실행이였을뿐 당연하다면 당연한것이지만. 당연히 재평가되면 재실행된다고 생각했다.

 

리랜더링을 일으킬려면 재실행 재평가가 되어야되는건 맞다. 

 

이 차이를 인지하고 있는 것이 나중에 어느 부분을 최적화 시킬것인지 온전히 스스로 탐구하는데에 도움이 될거라생각해서 적어둠 

 

재평가!== 리랜더링의 예시는 다음과 같다. 

 

- 부모 컴포넌트 

import React, { useState } from 'react';

import Button from './components/Button';
import Output from './components/Output';

function App() {
  const [showP, setShowP] = useState(false);
 
  console.log('확인');
  const toggle =() => setShowP((prev) => !prev);

  return (
    <div className="app">
      <Output show={false}></Output>
      <Button onClick={toggle}>토클버튼</Button>
    </div>
  );
}

export default App;

 

- 자식 컴포넌트 

import React from 'react';

const Output = (props) => {
  console.log('자식');
  return <p>{props.show ? '트루' : ''}</p>;
};

export default Output;

 

 

버튼을 누르면 showP의 상태값이 토글이 된다. 

 

true일 경우에만 자식 컴포넌트에서 트루라는 글자가 새롭게 리랜더링 된다. 

 

 

각컴포넌트에는 확인 용 콘솔을 하나씩 적어놨으니 처음 나들어질때  '확인' 과 '자식' 이 콘솔에 하나씩 찍힌다.

 

그리고 보시다시피 Output  자식컴포넌트에는 show 가 스테이트가 아니라 하드코딩으로 boolean을 넣게 될 경우. 

 

토글을 누르면 state 가 변경되었음으로 콘솔의 '확인' 과 '자식' 역시  재실행된다.

 

하지만 false가 하드코딩 되어있음으로 돔상에서 변하게 없기 때문에 리랜더링이 다시 일어나는 것은 아니다.

 

 

 

 

#최적화 (useCallback, memo, useMemo )

 

그리하여 리액트의 최적화라고 하면 불필요한 재평가 재실행 리렌더링 을 막는것이 되겠다.

 

- memo 

 

컴포넌트의 export default 부분에 React.memo로 감싸주게 되면 사용할 수 있다. 

 

혹은 공식문서 처럼 

이렇게도 사용가능 (react 공식문서 예제)

 

export default React.memo(Output) 

 

그러면 기존의 props와 비교하여 변경되면 재평가를 실행하게된다. 

 

 이걸로 재평가!== 리랜더링의 예시 의 Output에서 '자식' 이라는 콘솔이 찍히는게 막히는것을 볼 수있다. 

 

- memo로 최적화가 되면 왜 모든 컴포넌트에 적용을 미리 안해놓는가? 

 

최적화 비용 때문.

 

 

memo는 props 변경이 생길때마다 기존값과 새로운 값을 비교하기위해 값을 각각 저장하기때문에 각각의 개별적인 성능 비용이 발생한다. 

 

즉 memo를 사용한다는 것은 재평가 비용과 props 비교 성능평가를 바꾸는 것이다. 어느쪽이 더 낫다고 말하기 힘들기에 규모에 따라서 최적화 필요성이 갈린다. 

 

 

- useCallback (feat 클로저 , 자료형 )

 

memo 만으로는 재평가 재실행을 완벽하게 막았다고 볼 수 없다.

 

아까의 동일 한 부모 컴포넌트에서 Button 컴포넌트도 memo로 감싸주고 console.log('버튼')을 만들어 테스트해보면

 

import React from 'react';



const Button = (props) => {
  console.log('버튼');
  return (
    <button
      onClick={props.onClick}
      disabled={props.disabled}
    >
      {props.children}
    </button>
  );
};

export default React.memo(Button);

 

여전히 계속해서 버튼을 누를때마다 재평가 재실행이 되는것을 볼 수있다.

 

 

이것의 이유는 자바스크립트 의 원시값과 객체에 대한 차이 때문. 

false의 경우는 원시라 false===false로 비교가되지만.

재평가 재실행 되서 만들어진 같은 기능 의 새로운 toggle 함수 라서 그렇다.

 

엄밀히 말하면 false도 새로운 false 지만 원시타입이라 상관없고 toggle은 객체라 엄격일치로 비교해도 다른 메모리로 fasle 가 뜬다. 

 

이걸 막아주는게 useCallback

 

useCallback을 쓰게 되면 함수가 캐싱 된다.

 

let obj1={} 

let obj2={}  

obj1===obj2 // false 이지만 

obj1 = obj2 ,  obj1===obj2 // true 로  비교하는것과 비슷하다. 

 

참고로 해당 자식 컴포넌트에 memo 씌우고  해당 함수에 useCallback도 씌워야된다  필자가 바보같이 useCallback만 했다가 왜안되?? 하고있었음 

 

-  useCallback 의존성 배열 

 

useCallback의 두번째 인자로는 useEffect와 동일하게 의존성 배열이 들어간다.

 

이에 대한 이해는 컴포넌트안에서 선언된 함수가 곧 내부함수로 자바스크립트에서 클로저라는것 에서 출발한다.

컴포넌트 = 함수 

컴포넌트안의 함수 => 함수 안에 함수 = 클로저 

 

클로저의 특징 ? => 내부함수 밖의 변수에 접근 가능 (이걸로 자바스크립트에서 private 변수 생성 가능)

 

function App() {
  const [showP, setShowP] = useState(false);
  const [istrue, setIstrue] = useState(false);
  console.log('확인');
  
  const toggle = useCallback(() => {
    if (istrue) {
      console.log('변경');
      setShowP((prev) => !prev);
    }
  }, []);

  const changeIstrue = () => {
    setIstrue(true);
  };
  return (
    <div className="app">
      <Output show={showP}></Output>
      <Button onClick={toggle}>토클버튼</Button>
      <Button onClick={changeIstrue}>isTrue버튼</Button>
    </div>
  );
}

export default App;

다음과 같이 isTrue여야 토글이 실행되게 바꾼 상태에서 useCallback 의존생 배열을 비워둔다면? 

=> 정상 작동 하지않는다.

 

왜냐면 toggle은 캐쉬된 상태로 외부 변수로 가져온 isTrue의값은 false로 고정되어있기때문이다. 

 

의존성 배열에 istrue를 넣어줌으로써  istrue의 값을 신선하게 바꿀 수 있다. 

 

 

#useMemo 

기본적으로 useCallback과 사용방법은 똑같다. 함수를 감싸주며 의존성 배열도 있다.

 

공식문서의 예제로는 props로 받은값을 필터링 하여 필터링 된 값을 리턴하는 함수를 감싸주고 해당 값에 변경이있을때 변경되도록 의존성 배열을 채워주었다.

 

visibleTodos가 뭘 반환하는지 는 이 것만으로는 명시되어있지않지만 배열이라 치고 

 

{visibleTodos.map(())}으로 활용가능 

 

무거운 계산 작업에 사용하면 좋다

 

# useMemo와 useCallback의 차이 

 

공식 문서 설명

 

useMemo : useMemo is a React Hook that lets you cache the result of a calculation between re-renders.

 

useCallback : useCallback is a React Hook that lets you cache a function definition between re-renders.

 

계산된 값을 캐시한다 = > useMemo

정의된 함수를 캐시한다 => useCallback 

 

값을 사용한다 => useMemo 

함수를 사용한다 => useCallback 

 

 

 

 

 

#정리

useMemo 나 memo , useCallback을 사용한다는 것은 처음 말했듯 비용이 발생하며  언제 어디서 사용하는 것인지 차이를 정확히 알고있어야  제대로된 성능 최적화가 가능하다. 

 

 

 

 

- 공부하고 이해한대로 정리했음으로 표현이 미숙하거나 이건 좀 세세하게 보면 아닌 표현인것 같다고 생각되는 부분이 있을 수있습니다. 

 

 

 


 

참고자료 

-  mdn 
js 자료구조 
https://developer.mozilla.org/ko/docs/Web/JavaScript/Data_structures

js 클로저 
https://developer.mozilla.org/ko/docs/Web/JavaScript/Closures

- react 공식문서 

useCallback 
https://react.dev/reference/react/useCallback#i-need-to-call-usememo-for-each-list-item-in-a-loop-but-its-not-allowed

memo
https://react.dev/reference/react/memo#memo

useMemo
https://react.dev/reference/react/useMemo