IT/React & Next.js

[React] 리액트 최적화하기

땅일단 2024. 9. 29. 02:44
 

React 참조 개요 – React

The library for web and native user interfaces

ko.react.dev

가장 큰 팁: 공식 문서를 잘 읽자.

 

1. useMemo

공식 문서 설명

  • 재렌더링 사이에 계산 결과를 캐싱할 수 있게 해주는 React Hook 입니다.
  • 최적화를 위해서만 사용해야 합니다.
  • 일반적으로 React는 재렌더링 시에 모든 본문(함수 및 변수)을 다시 실행하지만, useMemo로 감싼 함수는 의존성 배열의 값이 변경되었을 때만 다시 연산하도록 합니다.

 

useEffect는 useMemo와 달리...

  • 최적화가 아닌 '동작' 을 위해 사용
  • 렌더링 중이 아닌 렌더링 이후에 작업을 수행함

 

사용 예시

  • 연산하는 데 오랜 시간이 걸리는 함수(ex: 긴 배열을 필터링할 때)에 유용하게 사용됨. 의존성 배열의 값이 변경되지 않았다면 이전 계산값을 그대로 쓰기 때문.
const slowFunc = useMemo(() => {
    ...
}, [targetNum]);
  • useContext를 사용할 때, 값이 변경되지 않았더라도 context를 정의한 상위 컴포넌트가 재렌더링 될 경우 context를 전달받은 하위 컴포넌트가 재렌더링되는데, 이때 상위 컴포넌트에서 useMemo로 감싸면 재렌더링되지 않는다.
const contextValue = useMemo(() => ({
    userId
}), [userId]);
  • React.memo를 통한 재렌더링 방지가 작동하지 않는 상황 (새로 만드는 객체나 배열을 props로 전달할 때, 값이 같아도 메모리값이 다르므로 변경된 것으로 간주하고 재렌더링함) 에서 useMemo 이용
export default function TodoList({ todos, tab, theme }) {
    // filterTodos() 는 계속해서 메모리값이 변경되는 배열을 리턴.
    // todos, tab이 변하지 않는 이상 캐싱된 filterTodos를 사용하여 재렌더링을 방지함.
    const visibleTodos = useMemo(() => 
        filterTodos(todos, tab),
        [todos, tab]
    );
  
    return (
      <div className={theme}>
          <List items={visibleTodos} />
      </div>
    );
}

 

 

 

2. useCallback

공식 문서 설명

  • 재렌더링 간에 함수 정의를 캐싱해 주는 React Hook입니다.
  • 최적화를 위해서만 사용해야 합니다.
  • useMemo와의 차이점은, useMemo는 호출한 함수의 결과값을 캐싱하지만 useCallback은 함수 자체를 캐싱합니다.

 

사용 예시

사실상 아래 두 가지의 상황을 제외하고는 그닥 쓸 필요가 없다고 함

왜냐하면, 일반 함수는 보통 다시 재정의된다고 해도 자원낭비가 심하지 않기 때문. (굳이 캐싱할 필요가 없음)

  • 커스텀 훅을 만들 때, 반환하는 모든 함수를 useCallback으로 감싸는 것이 권장됨
const goBack = useCallback(() => {
    dispatch({ type: "back" });
}, [dispatch]);
  • React.memo로 감싸진 컴포넌트props로 함수를 넘길 때 (함수가 캐싱되지 않으면 재렌더링 될 때마다 하위 컴포넌트가 재렌더링되기 때문)
function Report({ item }) {
    const handleClick = useCallback(() => {
        sendReport(item)
    }, [item]);

    return (
      <figure>
          <Chart onClick={handleClick} />
      </figure>
    );
}

 

 

 

3. useRef

공식 문서 설명

  • 렌더링에 필요하지 않은 값을 참조할 수 있는 React Hook입니다.
  • useRef(initialValue) 의 형태로 사용합니다.
  • ref.current 프로퍼티는 state와 달리 변이될 수 있지만, 변이되었을 때 컴포넌트가 재렌더링되지는 않습니다.
  • 렌더링에 사용되는 객체는 변이되어서는 안 되며, 렌더링 중에 ref.current를 읽거나 쓰면 안 됩니다.

 

사용 예시

  • 일반적으로 DOM을 조작할 때 사용
export default function Form() {
    const inputRef = useRef(null);

    function handleClick() {
        inputRef.current.focus();
    }
    
    return (
    <>
        <input ref={inputRef} />
        <button onClick={handleClick}>
            Input 태그에 포커스하기
        </button>
    </>
  );
}
  • 렌더링이 일어날 때마다 컨텐츠가 재생성되는 것을 방지할 때 사용
function Video() {
    const playerRef = useRef(null);
    
    if (playerRef.current === null) {
        playerRef.current = new VideoPlayer();
    }
}

 

 

 

4. React.memo

공식 문서 설명

  • memo를 사용하면 컴포넌트의 props가 변경되지 않은 경우 재렌더링을 건너뛸 수 있습니다.
  • 컴포넌트를 memo로 감싸면 해당 컴포넌트의 memoized 버전을 얻을 수 있습니다.

 

사용 예시

// Greeting.jsx

const Greeting = memo(function Greeting({ name }) {
      return <h1>Hello, {name}!</h1>;
});

export default Greeting;
// MyApp.jsx

export default function MyApp() {
    const [name, setName] = useState('');
    
    return (
        <>
            <Greeting name={name} />
        </>
    );
}

 

 

 

5. 배열 렌더링 시 적절한 key값 설정

 

