[TIL] 성능최적화

신재욱·2023년 4월 27일
1
post-thumbnail

📒 오늘 공부한 내용

🔍수업목차

[31-1] 성능최적화
[31-2] 메모이제이션(Memoization)
[31-3] map과 memo의 관계
[31-4] CRP(Critical Rendering Path)
[31-5] Promise & Promise.all()

✅ 성능최적화


memoization 폴더 _ container 파일

const containerPage = ()=>{
	console.log("컨테이너가 렌더링 됩니다.")

	let countLet = 0
	const [countState,setCountState] = useState(0)

	const onClickCountLet = ()=>{
		console.log(countLet+1)
		countLet += 1
	}

	const onClickCountState = ()=>{
		console.log(countState+1)
		setCountState(countState+1)
	}
	return(
		<div> 
			<div>================<div> 
			<h1>이것은 컨테이너 입니다.</h1>

			<div> 카운트(let): {countLet} </div>
			<button onClick={onClickCountLet}> 카운트(let) +1 올리기! </button>

			<div> 카운트(state): {countLet} </div>
			<button onClick={onClickCountState}> 카운트(state) +1 올리기! </button>

			<div>================<div>

		<MemoizationPresenterPage/>
		</div>
	)
}
  • let은 버튼을 누르면 콘솔의 값은 올라가지만 리렌더는 일어나지 않아 “컨테이너가 렌더링 됩니다.”라는 콘솔이 찍히지 않고 있으며, 화면은 여전히 0 이다.
  • state는 버튼을 누름과 동시에 리렌더링되며 우리가 올려두었던 countLet이 0으로 초기화 된다.
  • useState를 제외한 모든 값이 다시 그려지고 있다는 것을 알 수 있다.

memoization 폴더 _ presenter 파일

const MemoizationPresenterPage = ()=>{
	console.log("프리젠터가 렌더링 됩니다.")


	return(
		<div>  
			<div>================<div>
			<h1>이것은 프리젠터 입니다.</h1>
			<div>================<div>
		</div>
	)
}

export default MemoizationPresenterPage
  • import 한 후 새로고침을하시면 콘솔에 “컨테이너가 렌더링 됩니다.” 와 “프리젠터가 렌더링 됩니다.” 가 찍힌다.
  • 부모의 state를 바꿨는데 자식도 다시 렌더링되고 있다. 이부분이 굉장히 비효율 적이다.

✅ 메모이제이션(Memoization)


  • 리렌더가 얼마나 일어나는지 신경쓰지 않고 만들어 왔다. 하지만 불필요한 리렌더링이 많아질 수록 서비스의 성능이 저하되게 된다. 성능이 저하된다는 것은 사용자의 이탈율을 높일 수 있기 때문에 매출과 관련하여 긴밀한 연관이 있다.
  • 따라서 리렌더링을 줄여줄 필요가 있다.

📂 useCallback(),useMemo()

  • 자식컴포넌트는 memo를 사용해 불필요한 리렌더가 더이상 일어나지 않도록 막아주었지만, 부모 컴포넌트는 지속적으로 렌더링이 일어나는 상태다.
  • 부모컴포넌트에서도 부분적으로 렌더링이 일어나지 않아도 되는 부분이 있다.

📌 useMemo() _ 변수 기억

  • 일반적으로 useMemo의 사용은 굉장히 복잡한 계산 이외에는 그렇게 흔하지는 않다.

memoization 폴더 _ presenter 파일

import {useMemo} from 'react'

const containerPage = ()=>{
	console.log("컨테이너가 렌더링 됩니다.")

	let countLet = 0
	const [countState,setCountState] = useState(0)

// 1. useMemo로 변수 기억
	const memo = useMemo( () => {
		const aaa = Math.random()
	}, [])
	console.log(`${memo}는 더이상 안 만들어`)

	const onClickCountLet = ()=>{
		console.log(countLet+1)
		countLet += 1
	}

	const onClickCountState = ()=>{
		console.log(countState+1)
		setCountState(countState+1)
	}
	return(
		<div> 
			<div>================<div> 
			<h1>이것은 컨테이너 입니다.</h1>

			<div> 카운트(let): {countLet} </div>
			<button onClick={onClickCountLet}> 카운트(let) +1 올리기! </button>

			<div> 카운트(state): {countLet} </div>
			<button onClick={onClickCountState}> 카운트(state) +1 올리기! </button>

			<div>================<div>

		<MemoizationPresenterPage/>
		</div>
	)
}

