-
[React] 심화FE 2023. 7. 18. 20:02
Virtual DOM
➡️ JavaScript 객체로 이루어진 가상의 DOM 트리를 사용하여 실제 DOM 조작을 최소화하고 성능을 최적화하는 기술
DOM의 형태 : 가상 DOM은 추상화된 자바스크립트 객체의 형태를 가지고 있음
Virtual DOM의 동작 과정
- 리액트는 상태 변경하는 작업 (e.g. 이벤트)이 일어났을 때, 가상 DOM에 저장된 현재와 이전을 비교하는데 리액트는 Diffing 알고리즘 사용하여 변경된 부분을 감지함
- 알고리즘이 직접 감지할 수 있도록 setState와 같은 메서드를 활용해 상태 변경
- 가상 DOM을 새로운 가상 DOM과 비교해 필요한 부분만 반영하는데 이것을 재조정 Reconciliation 이라고 함
- 여러개의 상태 변화가 있을 경우엔 한 번에 업데이트를 해서 성능 최적화, 불필요한 리렌더링 최소화함
Real DOM (DOM) : Document Object Model 문서 객체 모델. (문서 객체란 브라우저가 자바스크립트와 같은 스크립팅 언어가 <html>, <head>, <body>와 같은 태그들에 접근하고 조작할 수 있도록 문서를 트리구조로 객체화 한 것)브라우저가 HTML 문서를 조작할 수 있도록 트리 구조화한 객체 모델.
DOM 조작 속도가 느려지는 이유
- 저장된 데이터를 더 효과적으로 탐색하기 위해 사용, 빠른 자료 탐색 성능이 장점
- 자바스크립트 같은 스크립팅 언어가 접근하고 탐색하는 속도가 빠르기 때문
- DOM이 변경되고 업데이트가 되는 것은 브라우저의 렌더링 엔진 또한 리플로우 한다는 것을 의미
- DOM 조작이 많아질수록 리플로우가 발생하여 업데이트 비용이 커질 수 있고 프레임드롭인 치명적인 UX문제 발생할 수 있음
React가 DOM 트리 탐색하는 방법
➡️ 트리의 레벨 순서대로 순회 탐색. 같은 레벨(위치)끼리 비교 (너비 우선 탐색 BFS)
1. 다른 타입의 DOM 엘리먼트인 경우
➡️ DOM 트리는 각 HTML 태그마다 규칙이 있어 그 아래 들어가는 자식 태그가 한정적이라는 특징을 갖고 있음.
(e.g. <ul> 태그 밑에 <li> 태그만 와야한다던가 <p> 태그 안에 <p> 태그를 또 쓰지 못함)
부모 태그가 바뀌면 기존의 트리를 버리고 새로운 트리를 구축하기 때문에 이전 노드들은 전부 파괴됨.
<div> <Counter /> </div> //부모 태그가 div에서 span으로 바뀝니다. <span> <Counter /> </span>
<div>가 <span>으로 바뀌면 자식 노드인 <Counter />는 완전히 해제되며 <div> 태그 속 <Counter />는 파괴되고 <span> 태그 속 새로운 <Counter />가 실행됨
2. 같은 타입의 DOM 엘리먼트인 경우
➡️ React는 최대한 렌더링을 하지 않는 방향으로 최소한의 변경 사항만 업데이트함. 실제 DOM이 아닌 가상 DOM을 조작하기 때문.
하나의 DOM 노드를 처리한 뒤 리액트는 뒤이어서 해당 노드들 밑의 자식들을 순차적으로 동시에 순회하면서 차이가 발견될 때마다 변경하는데 이것을 재귀적 처리라고함
<div className="before" title="stuff" /> //기존의 엘리먼트가 태그는 바뀌지 않은 채 className만 바뀌었습니다. <div className="after" title="stuff" />
3. 자식 엘리먼트의 재귀적 처리
➡️ 자식 노드를 순차적으로 위에서부터 아래로 비교하면서 바뀐점을 찾고 첫 번째 자식노드와 두 번째 자식 노드가 일치하면 세 번째 자식 노드를 추가함. 위에서 아래로 비교하기 때문에 리스트의 처음에 엘리먼트를 삽입하게 되면 이전의 코드에 비해 훨씬 나쁜 성능을 냄. 이러한 문제를 해결하기 위해 key 속성을 사용함. (key가 없으면 비효울적으로 동작)
<ul> <li>first</li> <li>second</li> </ul> //자식 엘리먼트의 끝에 새로운 자식 엘리먼트를 추가했습니다. <ul> <li>first</li> <li>second</li> <li>third</li> </ul>
4. 키 (key)
➡️ 자식 노드가 key를 갖고 있으면 리액트는 그 key를 이용해 기존 트리의 자식과 새로운 트리의 자식이 일치하는지 확인할 수 있음. key는 보통 유니크한 값(ex. Id)를 부여해주는데 전역적으로 유일할 필요 없고 형제 엘리먼트 사이에서만 유일하면 됨. 만약 유니크한 값이 없으면 최후 수단으로 배열의 인덱스 key로 사용 가능(비효율로 동작할 수 있음)
<ul> <li key="2015">Duke</li> <li key="2016">Villanova</li> </ul> //key가 2014인 자식 엘리먼트를 처음에 추가합니다. <ul> <li key="2014">Connecticut</li> <li key="2015">Duke</li> <li key="2016">Villanova</li> </ul>
React Hooks
Class Component
➡️ 클래스 컴포넌트는 복잡해질수록 이해하기 어렵고 로직 재사용이 어렵다는 단점이 있음. 자바스크립트의 this 키워드 동작 방식도 알기 힘들기 때문에 클래스 컴포넌트에서 Hook이라는 개념을 도입해 함수 컴포넌트로 넘어감
class Counter extends Component { constructor(props) { super(props); this.state = { counter: 0 } this.handleIncrease = this.handleIncrease.bind(this); } handleIncrease = () => { this.setState({ counter: this.state.counter + 1 }) } render(){ return ( <div> <p>You clicked {this.state.counter} times</p> <button onClick={this.handleIncrease}> Click me </button> </div> ) } }
Function Component
➡️ 함수형 컴포넌트는 클래스형 컴포넌트보다 직관적이며 보기쉬움. Counter 컴포넌트에서 숫자를 올리기 위해 상태값을 저장하고 사용할 수 있게 해주는 useState()는 Hook임. Hook을 호출해 함수 컴포넌트 안에 state를 추가한 형태이며 state는 리렌더링 되어도 그대로 유지함(때에 따라 State Hook 여러개 사용 가능)
function Counter () { const [counter, setCounter] = useState(0); const handleIncrease = () => { setCounter(counter + 1) } return ( <div> <p>You clicked {counter} times</p> <button onClick={handleIncrease}> Click me </button> </div> ) }
Hook
➡️ class를 작성하지 않고도 state와 다른 React의 기능들을 사용할 수 있게 해줌. 함수형 컴포넌트에서 상태값 및 다른 여러 기능을 사용하기 편리하게 해주는 메서드(클래스형 컴포넌트에서는 동작하지 않음)
Hook 사용 규칙
1. 리액트 함수의 최상위에서만 호출 : 반복문, 조건문, 중첩된 함수 내에서 Hook을 실행하면 예상한대로 동작하지 않을 수 있음
2. 오직 리액트 함수 내에서만 사용 : 리액트 함수형 컴포넌트나 커스텀 Hook이 아닌 다른 일반 자바스크립트 함수 안에서 호출해서는 안 됨
useMemo
➡️ 특정 값(value)을 재사용하고자 할 때 사용하는 Hook
/* useMemo를 사용하기 전에는 꼭 import해서 불러와야 합니다. */ import { useMemo } from "react"; function Calculator({value}){ const result = useMemo(() => calculate(value), [value]); return <> <div> {result} </div> </>; }
value는 일종의 값으로 렌더링 할 때마다 값이 계속 바뀌는게 아니라고 할 때, 저장해뒀다가 다시 꺼내 쓸 수 있으면 calculate 함수를 호출할 필요가 없음. 이럴 때 useMemo를 호출하여 calculate를 감싸주면 이전에 구축된 렌더링과 새로 구축된 렌더링을 비교해 value 값이 동일할 경우 이전 렌더링의 value 값을 재사용할 수 있음. 메모이제이션(Memoization)과 긴밀한 관계가 있음
Memoization : 알고리즘에서 자주 나옴. 기존에 수행한 연산의 결과값을 메모리에 저장해서 동일한 입력이 들어오면 재활용하는 프로그래밍 기법. 앱의 성능을 최적화할 수 있음
useCallback
➡️ useMemo와 마찬가지로 메모이제이션 기법을 이용한 Hook. 함수의 재사용을 위해 사용함
/* useCallback를 사용하기 전에는 꼭 import해서 불러와야 합니다. */ import React, { useCallback } from "react"; function Calculator({x, y}){ const add = useCallback(() => x + y, [x, y]); return <> <div> {add()} </div> </>; }
해당 컴포넌트가 리렌더링 되더라도 x와 y가 바뀌지 않는다고 했을 때, 함수도 메모리 어딘가에 저장해 뒀다가 다시 꺼내서 쓸 수 있다. useCallback Hook을 사용하면 함수가 의존하는 값들이 바뀌지 않는한 기존 함수를 계속해서 반환하고 x와 y 값이 동일하면 다음 렌더링 때 이 함수를 다시 사용.
(메모리 어딘가에 함수를 꺼내서 호출하는 Hook이라 자식 컴포넌트의 props로 함수를 전달해줄 때 사용하기 좋음)
useCallback과 참조 동등성
➡️ useCallback은 참조 동등성에 의존하는데 기본적으로 자바스크립트 문법을 따라감.(자바스크립트에서 함수는 객체) 객체를 메모리에 저장할 때 값의 주소를 저장하기 때문에 반환하는 값이 같아도 일치연산자로 false가 출력됨
function doubleFactory(){ return (a) => 2 * a; } const double1 = doubleFactory(); const double2 = doubleFactory(); double1(8); // 16 double2(8); // 16 double1 === double2; // false double1 === double1; // true
double1과 2는 같은 함수를 할당했어도 주소값이 다르기 때문에 같지 않음. 리액트도 리렌더링 시 함수를 새로 만들어서 호출하는데 호출된 함수와 기존 함수는 같은 함수가 아님. useCallback을 이용해 함수 자체를 저장해서 다시 사용하면 함수의 메모리 주소 값을 저장했다가 다시 사용하는 것과 같음
'FE' 카테고리의 다른 글
React Custom Component (0) 2023.07.20 [React] 심화 (0) 2023.07.19 [솔로 프로젝트] (0) 2023.07.17 [솔로 프로젝트] (0) 2023.07.14 프로젝트 요구사항 분석 (0) 2023.07.13