본문 바로가기

Frontend Study - 2/React

React : 성능개선 lazy , memo, useMemo , useTransition, useDeferredValue

1. Lazy

 

리액트는 SPA (Single Page Application) 이기 때문에,

결국 하나의 js 파일에 모든 정보가 다 담기게 된다. 

 

그렇기 때문에 리액트로 만들어진 사이트에 들어갈 때, 처음에 모든 정보를 다 로딩하는 과정에서 버벅 거리는 경우가 있다.

 

사실 첫 페이지에서 굳이 로딩이 필요하지 않은 페이지들이 있다. 

그래서 그런 페이지들의 경우 바로 로딩하지 말고, 필요할 때 import 하도록 해 ! 라는 기능을 부여하는 것이 lazy 이다.

 

Import {lazy} from “react”;

//import Detail from ‘./routes/Detail.js’
//import Cart from ‘./routes/Cart.js’

const Detail = lazy(() => import(‘./routes/Detail.js’));
const Cart = lazy(() => import(‘./routes/Cart.js’));

 

위와 같이 import를 해오고 사용하게 되면,

Detail 페이지는 처음 사이트에 들어갈 때 같이 로딩되는 것이 아니라, 필요한 경우에 로딩 된다.  이렇게 하게 되면 사이트 발행할 때에도 별도의 Js파일로 분리된다. 

단점이라고 한다면, 결국 첫 화면에서 로딩해오지 않았으니, 따로 lazy걸어둔 페이지로 이동할 때 로딩시간이 발생 한다는 것이다.

 

그 로딩 시간 중간에 '로딩중입니다.' 와 같은 메시지나 페이지를 보여주고 싶다면 

'Suspense'를 통해 가능하다. 

import {lazy, Suspense} from “react”

 	<Suspense fallback={<div> 로딩중 ! </div>}>
          <Route path="/detail" element={<Detail />} />
        </Suspense>

위와 같이 <Suspense> </Suspense> 로 묶어 주게 되면 detail페이지 로딩시간 중에, <Suspense>의 속성값으로 준 fallback={} 안에 들어 있는 내용이 표시된다.

모든 화면들에서 같은 '로딩 중' 페이지가 나오게 하고 싶다면 아예 Routes 자체를 Suspense안에 넣는 것도 좋은 방법이다.

 

 

 

2. memo, useMemo

 

1) memo

부모 컴포넌트 안에 다른 자식 컴포넌트들이 있을 때, 

부모 컴포넌트의 state가 변화 됨에 따라 페이지가 재렌더링 되었을때,

굳이 자식 컴포넌트의 재렌더링이 필요하지 않은 경우가 있다. 

 

function Child(){
  console.log('재렌더링됨')
  return <div>자식임</div>
}

function Cart(){ 

  let [count, setCount] = useState(0)

  return (
    <Child />
    <button onClick={()=>{ setCount(count+1) }}> + </button>
  )
}

 

위의 경우 굳이 Child컴포넌트의 재렌더링이 필요하지 않은 상황이나, 버튼이 클릭되고 count +1 이 될 때마다 자식요소가 재렌더링 되고 있는 상황이다. 이것은 성능저하를 유발한다. 이 때 사용할 수 있는 것이 memo 이다. 메모이징! 

 

 

import {memo, useState} from 'react'

let Child = memo( function(){
  console.log('재렌더링됨')
  return <div>자식임</div>
})

 

이렇게 memo를 Import 해온 이후에, 컴포넌트를 memo안에 담아 변수 선언 해주면 된다. 

컴포넌트를 인자로 받아서 새로운 컴포넌트를 만들어 주는 것.

컴포넌트 -> react.memo -> 새로운 컴포넌트

memo의 원리는 이렇다. 

 

메모는 자식요소에 전송되는 Props가 변할 때만 재 렌더링 되게 한다. prop check 를 통해 변화된 내용이 있다면 렌더링 하고 없으면 기존의 것을 재사용. 너무 남용해도 좋지 않은 이유는, 이 '메모'를 사용할 때는 결국 기존 props와 신규 props가 같은 지에 대한 비교 작업이 필요하기 때문이다. 만약 자식 요소로 전달하는 props가 길고 복잡하다면, 오히려 메모를 사용하는 것이 성능적으로 손해일 수 있다.

 

그러므로 무거운 컴포넌트들에 선별적으로 사용하는 것이 좋겠다. 

 

리액트 메모는 props 변화에만 의존하는 최적화 방법

 

 

 

2) useMemo

useEffect와 매우 유사하다. 사용법도.

import {useMemo, useState} from 'react'

function 함수(){
  return 반복문10억번돌린결과
}

function Cart(){ 

  let result = useMemo(()=>{ return 함수() }, [])

  return (
    <Child />
    <button onClick={()=>{ setCount(count+1) }}> + </button>
  )
}

어떤 함수가 있을 때 그것을 useMemo 안에 넣고 실행할 경우 컴포넌트 로드시 1회만 실행된다. 

재렌더링마다 동작하지 않는 다는 것, useEffect처럼 dependency를 만들어서 특정 state나 props 변경시에도 활용할 수 도 있다. 리턴값과 비교해서 다른 경우에만 다시 렌더링한다. 

 

useEffect와의 차이점은 useEffect의 경우 렌더링이 끝나고 난 뒤에 실행이 되는 반면, 

useMemo는 렌더링 중 같이 실행이 된다. 

 

 

 

3. useTransition, useDefferedValue

 

1) useTransition

 

렌더링 시간이 매우 오래걸리는 컴포넌트가 있을 때, 

나중에 해도 되는 것들을 나중에 렌더링되게 해서 급한 것들을 빨리 렌더링되게 할 수 있는 기능이 있다. 

 

import {useState} from 'react'

let a = new Array(10000).fill(0)

function App(){
  let [name, setName] = useState('')
  
  return (
    <div>
      <input onChange={ (e)=>{ setName(e.target.value) }}/>
      {
        a.map(()=>{
          return <div>{name}</div>
        })
      }
    </div>
  )
}

인풋칸에 이름을 입력하면 name에 내용을 setName하는 과정과

a라는 0이 10000개 들어있는 배열을 매핑돌리는 과정.

과정이 매우 무겁다 보니, 그래서 <input> 타이핑같이 즉각 반응해야하는 작업이 버벅거리면서 실행된다. 

 

 

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>
  )
}

이렇게 transtion으로 묶어주면 해당 작업의 우선순위가 뒤로 미뤄지기 때문에

그래서 <input> 타이핑같이 즉각 반응해야하는걸 우선적으로 처리해줄 수 있다.

하지만 근본적인 성능개선이 아니니 html이 많으면 여러페이지로 쪼개는 것이 좋다.

 

{
  isPending ? "로딩중기다리셈" :
  a.map(()=>{
    return <div>{name}</div>
  })
}

startTransition() 으로 감싼 코드가 처리중일 때 true로 변하는 변수이다.

위의 코드는 useTransition으로 감싼게 처리완료되면 <div>{name}</div> 나오게된다.

 

 

2) useDefferedValue

transition과 비슷하지만 state나 변수하나를 집어넣을 수 있게 되어있다.

그래서 그 변수에 변동사항이 생기면 그걸 늦게 처리해준다. 

그리고 처리결과는 let 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>
  )
}