Hooks를 최적화하여 사용하기

Doeunnkimm·2023년 7월 7일
1

React

목록 보기
3/5
post-thumbnail

Hook

  • React에서 기존에 사용하던 Class형 컴포넌트에서 사용되던 메소드들 없이도
  • 함수형 컴포넌트에서 Hook을 통해 상태 관리와 여러 기능을 사용할 수 있도록 만든 기능

탄생 배경

  • React 컴포넌트는 클래스형 컴포넌트 / 함수형 컴포넌트로 나뉜다.
  • 기존의 개발 방식은 일반적으로 함수형 컴포넌트를 주로 사용하되
    • state나 Life Cycle Method를 사용해야 할 때에만 클래스형 컴포넌트를 사용하는 방식이었다.
    • 이유는 어려운 클래스 문법, 어려운 축소, 어려운 로직의 재사용성 등등
  • 이러한 단점이 있음에도, state나 Life Cycle Method를 사용하기 위해서는 클래스형 컴포넌트 사용을 해야만…

⭐ Hooks가 등장하고 함수형 컴포넌트에서도 state와 Life Cycle Method 사용이 가능해졌다.

  • 덕분에 클래스형 컴포넌트의 단점을 극복 + 상태 관리 + 생명주기 함수 사용까지도 가능해진 것!

State? Life Cycle Method?

State

  • 컴포넌트 내에서 관리되는 데이터
    • 컴포넌트의 동작과 상호작용을 제어하고
    • 컴포넌트가 렌더링될 때마다 변경되는 값을 저장하는 데 사용
    • state는 컴포넌트 내에서 변경 가능
  • state는 컴포넌트의 동적인 부분을 나타내며, 사용자 상호작용, 서버로부터의 데이터 로딩, 시간에 따른 변화 등과 같은 변동 사항을 표현하기 위해 사용

⭐ state는 렌더링의 트리거하는 주요한 역할을 한다.

  • state가 변경되지 않는다면, 불필요한 렌더링을 피하고, 성능을 개선하기 위해 UI 업데이트 수행 X
  • setState를 통해 React에게 상태 변경을 알리도록 되어 있다. → 그래서 직접 값을 변경한 경우 렌더링 X

Life Cycle Method(생명주기 메서드)

  • 컴포넌트가 브라우저상에 나타나고, 업데이트되고, 사라지게 될 때 호출하는 메서드들
  • 아래는 클래스형 컴포넌트의 생명주기 메서드들

  • 함수형 컴포넌트에서는 아래와 같이 사용되고 있다.
    분류클래스형 컴포넌트함수형 컴포넌트
    Mountingconstructor()함수형 컴포넌트 내부
    Mountingrender()return()
    MountingcomponentDidMount()ueEffect()
    UpdatingcomponentDidUpdate()useEffect()
    UnMountingcomponentWillUnmount()useEffect()

State 최적화를 위한 방법

State를 최적화한다는 개념

  • state는 렌더링의 트리거하는 주요한 역할을 한다고 했었다.
    ⭐ 이 말은 곧, UI를 업데이트하는 작업과 연관

⭐ UI 업데이트하는 데 비용이 많이 드는 DOM 작업 수를 최소화하는 것이 필요

State 업데이트 최적화가 중요한 이유

⭐ 성능 향상과 직접적인 연관

  1. 불필요한 렌더링 방지
    • 화면에 변화가 없는데도 불필요하게 컴포넌트를 다시 그리는 작업을 의미
    • 불필요한 렌더링은 불필요한 리소스 사용을 초래할 수 있다
    • 동일한 데이터를 중복해서 서버에게 요청하는 경우 네트워크 대역폭 낭비 및 서버의 부하까지도 이어질 수 있다
  2. 가상 DOM 비교 최소화
    • 가상 DOM 비교는 성능에 영향을 주는 계산적인 비용이 따르는 작업
    • 따라서 state 업데이트를 최적화하여 가상 DOM 비교를 최소화하면 React의 업데이트 성능 향상

목표는 불필요한 렌더링을 최소화하는 것, 이제 방법을 알아보자

