react 어플리케이션 성능 최적화

2025. 4. 24. 14:15Coding Study/React

1. useMeno

  호출 결과를 메모이제이션(저장) 하는 Hook

  컴포넌트가 리렌더링될 때, 특정 함수의 계산결과가 이전과 동일하다면 굳이 다시 계산하지 않고

  저장된 결과를 재사용한다.

 

const memoizedValue = useMemo(() => {
                // 복잡한 계산 식 
    },[의존성배열] );

 

  • 첫 번째 인자: 값을 계산하고 반환하는 함수.
  • 두 번째 인자 (의존성 배열): 이 배열 안의 값이 변경될 때만 첫 번째 인자의 함수가 다시 실행된다. 배열이 비어있으면([]) 컴포넌트가 처음 렌더링될 때만 함수가 실행됩니다.

2. useCallback

  함수 자체를 메모이제이션 하는 hook

  함수형 컴포넌트는 리렌더링될 때 마다 내부의 함수도 새로 생성된다. 

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);    // 함수를 저장
  },
  [a, b],                // 의존성 배열로 a,b 가 바껴야 함수 새로 생성
);

 

 

 

3. memo

  컴포넌트 자체를 메모이제이션 하는 고차 컴포넌트 이다. 

  memo 로 감싸진 컴포넌트는 전달받는 props 가 변경 되지 않았다면, 리렌더링 되지 않는다. 

 

const MemoizedComponent = memo((props) => {
           /* 렌더링 컴포넌트 */
 }
);

 

 

memo 및 useCallback 조합으로 리렌더링 방지 

 만약에 List 컴포넌트에만 memo 로 컴포넌트 리렌더링을 방지하려고 했다면 전달받은 props 가 배열의 형태이기 때문에 

 주소값이 바뀌면 다시 리렌더링이 되어서 memo의 효과가  없어진다.

 

 하지만 부모 컴포넌트에서 useCallback 과 같이 사용 하게 되면 함수자체가 재생성되지 않아

  todo 안의 객체는 주소값이 그대로이기 때문에  Props 로 전달받는 el 과 updateTodo , deleteTodo 의

  주소값도 동일하여 결과적으로는 List 컴포넌트는 리렌더링 되지 않는다. 

export default function Todo() {
  const [todo, setTodo] = useState([
    { id: 1, content: "밥먹기" },
    { id: 2, content: "공부하기" },
  ]);

  const inputRef = useRef(null);

  const addTodo = useCallback(() => {    // 함수 재 생성 방지
    setTodo((prev) => [     // todo는 바뀌지만 함수가동일하기때문에 안쪽 객체의 주소값은 동일하다
      ...prev,
      { id: Number(new Date()), content: inputRef.current.value },
    ]);
  }, []);

  const updateTodo = useCallback((id, newContent) => {   // 함수 재 생성 방지
    setTodo((prev) =>         
      prev.map((el) => (el.id === id ? { id, content: newContent } : el))
    );
  }, []);

  const deleteTodo = useCallback((id) => {  // 함수 재 생성 방지
    setTodo((prev) => prev.filter((el) => el.id !== id));
  }, []);

  return (
    <>     
        {todo.map((el) => (
          <List
            key={el.id}
            el={el}
            updateTodo={updateTodo}
            deleteTodo={deleteTodo}
          />
        ))}     
    </>
  );
}

 

const List = memo(({ el, updateTodo, deleteTodo }) => {
  const [inputValue, setInputValue] = useState("");

  return (
            // 렌더링 컴포넌트
  );
});

 

 

4. 디바운스(Debounce)와 쓰로틀 (Throttle)

  디바운스와 쓰로틀은 모두 연속적으로 발생하는 함수나 이벤트를 묶어서 처리하는 방식

  이렇게 묶어서 처리하는 주된 이유는 최적화를 통한 성능 향상 이다

  묶어서 덩어리로 처리하면 작업 횟수를 줄여 성능을 효과적으로 개선할 수 있다.

 

 1)디바운스(Debounce)

   이벤트 발생을 지켜보다가, 마지막 이벤트가 발생한 후 특정 시간 동안 추가 이벤트가 없으면 그 마지막 이벤트를 처리한다.

   

   이벤트 중일때는 실행을 안하다가 이벤트가 종료후 일정시간 뒤에 실행이 된다.

디바운스 (이벤트 종료 후 1초뒤 검색)

<실행순서>

1. 키입력

2. setTimeout 실행 1초 후

3. 키입력 -> setTimeout 제거

