성능최적화
// memoization 폴더 _ parent 파일
const MemoizationParentPage = ()=>{
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>
<MemoizationChildPage/>
</div>
)
}
export default MemoizationParentPage
이런 코드를 짜고나서 let과 state의 버튼을 누르고나서, 콘솔을 보면, let은 버튼을 누르면 콘솔의 값은 올라가지만 리렌더는 일어나지 않아 “컨테이너가 렌더링 됩니다.”라는 콘솔이 찍히지 않고 있으며, 화면에는 0이 찍히게 된다..
하지만 state는 버튼을 누름과 동시에 리렌더링되며 우리가 올려두었던 countLet이 0으로 초기화 된다.
이는 useState를 제외한 모든 값이 다시 그려지고 있다는 것을 알 수 있다.
프리젠터 페이지를 만든 후 앞서 만들어 둔 부모 컴포넌트 페이지에 import 한다.
// memoization 폴더 _ child 파일
const MemoizationChildPage = ()=>{
console.log("자식 컴포넌트가 렌더링 됩니다.")
return(
<div>
<div>================<div>
<h1>이것은 자식 컴포넌트입니다.</h1>
<div>================<div>
</div>
)
}
export default MemoizationChildPage
이렇게 import 한 후 새로고침을하면 콘솔에 “부모 컴포넌트가 렌더링 됩니다.” 와 “자식 컴포넌트가 렌더링 됩니다.” 가 찍혀 있는것 을 볼 수 있다.
하지만 부모의 state를 바꿨는데 자식도 다시 렌더링되고 있기때문에 굉장히 비효율적 이다.
메모이제이션(Memoization)
불필요한 리렌더링이 많아질 수록 서비스의 성능이 저하되게 된다.
성능이 저하된다는 것은 사용자의 이탈율을 높일 수 있기 때문에 매출과 관련하여 긴밀한 연관이 있다.
이 부분을 리렌더링을 줄여주실 필요가 있다.
useCallback(),useMemo()
자식컴포넌트는 memo를 사용해 불필요한 리렌더가 더이상 일어나지 않도록 막아주었지만, 부모 컴포넌트는 지속적으로 렌더링이 일어나는 상태이다.
하지만 부모컴포넌트에서도 부분적으로 렌더링이 일어나지 않아도 되는 부분이 있다.
예를들어 countState를 변경했을때 countLet의 값이 지속적으로 다시 만들어지고 있는 상황일때. (초기화라고 하지만 0이라는 값이 계속 다시 만들어지는 것 )
이런 불필요한 값들이 지속적으로 다시 만들어지지 않도록 유지시켜주는 hooks가 바로
**useMemo(),useCallback()**이다
useMemo() 사용방법 _ 변수 기억
// memoization 폴더 _ parent 파일
**import {useMemo} from 'react'**
const MemoizationParentPage = ()=>{
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>
<MemoizationChildPage/>
</div>
)
}
export default MemoizationParentPage
일반적으로 useMemo의 사용은 굉장히 복잡한 계산 이외에는 그렇게 흔하지 않다.
useCallback() 사용법 _ 함수기억
// memoization 폴더 _ parent 파일
**import {useCallback} from 'react'**
const MemoizationParentPage = ()=>{
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>
<MemoizationChildPage/>
</div>
)
}
export default MemoizationParentPage
useCallback으로 함수를 감싸주게 되면 해당 함수를 다시 불러오지 않고 이전에 불러왔던 함수를 실행시키게 되는데, 우리는 이를 눈으로 확인하기 위해 state를 담은 함수에도 useCallBack을 감싸준다.
카운트가 고정되어 있지 않았다 이것이 바로 useCallback의 결과 이다.
즉, 이전에 불러왔던 값을 유지시키는 것 이다.
이렇게 함수는 다시불러오지 않지만 값은 올려주고 싶을 때 사용할 수 있는 방법이 하나 있다.
prev를 사용하면된다..
setCountState((prev)=>prev+1) 이렇게 setCoutState함수를 작성해주면 onClickCountState 함수를 새로 그리지 않지만, state는 올려줄 수 있다.
작은 서비스에서라면 한번 더 렌더링 되는게 문제가 될 건 없지만 서비스가 점점 커지고 기능도 많아진다면, 이런 작은 부분 하나하나가 속도를 늦출 수 있다.
memo
**memo** 라는 기능도 존재하는데, react에서 제공하고 있는 기능이다. 따라서 react에서 import해서 사용해주면 된다.
// memoization 폴더 _ child 파일
**import {memo} from "react"**
const MemoizationChildPage = ()=>{
console.log("자식 컴포넌트가 렌더링 됩니다.")
return(
<div>
<div>================<div>
<h1>이것은 자식 컴포넌트입니다.</h1>
<div>================<div>
</div>
)
}
export default **memo(**MemoizationChildPage**)**
이렇게 **memo**를 사용하고 state카운트를 클릭해보시면 프리젠터는 렌더링이 되지않아 콘솔도 찍히지 않을 뿐더러 리액트 툴에도 보이지 않는다.
map과 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)
이렇게 memo를 걸어두면 우리가 위에서 배웠던 것 과 같이 처음의 값이 기억되어 업데이트 되지 않는다.
그러나 key값에 index가 아니라 uuid를사용하게 되면 상황이 달라진다.
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>
</>
)
}
import { memo } from "react"
export default function Word(props: any){
console.log("자식이 렌더링 됩니다!",props.el)
return <span>{props.el}</span>
}
export default memo(Word)
uuid를 사용하면 memo를 걸어놔도 key값이 변경되어 props로 넘어가기 때문에 변경된 부분이 모두 리렌더링 된다.
uuid는 불필요한 리렌더링을 초래하므로 사용하실때는 필요한 상황에서만 주의 해서 사용해야 한다.
CRP(Critical Rendering Path)
브라우저에서 코드를 그려주는 과정은 어떨까?
여기에서 중요한 개념으로 Critical Rendering Path 가 있다.
브라우저에서 렌더링을 해주는 과정은 아래와 같다.
먼저 화면을 그려주는데 필요한 리소스(html,css,js)를 다운로드 해온 뒤
HTML과 CSS에서 화면에 렌더해야 할 요소들을 구분 후 렌더되어야 할 HTML,CSS 요소를 합쳐 화면에 그려주게 된다.
화면에 그려줄때 해당 요소들이 어느 위치에 놓일지 먼저 그려주는 Layout Reflow와 해당 요소들을 색칠하는 Paint Repaint과정이 발생한다.
화면에 렌더해야 하는 요소를 구분하는 작업을 한다고 했는데 이때 HTML의 요소를 구분할 수 있도록 도와주는 것이 DOM(Document Object Model), CSS요소를 구분할 수 있도록 도와주는 것이 CSSOM(CSS Object Model)다.
이렇게 DOM과 CSSOM이 합쳐진것이 바로 렌더트리 이다. 즉 2번과정과 3번과정이 합쳐진 4번과정의 결과물이 렌더트리라고 보면 된다.
Reflow와 Repaint
reflow란 렌더링 되어야 할 요소들을 화면상 위치를 그려주는 과정이다.
그리고 repaint는 위치를 잡고 난 이후 색칠을 해주는 과정이다.
<!Doctype html>
<html lang="ko">
<head>
<title>리플로우/리페인트</title>
<style>
.qqq{
width : 100px;
height : 100px;
}
.qqq:hover{
width : 110px;
height : 110px;
background-color : pink;
}
.aaa{
width : 100px;
height : 100px;
background-color : green;
}
.aaa:hover{
background-color : red;
}
</style>
<head/>
<body>
<div class="qqq"> 리플로우 연습! </div>
<div class="aaa"> 리페인트 연습! </div>
</body>
</html>
위의 코드처럼 박스를 그려두고, 마우스를 hover하면 qqq 박스에는 리플로우가 aaa박스에는 리페인트가 일어날 것이다.
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>
index.html을 다운로드 받아올 때 network 탭을 보게 되면,
index.html을 받아온 후에 board.html 문서도 이어서 받아 온다.
index.html을 받아올 때 board.html을 같이 받아오기 때문에 게시판 페이지를 받아올때는 시간이 걸리지 않게 됩니다.
사진 쪽을 보면 Size 부분이 prefetch cache라고 적혀있을 것이다. 이는 이미 이전 페이지에서 prefetch를 이용해 해당 리소스를 다 받아와 캐시에 넣어두었기 때문에 따로 또 받지 않아도 되어 사이즈가 없는 것 이다.
따라서 prefetch를 이용하시면 페이지가 이동되어도 기다리지 않고 바로 보여주는 것이 가능하다.
preload(프리로드)
preload 란 현재페이지에서 쓸 이미지들을 모두 다운로드 받아놓는 것 이다.
따라서 prefetch와는 다르게 이미지를 index.html를 받아올때 css와 js보다 먼저 받아오게 된다.
<!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>
위 코드처럼 img 태그를 preload를 걸어주면, 제일 나중에 받아와야 하는 이미지를 상단으로 끌어올려 받아오게 된다.
preload를 이용하면 이미지를 먼저 받아오기때문에 화면로드 지연을 방지 할 수 있다.