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(생명주기 메서드)
- 컴포넌트가 브라우저상에 나타나고, 업데이트되고, 사라지게 될 때 호출하는 메서드들
- 아래는 클래스형 컴포넌트의 생명주기 메서드들
- 함수형 컴포넌트에서는 아래와 같이 사용되고 있다.
분류 | 클래스형 컴포넌트 | 함수형 컴포넌트 |
---|
Mounting | constructor() | 함수형 컴포넌트 내부 |
Mounting | render() | return() |
Mounting | componentDidMount() | ueEffect() |
Updating | componentDidUpdate() | useEffect() |
UnMounting | componentWillUnmount() | useEffect() |
State 최적화를 위한 방법
State를 최적화한다는 개념
- state는 렌더링의 트리거하는 주요한 역할을 한다고 했었다.
⭐ 이 말은 곧, UI를 업데이트하는 작업과 연관
⭐ UI 업데이트하는 데 비용이 많이 드는 DOM 작업 수를 최소화하는 것이 필요
State 업데이트 최적화가 중요한 이유
⭐ 성능 향상과 직접적인 연관
- 불필요한 렌더링 방지
- 화면에 변화가 없는데도 불필요하게 컴포넌트를 다시 그리는 작업을 의미
- 불필요한 렌더링은 불필요한 리소스 사용을 초래할 수 있다
- 동일한 데이터를 중복해서 서버에게 요청하는 경우 네트워크 대역폭 낭비 및 서버의 부하까지도 이어질 수 있다
- 가상 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 확인
예상대로, 불필요한 리렌더링을 줄여 렌더링 시간을 줄일 수 있었다.
방법2. Minimal states, Minimal render
- 최소한으로 state를 선언하도록 해야 한다.
- 한 state로 파생되어 사용될 수 있는 값들이 존재한다면
- 따로 state를 선언하는 방법이 아니라
- 기존 state를 가지고 화면을 렌더링할 수 있도록 해야 한다.
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>
</>
)
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을 활용하면 유용한 경우
- 주로 복잡한 계산, 연산, 데이터 변화 등의 경우에 유용
- 불필요한 연산을 줄이고 성능을 향상시키는 데 도움
- 계산 비용이 높은 연산
- 계산 비용이 높은 연산을 수행해야 하는 경우
- 메모이제이션을 사용하여 연산 결과를 캐시 가능
- 이를 통해 동일한 입력에 대해 다시 계산하지 않고 이전에 계산된 결과를 재사용 가능
- 그런데, 수천 개의 항목에 대해 루프를 수행하거나 팩토리얼 계산을 수행하지 않는 한 비용이 많이 들지 않을 수 있다
- 렌더링 성능 최적화
- 컴포넌트의 state나 props가 변경되지 않았다면
- 이전 결과를 다시 계산하지 않고 이전에 계산된 결과를 재사용하여 불필요한 렌더링 방지
React에서 모든 것을 메모해야 할까? “No!”
⭐ 메모이제이션은 무료가 아니다.
⭐ 무분별하거나 과도한 메모이제이션은 그만한 가치가 없을 수 있다.
- 메모이제이션을 추가할 때 3가지 주요 비용이 발생
- 메모리 사용량 증가
- 너무 많은 것을 메모하면 메모리 사용량 관리에 어려움
- 메모리가 부족해지면 컴포넌트의 렌더링과 상태 업데이트에 소요되는 시간 증가할 수 있다
- 메모리 누수 가능성
- 메모이제이션은 결과를 캐시하여 재사용
- 이를 관리하지 않고 사용하는 경우 메모리 누수가 발생할 수 있다
- 캐시된 결과가 더 이상 필요하지 않은 경우에도 계속해서 메모리에 남아있게 되는 경우
- 메모이제이션을 위한 추가적인 코드로, 코드 복잡성 증가
- 메모이제이션은 캐시를 관리하고 관련된 종속성을 처리하는 추가적인 로직을 도입하게 된다
- 너무 많이 사용하면 코드를 이해하기 어려워질 수 있고, 디버깅과 유지보수에 어려움이 생길 수 있다
어떤 경우에 메모이제이션을 피해야 할까?
- 최적화하려는 계산의 비용이 크지 않은 경우
- 이러한 경우 메모이제이션 할 때 발생하는 오버헤드가 이점보다 클 수 있다.
- React 공식 홈페이지에서는 1ms 이상 걸리는 경우 메모해두는 것이 좋다고 이야기
- 메모이제이션이 필요한지 확실하지 않은 경우
- 우선 없이 작업을 하고, 문제가 발생하면 점진적으로 최적화를 적용하는 방향이 올바르다
- 의존성 배열이 너무 자주 변경되는 경우
- 계산되는 경우가 많으면 성능적인 이점을 얻을 수 없다
참고 문서