React hooks 최적화를 위한 간편 가이드 (2) - Memoization 은 꼭 필요할까?

WE SEE WHAT WE WANT·2023년 11월 12일
0

React

목록 보기
1/6
post-thumbnail

결론부터 말하자면, 모든 경우에 절대적으로 필요한 것은 아니다.

불필요한 메모아이제이션은 메모아이제이션을 아예 하지않은것만큼 나쁘다!!
(an unnecessary memoization is as bad as not memoizing at all!)


우선, Memoization이란 ?

  • 이전 포스팅에서도 언급했듯이, React 성능 최적화를 위해 사용되는 방법 중 하나
  • 이전에 계산한 결과를 저장하고 이후 동일한 입력 값에 대한 함수를 호출 할 때, 이전 결과를 반환하는 기술
  • React에서는 React.memo나 useMemo 훅과 함께 사용하여 컴포넌트 또는 값을 memoize 함.
  • 계산 비용이 높은 함수 성능을 향상시키는 데 많이 쓰임

Memoization의 동작원리

- 동작단계

  1. 함수호출 : 함수 컴포넌트가 렌더링될 때, 해당 함수가 호출
  2. 인자검사 : 이전에 호출된 함수와 현재 호출된 함수의 인자를 비교 + 동일한지 체크
  3. 캐시조회 : 이전에 계산된 결과를 저장하는 캐시를 조회
  4. 계산수행 : 캐시 조회에서 일치하는 결과가 없거나 인자가 변경되었을 경우, 함수 실행하여 계산을 수행
  5. 캐시저장 : 계산된 결과를 캐시에 저장
  6. 결과반영 : 계산된 결과를 return 함 ⇒ 화면랜더링

Memoization .. 언제써야할까?

1. 두가지 요소를 생각하자!

  1. 계산 비용이 높은 함수일 경우
  2. 컴포넌트의 불필요한 랜더링을 방지하고 싶을떄

2. 사용예시 3개

(1) 컴포넌트 렌더링 최적화

React에서 컴포넌트 렌더링 최적화를 위해 React.memo와 useMemo를 사용합니다. 이를 통해 컴포넌트가 동일한 props를 가질 때 렌더링을 방지하고 성능을 향상시킬 수 있습니다.

1. React.memo 활용 예시

  • 컴포넌트가 동일한 name prop을 받을 때에만 렌더링이 발생하고, name prop이 변경되지 않으면 렌더링을 하지 않습니다.
import React from 'react';

const MyComponent = React.memo(({ name }) => {
  console.log('컴포넌트 랜더링 확인용');
  return <div>Hello, {name}!</div>;
});

export default MyComponent;

2. useMemo 활용 예시

  • NumberList 컴포넌트는 useMemo를 사용하여 numbers 배열의 합계를 계산합니다. 합계를 메모이제이션하므로 numbers 배열이 변경되지 않는 한, 합계를 다시 계산하지 않고 이전 결과를 재사용합니다.
  • addNumber 버튼을 클릭할 때마다 numbers 배열이 업데이트되지만, 합계는 변경되지 않는 한 이전 결과를 사용하여 성능을 최적화합니다.
import React, { useState, useMemo } from 'react';

const NumberList = ({ numbers }) => {
  // 합계 계산 결과를 useMemo로 메모아이제이션
  const sum = useMemo(() => {
    return numbers.reduce((acc, val) => acc + val, 0);
  }, [numbers]); // numbers 배열이 변경될 때만 다시 계산

  return (
    <div>
      <h2>리스트</h2>
      <ul>
        {numbers.map((number, index) => (
          <li key={index}>{number}</li>
        ))}
      </ul>
      <p>Sum: {sum}</p>
    </div>
  );
};

const App = () => {
  const [numbers, setNumbers] = useState([1, 2, 3, 4, 5]);

  const addNumber = () => {
    const newNumber = Math.floor(Math.random() * 10) + 1;
    setNumbers([...numbers, newNumber]);
  };

  return (
    <div>
      <button onClick={addNumber}>+</button>
      <NumberList numbers={numbers} />
    </div>
  );
};

export default App;

(2) 네트워크 요청 결과 캐싱

네트워크 요청 결과를 메모이제이션하여 동일한 요청을 여러 번 보내지 않고 캐싱된 결과를 사용할 수 있습니다. 이렇게 하면 서버 부하를 줄이고 응답 시간을 단축할 수 있습니다.

import { useState, useEffect } from 'react';

const fetchData = async (url) => {
  const response = await fetch(url);
  const data = await response.json();
  return data;
};

