혹시나 잘못된 개념 전달이 있다면 댓글 부탁드립니다. 저의 성장의 도움이 됩니다

Virtual DOM

가상의 DOM으로 Real DOM의 라이트 버전 객체

  • 최소한의 리플로우, 리렌더링을 위한 객체
    : React는 코드가 변경(UI 변경)될 때 무거운 DOM 구조를 변경하기 전에 최소한의 수정을 위한 계산을 시작한다. 이때 사용되는 것이 Virtual DOM으로 React는 트리가 변경되면 가상 DOM 객체를 생성한다. 변경되기 전의 Virtual DOM과 변경 후의 Virtual DOM을 비교하여 리플로우, 리렌더링을 최소화는 작업을 모색한다.
    -> Virtual DOM의 변화를 탐색하고 바뀐 부분만 리렌더링, Real Dom을 변경
    -> 성능 저하 감소

cf. Real DOM(= Document Object Model)
: 브라우저의 렌더링 엔진은 Javascript 기반으로 html 등의 문서를 객체화(노드의 트리구조로 변환)하여 접근한다.
cf.


// 가상 DOM 예시 -> 트리구조
const vDom = {
	tagName: "html",
	children: [
		{ tagName: "head" },
		{ tagName: "body",
			children: [
				tagName: "ul",
				attributes: { "class": "list"},
				children: [
					{
						tagName: "li",
						attributes: { "class": "list_item" },
						textContent: "List item"
					}
				]
			]
		}
	]
}

React의 비교 알고리즘

React Diffing Algorithm
가상 DOM을 비교할 때 적용하는 알고리즘

  • 기존의 UI가 변경될 때, 효율적으로 갱신하는 방법을 모색
    -> 시간 복잡도 O(n)

2가지 가정

  • 각기 서로 다른 두 요소는 다른 트리를 구축할 것이다.
  • 개발자가 제공하는 key 프로퍼티를 가지고, 여러 번 렌더링을 거쳐도 변경되지 말아야 하는 자식 요소가 무엇인지 알아낼 수 있을 것이다.

탐색 방법

  1. 같은 레벨 노드를 비교
    : 너비 우선 탐색(BFS)의 일종으로 동일선상의 트리를 탐색한다.
    • 다른 타입의 DOM 요소(태그)인 경우
      : 특정 태그에는 자식 태그가 한정되는 특성이 있어서 React는 이전의 트리를 버리고 새로 구축하는데, 자식 태그 속의 state 도 사라진다.
      e.g. <ul> 태그 밑에는 <li> 태그만 오거나, <p> 태그 안에 <p> 태그를 또 쓰지 못하는 등의 규칙이 존재한다.
    • 같은 타입의 DOM 요소인 경우
      : 리렌더링이 최소한이 되도록 탐색한다. Virtual DOM의 속성만 수정했다가 후에 real Dom으로 렌더링을 시도한다.
      **
  1. 자식 노드 비교(=재귀적 처리)
    • key 속성 비교
      : React는 순차적으로 코드를 읽어 내려가는 Javascript 기반이므로 자식 코드의 뒤에 요소를 추가하는 것이 더 효율적이다. 만약 앞에 추가된다면 뒤로 밀린 뒷 요소들이 같더라도 변경되었다고 인식한다. 이러한 비효율을 보완하기 위해 key 속성을 사용하여 비교한다.
      cf. key 속성값은 형제 요소 사이에서만 unique하면 된다.
      추가로, 배열의 인덱스를 key 속성값으로 사용하는 것을 지양하라 이유는 이처럼 요소가 추가될 때, 배열의 인덱스값이 변경되기 때문이다.
      **


React Hook

클래스형보다 더 직관적이고 재사용성이 높은 함수형 컴포넌트에서 상태를 사용하거나 최적화 할 수 있는 방법 등이 기능을 사용할 수 있는 메서드(React 16.8 버전부터 추가)

  • 함수형 컴포넌트에만 사용 가능
  • use~ 형태의 네이밍

Hook 사용 규칙

  • React 함수 속 최상위에서 Hook 적용
    -> 반복문, 조건문, 중첩함수에서 호출 X
    : 컴포넌트 안에 hook을 호출되는 순서로 저장하는데, 조건문 등에 적용되면 예기치 못한 오류가 발생한다.

  • React 함수 내부 O, Javascript 함수 내부 X


useMemo

중복 연산을 줄이기 위해 특정 값을 재사용할 때 사용하는 메서드

  • 메모이제이션(Memoization) 활용
    -> 성능 최적화

  • 두번째 전달인자의 변경 시 연산
    -> 변경되지 않으면 리렌더링 X
    (의존성 배열이 없으면 매번 리렌더)

