S4 Unit 4. [React] 심화

나현·2022년 11월 25일
0

학습일지

목록 보기
43/53
post-thumbnail

💡 이번에 배운 내용

  • Section4. 사람과 기계가 모두 쉽고 빠르게 접근 가능한 Web App을 만들 수 있다.
  • Unit4. React 심화: 리액트의 동작 방식과, 리액트 Hooks에 대해 배우며 리액트를 심화 학습한다.

느낀점

이번에도 실습을 하면서 학습했는데, 개념 공부 시간보다 예제 풀어보고 과제하는 시간이 월등히 오래걸렸다. 삽질을 많이 했다. 그래도 삽질은 개발자의 실력에 밑거름이 된다는 말을 믿어보며, 시간을 허투루 쓰지 않았을 거라 생각해본다...! 피곤하지만 힘을 좀 더 내서 과제 레퍼런스 코드를 보며 공부해야 겠다. 공부하는 나 자신 화이팅! 😂


키워드

Virtual DOM 가상 돔, React Diffing Algorithm, React Hooks, Class Component, Function Component, UseMemo, useCallback, Memoization, 참조 동등성, Custom Hooks 커스텀 훅 사용자 훅, Code Spliting 코드 분할, dynamic import 동적 불러오기, React.lazy(), Suspense


학습내용

Ch1. Virtual DOM

Virtual DOM이란?

Virtual DOM(이하 가상 DOM)은 실제 DOM의 사본을 이용하여 만든 가상의 DOM 객체로 리액트에서 활용하는 개념이다. 가상 DOM은 추상화된 자바스크립트 객체의 형태를 가지고 있다.
React는 이 가상 DOM을 활용해 DOM의 바뀐 부분을 확인한 뒤 실제 DOM에 적용하는 방식으로 동작한다.
실제 DOM은 기존의 DOM으로 리액트의 가상 DOM과 구분하기 위해 앞에 '실제'라는 말이 붙었다. 기존의 DOM은 브라우저가 트리 구조로 만든 문서 요소의 객체 모델이다.

Virtual DOM을 사용하는 이유

DOM은 앱 상태가 변경되면 업데이트되는데, 이 변경이 잦다면 리렌더링 되므로 조작 속도가 느려지게 된다. 트리 구조는 빠른 탐색이 가능하지만, 브라우저의 렌더링 엔진이 리플로우, 리페인트하기 때문이다.(지난 🔗 유닛 2 참고)
물론 모던 웹이 성능이 좋긴 하나, 하나의 요소만 바뀌어도 모든 요소가 렌더링 된다면 비효율적이다. 따라서 바뀐 부분만 렌더링하기 위해 리액트는 가상 DOM을 사용한다.

Virtual DOM의 작동원리

React에는 모든 DOM 객체에 대응하는 가상의 DOM 객체가 있다. 이 가상 DOM은 가상의 UI 요소를 ReactDOM 등의 라이브러리를 이용해 실제 DOM과 동기화시키고 메모리에 유지시킨다.

리액트는 가상 DOM을 사용해 다음과 같이 실제 DOM에 변경사항을 적용한다.

  • 새로운 요소가 UI에 추가되면 가상의 DOM 트리를 만든다.
  • 또다시 상태가 변경이 되면 새로운 가상의 DOM 트리를 만든다.
  • 이전의 가상의 DOM과 이후의 가상의 DOM의 차이를 비교한다.
  • 가상의 DOM은 실제 DOM에 변경을 적용하는 방법을 계산한다.

이 과정을 통해 최소한의 작업으로 실제 DOM을 업데이트하여 더 빠른 렌더링이 가능해진다.
여기서 리액트가 가상 DOM으로 비교한 변경과정을 실제 DOM에 적용하는 알고리즘에 대해서 다음 챕터에서 설명한다.


Ch2. React Diffing Algorithm

리액트가 가상 DOM으로 비교한 변경과정을 실제 DOM에 적용할 때 어떤 알고리즘을 사용하는지 알 필요가 있다.
하나의 트리를 다른 트리로 바꾸는 알고리즘은 O(n^3)의 복잡도를 가지고 있는데, 이는 비효율적인 연산이다. 때문에 리액트에서는 두 가지 가정(다른 두 요소는 다른 트리를 구축, key 프로퍼티 등)을 세우고 이에 맞게 시간 복잡도 O(n)의 새로운 알고리즘을 만들어 내었다. 이 알고리즘 자체보다 두 가정과 알고리즘의 원리에 대해 알아본다.

