[S4]Chapter4. [React] 심화

박현석·2022년 11월 30일
1

코드스테이츠

목록 보기
32/40
post-thumbnail

React 심화

Virtual DOM

  • React에는 Virtual DOM이라고 하는 가상의 DOM 객체있다.
  • 실제 DOM의 사본 같은 개념으로, React는 실제 DOM 객체에 접근하여 조작하는 대신 이 가상의 DOM 객체에 접근하여 변화 전과 변화 후를 비교하고 바뀐 부분을 적용

Virtual DOM이 나오게 된 배경

  • virtual DOM 은 Real DOM 의 가벼운 사본과 같다.
  • DOM을 Real DOM으로 부르는 이유는 Virtual DOM과 구분하기 위해서이다.

Real DOM (DOM)

  • DOM은 브라우저가 트리 구조로 만든 객체 모델
  • 트리 구조로 DOM 객체가 이뤄져 있기 때문에 JavaScript는 쉽게 DOM 객체에 접근할 수 있고, DOM 객체를 조작할 수 있다.
  • DOM은 애플리케이션의 UI 상태가 변경될 때마다 해당 변경 사항을 나타내기 위해 업데이트가 된다.
  • DOM을 조작하는 정도가 잦다면 성능에 영향을 미치게 될 것이고, DOM의 렌더링은 브라우저의 파워, 즉 브라우저의 구동 능력에 의존하기 때문에 DOM의 조작 속도는 느려지게 된다.

DOM의 조작 속도가 느려지는 이유

  • 트리는 “데이터 저장"의 의미보다는 “저장된 데이터를 더 효과적으로 탐색”하기 위해 사용되므로, 빠른 자료 탐색 성능이 장점인 자료구조
  • DOM이 변경되고 업데이트가 된다는 것은 결국 브라우저의 렌더링 엔진 또한 리플로우(Reflow)한다는 것을 의미
  • 브라우저의 리플로우와 리페인트 과정은 다시금 레이아웃 및 페인트에 해당하는 재연산을 해야 하기 때문에 속도가 그만큼 느려지게된다.
  • JavaScript로 조작하는 DOM의 요소가 많을수록 모든 DOM 업데이트에 대하여 리플로우를 해야 하므로 DOM의 업데이트에 대한 비용이 많이 들게 된다.
  • 예를 들어 지금 6개의 컨텐츠가 있는데 그 컨텐츠 중 단 1개의 컨텐츠만 색상이 바뀌는 동작을 한다고 가정
  • 나머지 5개의 컨텐츠는 내버려두고 그 1개의 컨텐츠만 색상을 바꿔 그리는 것이 가장 효율적이라는 걸 알지만, 대부분의 JavaScript 프레임워크는 그 1개의 컨텐츠만 색상을 바꿔 그릴 뿐 아니라 나머지 컨텐츠도 다시 그린다.
  • 비효율적인 업데이트를 반복한다면 극단적인 예로 프레임 드랍(frame drop)과 같은 치명적인 UX 문제가 발생할 수 있다.
  • 결국 “바뀐 부분만 비교해서 그 부분만 렌더링을 할 수는 없을까?“ 라는 아이디어를 기반으로 React는 Virtual DOM을 세상에 내놓게 된 것

Virtual DOM이란

  • 상대적으로 무거운 DOM에 비하여 React의 가상 DOM 객체는 실제 DOM 객체와 동일한 속성을 가지고 있음에도 “훨씬 가벼운 사본”이라고 표현할 수 있다.
  • 다만 가상 DOM 객체는 화면에 표시되는 내용을 실제 DOM 객체처럼 직접 변경하는 것은 아니다.
  • 가상 DOM은 가상의 UI 요소를 메모리에 유지시키고, 그 유지시킨 가상의 UI 요소를 ReactDOM과 같은 라이브러리를 통해 실제 DOM과 동기화
  • 실제 DOM을 조작하는 것은 실제로 브라우저 화면에 그리기 때문에 느리지만, 가상 DOM을 조작하는 것은 실제 DOM처럼 실제로 브라우저 화면에 그리는 것이 아니기 때문에 훨씬 속도가 빠르다.

