[React] 심화 - useMemo,useCallback,Cumstom Hooks

EC kim·2022년 11월 25일
0
  • Virtural DOM
    가상의 DOM객체
    실제 DOM의 사본 같은 개념으로 React는 실제 DOM객체에 접근하여 조작하는 대신 가상의 DOM객체에 접근하여 변화 전과 후를 비교하고 바뀐 부분을 적용한다.

DOM이란??
< html >, < head >와 같은 태그들에 접근하여 조작할 수 있도록 태그들을 트리 구조로 객체화 시킨것이다. DOM은 브라우저가 트리 구조로 만든 객체모델

-DOM의 구조는 앞서 설명했듯이 계층적 구조로 되어 있는 트리
-자료구조 중에서 특히 트리는 “데이터 저장"의 의미보다는 “저장된 데이터를 더 효과적으로 탐색”하기 위해 사용
-트리구조로 된 DOM은 JavaScript 같은 스크립팅 언어가 접근하고 탐색하는 속도가 빠릅니다.
-DOM의 변경 및 업데이트는 브라우저 또한 화면을 리플로우(Reflow)한다는 것을 의미
-React에는 모든 DOM 객체에 대응하는 가상의 DOM 객체가 있다.
-상대적으로 무거운 DOM에 비하여 React의 가상 DOM 객체는 실제 DOM 객체와 동일한 속성을 가지고 있음에도 “훨씬 가벼운 사본”이라고 표현
-실제 DOM을 조작하는 것은 실제로 브라우저 화면에 그리기 때문에 느리지만, 가상 DOM을 조작하는 것은 실제 DOM처럼 실제로 브라우저 화면에 그리는 것이 아니기 때문에 훨씬 속도가 빠르다.


-이전의 가상의 DOM과 이후의 가상의 DOM의 차이를 비교
-이 작업이 완료가 되면 가상의 DOM은 실제 DOM에 변경을 수행할 수 있는 최상의 방법을 계산하기 시작 -> 실제 DOM의 업데이트 비용을 줄일 수 있게 된다.


  • React Diffing Algorithm
    React가 기존 가상 DOM과 변경된 새로운 가상 DOM을 비교할 때, React 입장에서는 변경된 새로운 가상 DOM 트리에 부합하도록 기존의 UI를 효율적으로 갱신하는 방법을 알아낼 필요가 있었다.