📌 useCallback() _ 함수기억

  • useCallback으로 함수를 감싸주게 되면 해당 함수를 다시 불러오지 않고 이전에 불러왔던 함수를 실행시키게 된다.
  • 즉, 이전에 불러왔던 값을 유지시키는 것 이다.
  • 이전에 배웠던 prev를 이용하면 함수는 다시불러오지 않지만 값은 올려줄 수 있다. - setCountState((prev)=>prev+1) 이렇게 setCoutState함수를 작성해주시면 onClickCountState 함수를 새로 그리지 않지만, state는 올려줄 수 있다.
  • 작은 서비스에서라면 한번 더 렌더링 되는게 문제가 될 건 없지만 서비스가 점점 커지고 기능도 많아진다면, 이런 작은 부분 하나하나가 속도를 늦출 수 있다.

memoization 폴더 _ presenter 파일

import {useCallback} from 'react'

const containerPage = ()=>{
	console.log("컨테이너가 렌더링 됩니다.")

	let countLet = 0
	const [countState,setCountState] = useState(0)

// 2. useCallback으로 함수 기억하기	
// useCallback을 사용하게 되면 함수를 다시 만들지 않습니다.
	const onClickCountLet = useCallback(()=>{
		console.log(countLet+1)
		countLet += 1
		},[])

// 3. usecallback의 잘못된 사용사례 _ state를 기억하기 때문에 아무리 count를 올려도 1만 찍히게 됩니다.
	const onClickCountState = useCallback(()=>{
		console.log(countState+1)
		setCountState(countState+1)
		},[])

// 4. 3번의 잘못된 사용사례 보완
	const onClickCountState = useCallback(()=>{
		setCountState((prev)=>prev+1)
		},[])

	return(
		<div> 
			<div>================<div> 
			<h1>이것은 컨테이너 입니다.</h1>

			<div> 카운트(let): {countLet} </div>
			<button onClick={onClickCountLet}> 카운트(let) +1 올리기! </button>

			<div> 카운트(state): {countLet} </div>
			<button onClick={onClickCountState}> 카운트(state) +1 올리기! </button>

			<div>================<div>

		<MemoizationPresenterPage/>
		</div>
	)
}

💡 알아두면 유용한 개발자 도구

1. Apollo Client Devtools
→ 설치후 app.tsx에서 client 설정 부분에 connectToDevTools : true로 설정해줘야한다.

2.wappalyzer
→특정 사이트에 들어가시면 해당 사이트가 사용한 스택을 분석해준다.

🎯 useCallback을 쓰지 말아야 할 때

의존성 배열의 인자가 1~2개보다 많아질 때는 차라리 리렌더를 하는것이 유지 보수에는 더 좋은방법 이다. 성능이 조금이나마 좋아지는 것 보다는 유지보수가 편리한 편이 훨씬 좋다. 따라서 의존성 배열의 인자가 2개를 초과할때는 그냥 리렌더를 해주시는게 좋다.

📂 memo

  • memo를 사용하고 state카운트를 클릭해보시면 프리젠터는 렌더링이 되지않아 콘솔도 찍히지 않을 뿐더러 리액트 툴에도 보이지 않는다.

memoization 폴더 _ presenter 파일

import {memo} from "react"

const MemoizationPresenterPage = ()=>{
	console.log("프리젠터가 렌더링 됩니다.")


	return(
		<div>  
			<div>================<div>
			<h1>이것은 프리젠터 입니다.</h1>
			<div>================<div>
		</div>
	)
}

export default memo(MemoizationPresenterPage)

