[CodeStates-Section4]U4.React-심화

소이뎁·2023년 1월 26일
0

CodeStates_Frontend_42기

목록 보기
36/39

후기

나만 어렵나? 복습이 필요한 시기!

Chapter1. React 심화
-1. Virtual DOM
-2. React Diffing Algorithm
Chapter2. React Hooks
-1. Component와 Hook
-2. useMemo
-3. useCallback
-4. Custom Hooks
Chapter3. React의 주목해야할 기능
-1. 코드 분할(Code Splitting)
-2. React.lazy()와 Suspense
과제 - React Hooks 적용하기

<Chapter1. React 심화>

1.Real DOM

1) Real DOM

DOM, Document Object Model, 문서 객체 모델

-정의
브라우저가 html 태그들을 트리 구조로 만든 객체 모델

-목적
스크립팅 언어(JavaScrpt)가 html 태그들에 접근하고 조작할 수 있도록 함

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

빠른 데이터 탐색(Tree 구조의 장점) -> 빠른 DOM 변경 -> 잦은 브라우저 리플로우, 리페인트 -> 잦은 재연산

대부분의 JavaScript 프레임워크는 변경된 요소 뿐만 아니라 나머지 요소도 리렌더링 함 -> “바뀐 부분만 비교해서 그 부분만 렌더링을 할 수는 없을까?“라는 생각에 도달

2.Virtual DOM

1) Virtual DOM

-정의
UI의 이상적인 또는 “가상”적인 표현을 메모리에 저장하고 ReactDOM과 같은 라이브러리에 의해 “실제” DOM과 동기화하는 프로그래밍 개념

-목적
Real DOM을 조작하는 것보다 더 빠르게 렌더링하기 위해

2) 가상 DOM이 더 빠른 이유

Virual DOM 변경 사항 발생
-> 이전 Virtual DOM과 현재 Virtual DOM의 차이 비교
-> Real DOM에 변경을 수행할 수 있는 최상의 방법 계산
-> Virual DOM 부분적 리렌더링
-> Real DOM에서 한번에 업데이트(최소의 Real DOM 업데이트 비용, 최소의 브라우저 파워 사용, 최소의 렌더링 시간)

3) 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"
					}
				]
			]
		}
	]
}

// Real DOM, Virtual DOM -> HTML 문서 객체 기반
// Virtual DOM -> Real DOM을 건드리지 않고 자유롭게 조작 가능

3.React Diffing Algorithm

1) 정의

두 개의 트리를 비교할 때, 두 엘리먼트의 루트 엘리먼트부터 비교하는 알고리즘

2) 목적

React가 Virtual DOM을 변경 시 효율적인 작업을 위해 사용

3) Diffing Algorithm의 가정

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

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

이전 트리를 버리고 완전히 새로운 트리를 구축
이전 트리와 연관된 모든 state 사라짐

// 이전 Counter는 사라지고 새로운 Counter가 remount
// 이전 Counter와 연관된 모든 state 사라짐
<div>
  <Counter />
</div>

<span>
  <Counter />
</span>

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

두 엘리먼트의 속성을 확인하여, 변경된 속성들만 갱신
하나의 DOM 노드를 처리한 뒤 해당 노드들 밑의 자식들을 순차적으로 동시에 순회하면서 차이가 발견될 때마다 변경(재귀적 처리)

// 현재 DOM 노드 상에 className만 수정
<div className="before" title="stuff" />
<div className="after" title="stuff" />

// 현재 DOM 노드 상에 color만 수정
<div style={{color: 'red', fontWeight: 'bold'}} />
<div style={{color: 'green', fontWeight: 'bold'}} />

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

동시에 두 리스트 순차적으로(위 -> 아래) 비교하고 차이점이 있으면 변경

<ul>
  <li>first</li>
  <li>second</li>
</ul>

<ul>
  <li>first</li> // 일치
  <li>second</li> // 일치
  <li>third</li> // 불일치 -> 트리에 추가
</ul>

발생하는 문제점: 첫번째 자식으로 추가 시 비효율적

<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

<ul>
  <li>Connecticut</li> // 불일치 -> 트리 변경
  <li>Duke</li> // 불일치 -> 트리 변경
  <li>Villanova</li> // 불일치 -> 트리 변경
</ul>

7) Key

이러한 문제를 해결하기 위해, React는 key 속성 지원. 자식들이 key를 가지고 있다면, React는 key를 통해 기존 트리와 이후 트리의 자식들이 일치하는지 확인.

<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