어떻게 가상 DOM이 더 빠를까?

  • 이전의 가상의 DOM과 이후의 가상의 DOM의 차이를 비교
  • 가상의 DOM은 실제 DOM에 변경을 수행할 수 있는 최상의 방법을 계산하기 시작
  • 실제 DOM은 최소한의 작업만 수행해도 렌더링을 할 수 있게 된다.
  • 노란색 원들은 업데이트된 노드, 즉 UI 요소를 의미
  • “상태가 변경된” UI 요소로 볼 수 있다.
  • 가상의 DOM 트리의 이전 버전과 현재 가상 DOM 트리 간의 차이가 계산
  • 트리가 업데이트된 UI를 제공하기 위해 부분적으로 리렌더링
  • 트리는 실제 DOM으로 한꺼번에 업데이트가 된다.

Virtual 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"
					}
				]
			]
		}
	]
}
  • 위 DOM 트리를 JavaScript 객체로 표현.

React Diffing Algorithm

  • 하나의 트리를 다른 트리로 변형을 시키는 가장 작은 조작 방식을 알아내야만 했는데, 알아낸 조작 방식 알고리즘은 O(n^3)의 복잡도를 가지고 있다.
  • 만약 이 알고리즘을 그대로 React에 적용한다면 1000개의 엘리먼트를 실제 화면에 표시하기 위해 10억(1000^3)번의 비교 연산을 해야만 한다.
  • 너무 비싼 연산이기 때문에 React는 두 가지의 가정을 가지고 시간 복잡도 O(n)의 새로운 휴리스틱 알고리즘(Heuristic Algorithm)을 구현해낸다.
  1. 각기 서로 다른 두 요소는 다른 트리를 구축할 것이다.
  2. 개발자가 제공하는 key 프로퍼티를 가지고, 여러 번 렌더링을 거쳐도 변경되지 말아야 하는 자식 요소가 무엇인지 알아낼 수 있을 것이다.

React가 DOM 트리를 탐색하는 방법

  • React는 기존의 가상 DOM 트리와 새롭게 변경된 가상 DOM 트리를 비교할 때, 트리의 레벨 순서대로 순회하는 방식으로 탐색 (너비 우선 탐색(BFS)의 일종)

다른 타입의 DOM 엘리먼트인 경우

  • DOM 트리는 각 HTML 태그마다 각각의 규칙이 있어 그 아래 들어가는 자식 태그가 한정적이라는 특징이 있다.
  • 자식 태그의 부모 태그 또한 정해져 있다는 특징이 있기 때문에, 부모 태그가 달라진다면 React는 이전 트리를 버리고 새로운 트리를 구축해버린다.
<div>
	<Counter />
</div>
//부모 태그가 div에서 span으로 바뀝니다.
<span>
	<Counter />
</span>
  • 부모 태그가 바뀌어버리면, React는 기존의 트리를 버리고 새로운 트리를 구축하기 때문에 이전의 DOM 노드들은 전부 파괴

같은 타입의 DOM 엘리먼트인 경우

  • 타입이 바뀌지 않는다면 React는 최대한 렌더링을 하지 않는 방향으로 최소한의 변경 사항만 업데이트
