- Published on
성능최적화
- Authors
- Name
- 길재훈
 
 
성능최적화
성능 최적화를 위해 여러가지 방법이 존재한다. 각각의 방법을 살펴본다.
기본적으로 리렌더링 되는것을 알아보는것은 중요하다. 다음을 살펴보자
import { useState } from 'react'
const MemoizationPage = (): JSX.Element => {
  console.log('컴포넌트가 렌더링 되었습니다')
  let countLet = 0
  const [countState, setCountState] = useState(0)
  const onClickCountLet = (): void => {
    console.log(countLet + 1)
    countLet += 1
  }
  const onClickCountState = (): void => {
    console.log(countState + 1)
    setCountState(countState + 1)
  }
  return (
    <>
      <div>카운트[let]: {countLet}</div>
      <button onClick={onClickCountLet}>카운트[let] + 1 올리기</button>
      <div>카운트[state]: {countState}</div>
      <button onClick={onClickCountState}>카운트[state] + 1 올리기</button>
    </>
  )
}
export default MemoizationPage
위의 로직을 살펴보면 매우 명확하게 rerender 이 되는것을 알수있다. 카운트[let] + 1 올리기 를 누르면, 해당 값이 올라가지만 화면상에 나오지는 않는다.
하지만 카운트[state] + 1 올리기 를 누르면 리렌더링 되면서 let의 값이 초기화되는것을 알수 있다. 그렇다면 해당 값을 지속 유지할 수 있는 방법이 없을까?
그때 사용하는 hooks 가 바로 useMemo 이다. useMemo 는 해당 값을 cache 해 놓고, 해당 cache 한 값을 재사용할 수 있다.
useMemo 의 특징으로 한번 연산해 놓고 연산된 값을 리턴하면 그 다음부터는 리턴된 값만을 사용한다. 다음을 보자
const bbb = useMemo(() => {
  let result = 0
  for (let i = 0; i < 900000; i += 1) {
    result += i
  }
  return result // 이후 연산된 result를 저장해서 사용한다.
}, [])
useCallback
useCallback 역시 useMemo 와 비슷하지만, 함수를 저정한다는 것이 다르다 다음을 보자.
const onClickCountLet = useCallback((): void => {
  console.log(countLet + 1)
  countLet += 1
}, []) // useCallback 으로 함수를 저장함
useCallback 사용시 주의해야할 사항이 존재한다. useCallback 은 state 를 사용시 주의해야 한다.
다음을 보자
const onClickCountState = useCallback((): void => {
  console.log(countState + 1)
  setCountState(countState + 1) // 이때 countState 값 역시 기억하므로, 값이 변경되지 않는다.
}, [])
위의 setCountState 를 사용하면 제대로 동작하지 않는다. usecallback 시 해당 내용을 기억하고 있기에 변경자체가 되지 않는것이다.
useCallback 사용시 state 를 사용한다면 다음처럼 prev state 를 사용하여 확인해야 한다.
const onClickCountState = useCallback((): void => {
  console.log(countState + 1)
  setCountState((prev) => prev + 1) // 제대로 작동한다
}, [])
이러한 방식으로 useCallback 을 사용할 수 있다
useMemo 를 이용한 useCallback
useMemo 를 사용해 단순한 useCallback 을 만들 수 있다
const onClickCountState = useMemo(() => {
  return (): void => {
    console.log(countState + 1)
    setCountState((perv) => prev + 1)
  }
}, [])
위처럼 함수를 useMemo 해서 return 하면 useCallback 과 똑같이 행동한다. useCallback 처럼 작동하므로 setCountState 를 perv 를 사용해서 +1 시켰다.
memo
memo 는 컴포넌트 자체를 memo 하여, 부모 컴포넌트가 리렌더링 될때 자식 컴포넌트의 변경사항이 없다면 리렌더링 하지 않는다
이는 성능최적화 작업시 중요한 부분이다. 다음을 살펴보자
const MemoizationPage = (): JSX.Element => {
  console.log('부모 컴포넌트가 렌더링 되었습니다')
  let countLet = 0
  const [countState, setCountState] = useState(0) // 기존의 값을 기억하고 있다.
  const onClickCountState = useCallback((): void => {
    console.log(countState + 1)
    // setCountState(countState + 1); // 이때 countState 값 역시 기억하므로, 값이 변경되지 않는다.
    setCountState((countState: number) => countState + 1)
  }, [])
  return (
    <>
      <div>카운트[state]: {countState}</div>
      <button onClick={onClickCountState}>카운트[state] + 1 올리기</button>
      <MemoizationWithChildPage />
    </>
  )
}
export default MemoizationPage
위처럼 부모 컴포넌트가 자식 컴포넌트를 가진 상황이라고 생각해보자. 만약 MemoizationWithChildPage 가 memo 된 상황이 아니라면 MemoizationPage 의 setCountState가 실행될때마다 MemoizationWithChildPage 는 리렌더링 된다.
굉장히 불필요한 상황이다. MemoizationWithChildPage 는 변경되는 값이 없는데 부모 컴포넌트가 렌더링 되었다는것 하나만으로 지속 리렌더링 되는것이다.
memo 를 사용하면 이러한 불필요한 rerendering 이 발생하지 않는다
import React, { memo } from 'react'
const MemoizationWithChildPage = (): JSX.Element => {
  console.log('자식이 렌더링 됩니다.')
  return (
    <>
      <div>저는 자식 컴포넌트 입니다!!!</div>
    </>
  )
}
export default memo(MemoizationWithChildPage)
이때 memo 사용시 주의해야할 점이 있다 만약 memo 를 map 에 적용해 보도록 해보자.
// MemoizationWithMapParentPage.tsx
const MemoizationWithMapParentPage = (): JSX.Element => {
  const [data, setData] = useState('철수는 오늘 점심을 맛있게 먹었습니다.')
  const onClickChange = (): void => {
    setData('영희는 오늘 저녁을 맛없게 먹었습니다.')
  }
  return (
    <>
      {data.split(' ').map((el, index) => (
        <Word key={index} el={el} />
      ))}
      <button onClick={onClickChange}>클릭</button>
    </>
  )
}
export default MemoizationWithMapParentPage
import React, { memo } from 'react'
interface IWordProps {
  el: string
}
const Word = (props: IWordProps): JSX.Element => {
  console.log('자식이 렌더링 됩니다', props.el)
  return <span>{props.el}</span>
}
export default memo(Word)
위의 로직은 Wrod 컴포넌트 를사용하여 map 을 순회한다. 순회된 map 에 key 값으로 고정된 index 를 주므로, change 된 문자만 변경된다.
즉 영희는, 저녁을, 맛없게 만 리렌더링 된다.
이는 우리가 원하는 map 으로써의 기능에 충실한 상황이다. 그런데 key 값을 index 로 주는것은 탐탁치 않다. 그러므로 uuid 를 주도록 하자.
import React, { useState } from "react";
import Word from "./02-child";
import { v4 as uuidv4 } from "uuid";
const MemoizationWithMapParentPage = (): JSX.Element => {
  const [data, setData] = useState("철수는 오늘 점심을 맛있게 먹었습니다.");
  const onClickChange = (): void => {
    setData("영희는 오늘 저녁을 맛없게 먹었습니다.");
  };
  return (
      {data.split(" ").map((el) => (
        <Word key={uuidv4()} el={el} />
      ))}
      <button onClick={onClickChange}>체인지</button>
    </>
  );
};
export default MemoizationWithMapParentPage;
이때 변경되는 문구는 영희는, 오늘, 저녁을, 맛없게, 먹었습니다. 로 전부다 변경되는것을 볼수 있다.
이러한 이유는 memo 는 전달되는 props 가 변경되어도 리렌더링 되기 때문이다. 위의 key 는 props 로 넘어가는 uuidv4() 를 사용하여 매번 key 값이 변경된다.
변경된 key 값으로 인해 memo 가 재역할을 못하고 있는 상황으로 매번 리렌더링 된다. 이러한 부분을 상당히 조심히 생각하고 코드를 작성해야 한다.
브라우저의 rendering 과정
                  |--[ HTML ] -- [ DOM ]   |
                  |                        |
[ download ] -->  |                        |-[Attach]--[reflow]--[repaint]--[composition]--[display]
                  |                        |
                  |--[ CSS ] -- [ CSSDOM ] |
실상 repaint 만 작동시 큰 영향을 미치지는 않는다. 여기서 문제를 일으키는 상황은 reflow 이다.
reflow 시 repaint 까지 같이 진행되므로, 레이아웃 변경이 커질수로 재계산이 많이 일어난다 이러한 모든 상황을 예상하여 코드를 작성하는 것은 한계가 있지만, 관심을 갖고 생각보는것도 좋다.
Layout shift
Layout shift는UX/UI에서 중요하게 생각하는 것중하나이다.
이미지의 로딩이 느려 화면 레이아웃이 밀리는 현상을 말한다.
frontend 는 UX/UI 에 대해서 중요하게 생각해야 하므로, 이러한 detail 한 부분도 신경써서 만들 필요가 있다