68일차

JiHun·2023년 7월 18일

부트캠프

목록 보기
48/56
post-thumbnail

React 심화

💼🗂️📑📄📝

💼 Virtual DOM

React에는 Virtual DOM이라는 가상의 DOM 객체를 사용한다. React에서 가상 DOM을 변경하게 되면 실제 DOM과 비교를 해서 차이점을 업데이트하는 방식으로 적용하여 실제 DOM을 바꾼다.

실제 DOM을 비교하는 것이 아니라, 실제 DOM을 비교적 가벼운 가상 DOM으로 바꾼 다음, 변경된 가상 DOM과 이 실제 DOM의 복사본을 비교한다.

🗂️ 왜 Virtual DOM이 생겼나?

이걸 설명하기 전에 Real DOM에 대해서 알 필요가 있다.

📑 Real DOM

Virtual DOM과 구분하기 위해 Real DOM을 썼지만 그냥 DOM이다. Document Object Model을 가리키며, HTML 태그를 JavaScript같은 스크립팅 언어가 접근해서 조작할 수 있도록 HTML 문서를 읽어서 문서를 트리 구조로 객체화한 것이다.

HTML을 브라우저에서 읽는다면 DOM이 생성되고, 조작을 할 수 있게 된다.

그래서 왜? Vitual DOM이 생겼나? 그냥 조작하면 되는거 아닌가?

📑 브라우저 렌더링 과정

그걸 알려면 브라우저의 렌더링 과정을 알아야 한다.

  1. HTML 파싱: 브라우저는 서버로부터 전송된 HTML 문서를 받아서 파싱한다. 이 단계에서는 HTML 문서의 구조를 이해하고 DOM(Document Object Model)트리를 생성한다.
  2. CSS 파싱: CSS 스타일 시트를 파싱한다. 이 단계에서는 각 요소의 스타일 정보를 추출하고, 스타일 규칙을 적용하여 각 요소의 최종 스타일을 계산한다.
  3. 렌더 트리 구성: DOM 트리와 CSS 스타일 정보를 결합하여 렌더 트리(Render Tree)를 구성한다. 렌더 트리는 실제로 화면에 표시되는 요소들로 이루어져 있으며, 각 요소는 화면에 어떻게 배치되는지에 대한 정보를 가지고 있다.
  4. 레이아웃: 렌더 트리를 기반으로 각 요소의 크기와 위치를 계산하는 단계. 브라우저는 요소의 크기, 위치, 여백 등을 결졍하여 화면에 배치한다. 이 단계는 "리플로우(reflow)" 또는 "레이아웃(layout)"이라고 한다.
  5. 페인팅: 렌터 드리의 각 요소에 대해 실제로 화면에 그리는 단계. 브라우저는 각 요소의 스타일을 기반으로 화면에 픽셀, 배경색, 텍스트, 이미지 등을 표시한다. 이 단계는 "페인팅(painting)" 또는 "렌더링(rendering)"이라고 한다.
  6. 반응성 및 상호작용: 브라우저는 사용자의 입력에 따라 렌더링 과정을 업데이트하고, 반응성을 제공한다. 이 단계에서 이벤트 처리, 애니메이션, 스크롤 등의 기능이 수행된다.

DOM이 업데이트 된다는 것은 결국 브라우저를 렌더링하고 리플로우한다는 것이다.

DOM을 직접 조작하게 되면, 각 요소의 크기와 위치를 계산하는 레이아웃(리플로우), 화면에 그려내는 페인팅(렌더링)이 재실행되게 된다. 이때 변화가 필요없는 부분도 변경되면서 성능을 떨어뜨리는 문제가 발생하게 된다.

그래서 실제 DOM을 건드려 조작하는 것은 리플로우, 리페인팅 부분에서 쓸데없는 자원 낭비가 있을 수 있다는 것이다. 바뀌는 부분만 업데이트를 하는 것의 필요성이 생겼다.

Virtual DOM을 사용하여 실제 DOM과 Virtual DOM의 차이점을 비교하여 부분만 업데이트한다. 조작을 최소화하고 성능을 최적화 시키는 기술이다.

🗂️ React Diffing Algorithm

React는 기존 가상 DOM과 변경된 새로운 가상 DOM을 비교할 때, React 입장에서는 변경된 새로운 가상 DOM 트리에 부합하도록 기존의 UI를 효율적으로 갱신하는 법이 필요했다.


💼 React Hooks

지금은 React에서 함수형 컴포넌트를 쓴다. 과거 클래스형 컴포넌트 대신에.
React에서는 Hook을 사용한다. Hook은 함수 컴포넌트에서 사용하는 함수.

🗂️ Component & Hook

📑 Class Component

클래스 컴포넌트를 복잡해질수록 이해하기 어려워졌고, 컴포넌트 사이에서 상태 로직을 재사용하기 어렵다는 단점이 있었다.