const DataFetchingComponent = () => {
  const [data, setData] = useState(null);

  useEffect(() => {
    // 데이터를 가져와서 메모아이제이션
    const fetchDataAndMemoize = async () => {
      const cachedData = localStorage.getItem('cachedData');
      if (cachedData) {
        setData(JSON.parse(cachedData));
      } else {
        const result = await fetchData('https://localhost:3000/data');
        setData(result);
        localStorage.setItem('cachedData', JSON.stringify(result));
      }
    };

    fetchDataAndMemoize();
  }, []);

  if (!data) {
    return <div>로딩중...</div>;
  }

  return (
    <div>
      <h2>임시데이터:</h2>
      <div>{JSON.stringify(data, null, 2)}</div>
    </div>
  );
};

export default DataFetchingComponent;

3. 주의할 점

Memoization은 순수함수에만 적용할 수 있습니다.

💡 순수함수란 ?
- 동일한 입력에 대해 항상 동일한 출력을 반환하는 함수
- 아래 요소들을 포함한 부작용을 일으키지 않는 함수 .
(네트워크 요청, data mutation, 파일 로깅, state 변경)


Memoization 외 상태 관리와 성능 최적화 하는 방법?

☑️ 의존성 주입을 구현하는 방식을 생각해보자.

Custom Hook과 Context API를 사용해보자!

  • (+) Custom Hook과 Context API를 활용한 의존성 주입은 Memoization 과 함께 사용되는 경우도 많습니다.

Custom hook과 Context-API를 활용한 의존성 주입

실제 진행한 프로젝트에서 예시를 가지고 왔다.

1. Custom Hook을 사용한 의존성 주입

  • 뒤로가기 방지 로직 빼기
  • 쓰이는 페이지 (SignUp, mainPage, Register, Review)
  • 공통부분만 빼고, 경고메세지와 같은건 goBackCallBack 함수에 직접 담아서 쓰도록 해둠
    import { useEffect } from 'react';
    
    const usePreventGoingBack = goBackCallback => {
    	const preventGoBack = () => {
    		history.pushState(null, '', location.href);
    		goBackCallback?.();
    	};
    
    	useEffect(() => {
    		history.pushState(null, '', location.href);
    		window.addEventListener('popstate', preventGoBack);
    
    		return () => {
    			window.removeEventListener('popstate', preventGoBack);
    		};
    	}, []);
    };
    
    export default usePreventGoingBack;

2. Context API를 사용한 의존성 주입

  • AccessToken AuthProvider로 전역관리
  • 기존 privateRoute에서 설정한것에서 업그레이드 처리
  • 문제점: 새로고침을 할때마다 자꾸 landing Page로 돌아가게되었었음
  • 해결한 방법 :
    • 원인파악 : (context > auth.js 내에 useState로 관리하는 accessToken의 default 값이 null 이었는데, 이는 로그인 후 어느페이지에서든 새로고침을 하면 null 값으로 처리되어 privateRoute 로직 상 자꾸 랜딩페이지로 redirect 되었음)
    • 처리 : useState(TokenService.getToken()) 으로 기본값을 변경하여 처리함.

< 예시 >

  • Context / auth.js
        // Context / auth.js

        import TokenService from 'Repository/TokenService';
        import { useContext, useState, createContext, useEffect } from 'react';
        
        const AuthContext = createContext();
        export const useAuth = () => useContext(AuthContext);
        
        function AuthProvider({ children }) {
        	const [accessToken, setAccessToken] = useState(TokenService.getToken());
        
        	useEffect(() => {
        		const token = TokenService.getToken();
        		if (!token) return;
        		setAccessToken(token);
        	}, []);
        
        	const login = token => {
        		TokenService.setToken(token);
        		setAccessToken(token);
        	};
        
        	const logout = () => {
        		TokenService.removeToken();
        		setAccessToken(null);
        	};
        
        	return (
        		<AuthContext.Provider value={{ accessToken, login, logout }}>
        			{children}
        		</AuthContext.Provider>
        	);
        }
        
        export default AuthProvider;
  • PrivateRoute.js
        // PrivateRoute.js
        import { useAuth } from 'Context/auth';
        import { Navigate } from 'react-router-dom';
        
        const PrivateRoute = ({ children }) => {
        	const {accessToken} = useAuth(); // 변경사항
        
        	return accessToken ? children : <Navigate to={`/`} />;
        };
        
        export default PrivateRoute;
  • App.js
        <AuthProvider>
        			<GlobalStyles />
        			<RouterProvider router={router} />
        </AuthProvider>
``
        
profile
프론트엔드 주니어입니다. 그런데 서비스 기획을 곁들인,,,

0개의 댓글