import { useMemo } from "react";  /* 호출 후 사용 가능 */

function Calculator({value}){
	const result = useMemo(() => calculate(value), [value]);
		                     	// 의존성 배열의 값이 변경되면 실행
	return <div>{result}</div>;
};

You may rely on useMemo as a performance optimization, not as a semantic guarantee. ... Write your code so that it still works without useMemo — and then add it to optimize performance.


uesCallback

함수를 재사용하기 위한 메서드

  • 함수의 메모리 주소값 통일(= 참조 동등성에 의존)
    : 컴포넌트 안에서 함수를 호출할 경우 렌더링될 때마다 새로운 함수가 정의, 실행되어 주소값이 변경된다. react는 javascript 기반으로 동작하는데 함수는 참조 자료형(객체 타입)이므로 메모리의 주소값이 저장된다. 같은 형태이라도 참조하는 주소가 달라지므로 리렌더링시 함수 자체를 저장, 실행한다면 동일한 주소값을 유지할 수 있다.

  • props로 전달에 활용
    : 자식 컴포넌트로 함수를 전달할 때 사용하면 동일한 주소값으로 함수가 변하지 않았기 때문에 불필요한 렌더링을 줄일 수 있다.

  • 두번째 전달인자가 동일하면 기존 함수를 반환(메모이제이션 활용)
    cf. useCallback(fn, deps)useMemo(() => fn, deps)

import React, { useCallback } from "react"; /* 호출 후 사용 가능 */

function Calculator({x, y}){
	const add = useCallback(() => x + y, [x, y]);
 						// 의존성 배열의 값이 변경되지 않으면 기존 함수를 반환
	return <div>{add()}</div>;
}

코플릿 예시


Custom Hooks

반복되는 로직을 재사용하기 위해 직접 함수화 한 것

  • use~ 형태의 네이밍

  • 독립적인 state를 가진 hook

  • React 내장 Hook 사용 가능
    : 커스텀 hook도 React 함수이므로 useState 같은 훅을 사용할수 있다.

cf. 정의한 커스텀훅을 주로 하나의 hooks 디렉토리에 정리한다.


예시 1. fetch를 위한 Hook

const useFetch = ( initialUrl:string ) => {
	const [url, setUrl] = useState(initialUrl);
	const [value, setValue] = useState('');
	
    // 입력받은 url로 get 요청
	const fetchData = () => axios.get(url).then(({data}) => setValue(data));	

	useEffect(() => {
		fetchData();
	},[url]);

	return [value];
};

export default useFetch;

예시 2. input 변경에 대응하는 Hook

import { useState, useCallback } from 'react';

function useInputs(initialForm) {
  const [form, setForm] = useState(initialForm);
  
  // 변경 이벤트 발생
  const onChange = useCallback(e => {
    const { name, value } = e.target;
    setForm(form => ({ ...form, [name]: value }));
  }, []);
  const reset = useCallback(() => setForm(initialForm), [initialForm]);
  return [form, onChange, reset];
}

export default useInputs;


코드 분할

Code Spliting

번들링 할 때 javascript 코드가 너무 커져서 초기 렌더링에 시간이 오래걸리는 것을 막고자 코드가 실행될 때 필요한 부분만 불러올 수 있도록 코드를 작성하는 것
-> 로딩 속도 개선

  • 번들 분할 or 번들 용량 줄이기
    : 번들에는 설치한 서드파티 라이브러리도 포함하는데, 전체가 아닌 사용하는 기능만 번들에 저장되도록하여 성능을 개선한다.
    e.g. import _ from 'lodash'; 보다 import find from 'lodash/find'; 처럼 수정하는 것이 더 좋다.

  • Dynamic Import 적용
    : 콜백함수의 실행부에서 사용되며, React.lazy API와 함께 사용하거나 이벤트가 발생할 때 적용한다.

Dynamic Import

렌더링 될 때 필요한 요소들만 import 할 수 있는 방법
-> 구문 분석 및 컴파일해야 하는 스크립트의 양을 최소화
-> 초기 렌더링 속도 감소

  • import()
    : 렌더링할 때 비동기적으로 처리하기 위해 Promise가 반환된다.
// case 1
const someFunction = () => { /* moduleA 사용 */ }

form.addEventListener("submit", e => {
  e.preventDefault();
  import('library.moduleA')  /* <------- 상단에 import 대신 사용 */
    .then(module => module.default)
    .then(someFunction()) 
     /* moduleA를 import 후에 사용할 수 있으므로 then을 적용 하여 순서를 명시*/
    .catch(handleError());
});