방법1. Independent child, Careless parent

Render Waterfall

  • 구성 요소의 부모가 렌더링되면 모든 자식도 렌더링이 된다.
  • 부모 컴포넌트로 인한 복잡한 자식 컴포넌트들로 인해 불필요하게 자식 컴포넌트들까지 렌더링되는 경우

상태는 독립된 작은 부분에서만 관리하자

  • 특정 컴포넌트랑만 연관이 있는 상태를 부모 컴포넌트에서 관리할 경우
  • 해당 상태와 관련이 없는 자식 컴포넌트까지도 리렌더링을 하게 된다

⭐ 부모 컴포넌트는 자식 컴포넌트의 상태 변경에 신경쓰지 않고, 자식 컴포넌트가 자체적으로 상태를 관리하도록

→ 자식 컴포넌트의 상태 변경과 관련된 로직을 신경쓰지 않고,
→ 자식 컴포넌트를 렌더링하는 역할에 집중

📌 실험 내용

✔️ 부모 컴포넌트 - Child1 & Child2가 있다.
1️⃣ 상황1 : Child1와만 관련이 되어있는 상태를 부모 컴포넌트에서 관리
2️⃣ 상황2 : Child1와만 관련이 있는 상태를 Child1에서만 관리

※ Child2를 통해 불필요한 렌더링으로 인한 성능 차이를 유의미하게 측정하기 위해 Child2 컴포넌트에는 고화질의 사진이 포함되어 있다.

⚙️ 성능 측정은 Profiler 를 통해 진행 → Render duration 확인

상황1상황2
1.5ms0.8ms

예상대로, 불필요한 리렌더링을 줄여 렌더링 시간을 줄일 수 있었다.

방법2. Minimal states, Minimal render

  • 최소한으로 state를 선언하도록 해야 한다.
  • 한 state로 파생되어 사용될 수 있는 값들이 존재한다면
    • 따로 state를 선언하는 방법이 아니라
    • 기존 state를 가지고 화면을 렌더링할 수 있도록 해야 한다.
// Bad
const [count, setCount] = useState(0)
const [isEven, setIsEven] = useState(false)
const [isPrime, setIsPrime] = useState(false)
const [isPositive, setIsPositive] = useState(false)
const [isMultipleOfFive, setIsMultipleOfFive] = useState(false)

return (
    <>
      <h1>To Much State 😈</h1>
      <h3>count: {count}</h3>
      <h3>Is Even : {isEven ? 'Yes' : 'No'}</h3>
      <h3>Is Prime : {isPrime ? 'Yes' : 'No'}</h3>
      <h3>Is Positive: {isPositive ? 'Yes' : 'No'}</h3>
      <h3>Is Multiple of Five: {isMultipleOfFive ? 'Yes' : 'No'}</h3>
      <hr />
      <button onClick={increment}>증가</button>
    </>
  )

// Good
const [count, setCount] = useState(0)

return (
    <>
      <h1>Minimal State 👶🏻</h1>
      <h3>count: {count}</h3>
      <h3>Is Even : {count % 2 === 0 ? 'Yes' : 'No'}</h3>
      <h3>Is Prime : {count % 2 !== 0 ? 'Yes' : 'No'}</h3>
      <h3>Is Positive: {count > 0 ? 'Yes' : 'No'}</h3>
      <h3>Is Multiple of Five: {count % 5 === 0 ? 'Yes' : 'No'}</h3>
      <hr />
      <button onClick={increment}>증가</button>
    </>
  )

방법3. React.memo

  • 부모 컴포넌트 - Child1, Child2, Child3 컴포넌트가 있다고 했을 때
  • 부모 컴포넌트에서 관리하고 있는 state를 Child1과 Child2에게만 넘겨주어야 한다 하면
  • Child3는 부모 컴포넌트에서 state 업데이트가 일어났다고 한들 다시 그려질 필요가 없다
  • 이럴 때 Child3에 React.memo를 적용해주면 불필요한 리렌더링을 막아줄 수 있다.

Memoization 꼭 필요한가?