<div className="before" title="stuff" />
//기존의 엘리먼트가 태그는 바뀌지 않은 채 className만 바뀌었습니다.
<div className="after" title="stuff" />
//className이 before인 컴포넌트
<div style={{color: 'red', fontWeight: 'bold"}} title="stuff" />
//className이 after인 컴포넌트
<div style={{color: 'green', fontWeight: 'bold"}} title="stuff" />
  • 하나의 DOM 노드를 처리한 뒤 React는 뒤이어서 해당 노드들 밑의 자식들을 순차적으로 동시에 순회하면서 차이가 발견될 때마다 변경 (재귀적으로 처리)

자식 엘리먼트의 재귀적 처리

<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>
//자식 엘리먼트를 처음에 추가합니다.
<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>
  • 이렇게 구현하게 되면 React는 우리의 기대대로 최소한으로 동작하지 못하게 된다.
  • React는 원래의 동작하던 방식대로 처음의 노드들을 비교하게 된다.
  • 처음의 자식 노드를 비교할 때,
  • Duke
  • Connecticut
  • 로 자식 노드가 서로 다르다고 인지하게 된 React는 리스트 전체가 바뀌었다고 받아들인다.
  • Duke
  • Villanova
  • 는 그대로기 때문에 두 자식 노드는 유지시켜도 된다는 것을 깨닫지 못하고 전부 버리고 새롭게 렌더링 해버린다.(굉장히 비효율적인 동작 방식)
  • React는 이 문제를 해결하기 위해 key라는 속성을 지원한다.

키(key)

  • 자식 노드들이 이 key를 갖고 있다면, React는 그 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>
  • key 속성을 통해 React는 기존의 동작 방식대로 다른 자식 엘리먼트는 변경하지 않고 추가된 엘리먼트만 변경한다.
  • key 속성에는 보통 데이터 베이스 상의 유니크한 값(ex. Id)을 부여해주면 된다. 키는 전역적으로 유일할 필요는 없고, 형제 엘리먼트 사이에서만 유일하면 된다.
  • 만약 이런 유니크한 값이 없다면 최후의 수단으로 배열의 인덱스를 key로 사용할 수 있다.
    - 다만 배열이 다르게 정렬될 경우가 생긴다면 배열의 인덱스를 key로 선택했을 경우는 비효율적으로 동작할 것이다.
    • 배열이 다르게 정렬되어도 인덱스는 그대로 유지되기 때문이다.

React Hooks

Component와 Hook

Class Component

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>
       ) 
    }
}

  • 클래스 컴포넌트는 복잡해질수록 이해하기 어려워졌고, 컴포넌트 사이에서 상태 로직을 재사용하기 어렵다는 단점
  • React의 클래스 컴포넌트를 사용하기 위해서는 JavaScript의 this 키워드가 어떤 방식으로 동작하는지 알아야 하는데, 이는 문법을 정확히 알지 못하면 동작 방식 자체를 정확히 이해하기 어렵게 만들곤 했다.

Function Component

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>
    )
}
  • 함수형 컴포넌트는 클래스형 컴포넌트에 비해 훨씬 더 직관적이고, 보기 쉽다는 특징이 있다.
  • Counter 컴포넌트에서 숫자를 올리기 위해 상태값을 저장하고 사용할 수 있게 해주는 useState() 가 있는데, 여러분도 익히 알고 있는 이 메서드가 바로 Hook

Hook이란?

  • Hook은 다르게 말하면 함수형 컴포넌트에서 상태 값 및 다른 여러 기능을 사용하기 편리하게 해주는 메소드를 의미
  • Hook은 class가 아닌 function으로만 React를 사용할 수 있게 해주는 것이기 때문에 클래스형 컴포넌트에서는 동작하지 않는다.

Hook 사용 규칙

1. 리액트 함수의 최상위에서만 호출해야 합니다.

  • 반복문, 조건문, 중첩된 함수 내에서 Hook을 실행하면 예상한 대로 동작하지 않을 우려가 있다.

2. 오직 리액트 함수 내에서만 사용되어야 합니다.

  • 리액트 함수형 컴포넌트나 커스텀 Hook이 아닌 다른 일반 JavaScript 함수 안에서 호출해서는 안 된다는 의미

useMemo

  • useMemo은 특정 값(value)를 재사용하고자 할 때 사용하는 Hook