<ul>
  <li key="2014">Connecticut</li> // 새로운 key -> 추가
  <li key="2015">Duke</li> // 이동
  <li key="2016">Villanova</li> // 이동
</ul>

-key 값으로 사용할 수 있는 것(❗️key 값은 형제 사이에서만 유일하면 됨)

  • 엘리먼트의 식별자
  • id 속성
  • 데이터 일부에 해시를 적용한 값
  • 배열의 인덱스(권장하지 않음. 항목들이 재배열되지 않는다면 잘 동작하지만, 재배열되는 경우 비효율적이거나 의도하지 않은 방식으로 동작)

<Chapter2. React Hooks>

1.Component와 Hook

1) Class Component

-React에서 Function Component 이전에 사용한 컴포넌트
-문제점

  • 컴포넌트 사이에서 상태 로직을 재사용 어려움
  • 복잡한 컴포넌트들은 이해하기 어려움
  • Class은 사람과 기계를 혼동시킴(JavaScript의 this키워드는 대부분의 다른 언어에서와는 다르게 작동하므로)

이러한 문제를 해결하기 위해, Hook은 Class없이 React 기능들을 사용하는 방법(Function Component)을 제시

2) Function Component

-Hook 사용 가능(Class Component에서 Hook 호출 불가)
-Class Component에 비해 직관적

3) Hook

-역할
class를 작성하지 않고도 state와 다른 React의 기능들을 사용할 수 있게 해줌(React 16.8에 새로 추가된 기능).

-사용 규칙
React에서는 아래의 규칙들을 자동으로 강제하기 위한 linter 플러그인을 제공

  • 최상위에서만 Hook을 호출해야 함
    ❌ 반복문, 조건문, 중첩된 함수 내부에서 Hook 호출
    React는 Hook을 호출되는 순서대로 저장. 그런데 조건문, 반복문 안에서 Hook을 호출하게 되면 호출되는 순서대로 저장을 하기 어려워지고, 결국 예기치 못한 버그를 초래하게 될 수 있음.

  • 오직 React 함수 내에서 Hook을 호출 함
    ❌ 일반적인 JavaScript 함수에서 호출
    ✅ React Function Component에서 호출
    ✅ Custom Hook에서 호출

2.useMemo

1) 역할

memoization을 통해 특정 을 재사용하여 렌더링 최적화, 값을 리턴

2) 형식

// 의존성 배열([a, b])이 변경되었을 때만 생성 함수(() => computeExpensiveValue(a, b)) 호출
import { useMemo } from "react";

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
// 함수 안에서 참조되는 모든 값은 의존성 배열에 나타나야 함

3) Memoization

기존에 수행한 연산의 결과값을 메모리에 저장을 해두고, 동일한 입력이 들어오면 재활용하는 프로그래밍 기법. 중복 연산을 할 필요가 없기 때문에 앱의 성능을 최적화. useMemo는 memoizeion 이용하여 복잡한 연산의 중복을 피하고 React 앱의 성능을 최적화.

3.useCallback

1) 역할

memoization을 통해 함수를 재사용하여 렌더링 최적화, 함수를 리턴

2) 형식

// 의존성 배열([a, b])이 변경되었을 때만 콜백 함수(() => doSomething(a, b)) 호출
import React, { useCallback } from "react";

const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]);
// 콜백 안에서 참조되는 모든 값은 의존성 배열에 나타나야 함
// useCallback(fn, deps) === useMemo(() => fn, deps)

3) useCallback의 유용성

-useCallback이 큰 의미가 없는 경우
useCallback은 값을 저장하는 것이 아닌 함수를 저장하여 호출하는 Hook이기 때문에 단순히 컴포넌트 내에서 함수를 반복해서 생성하지 않기 위해서 사용하는 것은 큰 의미가 없거나 오히려 손해인 경우도 있음

-useCallback을 사용하면 좋은 경우
자식 컴포넌트의 props로 함수를 전달해줄 때(useCallback의 참조 동등성과 관련)

-useCallback과 참조 동등성
JavaScript를 기반으로 하는 React에서 함수는 객체 -> 같은 함수를 할당해도 주소값이 다르면 다른 함수

function doubleFactory(){
    return (a) => 2 * a;
}
  
const double1 = doubleFactory();
const double2 = doubleFactory();
  
double1(8); // 16
double2(8); // 16
  
double1 === double2;  // false
double1 === double1;  // true