리스트 렌더링 – React

The library for web and native user interfaces

ko.react.dev

key는 배열의 각 항목을 식별하는 값으로, 리액트는 이를 통해 배열의 추가, 삭제, 변경 등을 파악한다.

각 배열 항목에서 고유하게 식별할 수 있는 문자열 또는 숫자를 key로 지정해야 한다. (ex: seq)

 

흔히 index를 key로 많이 사용하지만, 항목이 추가되거나 삭제되면 해당 요소뿐만이 아닌 다른 요소의 index 값들도 변경되어 새로운 값으로 인식하기에 불필요한 재렌더링을 발생시킨다.

 

 

 

6. 필요할 때 컴포넌트를 렌더링하여 초기 로딩 속도 향상

1) React.lazy

공식 문서 설명

lazy는 로딩 중인 컴포넌트 코드가 처음으로 렌더링 될 때까지 연기할 수 있습니다.

 

2) Suspense

공식 문서 설명

<Suspense> 는 자식 요소가 로드되기 전까지 화면에 대체 UI를 보여줍니다.

 

lazy, Suspense 적용 예시

// Loading.jsx

export default function Loading() {
    return <p><i>Loading...</i></p>;
}
// App.jsx

import { useState, Suspense, lazy } from "react";

const MarkdownPreview = lazy(() => delayForDemo(import('./MarkdownPreview.js')));

export default function MarkdownEditor() {
  const [showPreview, setShowPreview] = useState(false);
  const [markdown, setMarkdown] = useState("Hello, **world**!");
  
  return (
    <>
      {showPreview && (
        <Suspense fallback={<Loading />}>
          <h2>Preview</h2>
          <MarkdownPreview markdown={markdown} />
        </Suspense>
      )}
    </>
  );
}

 

3) Webpack의 Code Splitting

코드를 청크로 나누어 필요한 부분만 로딩하는 방식. 사실 React가 아니더라도 Webpack을 쓰는 프로젝트라면 가능.

기본적으론 entry를 이용하여 분할한다.

자세한 방법은 아래 링크 참고

 

Code Splitting | 웹팩

웹팩은 모듈 번들러입니다. 주요 목적은 브라우저에서 사용할 수 있도록 JavaScript 파일을 번들로 묶는 것이지만, 리소스나 애셋을 변환하고 번들링 또는 패키징할 수도 있습니다.

webpack.kr

 

 

 

7. 불변성 유지

불변성이 제대로 유지되지 않으면 필요한 상황에 렌더링이 일어나지 않거나, 불필요하게 발생할 수 있음

 

[React] 불변성과 immer 라이브러리

(이번 포스팅 예시코드는 TypeScript로 작성함) React에서 상태를 업데이트할 때, 기존의 값을 유지하면서 상태값을 변경하는 것은 중요한데, 이를 불변성(Immutability)이라고 한다. const [nums, setNums] =

doringri.tistory.com

이전 포스팅 참고

 

 

 

8. lodash를 이용한 Debouncing / Throttling 기법으로 비동기 작업 최적화

lodash는 여러가지 자바스크립트 유틸리티를 제공하는 라이브러리이다.

(필자는 중첩된 객체의 비교와 깊은 복사를 위해 써본 적이 있었으나, 이 밖에도 상당히 많은 유틸리티가 있다)

 

Debounce는 특정 동작이 빠른 속도로 과도하게 반복된다면 강제적으로 대기하도록 하는 것이다.

가장 흔하게 쓰는 부분은 검색창에 사용자가 타이핑을 하면 API 요청 등 이벤트를 실행시키는 부분이다.

유의할 점은, setState 등으로 리렌더링되어 debounce 함수가 초기화될 가능성이 있다면 useCallback으로 감싸야 한다.

import _ from "lodash";

const handleSearch = useCallback(
    _.debounce((query) => {
      // 동작하는 부분
    }, 500),
    []
);

 

Throttling은 동작 감지가 일정 시간 안에 여러 번 일어나는 경우 한 번만 일어난 것으로 간주하는 것이다.

스크롤 이벤트나 마우스 움직임 감지 이벤트, 창 크기 조절 이벤트에서 사용된다.

import _ from "lodash";

const handleScroll = useCallback(
    _.throttle(() => {
        setScrollPosition(window.scrollY);
    }, 1000), // 1초에 한 번만 호출
    []
);

 

 

 

9. 성능 검사 툴 사용

React Profiler 크롬 확장 프로그램을 설치하여 리액트의 성능을 측정할 수 있다.

특히 Memoization을 사용했다면 이 프로그램으로 검증하는 것이 권장된다.

 

React Developer Tools - Chrome 웹 스토어

Adds React debugging tools to the Chrome Developer Tools. Created from revision ccb20cb88b on 7/3/2024.

chromewebstore.google.com

설치하면, 개발자 도구에 Profiler라는 탭이 생긴다.

 

Profiler에 대한 자세한 설명은 아래 링크를 참조한다.

 

Introducing the React Profiler – React Blog

React 16.5 adds support for a new DevTools profiler plugin. This plugin uses React’s experimental Profiler API to collect timing information about each component that’s rendered in order to identify performance bottlenecks in React applications. It wil

legacy.reactjs.org

 

 

 

10. 기타

- CSS-in-JS 최적화

- state를 되도록이면 상위 컴포넌트보다는 하위에 유지시켜 전체가 렌더링되는 것을 방지

- state를 가능한 해당 컴포넌트에 유지시켜 불필요한 렌더링 방지