function Calculator({value}){
	const result = calculate(value);
	return <>
      <div>
					{result}
      </div>
  </>;
}
  • calculate가 내부적으로 복잡한 연산을 해야 하는 함수라 계산된 값을 반환하는 데에 시간이 몇 초 이상 걸린다고 가정해 본다.
  • 그렇다면 해당 컴포넌트는 렌더링을 할 때마다 이 함수를 계속해서 호출할 것이고, 그 때마다 시간이 몇 초 이상 소요가 될 것이다.
/* useMemo를 사용하기 전에는 꼭 import해서 불러와야 합니다. */
import { useMemo } from "react";
function Calculator({value}){
	const result = useMemo(() => calculate(value), [value]);
	return <>
      <div>
					{result}
      </div>
  </>;
}
  • useMemo를 호출하여 calculate를 감싸주면, 이전에 구축된 렌더링과 새로이 구축되는 렌더링을 비교해 value값이 동일할 경우에는 이전 렌더링의 value값을 그대로 재활용할 수 있게 된다.
  • 메모이제이션(Memoization) 개념과 긴밀한 관계

Memoization

  • 기존에 수행한 연산의 결과값을 메모리에 저장을 해두고, 동일한 입력이 들어오면 재활용하는 프로그래밍 기법

useCallback

  • seCallback 또한 useMemo와 마찬가지로 메모이제이션 기법을 이용한 Hook
  • useCallback은 함수의 재사용을 위해 사용하는 Hook
function Calculator({x, y}){
	const add = () => x + y;
	return <>
      <div>
					{add()}
      </div>
  </>;
}
  • useMemo와 마찬가지로, 해당 컴포넌트가 리렌더링 되더라도 그 함수가 의존하고 있는 값인 x와 y가 바뀌지 않는다고 생각해 본다.
  • 그렇다면 함수 또한 메모리 어딘가에 저장해 뒀다가 다시 꺼내서 쓸 수 있을 것 이다.
/* useCallback를 사용하기 전에는 꼭 import해서 불러와야 합니다. */
import React, { useCallback } from "react";
function Calculator({x, y}){
	const add = useCallback(() => x + y, [x, y]);
	return <>
      <div>
					{add()}
      </div>
  </>;
}
  • useCallback만 사용해서는 useMemo에 비해 괄목할 만한 최적화를 느낄 수는 없다.
  • useCallback은 함수를 호출을 하지 않는 Hook이 아니라, 그저 메모리 어딘가에 함수를 꺼내서 호출하는 Hook이기 때문이다.
  • 단순히 컴포넌트 내에서 함수를 반복해서 생성하지 않기 위해서 useCallback을 사용하는 것은 큰 의미가 없거나 오히려 손해인 경우도 있다.
  • 자식 컴포넌트의 props로 함수를 전달해줄 때 이 useCallback을 사용하기가 좋다.

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
  • 두개의 함수는 동일한 코드를 공유하더라도 메모리 주소가 다르기 때문에, 메모리 주소에 의한 참조 비교 시 다른 함수로 본다.
  • useCallback을 이용해 함수 자체를 저장해서 다시 사용하면 함수의 메모리 주소 값을 저장했다가 다시 사용한다는 것과 같다고 볼 수 있다.
  • React 컴포넌트 함수 내에서 다른 함수의 인자로 넘기거나 자식 컴포넌트의 prop으로 넘길 때 예상치 못한 성능 문제를 막을 수 있다.

Custom Hooks

  • 개발자가 스스로 커스텀한 훅을 의미하며 이를 이용해 반복되는 로직을 함수로 뽑아내어 재사용할 수 있다.
  • 여러 url을 fetch할 때, 여러 input에 의한 상태 변경 등 반복되는 로직을 동일한 함수에서 작동하게 하고 싶을 때 커스텀 훅을 주로 사용
  1. 상태관리 로직의 재활용이 가능하고
  2. 클래스 컴포넌트보다 적은 양의 코드로 동일한 로직을 구현할 수 있으며
  3. 함수형으로 작성하기 때문에 보다 명료하다는 장점이 있습니다. (e.g. useSomething)