단점

  1. 상속 기반 구조: 상속을 통해 컴포넌트를 확장한다. 하지만 상속은 강한 결합을 유발한다. 즉, 상속은 상위 클래스의 변경이 하위 클래스에도 영향을 줄 수 있으므로, 컴포넌트의 구조를 수정하기 어려울 수 있다.
  2. 생명주기 관리: 클래스형 컴포넌트는 복잡한 생명주기 메서드를 가지고 있다. 생명주기 메서드는 컴포넌트의 초기화, 업데이트, 소멸과 관련된 작업을 해야 한다. 이러한 생명주기 메서드는 컴포넌트의 상태 변화를 추적하고 제어하기 위해 사용되는데, 이로 인해 컴포넌트의 로직이 복잡해 질 수 있고, 재사용성을 저하 시킬 수 있다.
  3. 코드 가독성과 복잡성

📑 Function Component

클래스 컴포넌트에는 constructor 생성자에 state를 직접 작성해서 사용했지만 함수형 컴포넌트에서는 useState() Hook을 사용해 state를 사용할 수 있게 한다.

🗂️ Hook이란?

Hook은 클래스형 컴포넌트를 쓰지 않아도 함수형 컴포넌트에서 state와 같은 다른 React의 기능을 사용할 수 있게 해주는 것이다.

Hook은 리액트 함수의 최상위에서만 호출해야한다.
오직 리액트 함수 내에서만 사용해야 한다.

React Hook은 함수 컴포넌트가 상태를 조작 및 최적화 기능을 사용할 수 있게끔 하는 메서드다.

하지만, 컴포넌트는 기본적으로 상태가 변경되거나 부모 컴포넌트가 렌더링 될 때마다 리렌더링을 하는 구조로 이루어져 있다. 많은 렌더링은 성능에 영향을 미친다.

그래서, 렌더링 최적화를 위한 Hook이 존재한다. useMemo, useCallback 같은 것들이 있다.

useMemo

const [a, setA] = useState(0);
const [b, setB] = useState(0);

const add = (a, b) => {
	return a + b;
}

const memoizedValue = useMemo(() => add(a, b), [a, b]);

컴포넌트 안에 이런 코드가 작성되어있다고 하자. 그냥 add() 함수만 있다면, a나 b의 상태가 업데이트 될때마다, add() 함수가 계속해서 호출될 것이다. a,b의 값을 가지고 업데이트되게 때문에 상관은 없지만,add() 함수에 들어가는 a, b 인자와 전혀 상관없은 다른 상태가 업데이트 된다면 어떻게 될까?
바로 add() 함수가 재정의 된다!

이렇게 되면 렌더링에서 비효율적인 부분이 생겨나는 것이다. 이걸 해결하기 위해 memoization 개념을 사용한다.

useMemo를 활용하여, 렌더링 되었을 때 a,b 값이 이전과 같다면 함수를 실행하지 않고 a,b 값을 재활용하고, a, b 값이 달라지면 새로 리턴하면서 값을 저장한다.

🗂️ useCallback

useCallback도 useMemo처럼 Memoization을 활용하는 Hook이다. 이건 함수를 대상으로 한다.
결국 useCallback이나 useMemo는 dependency에 들어가는 상태에 따라 렌더링 여부를 결정한다.


const memoizedCallback = useCallback(() => doSomething(), [dependency])

🗂️ Memoization

알고리즘에서 자주 나오는 개념이다. 기존에 수행한 연산의 결과값을 메모리에 저장해두고, 동일한 입력이 들어오면 재활용하는 프로그래밍 기법이다. 이것은 중복 연산을 할 필요가 없이 때문에 앱의 성능을 최적화할 수 있다.

요약

useMemo는 계산 비용이 많이 드는 함수의 결과 값을 저장하고 재사용하기 위해 사용된다. 이 Hook은 의존성 배열(dependency array)을 가지며, 의존성 배열에 포함된 값이 변경되지 않으면 이전에 계산한 값을 반환한다. 따라서, 'useMemo'를 사용하면 계산 비용이 큰 함수의 결과를 캐시하여 성능을 향상시킬 수 있다.

useCallback은 함수를 저장하고 재사용하기 위해 사용된다. 이 Hook은 함수와 의존성 배열을 가지며, 의존성 배열에 포함된 값이 변경되지 않으면 이전에 저장한 함수를 반환한다. useCallback은 주로 자식 컴포넌트에게 전달되는 콜백 함수들을 최적화하는 데 사용된다. 예를 들어, 부모 컴포넌트에서 자식 컴포넌트에게 콜백 함수를 전달하는 경우, useCallback을 사용하여 해당 함수를 메모이제이션(Memoization)하고 자식 컴포넌트가 다시 렌더링되어도 동일한 함수 인스턴스를 사용할 수 있다.

profile
안녕하세요. 프론트엔드 개발자 송지훈입니다.

1개의 댓글

comment-user-thumbnail
2023년 7월 18일

정말 잘 읽었습니다, 고맙습니다!

답글 달기