✅ map과 memo의 관계


  • memo를 걸어두면 우리가 위에서 배웠던 것 과 같이 처음의 값이 기억되어 업데이트 되지 않는다.
    import { useState } from "react"
    import Word from "./02-child"
    
    export default function MemoizationParentPage(){
    	const [data,setData] = useState("철수는 오늘 점심을 맛있게 먹었습니다.")
    
    	const onClickChange = ()=>{
    		setData("영희는 오늘 저녁을 맛없게 먹었습니다.")
    	}
    	return(
    		<>
    			{data.split("").map((el)=>(
    				<Word key={index} el={el}/>
    			))}
    			<button>체인지</button>
    		</>
    	)
    }             
    import { memo } from "react"
    
    export default function Word(props: any){
    	console.log("자식이 렌더링 됩니다!",props.el)
    	return <span>{props.el}</span>
    }
    
    export default memo(Word)
              

📂 key값을 uuid로 설정시 문제점

import { useState } from "react"
import Word from "./02-child"
import {v4 as uuidv4} from "uuid"

export default function MemoizationParentPage(){
	const [data,setData] = useState("철수는 오늘 점심을 맛있게 먹었습니다.")

	const onClickChange = ()=>{
		setData("영희는 오늘 저녁을 맛없게 먹었습니다.")
	}
	return(
		<>
			{data.split("").map((el)=>(
				<Word key={uuidv4} el={el}/>
			))}
			<button>체인지</button>
		</>
	)
}
  • uuid를 사용하면 memo를 걸어놔도 key값이 변경되어 props로 넘어가기 때문에 변경된 부분이 모두 리렌더링 된다.
  • uuid는 불필요한 리렌더링을 초래하므로 사용하실때는 필요한 상황에서만 주의 해서 사용 해야 한다.

✅ CRP(Critical Rendering Path)


1️⃣ 화면을 그려주는데 필요한 리소스(html,css,js)를 다운로드
2️⃣ HTML과 CSS에서 화면에 렌더해야 할 요소들을 구분 후 렌더되어야 할 HTML,CSS 요소를 합쳐 화면에 그려주게 된다.
3️⃣ 화면에 그려줄때 해당 요소들이 어느 위치에 놓일지 먼저 그려주는 Layout Reflow와 해당 요소들을 색칠하는 Paint Repaint과정이 발생한다.

📂 reflow & repaint with Board

  • 목록을 조회해올 때 느린 화면으로 보시면, 데이터가 비어있다 나중에 들어와 UI 상으로 데이터를 뿌려주는 부분의 크기가 달라지는 것을 확인할 수 있다.

index.tsx

import { useQuery, gql } from "@apollo/client";
import { MouseEvent } from "react";
import {
  IQuery,
  IQueryFetchBoardsArgs,
} from "../../src/commons/types/generated/types";

const FETCH_BOARDS = gql`
  query fetchBoards($page: Int) {
    fetchBoards(page: $page) {
      _id
      writer
      title
      contents
    }
  }
`;

export default function StaticRoutedPage() {
  const { data, refetch } = useQuery<
    Pick<IQuery, "fetchBoards">,
    IQueryFetchBoardsArgs
(FETCH_BOARDS);

  console.log(data?.fetchBoards);

  const onClickPage = (event: MouseEvent<HTMLSpanElement>) => {
    void refetch({ page: Number(event.currentTarget.id) });
  };

  return (
    <>
      {/* 임시 배열 10개를 생성하여, 데이터가 없을 때도 높이 30px을 유지하여 reflow 방지  */}
      {(data?.fetchBoards ?? new Array(10).fill(1)).map((el) => (
        <div key={el._id} style={{ height: "30px" }}>
          <span style={{ margin: "10px" }}>{el.writer}</span>
          <span style={{ margin: "10px" }}>{el.title}</span>
        </div>
      ))}
      {new Array(10).fill(1).map((_, index) => (
        <span key={index + 1} id={String(index + 1)} onClick={onClickPage}>
          {index + 1}
        </span>
      ))}
    </>
  );
}
  • 위와 같이 데이터를 뿌려주는 곳 height를 고정시키면 위치상으로 다시 그려야 하는 일이 없기 때문에 reflow가 일어나는 것을 방지 할 수 있다.