비교 알고리즘(Diffing Algorithm)

위에서 언급한 알고리즘의 가정은 검증 결과 리액트의 특징이 되었다.
그 두가지는 다음과 같다.

  1. 각기 서로 다른 두 요소는 다른 트리를 구축한다.
  2. key 프로퍼티 사용해 여러 번 렌더링을 거쳐도 변경되지 말아야 하는 자식 요소가 무엇인지 알아낼 수 있다.

리액트 DOM 적용 알고리즘은 가상 DOM 의 전후를 비교할 때 너비 우선 탐색(BFS), 즉 트리의 레벨 순서대로 순회하며 위 두가지 사실을 활용한다.
위의 두 가지 사항을 바탕으로 리액트의 비교 알고리즘을 좀 더 자세히 살펴본다.

1) 각기 서로 다른 두 요소는 다른 트리를 구축한다.

리액트는 변경 전 가상의 DOM과 변경 후 가상의 DOM을 같은 레벨부터 파악한다.
각 레벨의 노드를 파악한 뒤 다음 자식 세대의 노드를 순차적으로 파악한다.
이 때 그냥 파악하지 않고, 각기 서로 다른 두 요소인지 확인하며 파악한다.

HTML태그는 각각 규칙이 있고, ul, li, 블록요소 인라인 요소처럼 제약이 있기에 DOM 트리 역시 일정한 규칙이 있다. 이런 특성으로 비교 후 부모 태그가 달라졌다면 이전 트리를 버리고 새로운 트리를 구축한다.
이런 식으로 부모가 바뀌면 자식 노드와 기존의 컴포넌트는 파괴되며 기존 state 역시 파괴된다.

그러나 만약 같은 타입의 DOM 요소라면 최소한의 변경사항을 적용한다.
가상 DOM 내부의 바뀐 프로퍼티만 수정하고 모든 노드의 업데이트가 끝나면 한 번에 실제 DOM으로 렌더링한다.

2) key 프로퍼티를 사용해 렌더링을 거쳐 변경되지 말아야 하는 자식 요소가 무엇인지 알아낼 수 있다.

리액트는 같은 레벨의 노드부터 순차적으로 처리하므로 자식 노드를 차례로 비교하면서 변경사항을 탐색한다.
이 때 만약 요소의 처음에 노드가 하나가 추가되었다면 순서가 전부 바뀌었으므로 모든 노드를 렌더링해야 한다. 그러나 실제로는 하나만 추가되었으므로 이런 경우 비효율적으로 되버린다.
이 때 key 프로퍼티를 자식 노드에 삽입하면, 자식 노드의 변경 여부를 확인하여 key가 일치하는 바뀐 노드만 적용할 수 있다. 이 경우 노드가 앞에 추가되어도 key속성을 통해 다른 노드는 위치만 이동했음을 알 수 있다. 즉 추가된 노드만 변경하는 것이다.

다만 이미 배웠지만 key 프로퍼티를 사용할 때 주의할 점이 있다.

  • 중복되지 않는 유일한 값을 부여해야 하며, 주로 데이터일 경우 id나 PK(데이터의 primary key. 기본키)를 사용한다.
  • 배열의 인덱스를 사용할 수는 있지만, 배열이 다르게 정렬된다면 인덱스가 바뀔 수 있으므로 비효율적으로 동작할 수 있다.

Ch3. React Hooks (중요)

리액트에는 Hook이라는 개념이 있으며 이를 알아보기 위해 먼저 등장한 배경부터 알아야 한다.

리액트의 Hook이 등장한 배경

리액트에서 함수 컴포넌트를 사용하기 이전에는 클래스 컴포넌트를 사용했다.
요약하자면 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>버튼을 {this.state.counter}번 클릭했어요</p>
                <button onClick={this.handleIncrease}>
                    Click
                </button>
            </div>
       ) 
    }
}
  • 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>
    )
}

코드를 보면 함수형 컴포넌트가 좀 더 직관적이고 보기 쉽다.

클래스 컴포넌트는 복잡할수록 상태 로직을 재사용하기 어려웠는데, 함수형 컴포넌트로 넘어가면서 상태를 저장하고 사용하는 등 여러 기능을 보완하고자 Hook이 등장했다.