//FriendStatus : 친구가 online인지 offline인지 return하는 컴포넌트
function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}
//FriendListItem : 친구가 online일 때 초록색으로 표시하는 컴포넌트
function FriendListItem(props) {
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}
  • FriendStatus 컴포넌트는 사용자들이 온라인인지 오프라인인지 확인하고, FriendListItem 컴포넌트는 사용자들의 상태에 따라 온라인이라면 초록색으로 표시하는 컴포넌트
  • 두 컴포넌트는 정확하게 똑같이 쓰이는 로직이 존재하고 있다.
  • 이 로직을 빼내서 두 컴포넌트에서 공유할 수는 없을까요? Custom Hook을 사용한다면 가능
function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });
  return isOnline;
}
  • 두 컴포넌트에서 사용하기 위해 동일하게 사용되고 있는 로직을 분리하여 함수 useFriendStatus로 만든다.
  • 커스텀 훅을 만들 때 규칙
    - Custom Hook을 정의할 때는 함수 이름 앞에 use를 붙이는 것이 규칙
    • 대개의 경우 프로젝트 내의 hooks 디렉토리에 Custom Hook을 위치 시킨다.
    • Custom Hook으로 만들 때 함수는 조건부 함수가 아니어야 합니다.
      즉 return 하는 값은 조건부여서는 안 됩니다.
      그렇기 때문에 위의 이 useFriendStatus Hook은 온라인 상태의 여부를 boolean 타입으로 반환하고 있다.
  • 만들어진 Custom Hook은 Hook 내부에 useState와 같은 React 내장 Hook을 사용하여 작성할 수 있다.
function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);
  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);
  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}
  • useFriendStatus Hook을 두 컴포넌트에 적용
  • 같은 Custom Hook을 사용했다고 해서 두 개의 컴포넌트가 같은 state를 공유하는 것은 아니다.
  • 그저 로직만 공유할 뿐, state 는 컴포넌트 내에서 독립적으로 정의 되어 있다.

Custom Hook의 예시

  • 여러 url을 fetch할 때 쓸 수 있는 useFetch Hook
const useFetch = ( initialUrl:string ) => {
	const [url, setUrl] = useState(initialUrl);
	const [value, setValue] = useState('');
	const fetchData = () => axios.get(url).then(({data}) => setValue(data));	
	useEffect(() => {
		fetchData();
	},[url]);
	return [value];
};
export default useFetch;
  • 여러 input에 의한 상태 변경을 할 때 쓸 수 있는 useInputs Hooks
