성능개선을 위한 3가지 방법에 대해 소개하려고 한다.
개발 중 버그가 생길때 개발자도구에서 elements를 봐도 되지만 리액트개발자도구를 확장프로그램을 설치해서 보면 더 편함
React Developer Tools를 설치하고 개발자도구를 열어보면 Components탭과 Profiler탭이 생김
여기서 개발중인 리엑트사이트를 컴포넌트로 미리볼 수 있음
왼쪽에서 컴포넌트의 구조를 파악할 수 있고,
왼쪽 상단 마우스이모티콘을 눌러 컴포넌트를 찍어보면
거기 있는 state, props등을 조회&수정이 가능
그리고 우측 상단 여러 버튼들로 컴포넌트가 사용되고 있는 코드를 확인하거나 디버깅 등이 가능함.
Profiler탭에서 녹화버튼 클릭 후 페이지 이동이나 버튼조작을 하면
방금 렌더링 된 모든 컴포넌트의 렌더링 시간을 측정해줌
이상하게 느린 컴포넌트가 있다면 여기서 범인을 찾을 수 있음.
리액트는 build 시 html, js파일을 하나로 합쳐줌.(SPA)
그 안에 지금까지만든 모든 내용이 들어가있기에 파일사이즈가 좀 큼.
그래서 리액트사이트들은 첫 페이지 로딩속도가 매우 느릴 수 있다.
그게 싫다면 lazy import를 사용해 js파일을 잘게 쪼개면 됨.
예를 들어 페이지에서 Detail, Cart컴포넌트를 import해서 쓰고있다고 가정해보자.
하지만 메인페이지에서 Detail컴포넌트나 Cart컴포넌트들은전혀 보이고 있지 않다.
그럼 컴포넌트들이 필요해지면 나중에 import하면 될 듯?
사용법은 아래와 같다.
(기존사용법)
import Detail from './routes/Detail.js'
import Cart from './routes/Cart.js'
↓
(lazy import 사용)
import {lazy, Suspense} from 'react'
const Detail = lazy( () => import('./routes/Detail.js') )
const Cart = lazy( () => import('./routes/Cart.js') )
<Suspense fallback={ <div>로딩중임</div> }>
<Detail shoes={shoes} />
</Suspense>
이렇게 해주면 Detail컴포넌트 내용을 다른 js파일로 쪼개주기 때문에 첫 페이지 로딩속도를 향상시킬 수 있음.
lazy를 사용하면 Detail이나 Cart컴포넌트 로드까지 지연시간이 발생할 수 있다.
이때
1. Suspense import
2. Detail 컴포넌트를 감싸주면
Detail컴포넌트가 로딩중일 때 대신 보여줄 html을 작성할 수 있다.
+) <Suspense>로 <Routes>부분을 전부 감싸도 됨.
컴포넌트가 재렌더링되면 그 안에 있는 자식컴포넌트도 항상 같이 재렌더링된다.
만약 자식컴포넌트의 렌더링시간이 오래걸리게 되면 부모컴포넌트를 누를 때마다 버벅거림이 발생할 수 있음.
이럴 때 자식컴포넌트를 memo로 감싸놓으면 됨.
function Child(){
console.log('재랜더링됨');
return <div>자식컴포넌트임</div>
}
function Parent(){
let [count, setCount] = useState(0);
return (
<Child />
<button onClick={()=>{ setCount(count+1) }}>+</button>
)
}
Parent컴포넌트안에 Child컴포넌트를 만들었음
버튼을 누를 때마다 부모컴포넌트가 재렌더링되게 만들었는데
이 경우 Child컴포넌트도 재랜더링 됨
이때 memo함수를 스면 꼭 필요할 때만 <Child>컴포넌트를 재렌더링 해달라고 코드짤 수 있음
import {memo, useState} from 'react'
let Child = memo( function(){
console.log('재렌더링됨')
return <div>자식임</div>
})
function Cart(){
let [count, setCount] = useState(0)
return (
<Child />
<button onClick={()=>{ setCount(count+1) }}> + </button>
)
}
ler 컴포넌트명 = function(){}
이런식으로 만들어야 감쌀 수 있음이제 Child로 전송되는 props가 변하거나 할 때만 재렌더링 됨
❗ 주의
memo로 감싼 컴포넌트는 기존props와 바뀐props를 비교하는 연산이 추가로 진행되기 때문에 꼭 필요한 곳에만 사용할 것
useMemo는 useEffet와 비슷함
컴포넌트 로드 시 1회만 실행하고 싶은 코드가 있으면 여기에 담으면됨.
import {useMemo, useState} from 'react'
function 함수(){
return 반복문100만번 돌린 결과
}
function Cart(){
let result = useMemo(()=>{ return 함수() }, [])
return (
<Child />
<button onClick={()=>{ setCount(count+1) }}> + </button>
)
}
예를들어 반복문 100만번 돌려야하는 경우
그 함수를 useMemo안에 넣어두면 컴포넌트 로드 시 1회만 실행됨. (재랜더링마다 동작하지 않으니 좀 더 효율적)
useEffet처럼 dependency도 넣을 수 있어서 특정 stats, props가 변할 때 마다만 실행할 수도 있음.
React 18버전 이후로부터 추가된 기능이 있다.
automatic batching이라는 기능이 있는데
setCount(1) // 재랜더링되지 않음
setName(2) // 재랜더링되지 않음
setValue(3) // 여기서 1번만 재렌더링 됨
state변경함수를 연달아 3개 사용하면 재렌더링도 원래 3번 되지만,
리액트에서 이를 마지막 1회에만 재랜더링해줌.
쓸데없는 재렌더링 방지기능이고 batching이라고 함.
fetch().then(()=>{
setCount(1) //재랜더링됨
setName(2) //재랜더링됨
})
React v17까진 ajax요청, setTimeout안에 state변경함수가 있는 경우 일관적으로 batching이 일어나지 않았는데,
React v18부터는 어디에 있든 재랜더링은 마지막 1번만 됨
📎 참고
batching되는게 싫고 state변경함수 실행마다 재렌더링 시키고 싶으면 flushSync라는 함수를 쓰면 됨.
렌더링 성능이 저하되는 컴포넌트에서 쓸 수 있는 혁신적인 기능이 하나 추가됨.
useTransition인데,
오래걸리는 부분을 이걸로 감싸면 렌더링 시 버벅임이 줄어듦.
여기를 참고해보면 이런 비유를 한다.
"주문을 할 때 하나 고를 때마다 직원이 주방으로 달려가지 않고, 오더를 완성시킬 때까지 대기하는 것과 같다."
렌더링 시간이 매우 오래 걸리는 컴포넌트가 있다고 가정해보자.
버튼 클릭 타이핑 시 마다 그 컴포넌트를 렌더링해야 한다면 반응속도가 매우 늦으질텐데 이럴때 useTransition을 사용하면 됨.
import { useState } from 'react';
let a = new Array(10000).file(0);
function App(){
let [name, setName] = useState('');
return(
<div>
<input onChange={ (e)=>{ setName(e.target.value) }}/>
{
a.map(()=>{
return <div>{name}</div>
})
}
</div>
)
}
유저가 <input>에 타이핑하면 그 글자를 <div> 1만개 안에 집어넣어주어야 하는데
<div> 1만개를 렌더링해주느라 도 많은 지연이 발생한다.
타이핑한 결과가 n초씩 느리니 답답함..
import {useState, useTransition} from 'react'
let a = new Array(10000).fill(0)
function App(){
let [name, setName] = useState('')
let [isPending, startTransition] = useTransition()
return (
<div>
<input onChange={ (e)=>{
startTransition(()=>{
setName(e.target.value)
})
}}/>
{
a.map(()=>{
return <div>{name}</div>
})
}
</div>
)
}
useTransition()을 쓰면 그 자리에 [변수, 함수]가 남음
그 중 우측에 있는 startTransition()함수로 state변경함수를 묶어주면 그건 다른 코드들보다 나중에 처리해줌
그래서 <input> 타이핑같이 즉각 반응해야 하는걸 우선 처리해줄 수 있음.
물론 근본적인 성능개선이라기 보다는 특정 코드의 실행시점을 뒤로 옮겨주는 것 뿐임.
html이 많으면 여러 페이지로 쪼갤 것
isPending은 startTransition()으로 감싼 코드가 처리중일때 true로 변하는 변수임
{
isPending ? "로딩중기다리셈" :
a.map(()=>{
return <div>{name}</div>
})
}
그래서 이런식으로 코드짜는것도 가능함
useDeferredValue위 코드는 useTransition으로 감싼게 아직 처리중이면 "로딩중"을 보여주고, 처리완료되면 <div>{name}</div>
를 보여줌
startTransition() 와 용도는 같지만
useDeferredValue는 state나 변수를 하나만 집어넣을 수 있음.
그래서 그 변수에 변동사항이 생기면 그걸 늦게 처리해줌
import {useState, useTransition, useDeferredValue} from 'react'
let a = new Array(10000).fill(0)
function App(){
let [name, setName] = useState('')
let state1 = useDeferredValue(name)
return (
<div>
<input onChange={ (e)=>{
setName(e.target.value)
}}/>
{
a.map(()=>{
return <div>{state1}</div>
})
}
</div>
)
}
이렇게 쓰면 아까랑 같은 기능을 개발가능함.
useDefeferredValue안에 state를 집어넣으면 그 state가 변동사항이 생겼을 때 나중에 처리해줌.
그리고 처리결과를 let state에 저장해준다.