Hook 사용 규칙

Hook은 React 16.8에 새로 추가된 기능으로, class를 작성하지 않고도 state와 다른 리액트의 기능들을 사용할 수 있게 해준다. 때문에 클래스형 컴포넌트에서는 동작하지 않는다.

이 Hook 사용 규칙은 다음과 같다.

  • 리액트 함수의 최상위에서만 호출해야 한다.
    반복문, 조건문, 중첩된 함수 내에서는 사용할 수 없다.
  • 리액트 함수형 컴포넌트, 커스텀 Hook 내에서만 사용해야 한다.
    일반 JavaScript 함수 안에서는 사용할 수 없다.

이 다음부터는 자주 사용하는 Hook인 useCallback과 useMemo에 대해 알아본다.
두 훅은 함수형 컴포넌트가 상태를 조작하고 최적하 기능을 할 수 있게 도와준다.

주요 Hook: UseMemo

useMemo는 특정 값(value)를 재사용하고자 할 때 사용하는 Hook이다. 리액트에서는 리렌더링을 최소화 하는 것이 성능에 좋으므로 useMemo를 통해 값을 재사용하면 최적화에 도움이 된다.

기본 문법은 다음과 같다.

import { useMemo } from "react";

function MyComponent({value}){
	const result = useMemo(() => myFunction(value), [value]);
  //...
	return (
      <div>
        {result}
      </div>
    );
}

만약 함수 myFunction(value)이 복잡한 연산을 하는 함수라면 이 MyComponent가 리렌더링 될 때마다 실행되면서 영향이 갈 것이다.
그래서 만약 이 value값이 바뀌지 않는다면 함수를 한번 실행하고 결과를 저장하여 재사용하는 것이 효율적이다. 바로 이 재사용할 수 있도록 저장하는 기능을 useMemo가 한다. [value] 부분은 의존성 배열 부분이다.
이렇게 결과를 저장하고 재활용하는 것을 다른말로 메모이제이션(Memoization)이라고 한다.

Memoization
메모이제이션(Memoization)은 기존에 수행한 연산의 결과값을 메모리에 저장하고, 동일한 입력값을 받았을 때 재활용하는 프로그래밍 기법을 의미한다. 주로 알고리즘에서 자주 사용하는 개념으로 이 메모이제이션을 사용해 앱의 성능을 최적화할 수 있다.

useCallback

useCallback은 함수를 재사용하기 위한 Hook으로, useMemo처럼 메모이제이션 기법을 이용한다.
사용법은 아래와 같다.

import { useCallback } from "react";

function MyComponent({value}){
	const myFunction = useCallback(() => value*2, [value]);
	  //...
	return (
      <div>
        {myFunction()}
      </div>
    );
}

useMemo처럼 의존성 배열인 [value]가 바뀌지 않는다면, MyComponent가 리렌더링 될 때 함수를 새로 선언하지 않고 메모리에 저장하여 재사용할 수 있다.

참조 동등성
자바스크립트의 객체(함수 포함)는 변수에 할당하면 주소값을 저장한다. 때문에 새로운 변수에 반환값이 같거나 내용이 같은 함수를 할당해도, 주소값이 다르기 때문에 다른 함수라 할 수 있다.
그런데 useCallback을 사용하면 이 주소값(참조)을 저장했다가 같은 함수를 재사용한다.
이를 참조 동등성이라고 한다.

Custom Hooks

Hook은 직접 만들어서 사용이 가능하며 이를 커스텀 훅이라고 한다.
커스텀 훅을 사용하면 반복되는 로직을 Hook으로 묶어 재사용할 수 있다.

아래 공식문서에서 온 두 가지 예제가 있다.
이 예제를 바탕으로 커스텀 훅을 만드는 방법을 알 수 있다.

FriendStatus , FriendListItem 이렇게 두 가지 예제가 있다.

  1. 두 예제에서 공통된 부분을 찾는다.
//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>
  );
}
  1. 중복되는 부분을 커스텀 훅으로 만든다.
    커스텀 훅을 이용하면 재사용이 가능하며 기존 useState, useEffect 등을 사용할 수 있다.
    아래는 중복되는 부분으로 만든 커스텀 훅의 내용이다.
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;
}
  1. 기존 코드에 커스텀 훅을 적용한다.
    아래는 1번의 FriendStatus , FriendListItem 에 2번에서 만든 커스텀 훅 useFriendStatus를 적용한 것이다.
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>
  );
}