두가지의 가정
1. 각기 서로 다른 두 요소는 다른 트리를 구축할 것이다.
2. 개발자가 제공하는 key 프로퍼티를 가지고, 여러 번 렌더링을 거쳐도 변경되지 말아야 하는 자식 요소가 무엇인지 알아낼 수 있을 것이다.
실제 이 두 가정은 거의 모든 실제 사용 사례에 들어맞게 됩니다. 여기서 React는 비교 알고리즘(Diffing Algorithm)을 사용

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

  1. 다른 타입의 DOM엘리먼트인 경우
    DOM 트리는 각 HTML 태그마다 각각의 규칙이 있어 그 아래 들어가는 자식 태그가 한정적이라는 특징이 있다. (예를 들어 < ul> 태그 밑에는 < li> 태그만 와야 한다던가, < p> 태그 안에 < p> 태그를 또 쓰지 못하는 것이다. 이럴 경우 react는 이전 트리를 버리고 새로운 트리를 구축한다. 컴포넌트가 안에 있었다면 기존의 컴포넌트는 파괴하고 새로운 컴포넌트가 실행되지만 기존의 컴포넌트가 가지고 있던 state또한 파괴된다.

  2. 같은 타입의 DOM엘리먼트인 경우
    반대로 타입이 바뀌지 않는다면 React는 최대한 렌더링을 하지 않는 방향으로 최소한의 변경 사항만 업데이트한다. 이것이 가능한 이유는 앞서 React가 실제 DOM이 아닌 가상 DOM을 사용해 조작하기 때문이다. 업데이트 할 내용이 생기면 virtual DOM 내부의 프로퍼티만 수정한 뒤, 모든 노드에 걸친 업데이트가 끝나면 그때 단 한번 실제 DOM으로의 렌더링을 시도한다.

-자식 엘리트먼트의 재귀적 처리
첫번째 노드가 바뀌면 React에서는 전부가 바뀌었다고 받아들이고 전부 새롭게 렌더링한다. 이는 비효율적인 동작방식이다. 그래서 React는 key라는 속성을 지원한다.

-key
만약 자식 노드들이 이 key를 갖고 있다면, React는 그 key를 이용해 기존 트리의 자식과 새로운 트리의 자식이 일치하는지 아닌지 확인할 수 있다.
키는 전역적으로 유일할 필요는 없고, 형제 엘리먼트 사이에서만 유일하면 된다.


  • React Hooks

    Hook이란?
    -Hook은 React 16.8에 새로 추가된 기능입니다. Hook은 class를 작성하지 않고도 state와 다른 React의 기능들을 사용할 수 있게 해준다.
    -React Hook은 함수 컴포넌트가 상태를 조작하고 및 최적화 기능을 사용할 수 있게끔 하는 메소드이다.
    -그 중 렌더링 최적화를 위한 Hook도 존재하는데, useCallback과 useMemo가 바로 그 역할을 하는 Hook

Hook사용규칙

  1. 리액트 함수의 최상위에서만 호출해야한다.
  2. 오직 리액트 함수 내에서만 사용되어야만 한다.
  3. React Hook은 class 없이 function으로만 React를 사용할 수 있게 해주는 것이기 때문에 클래스형 컴포넌트에서는 동작하지 않는다.
  • useMemo
    useMemo은 특정 값(value)를 재사용하고자 할 때 사용하는 Hook

import { useMemo } from "react";
function Calculator({value}){

const result = useMemo(() => calculate(value), [value]);

return <>
  <div>
				{result}
  </div>

< / >;
}
=> props로 받아온 value를 가지고 calculate함수에 집어넣어서 result값을 반환한다. calculate함수를 계속 호출하는 이 과정은 렌더링될 때마다 반복되어 시간이 소요된다. 그래서 useMemo를 호출하여 calculate를 감싸주면 이전에 구축된 렌더링과 새로이 구축되는 렌더링을 비교해 value값이 동일할 경우에는 이전 렌더링의 value값을 그대로 재활용할 수 있게 된다.
(Memorization : 기존에 수행한 연산의 결과값을 메모리에 저장을 해두고 동일한 입력이 들어오면 재활용하는 프로그래밍 기법)

  • useCallback
    useCallback 또한 useMemo와 마찬가지로 메모이제이션 기법을 이용한 Hook이다.
    useMemo는 값의 재사용을 위해 사용하는 Hook이라면, useCallback은 함수의 재사용을 위해 사용하는 Hook

import React, { useCallback } from "react";
//import로 불러와야한다.

function Calculator({x, y}){

const add = useCallback(() => x + y, [x, y]);

return <>
  <div>
				{add()}
  </div>

</>;
}

사실 useCallback만 사용해서는 useMemo에 비해 괄목할 만한 최적화를 느낄 수는 없다.
useCallback은 함수를 호출을 하지 않는 Hook이 아니라, 그저 메모리 어딘가에 함수를 꺼내서 호출하는 Hook이기 때문이다.
따라서 단순히 컴포넌트 내에서 함수를 반복해서 생성하지 않기 위해서 useCallback을 사용하는 것은 큰 의미가 없거나 오히려 손해인 경우도 있습니다.
그러면 언제 사용하는 게 좋을까요?
!!!!!자식 컴포넌트의 props로 함수를 전달해줄 때 이 useCallback을 사용하기가 좋습니다.!!!!

-useCallback과 참조 동등성

useMemo는 함수의 연산량이 많을때 이전 결과값을 재사용하는 목적이고, useCallback은 함수가 재생성 되는것을 방지하기 위한 목적이다
useMemo는 값을 저장 , useCallback은 함수를 저장


  • Custom Hooks
    개발자가 스스로 커스텀한 훅이며 이를 이용해 반복되는 로직을 뽑아내어 재사용할 수 있다.
  1. 상태관리 로직의 재활용이 가능하고
  2. 클래스 컴포넌트보다 적은 양의 코드로 동일한 로직을 구현할 수 있고
  3. 함수형으로 작성하기 때문에 보다 명료하다.
  • Custom Hooks 규칙
  1. Cumstom Hook을 정의할 때는 함수 이름 앞에 use를 붙이는 것이 규칙
  2. 대부분 프로젝트 내의 hooks디렉토리에 Custom Hook을 위치시킨다.
  3. Custom Hook으로 만들 때 함수는 조건부 함수가 아니어야한다. 즉 return 하는 값은 조건부여서는 안된다.

Custom hHook은 hook내부에 useState와 같은 React 내장 Hook을 사용하여 작성할 수 있다. 일반함수 내부에서는 React내장hook을 불러 사용할 수 없지만 Custom hook에서는 가능하다.
두개의 컴포넌트에 Custom hook을 만들어서 사용해도 두개의 컴포넌트가 같은 state를 공유하는 것은 아니다.

예제1) Custom Hook - useFetch

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;

예제2) Custom Hook - useInputs
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의 주목해야 할 기능

  • 코드분할
    번들링된 앱의 코드양이 많아지게 되면 코드를 해석하고 실행하는 정도가 느려진다. 그래서 번들을 물리적으로 나누어 지금 당장 필요한 코드가 아니라면 따로 분리를 시키고 나중에 필요할 때 불러와서 사용할 수 있도록 만든다.

번들링 되는 파일에는 npm을 통해 다운받는 서드파티 라이브러리도 포함된다. 서드파티 라이브러리는 사용자에게 다양한 메소드를 제공하기 때문에 코드의 양이 많고 번들링 시 많은 공간을 차지한다.

  • React에서의 코드분할
    static import(동적) :
    항상 import 구문은 문서의 상위에 위치, 블록문 안에서는 위치할 수 없는 제약 사항
    ex_) import moduleA from "library";
    Dynamic import:
    dynamic import는 then 함수를 사용해 필요한 코드만 가져온다.
    가져온 코드에 대한 모든 호출은 해당 함수 내부에 있어야한다.

  • React.lazy()
    React.lazy 함수를 사용하면 dynamic import를 사용해 컴포넌트를 렌더링할 수 있다.
    const Component = React.lazy(() => import('./Component'));이 React.lazy로 감싼 컴포넌트는 단독으로 쓰일 수는 없고, React.suspense 컴포넌트의 하위에서 렌더링을 해야한다.

  • React.Suspense
    Router로 분기가 나누어진 컴포넌트들을 위 코드처럼 lazy를 통해 import하면 해당 path로 이동할때 컴포넌트를 불러오게 되는데 이 과정에서 로딩하는 시간이 생긴다.
    Suspense는 아직 렌더링이 준비되지 않은 컴포넌트가 있을 때 로딩 화면을 보여주고, 로딩이 완료되면 렌더링이 준비된 컴포넌트를 보여주는 기능

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

profile
프론트엔드 개발자 일기

0개의 댓글