[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으로 초기화 된다.memoization 폴더 _ presenter 파일
const MemoizationPresenterPage = ()=>{ console.log("프리젠터가 렌더링 됩니다.") return( <div> <div>================<div> <h1>이것은 프리젠터 입니다.</h1> <div>================<div> </div> ) } export default MemoizationPresenterPage
📂 useCallback(),useMemo()
memo
를 사용해 불필요한 리렌더가 더이상 일어나지 않도록 막아주었지만, 부모 컴포넌트는 지속적으로 렌더링이 일어나는 상태다. 📌 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() _ 함수기억
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
memoization 폴더 _ presenter 파일
import {memo} from "react" const MemoizationPresenterPage = ()=>{ console.log("프리젠터가 렌더링 됩니다.") return( <div> <div>================<div> <h1>이것은 프리젠터 입니다.</h1> <div>================<div> </div> ) } export default memo(MemoizationPresenterPage)
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> </> ) }
1️⃣ 화면을 그려주는데 필요한 리소스(html,css,js)를 다운로드
2️⃣ HTML과 CSS에서 화면에 렌더해야 할 요소들을 구분 후 렌더되어야 할 HTML,CSS 요소를 합쳐 화면에 그려주게 된다.
3️⃣ 화면에 그려줄때 해당 요소들이 어느 위치에 놓일지 먼저 그려주는 Layout Reflow와 해당 요소들을 색칠하는 Paint Repaint과정이 발생한다.
📂 reflow & repaint with Board
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
: 다음페이지에서 쓰려고 미리 받는 것 이며, 현재페이지를 모두 받아온 이후 제일 나중에 다운로드 해오게 된다.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
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()
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 ==="); };