React는 리렌더링 시 함수를 새로이 만들어서 호출. 새로이 만들어 호출된 함수는 기존의 함수와 같은 함수가 아님. 그러나 useCallback을 이용해 함수 자체를 저장해서 다시 사용하면 함수의 메모리 주소 값을 저장했다가 다시 사용한다는 것과 같다고 볼 수 있음. 따라서 React 컴포넌트 함수 내에서 다른 함수의 인자로 넘기거나 자식 컴포넌트의 prop으로 넘길 때 예상치 못한 성능 문제를 막을 수 있음.

4.Custom Hooks

1) 정의

개발자가 스스로 커스텀한 훅

2) 목적

반복되는 로직을 함수로 뽑아내어 재사용

3) 장점

-상태관리 로직의 재활용이 가능
-클래스 컴포넌트보다 적은 양의 코드로 동일한 로직을 구현 가능
-함수형으로 작성하기 때문에 보다 명료

4) 규칙

-Custom Hook을 정의할 때는 함수 이름 앞에 use를 붙임.
-대개의 경우 프로젝트 내의 hooks 디렉토리에 Custom Hook을 위치 시킴.
-Custom Hook으로 만들 때 함수는 조건부 함수가 아니어야 함. 즉 return 하는 값은 조건부여서는 안 됨.
-일반 함수 내부에서는 React 내장 Hook을 불러 사용할 수 없지만 Custom Hook은 Hook 내부에 useState와 같은 React 내장 Hook을 사용하여 작성 가능.
-같은 Hook을 사용하는 두 개의 컴포넌트는 state를 공유하지 않음. 그저 로직만 공유할 뿐, state는 컴포넌트 내에서 독립적으로 정의 되어 있음.

5) 예시1

-Custom Hooks 사용 전
*** 부분 중복

//FriendStatus : 친구가 online인지 offline인지 return하는 컴포넌트
import React, { useState, useEffect } from 'react';

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일 때 초록색으로 표시하는 컴포넌트
import React, { useState, useEffect } from 'react';

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

-Custom Hooks 사용 후

// Custom Hook 정의
import { useState, useEffect } from 'react';

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

// FriendStatus
function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

// FriendListItem
function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

6) 예시2

여러 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;

7) 예시3

여러 input에 의한 상태 변경을 할 때 쓸 수 있는 useInputs Hooks

// <useInput.js>
import { useState } from "react";

function useInput(initialValue) {
  const [value, setValue] = useState(initialValue);

  const reset = () => {
    setValue(initialValue);
  };

  const bind = {
    value,
    onChange: (e) => {
      setValue(e.target.value);
    }
  };

  return [value, bind, reset];
}

export default useInput;

// <App.js>
import { useState } from "react";
import Input from "./component/Input";
import "./styles.css";
import useInput from "./util/useInput";

export default function App() {
  // useInput의 return 값 차례대로 할당
  const [firstValue, firstBind, firstReset] = useInput(""); 
  const [secondValue, secondBind, secondReset] = useInput("");
  
  const [nameArr, setNameArr] = useState([]);

  const handleSubmit = (e) => {
    e.preventDefault();
    setNameArr([...nameArr, `${firstValue} ${secondValue}`]);
    firstReset();
    secondReset();
  };

  return (
    <div className="App">
      <h1>Name List</h1>
      <div className="name-form">
        <form onSubmit={handleSubmit}>
          <Input labelText={"성"} value={firstBind} />
          <Input labelText={"이름"} value={secondBind} />
          <button>제출</button>
        </form>
      </div>
      <div className="name-list-wrap">
        <div className="name-list">
          {nameArr.map((el, idx) => {
            return <p key={idx}>{el}</p>;
          })}
        </div>
      </div>
    </div>
  );
}

<Chapter3. React의 주목해야할 기능>

1. 번들 분할

: 서드파티 라이브러리 부분만 불러오기

번들링 되는 파일에는 앱을 만들면서 npm을 통해 다운받는 서드파티(Third Party) 라이브러리도 포함. 그러므로 서드파티의 용량을 줄이면 번들의 크기도 줄어듬.

/* 이렇게 lodash 라이브러리를 전체를 불러와서 그 안에 들은 메소드를 꺼내 쓰는 것은 비효율적입니다.*/
import _ from 'lodash';

...

_.find([]);

/* 이렇게 lodash의 메소드 중 하나를 불러와 쓰는 것이 앱의 성능에 더 좋습니다.*/
import find from 'lodash/find';

find([]);

2.코드 분할(Code Splitting)

1) 코드 분할 출현