4. 새로운 setTimeout 실행 1초뒤 검색

 useEffect(() => {
    const debounceTimeer = setTimeout(() => {
      const newfilteredData = data.filter((el) => el.name.match(reg));
      setFilteredData(newfilteredData);
    }, 1000);                                        // 1초후 실행
    return () => clearTimeout(debounceTimeer);       // params 가 새로 들어오면 setTimeout 제거
  }, [params]);

 

 

 

2) 쓰로틀 (Throttle)

    이벤트가 발생할 때마다 설정된 특정 텀을 기준으로 묶어서 처리합니다.

 

    이벤트 시작 후 일정 시간마다 실행 한다.

쓰로틀 (이벤트 중 일정시간마다 실행됨)

<실행순서>

1. time이 셋팅 된다.  ex> 200 ms  에 입력

2. 키입력 시 newTime 이 셋팅 된다. ex > 200 ms 에 세팅

3. setTimeout 으로 (1000 -(200-200)) = 1000ms 뒤 실행

4. 다시 키입력 ex> 500ms                ->  여기서 time은 useRef 로선언되어서 리렌더링해도 값이 고정됨  (200ms)

5. setTimeout 제거

6. newTime 에 500ms 로 세팅

7. setTimeout 으로 (1000 - (500 -200)) = 700ms 뒤 실행

 

  이런식으로 키 입력을 하여도 규칙적으로 정한 시간동안 실행이 된다. 

const time = useRef(new Date());                   //(1) 기준시간 (usRef 로 되어서 리렌더링되어도 안바뀜 )

  useEffect(() => {
    const newTime = new Date();                    //(2) 키가 입력된 시간 (키입력마다 바뀜 )
    const debounceTimeer = setTimeout(() => {
      const newfilteredData = data.filter((el) => el.name.match(reg));
      setFilteredData(newfilteredData);
      time.current = new Date();                   // setTime 이 실행되기 전까지 time 은 바뀌지 않는다
    }, 1000 - (newTime - time.current));           //(2)newTime 은 점점 커지고 키입력을해도 1초마다 실행된다.
    return () => clearTimeout(debounceTimeer);     
  }, [params]);

 

 

5. Lazy 와 suspense

  1) CSR( 클라이언트사이드렌터링) 의 한계

    리엑트는 기본적으로 클라이어트 사이트 렌더링 방식을 사용한다.

    서버에서 비어있는 HTML 파일만 보내고, 브라우저가 자바스크립트를 받아와서 화면을 구성하는 방식

    그런데 자바스크립트 파일이 크다면, 사용자는 오랫동안 빈 화면만 보게 될 수도 있기 때문에 이는 사용자

    경험을 크게 해치는 요소이다. 

 

  2) 페이지 진입 시 모든 컴포넌트를 로딩하면

   예를 들어, 사용자가 메인 페이지만 보려고 해도,
   마이페이지, 블로그, 쇼핑, 팝업 등 모든 컴포넌트를 포함한 거대한 자바스크립트 파일이 함께 로딩된다면?

   ➡️ 대부분 사용자는 평생 한 번도 들어가지 않을 페이지의 코드까지 불러오게 된다.
   이런 상황에서는 불필요한 네트워크 낭비느린 초기 로딩이 발생한다.

 

  3) 해결방법 : Lazy & Suspense

    ✅ lazy란? 

  • 컴포넌트를 처음부터 불러오는 것이 아니라,
  • 실제로 렌더링할 때 필요에 따라 동적으로 불러오는 방식

기존에는 부모 컴포넌트가 렌더링 될때 모든 자식 컴포넌트까지 모두 다운로드 했지만, Lazy 를 쓰게 되면

클릭해서 들어갈때 즉 화면에 렌더링 될 때 페이지를 다운로드 하게 된다.

 

import { lazy } from 'react';

const MainPage = lazy(() => import('./pages/MainPage'));

 

 

✅ Suspense란?

lazy로 불러오는 컴포넌트를 로딩하는 동안 보여줄 임시 UI를 설정합니다.

import { Suspense } from 'react';

<Suspense fallback={<h1>Loading...</h1>}>
  <MainPage />
</Suspense>

'Coding Study > React' 카테고리의 다른 글

Joi (유효성 검증 라이브러리)  (1) 2025.08.09
useActionstate  (0) 2025.05.15
번들링과 코트스플리팅  (1) 2025.04.24
Redux ToolKit  (0) 2025.04.22
전역상태 관리 Redux  (0) 2025.04.22