import { useState, useCallback } from 'react';
function useInputs(initialForm) {
  const [form, setForm] = useState(initialForm);
  // change
  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;

React의 주목해야 할 기능

코드 분할(Code Spliting)

  • 대부분 React 앱들은 Webpack, Rollup과 같은 툴을 사용해 번들링(Bundling)한다.
    - HTML 웹 페이지에 JavaScript를 쉽게 추가할 수 있기 때문이다.
  • 번들된 앱은 모든 JavaScript가 한 곳에 있기 때문에 페이지를 설정하는 데 필요한 호출 수가 적은 링크 태그 하나만 필요하게 된다.
    - 과거에는 이렇게 해도 무리가 없었습니다. 모던 웹 이전의 웹 JavaScript 코드는 최소한의 수준으로 작성되었기 때문이다.
  • 이제는 번들링하게 되면 특정 지점에서 코드를 해석하고 실행하는 정도가 느려지게 되었다.
    - 모던 웹으로 발전하면서 점점 DOM을 다루는 정도가 정교해지며 JavaScript 코드 자체가 방대해지고 무거워졌기 때문
  • 코드 분할 아이디어 : 번들을 나눈 뒤 필요한 코드만 불러오면 어떨까?
  • 번들이 거대해지는 것을 방지하기 위한 좋은 해결 방법은 번들을 물리적으로 나누는 것
  • 코드 분할은 런타임 시 여러 번들을 동적으로 만들고 불러오는 것으로, Webpack, Rollup과 같은 번들러가 지원하는 기능
  • 코드 분할을 하게 되면 지금 당장 필요한 코드가 아니라면 따로 분리를 시키고, 나중에 필요할 때 불러와서 사용할 수 있다.
  • 대규모 프로젝트의 앱인 경우에도 페이지의 로딩 속도를 개선할 수 있게 된다.

번들 분할 혹은 줄이는 법

  • 번들링 되는 파일에는 여러분들이 앱을 만들면서 npm을 통해 다운받는 서드파티(Third Party) 라이브러리도 포함이 된다.
    - 서드파티 라이브러리는 개인 개발자나 프로젝트 팀, 혹은 업체등에서 개발하는 라이브러리로, 즉 제 3자 라이브러리
  • 서드파티 라이브러리는 플러그인이나 라이브러리 또는 프레임워크 등이 존재하며, 이 라이브러리를 잘 사용하면 편하고 효율적인 개발을 할 수 있다.
  • 서드파티 라이브러리는 사용자에게 다양한 메소드를 제공하기 때문에 코드의 양이 많고, 번들링 시 많은 공간을 차지
  • 사용 중인 라이브러리의 전부를 불러와서 사용하는 것보다 따로 따로 불러와서 사용할 수 있다면 많은 공간을 차지하지 않을 수 있게 된다.
/* 이렇게 lodash 라이브러리를 전체를 불러와서 그 안에 들은 메소드를 꺼내 쓰는 것은 비효율적입니다.*/
import _ from 'lodash';
...
_.find([]);
/* 이렇게 lodash의 메소드 중 하나를 불러와 쓰는 것이 앱의 성능에 더 좋습니다.*/
import find from 'lodash/find';
find([]);
  • lodash라는 라이브러리를 예시
  • lodash는 배열, 숫자, 객체, 문자열을 사용할 때 반복적인 작업 같은 것을 할 시 사용하기에 좋은 라이브러리
  • lodash라는 라이브러리는 하나의 폴더와 같고, 그 폴더 안에는 개발 시 다양한 상황에 쓰기 좋은 메소드들, 즉 함수 코드들이 들어 있다.
  • 이 함수 코드들의 양이 상당하기 때문에 전부 가져올 시, 정말로 필요한 것 한두 개만 쓰인다면 나머지는 그냥 쓰이지 않는 코드 뭉치로 앱 내부에 남게 된다.
  • 이는 앱의 성능을 저하시킬 요지가 있기 때문에, 필요한 것 한두 개만 가져다 쓰는 식으로 개발하는 것이 훨씬 좋다.

React에서의 코드 분할

  • React는 SPA(Single-Page-Application)인데, 사용하지 않는 모든 컴포넌트까지 한 번에 불러오기 때문에 첫 화면이 렌더링 될때까지의 시간이 오래걸린다.
    - 그래서 사용하지 않는 컴포넌트는 나중에 불러오기 위해 코드 분할 개념을 도입
  • React에서 코드 분할하는 방법은 dynamic import(동적 불러오기)를 사용하는 것
  • 그 전까지는 코드 파일의 가장 최상위에서 import 지시자를 사용해 사용하고자 하는 라이브러리 및 파일을 불러오는 방법을 사용(static import(정적 불러오기))

Static Import

/* 기존에는 파일의 최상위에서 import 지시자를 이용해 라이브러리 및 파일을 불러왔습니다. */
import moduleA from "library";
form.addEventListener("submit", e => {
  e.preventDefault();
  someFunction();
});
const someFunction = () => {
  /* 그리고 코드 중간에서 불러온 파일을 사용했습니다. */
}
  • 기존에는 항상 import 구문은 문서의 상위에 위치해야 했고, 블록문 안에서는 위치할 수 없는 제약 사항이 있었다.
    - 왜냐하면 번들링 시 코드 구조를 분석해 모듈을 한 데 모으고 사용하지 않는 모듈은 제거하는 등의 작업을 하는데, 코드 구조가 간단하고 고정이 되어 있을 때에야만 이 작업이 가능해지기 때문

Dynamic Import

form.addEventListener("submit", e => {
  e.preventDefault();
	/* 동적 불러오기는 이런 식으로 코드의 중간에 불러올 수 있게 됩니다. */
  import('library.moduleA')
    .then(module => module.default)
    .then(someFunction())
    .catch(handleError());
});
const someFunction = () => {
    /* moduleA를 여기서 사용합니다. */
}
  • dynamic import를 사용하게 되면 불러온 moduleA 가 다른 곳에서 사용되지 않는 경우, 사용자가 form을 통해 양식을 제출한 경우에만 가져오도록 할 수 있다.
  • dynamic import는 then 함수를 사용해 필요한 코드만 가져옵니다.
    가져온 코드에 대한 모든 호출은 해당 함수 내부에 있어야 합니다.
    이 방식을 사용하면 번들링 시 분할된 코드(청크)를 지연 로딩시키거나 요청 시에 로딩할 수 있다.

React.lazy()와 Suspense

React.lazy()

  • React.lazy 함수를 사용하면 dynamic import를 사용해 컴포넌트를 렌더링할 수 있다.
  • React는 React.lazy를 통해 컴포넌트를 동적으로 import를 할 수 있기 때문에 이를 사용하면 초기 렌더링 지연시간을 어느정도 줄일 수 있게 된다.
import Component from './Component';
/* React.lazy로 dynamic import를 감쌉니다. */
const Component = React.lazy(() => import('./Component'));
  • React.lazy로 감싼 컴포넌트는 단독으로 쓰일 수는 없고, React.suspense 컴포넌트의 하위에서 렌더링을 해야 한다.

React.Suspense

  • Router로 분기가 나누어진 컴포넌트들을 위 코드처럼 lazy를 통해 import하면 해당 path로 이동할때 컴포넌트를 불러오게 되는데 이 과정에서 로딩하는 시간이 생기게 된다.
  • Suspense는 아직 렌더링이 준비되지 않은 컴포넌트가 있을 때 로딩 화면을 보여주고, 로딩이 완료되면 렌더링이 준비된 컴포넌트를 보여주는 기능
/* suspense 기능을 사용하기 위해서는 import 해와야 합니다. */
import { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));
function MyComponent() {
  return (
    <div>
			{/* 이런 식으로 React.lazy로 감싼 컴포넌트를 Suspense 컴포넌트의 하위에 렌더링합니다. */}
      <Suspense fallback={<div>Loading...</div>}>
				{/* Suspense 컴포넌트 하위에 여러 개의 lazy 컴포넌트를 렌더링시킬 수 있습니다. */}
        <OtherComponent />
				<AnotherComponent />
      </Suspense>
    </div>
  );
}
  • Supense 컴포넌트의 fallback prop은 컴포넌트가 로드될 때까지 기다리는 동안 로딩 화면으로 보여줄 React 엘리먼트를 받아들인다.
  • Suspense 컴포넌트 하나로 여러 개의 lazy 컴포넌트를 보여줄 수도 있다.

React.lazy와 Suspense의 적용

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>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </Suspense>
  </Router>
);
  • 앱에 코드 분할을 도입할 곳을 결정하는 것은 사실 까다롭기 때문에,
    중간에 적용시키는 것보다는 웹 페이지를 불러오고 진입하는 단계인 Route에 이 두 기능을 적용시키는 것이 좋다.
profile
선한 영향력을 주는 사람

0개의 댓글