중요한 점은 같은 로직을 공유했을 뿐 같은 state를 공유한 것은 아니라는 것을 알아야 한다.
각 state는 컴포넌트 내에서 독립적으로 정의되어 있다.

Custom Hook 작성 규칙

  • Custom Hook을 정의할 때 함수 이름 앞에 use를 붙여야 한다.
    그래야 리액트에서 hook으로 인식한다.
  • 보통 프로젝트에서 hooks 디렉토리에 Custom Hook을 위치시킨다.
  • Custom Hook의 return 값은 boolean, 배열, 객체등 값이어야 한다.
    조건부여서는 안 된다.

Ch4. 코드 분할 (Code Spliting)

리액트에서 코드 분할 (Code Spliting)이란 필요한 코드를 분할하여 사용 시기와 용도에 맞게 필요할 때만 불러와 사용하는 것을 의미한다.
코드 분할의 개념을 번들에 적용하면, 번들링한 앱의 용량이 커지는 것을 방지하기 위해 여러 코드로 분할하고 동적으로 불러오는 방법으로 번들링할 수 있다.

번들 분할

번들 분할은 서드파티(Third Party) 라이브러리같은 것들을 사용할 때 동적으로 불러오는 방식으로 필요한 곳에서 사용할 수 있게 할 수 있다. 또한 라이브러리의 일부분만을 불러와서 사용할 수도 있다.

다음은 라이브러리에서 필요한 메서드만을 가져온 예시이다.

import myMethod from './myLibrary/MyMetod';

MyMetod([]);

동적 불러오기(dynamic import)

React에서 코드 분할 방법 중 하나로 dynamic import(동적 불러오기)가 있다.
기존의 최상위에서 import하는 구분을 static 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 가 다른 곳에서 사용되지 않는 경우, 사용자가 필요한 경우에만 가져오도록 할 수 있다.
dynamic import는 then 함수를 사용해 필요한 코드만 가져오며, 가져온 코드에 대한 모든 호출은 해당 함수 내부에 있어야 한다.

이 dynamic import는 React.lazy 와 함께 사용할 수 있다.

Ch5. React.lazy(), Suspense

React.lazy()

React.lazy 함수를 사용해 dynamic import로 컴포넌트를 렌더링할 수 있다. 즉 컴포넌트를 렌더링할 때 필요한 컴포넌트만 먼저 렌더링하고 다른 컴포넌트는 나중에 렌더링 하도록 할 수 있다. 이는 초기 렌더링 시간을 줄여준다.
사용방법은 아래와 같다.

import { lazy } from 'react';

//...
const Component = React.lazy(() => import('./Component'));

이 React.lazy는 단독으로 사용하지 않고 React.suspense 와 함께 사용한다.

Suspense

lazy를 사용해 import할 경우 컴포넌트를 불러오면서 로딩하는 시간이 생기게 된다.
이 때 Suspense는 컴포넌트를 불러오기 전 로딩 화면을 보여주고, 로딩이 완료되면 컴포넌트를 보여준다.
fallback 속성에 로딩시 필요한 컴포넌트를 삽입할 수 있다.
사용법은 아래와 같다.

import { Suspense } from 'react';

const Home = React.lazy(() => import('./Home'));
const AComponent = React.lazy(() => import('./AComponent'));
const BComponent = React.lazy(() => import('./BComponent'));
const Loading=lazy(() => import('./Loading')); //lazy 생략가능

function App() {
  return (
  <Router>
    {/* fallback 안에 <Loading/> 이렇게 로딩 컴포넌트를 따로 넣을 수도 있다. */}
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/A" element={<AComponent />} />
        <Route path="/B" element={<BComponent />} />
      </Routes>
    </Suspense>
  </Router>
  );
}

위에서 소개한 lazy, suspense는 예시처럼 주로 Route로 분기된 페이지의 내용을 불러올 때 사용한다.


학습자료

1. Custom Hook에 대한 설명
🔗 React 공식문서 - 자신만의 Hook 만들기

profile
프론트엔드 개발자 NH입니다. 시리즈로 보시면 더 쉽게 여러 글들을 볼 수 있습니다!

0개의 댓글