-모던 웹 발전 이전: Webpack, Rollup과 같은 툴로 번들링하여 JavaScript를 한 파일로 만든 후 HTML에 링크 태그로 추가
-모던 웹 발전: JavaScript 코드가 많아져 특정 지점에서 코드를 해석하고 실행하는 정도가 느려지게 되면서 '번들을 나눈 뒤 필요한 코드만 불러오면 어떨까?'라는 생각을 함
-모던 웹 발전 이후: 코드 분할

2) Dynamic Import 사용하기

-Static Import(코드 분할❌)
기존에는 항상 import 구문은 문서의 상위에 위치해야 했고, 블록문 안에서는 위치할 수 없는 제약 사항이 있었음. 왜냐하면 번들링 시 코드 구조를 분석해 모듈을 한 데 모으고 사용하지 않는 모듈은 제거하는 등의 작업을 하는데, 코드 구조가 간단하고 고정이 되어 있을 때에야만 이 작업이 가능해지기 때문.

/* 기존에는 파일의 최상위에서 import 지시자를 이용해 라이브러리 및 파일을 불러왔습니다. */
import moduleA from "library";

form.addEventListener("submit", e => {
  e.preventDefault();
  someFunction();
});

const someFunction = () => {
  /* 그리고 코드 중간에서 불러온 파일을 사용했습니다. */
}

-Dynamic Import(코드 분할✅)
then 함수를 사용해 필요한 코드만 가져옴. 가져온 코드에 대한 모든 호출은 해당 함수 내부에 있어야 함. 이 방식을 사용하면 번들링 시 분할된 코드(청크)를 지연 로딩시키거나 요청 시에 로딩 가능.

// 불러온 moduleA 가 다른 곳에서 사용되지 않는 경우, 사용자가 form을 통해 양식을 제출한 경우에만 가져옴
form.addEventListener("submit", e => {
  e.preventDefault();
	/* 동적 불러오기는 이런 식으로 코드의 중간에 불러올 수 있게 됩니다. */
  import('library.moduleA')
    .then(module => module.default)
    .then(someFunction())
    .catch(handleError());
});

const someFunction = () => {
    /* moduleA를 여기서 사용합니다. */
}

3) React.lazy & React.suspense 사용하기

-정의
React.lazy: dynamic import를 사용해서 컴포넌트 렌더링을 가능하게 함. React.suspense 컴포넌트 하위에서 렌더링 해야 함.
React.suspense: 아직 렌더링이 준비되지 않은 컴포넌트가 있을 때 로딩 화면을 보여주고, 로딩이 완료되면 렌더링이 준비된 컴포넌트를 보여줌. fallback prop은 컴포넌트가 로드될 때까지 기다리는 동안 로딩 화면으로 보여줄 React 엘리먼트를 인자로 받음(컴포넌트일 경우 로딩 컴포넌트도 React.lazy로 가져올 수 있음). Suspense 컴포넌트 하나로 여러 개의 lazy 컴포넌트를 보여줄 수도 있음.

-예시1: 기본

/* 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>
  );
}

-예시2: Route에 적용
앱에 코드 분할을 도입할 곳을 결정하는 것은 까다롭기 때문에, 중간에 적용시키는 것보다는 웹 페이지를 불러오고 진입하는 단계인 Route에 이 두 기능을 적용시키는 것이 좋음. 초기 렌더링 시간이 줄어드는 분명한 장점이 있으나 페이지를 이동하는 과정마다 로딩 화면이 보여지기 때문에 서비스에 따라서 적용 여부를 결정해야 함.

import { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';

// 라우터가 분기되는 컴포넌트에서 각 컴포넌트에 React.lazy를 사용하여 import
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>
);

<과제 - React Hooks 적용하기>

1.useParams, useNavigate

useParams: 현재 url에서 dynamic params를 객체로 반환해주는 hook
useNavigate: 인자의 주소로 이동하는 함수를 반환해주는 hook
참고 사이트: https://reactrouter.com/en/main/hooks/use-params

2.페이지 맨 위로 스크롤

방법1. window.scrollTo(0,0)
방법2. history(npm 라이브러리) history.action === 'POP'

3.console.error()

웹 콘솔에 에러 메시지를 출력

// 게시글 삭제, Home으로 리다이렉트
const handleDeleteClick = () => {
    fetch(`http://localhost:3001/blogs/${id}`, {
      method: 'DELETE',
    })
      .then((res) => {
        if (!res.ok) {
          throw Error('could not fetch the data for that resource');
        }
        return res.json();
      })
      .then((data) => {
        navigate('/');
        window.location.reload();// 변경 사항이 반영되지 않으므로 새로고침 해줌
      })
      .catch((err) => {
        console.error('Error', err);// 콘솔 에러 메세지 설정
      });
  };

0개의 댓글