// case 2
import("./math")
  .then(math => {
  		console.log(math.add(16, 26));
});

cf. Static Import
: SPA(Single-Page-Application)에서 코드의 상단부분에 바로 사용하지 않는 컴포넌트까지 import 했기 때문에 첫 화면에 렌더링 시간이 오래 걸린다.

import moduleA from "library";

 const someFunction = () => { /* moduleA 사용 */ }

 form.addEventListener("submit", e => {
    e.preventDefault();
    someFunction(); 
 });
 /* 
 이벤트가 발생해야 import한 것을 사용하는 형태로, 
 초기 렌더링에 불필요하게 시간을 오래 걸리게 하는 요인
 */

React.lazy

Dynamic Import로 컴포넌트를 렌더링 할 수 있게 하는 메서드

// Static Import
import Component from './Component';

// Dynamic Import : 렌더링 될 때 import
const Component = React.lazy(() => import('./Component'));
  • 전달인자는 Dynamic Import를 호출하는 콜백함수
    cf. promise 관련 오류 해결

  • React.Suspense 컴포넌트의 아래에서 사용 가능
    : 해당 컴포넌트가 렌더링 될 때까지 Suspense 컴포넌트에 지정된 fallback 속성값을 보여준다.

    import React, { Suspense } from 'react';
    
     const OtherComponent = React.lazy(() => import('./OtherComponent'));
     const AnotherComponent = React.lazy(() => import('./AnotherComponent'));
    
     function MyComponent() {
        return (
          <div>
              <Suspense fallback={<div>Loading...</div>}>
                  /* Suspense 컴포넌트의 하위에 적용 */
                  <OtherComponent />  
                  <AnotherComponent />
              </Suspense>
          </div>
        );
     }

React.Suspense

Dynamic Import로 로딩이 발생할 때 미리 설정한 로딩 화면을 보여주는 기능을 가진 컴포넌트

  • Dynamic Import가 로딩 완료되면 해당 컴포넌트로 화면 전환

  • fallback 속성
    : props로 속성값을 렌더링 중일 때 화면에 표시한다. Suspense 컴포넌트 아래에 있는 모든 컴포넌트가 각 각 로딩 중일 때 적용된다.

  • Routes 의 상위 컴포넌트에 적용
    : 초기 렌더링에는 시간이 단축되지만 페이지 이동에 로딩이 발생하므로 선택적으로 위치를 정한다.

    import { Suspense, lazy } from 'react';
     import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
    
     const Home = lazy(() => import('./routes/Home'));
     const About = lazy(() => import('./routes/About'));
    
     const App = () => (
        <Router>
          <Suspense fallback={<div>Loading...</div>}> /* <--- Suspense 컴포넌트 */
            <Routes>
              <Route path="/" element={<Home />} /> /* <----- 라우팅 시점에 import */
              <Route path="/about" element={<About />} />
            </Routes>
          </Suspense>
        </Router>
     );



참고

react, react-dom 객체 형태



ReactDOM.render is no longer supported ...

ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it’s running React 17. Learn more: https://reactjs.org/link/switch-to-createroot

  • createRoot API의 등장
    : React 18 부터 기존의 렌더링 방식의 문법이 변경되었다. 기존에는 'react-dom'에서 render 를 호출하여 사용했지만, 변경된 문법에서는 'react-dom/client'에서 creactRoot 를 사용하여 ReactDOMRoot(객체)를 만들고, 객체의 render 메서드로 화면을 렌더링한다.
    How to Upgrade to React 18
// Before ver 1
import { render } from 'react-dom';
const container = document.getElementById('app');
render(<App tab="home" />, container);

// Before ver 2
import ReactDOM from 'react-dom';
const container = document.getElementById('app');
ReactDOM.render(<App tab="home" />, container);


// After
import { createRoot } from 'react-dom/client';
const container = document.getElementById('app');
const root = createRoot(container); 
root.render(<App tab="home" />);




오늘의 나

느낀점

코더 말고 개발자

선참시에서 나온 말이다. 매일 이론만 공부하다가 실제로 뭔갈 만들어보질 못했다. 자고로 개발자는 뭔가 만들어내야하는데 과연 내가 개발자인가, 학생인가 의문이다. 특히 요즘 체력적인 한계로 엄청나게 피곤해서 새벽까지 공부하지도 못하는데, 프로젝트는 코앞이다. 개념 정리, 복습보다 만들어보는게 더 중요하지 않을까...

**

0개의 댓글