📂 prefetch(프리페치)

  • prefetch : 다음페이지에서 쓰려고 미리 받는 것 이며, 현재페이지를 모두 받아온 이후 제일 나중에 다운로드 해오게 된다.
  • 이미 이전 페이지에서 prefetch를 이용해 해당 리소스를 다 받아와 캐시에 넣어두었기 때문에 따로 또 받지 않아도 되어 사이즈가 없다.
  • prefetch를 이용하시면 페이지가 이동되어도 기다리지 않고 바로 보여주는 것이 가능하다.

index.html

<!DOCTYPE html>
<html lang="ko">
  <head>
    <title>프리페치</title>

    <!-- 프리페치: 다음페이지를 미리 다운로드 받으므로, 버튼 클릭시 페이지이동 빠름 -->
    <link rel="prefetch" href="board.html" />
  </head>
  <body>
    <a href="board.html">게시판으로 이동하기</a>
  </body>
</html>

board.html

<!DOCTYPE html>
<html lang="ko">
  <head>
    <title>게시판</title>
  </head>
  <body>
    여기는 게시판입니다
  </body>
</html>

📂 preload(프리로드)

preload : 현재페이지에서 쓸 이미지들을 모두 다운로드 받아놓는 것

<!DOCTYPE html>
<html lang="ko">
  <head>
    <title>프리로드</title>

    <!-- 프리로드: 한 번에 6개씩 받아오므로, body태그의 이미지는 가장 마지막에 다운로드 -->
    <!--         눈에 보이는 이미지를 먼저 다운로드 받아서 보여주고, 클릭하면 실행되는 JS는 나중에 받아오기 -->
    <!--         따라서, DOMContentedLoaded 이후, Load까지 완료되는 최종 로드 시간이 더 짧아짐 -->
    <link rel="preload" as="image" href="./dog.jpeg" />

    <!-- 일반로드 -->
    <link rel="stylesheet" href="./index.css" />
    <script src="index1.js"></script>
    <script src="index2.js"></script>
    <script src="index3.js"></script>
    <script src="index4.js"></script>
    <script src="index5.js"></script>
    <script src="index6.js"></script>
  </head>
  <body>
    <img src="./dog.jpeg" />
  </body>
</html>

✅ Promise & Promise.all()


📂 Promise

  • result1 이 실행되고 난 후에 result2가 실행이 되고, result2가 실행된 후에, result3이 실행된다.. 따라서 onClickPromise 함수의 경우 약 6초의 시간이 소요된다.
const startPromise = async () => {
        console.time("=== 개별 Promise 각각 ===");
        const result1 = await new Promise((resolve, reject) => {
          setTimeout(() => {
            resolve("성공");
          }, 2000);
        });
        const result2 = await new Promise((resolve, reject) => {
          setTimeout(() => {
            resolve("성공");
          }, 3000);
        });
        const result3 = await new Promise((resolve, reject) => {
          setTimeout(() => {
            resolve("성공");
          }, 1000);
        });
        console.timeEnd("=== 개별 Promise 각각 ===");
      };

📂 Promise.all()

  • Promise.all() 의 경우에는 Promise.all()에 포함되어 있는 함수들을 동시에 실행을 한다. 따라서 onClickPromiseAll 함수의 경우 약 3초의 시간이 소요된다.
  • Promise보다 시간을 단축하지만, 결과값은 같도록 해주는 기능
	const startPromiseAll = async () => {
        // await Promise.all([promise, promise, promise])

        console.time("=== 한방 Promise.all ===");
        const result = await Promise.all([
          new Promise((resolve, reject) => {
            setTimeout(() => {
              resolve("성공");
            }, 2000);
          }),
          new Promise((resolve, reject) => {
            setTimeout(() => {
              resolve("성공");
            }, 3000);
          }),
          new Promise((resolve, reject) => {
            setTimeout(() => {
              resolve("성공");
            }, 1000);
          }),
        ]);
        console.log(result);
        console.timeEnd("=== 한방 Promise.all ===");
      };
profile
1년차 프론트엔드 개발자

0개의 댓글