Memoization

  • 메모이제이션은 메모리 공간을 더 많이 사용하는 대가로 컴퓨터 프로그램 속도를 높이는 데 사용되는 최적화 기술
  • 메모이제이션을 통한 속도 향상은 동일한 매개 변수가 제공될 때 결과의 반복 계산을 피한다.
  • 대신에 캐시된 결과를 사용한다.
  • 캐시된 결과가 추가 공간을 차지하기 때문에 메모리 공간 사용량 증가

React에서 Memoization

  • 복잡한 구성 요소를 다시 렌더링하는 경우 → 성능 문제 → 사용자 경험에 영향
  • 동일한 입력으로 다시 실행할 때마다 값을 다시 계산하지 않고 캐시된 값을 사용하고 싶다면
    • ex. props가 이전과 동일하다면 굳이 다시 처음부터 화면을 그릴 필요가 없음
  • 초기 렌더링 결과를 캡처하고 나중에 사용할 수 있도록 메모리에 캐시 가능

⭐ 웹 성능 향상에 도움

React에서 Memoization을 활용하면 유용한 경우

  • 주로 복잡한 계산, 연산, 데이터 변화 등의 경우에 유용
  • 불필요한 연산을 줄이고 성능을 향상시키는 데 도움
  1. 계산 비용이 높은 연산
    • 계산 비용이 높은 연산을 수행해야 하는 경우
    • 메모이제이션을 사용하여 연산 결과를 캐시 가능
    • 이를 통해 동일한 입력에 대해 다시 계산하지 않고 이전에 계산된 결과를 재사용 가능
    • 그런데, 수천 개의 항목에 대해 루프를 수행하거나 팩토리얼 계산을 수행하지 않는 한 비용이 많이 들지 않을 수 있다
  2. 렌더링 성능 최적화
    • 컴포넌트의 state나 props가 변경되지 않았다면
    • 이전 결과를 다시 계산하지 않고 이전에 계산된 결과를 재사용하여 불필요한 렌더링 방지

React에서 모든 것을 메모해야 할까? “No!”

⭐ 메모이제이션은 무료가 아니다.

⭐ 무분별하거나 과도한 메모이제이션은 그만한 가치가 없을 수 있다.

  • 메모이제이션을 추가할 때 3가지 주요 비용이 발생
    1. 메모리 사용량 증가
      • 너무 많은 것을 메모하면 메모리 사용량 관리에 어려움
      • 메모리가 부족해지면 컴포넌트의 렌더링과 상태 업데이트에 소요되는 시간 증가할 수 있다
    2. 메모리 누수 가능성
      • 메모이제이션은 결과를 캐시하여 재사용
      • 이를 관리하지 않고 사용하는 경우 메모리 누수가 발생할 수 있다
      • 캐시된 결과가 더 이상 필요하지 않은 경우에도 계속해서 메모리에 남아있게 되는 경우
    3. 메모이제이션을 위한 추가적인 코드로, 코드 복잡성 증가
      • 메모이제이션은 캐시를 관리하고 관련된 종속성을 처리하는 추가적인 로직을 도입하게 된다
      • 너무 많이 사용하면 코드를 이해하기 어려워질 수 있고, 디버깅과 유지보수에 어려움이 생길 수 있다

어떤 경우에 메모이제이션을 피해야 할까?

  1. 최적화하려는 계산의 비용이 크지 않은 경우
    • 이러한 경우 메모이제이션 할 때 발생하는 오버헤드가 이점보다 클 수 있다.
    • React 공식 홈페이지에서는 1ms 이상 걸리는 경우 메모해두는 것이 좋다고 이야기
  2. 메모이제이션이 필요한지 확실하지 않은 경우
    • 우선 없이 작업을 하고, 문제가 발생하면 점진적으로 최적화를 적용하는 방향이 올바르다
  3. 의존성 배열이 너무 자주 변경되는 경우
    • 계산되는 경우가 많으면 성능적인 이점을 얻을 수 없다

참고 문서

profile
개발자와 사용자 모두의 눈👀을 즐겁게 하는 개발자가 되고 싶어요 :) 👩🏻‍💻

0개의 댓글