React 심화

omegle·2022년 11월 28일
0

Virtual DOM

가상의 DOM 객체. DOM객체에 접근해 변화 전과 후를 비교하고 바뀐 부분을 적용한다.

Real DOM

DOM이 변경되면 브라우저의 렌더링 엔진도 리플로우하기 때문에 속도가 느려지게 된다. 자바스크립트로 조작하는 돔의 요소가 많을수록 돔 업데이트에 대한 비용이 많이 들게 된다.
언뜻 듣기만 해도 비효율적이라는 것을 알 수 있다. 바뀌는 부분만 렌더링되면 얼마나 좋을까!하는 생각으로 버추얼 돔이 나타나게 된 것이다.

리액트는 상태가 변경되면 새로운 가상 돔을 만든다. 기존의 가상 돔과 새로운 가상 돔을 비교하고, 비교가 끝나면 부분적으로 리렌더링이 되고 업데이트 된 트리는 실제 돔으로 한꺼번에 업데이트된다.

React Diffing Algorithm

리액트가 기존의 가상돔과 새로운 가상돔을 비교할때, 새로운 가상돔에 부합하도록 UI를 효율적으로 갱신하는 방법을 알아내야 한다. 하나의 트리를 다른 트리로 변형시키는 가장 효율적인 방식을 찾기 위한 알고리즘은 O(n^3)의 복잡도를 갖고 있다.
천개의 엘리먼트를 실제 화면에 표시하기까지 10억번의 비교연산을 해야 하는 것이다. WoW..
그래서 리액트는 두가지의 가정을 하고 휴리스틱 알고리즘을 구현한다. 휴리스틱이란 '추정'이라는 뜻이다!

두 가지 가정

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

리액트의 돔 트리 탐색 : 너비우선 탐색(DFS)

트리의 레벨 순서대로 비교한다.

다른 타입의 돔 엘리먼트일 경우 : 새로운 트리 구축

기존의 컴포넌트는 언마운트되어버려 기존의 state도 파괴된다.

같은 타입의 돔 엘리먼트일 경우 : 최소한의 변경사항만 업데이트

업데이트 할 내용이 생기면 버추얼 돔 내부의 프로퍼티만 수정한 뒤, 모든 노드 스캔이 끝나면 실제 돔으로의 렌더링을 실행한다.

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

리액트는 위에서 아래로 순차적으로 비교한다. 그렇기 때문에 아래에 노드를 추가하는 것이 아니라 위에 노드를 추가하게 되면 성능이 훨씬 나빠진다. 위에서 다른 노드를 발견한 순간 완전히 다른 노드라고 생각해 전부 버리고 새로 렌더링해버린다.
이런 문제를 방지하기 위해 key라는 속성을 활용하게 된다.

key

키를 이용해 기존 노드와 새로운 노드의 일치 여부를 확인한다.
여기서 키는 전역적으로 유일할 필요 없이 형제 엘리먼트 사이에서 유일하면 된다.
배열의 인덱스를 키로 사용하는 것은 지양하자. 만일 배열이 다르게 정렬될 경우가 생기면 배열의 인덱스를 키로 선택했을 경우 비효율적일 수 있다.

React Hooks

Component와 Hook

전에는 클래스 컴포넌트를 사용했었다. 클래스 컴포넌트는 이해하기 어렵고 상태 로직을 재사용하기 어려웠다. 그래서 Hook이라는 개념과 더불어 함수형 컴포넌트로 이를 대체하기 시작했다.

함수형 컴포넌트는 직관적이고 보기 쉽다. 그동안 사용해왔던 useState() 또한 Hook이다. 컴포넌트에서 Hook을 호출해 함수 컴포넌트 안에 state를 추가했다. 이 state는 컴포넌트가 리렌더링 되어도 그대로 유지되며 때에 따라서 여러개의 훅을 사용할 수도 있다.

Hook이란

Hook은 class를 작성하지 않고도 state와 다른 React 기능들을 사용할 수 있게 해준다.

휵은 클래스형 컴포넌트에서는 동작하지 않는다.

Hook 사용 규칙

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

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

렌더링 최적화를 위한 훅이 useMemo, useCallback이다.

useMemo

특정 을 재사용하고자 할 때 사용한다.
메모이제이션의 기능을 해준다고 보면 된다.
첫번째 인자로는 콜백함수를 넣어주고
두번째 인자로는 의존성배열을 넣는다.
이 배열 안에 넣은 내용이 바뀌는 등록된 함수를 이용해 연산해주고
바뀌지 않았다면 이전에 콜백함수를 통해 리턴된 값을 재사용한다.
메모리를 사용해 값을 저장해두는 것이기 때문에 남발하지 않고 적절하게 사용하는 것이 중요하다

import React, { useState, useMemo } from "react";

const twice = (num) => {
  return num * 2;
}

function App() {
  const [name, setName] = useState("");
  const [val, setVal] = useState(0);
  const answer = useMemo(() => { //최상위에 위치해야함. hook안에 hook을 쓸 수 없음.
    return twice(val);
  }, [val]);

  return (
    <div>
      <input
        className="name-input"
        value={name}
        type="text"
        onChange={(e) => setName(e.target.value)}
      />
      <input
        className="value-input"
        value={val}
        type="number"
        onChange={(e) => setVal(Number(e.target.value))}
      />
      <div>{answer}</div>
    </div>
  );
}

useCallback

마찬가지로 메모이제이션 기법을 사용한 훅이다.
함수의 재사용을 위해 사용한다.
단순히 함수를 반복해 생성하지 않기 위함이 아니라 자식 컴포넌트의 props로 함수를 전달할 때 사용하기 좋다.

참조 동등성

함수는 객체이다. 객체는 메모리에 저장할때 값이 주소를 저장하기 때문에 반환하는 값이 같아도 일치연산자로 비교하면 false가 출력된다.

리액트는 리렌더링 시 함수를 새로 만들어서 호출한다. 기존의 함수와 같은 함수가 아닌 것이다! 그러나 useCallback을 이용해 함수 자체를 저장해 다시 사용하면 함수의 메모리주소값을 저장했다 다시 사용하는 것과 같아 성능에 유효한 기여를 할 수 있다.

import { useState, useCallback } from "react";

export default function App() {
  const [input, setInput] = useState(1);
  const [light, setLight] = useState(true);

  const theme = {
    backgroundColor: light ? "White" : "grey",
    color: light ? "grey" : "white"
  };

  const getItems = useCallback(() => {
    return [input + 10, input + 100];
  }, [input]);

  const handleChange = (event) => {
    if (Number(event.target.value)) {
      setInput(Number(event.target.value));
    }
  };
}

Custom Hooks

https://ko.reactjs.org/docs/hooks-custom.html

훅을 만들어서 사용하는 것도 가능하다.
여러 url을 fetch할때, 여러 input에 의한 상태변경 등 반복되는 로직을 동일한 함수에서 작동하게 하고 싶을 때 커스텀 훅을 주로 사용한다.

커스텀훅을 쓰면 이런 점이 좋다

  1. 상태관리 로직의 재활용
  2. 클래스 컴포넌트보다 짧은 코드로 동일 로직 구현
  3. 함수형으로 작성하기 때문에 명료하다.

커스텀훅 규칙

  1. 함수이름 앞에 use를 붙인다.
  2. hooks 디렉터리에 커스텀 훅을 위치시킨다.
  3. 조건부 함수가 아니어야 한다. 필요하다면 불리언 타입으로 반환한다.

커스텀 훅을 여러 컴포넌트에서 사용했다 해서 같은 state를 공유하는 것이 아니다. 오직 로직만 공유한다. state는 각 컴포넌트 내에서 독립적으로 정의되어있다.

useFetch 예시

API를 통해 데이터를 받아와 처리하는 로직은 반복적일 수밖에 없기 때문에 custom 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;

useInputs 예시

input은 반복적으로 사용된다. 반복되는 로직을 관리해야 할 필요성이 있다. custon hook으로 반복되는 로직을 분리해 관리하자.

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;
```js
#### useFetch 실습
```js
//hooks/useFetch.js
import { useEffect, useState } from "react";

export const useFetch = () => {
  //useState
  const [data, setData] = useState();
  //useEffect
  useEffect((fetchUrl) => {
    fetch(fetchUrl, {
      headers: {
        "Content-Type": "application/json",
        Accept: "application/json"
      }
    })
      .then((response) => {
        return response.json();
      })
      .then((myJson) => {
        setData(myJson);
      })
      .catch((error) => {
        console.log(error);
      });
  }, [data]);

  return data; //data를 리턴해주어야함.
};

//app.js
import useFetch from "./util/useFetch";

export default function App() {
  const data = useFetch("./public/data.json");
  //useFetch 인자로 url을 넘겨준후 변수에 담는다.

  return (
    <div className="App">
      <h1>To do List</h1>
      <div className="todo-list">
        {data &&
          data.todo.map((el) => {
            return <li key={el.id}>{el.todo}</li>;
          })}
      </div>
    </div>
  );
}
useInputs 실습(내코드)
//hooks/useInputs.js
//이 곳에 input custom hook을 만들어 보세요.
//return 해야 하는 값은 배열 형태의 값이어야 합니다.
import { useState } from "react";
function useInput() {
  const [firstName, setFirstName] = useState("");
  const [lastName, setLastName] = useState("");
  const [nameArr, setNameArr] = useState([]);

  const handleSubmit = (e) => {
    e.preventDefault();
    setNameArr([...nameArr, `${firstName} ${lastName}`]);
  };

  return [
    firstName, setFirstName,
    lastName, setLastName,
    nameArr,
    handleSubmit
  ];
}
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() {
  //input에 들어가는 상태값 및 로직을 custom hook으로 만들어봅니다.
  //until 폴더의 useInput.js 파일이 만들어져 있습니다.
  const [
    firstName, setFirstName,
    lastName, setLastName,
    nameArr,
    handleSubmit
  ] = useInput({
    firstName: "",
    lastName: "",
    nameArr: []
  });

  return (
    <div className="App">
      <h1>Name List</h1>
      <div className="name-form">
        <form onSubmit={handleSubmit}>
          <Input
            labelText={"성"}
            value={firstName}
            handleInputChange={(e) => {
              setFirstName(e.target.value);
            }}
          />
          <div className="name-input">
            <label>이름</label>
            <input
              value={lastName}
              onChange={(e) => setLastName(e.target.value)}
              type="text"
            />
          </div>
          <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>
  );
}
useInput 레퍼런스
//hooks/useInputs.js
import { useState } from "react";
function useInput(initialValue) {
  const [value, setValue] = useState(initialValue);
  const bind = {
    value,
    onChange: (e) => {
      setValue(e.target.value);
    }
  };
  return [value, bind]; //아하 값이 아니라 기능(로직)을 보내주는 것이구나!!
}
//app.js
export default function App() {
  const [firstValue, firstBind] = useInput("");
  const [secondValue, secondBind] = useInput("");
  const [nameArr, setNameArr] = useState([]);

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

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

React 신기능

createRoot API

ReactDOM.render를 더이상 사용하지 않는다.

import { createRoot } from "react-dom/client";

const rootElement = document.getElementById("root");
const root = createRoot(rootElement);

root.render(
    <App />
);

코드분할 code spliting

자바스크립트 코드가 방대해지면서 속도가 현저히 낮아지게 되었다. 그래서 당장 필요한 자바스크립트 코드만 불러오고 나중에 필요한 코드는 나중에 불러올 수 있도록 코드분할이라는 아이디어가 나타났다.
런타임시 여러 번들을 동적으로 만들고 불러오는 것을 코드분할이라 한다. 이러한 기능을 통해 페이지 로딩 속도를 개선할 수 있다.

파일을 무겁게 만드는 것들 중 하나가 바로 서브파티 라이브러리이다. 여러 기능을 제공하기 때문에 불필요한 공간차지를 하게 될 것이다. 이때 필요한 메서드만 불러와 쓰는 것이 훨씬 효율적일 것이다.

//lodash의 메소드 중 find 하나만 불러와서 사용
import find from 'lodash/find';

find([]);

React 코드분할

리액트는 Single page Application이기 때문에 사용하지 않는 모든 컴포넌트까지 한번에 불러온다. 그래서 첫 화면이 렌더링될때까지의 시간이 오래걸리는 것이다.
리액트에서 코드분할을 하려면 dynamic import를 사용하면 된다. static import가 지금까지 해온 불러오기이다. 파일의 최상위에서 import문을 작성하는 것 말이다.
나는 관성적으로 최상위에 import를 했지만 블록문 안에서는 import구문을 작성할 수 없었다고 한다. 지금은 동적으로 import를 할 수 있도록 지원하고 있다.

dynamic import 동적 불러오기

  • 다른 곳에서 사용되지 않는 경우 사용한다.
  • then 함수를 사용해 필요한 코드만 가져온다.
  • 가져온 코드에 대한 모든 호출은 해당 함수 내부에 있어야 한다.
  • React.lazy와 함께 사용 가능하다.
form.addEventListener("submit", e => {
  e.preventDefault();
  import('library.moduleA')
    .then(module => module.default)
    .then(someFunction())
    .catch(handleError());
});

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

React.lazy

dynamic import와 함께 사용해 컴포넌트를 렌더링한다.
lazy를 통해서 초기 렌더링 시간을 줄일 수 있다.

import Component from './Component';

/* React.lazy로 dynamic import를 감쌉니다. */
const Component = React.lazy(() => import('./Component'));

React.lazy로 감싼 컴포넌트는 단독으로는 못씀. React.suspense 컴포넌트의 하위에서 렌더링을 해야한다.

React.Suspense

아직 렌더링이 준비되지 않은 컴포넌트가 있을 때 로딩화면을 보여주고, 로딩이 완료되었을때 렌더링하는 기능이다.

import { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
//1. 라우터가 분기되는 컴포넌트에서 각 컴포넌트에 lazy 사용해 import한다.
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));

const App = () => (
  <Router>
    //2. suspense로 감싼다.
    <Suspense fallback={<div>Loading...</div>}>
     //3. fallback: 로딩화면으로 사용할 컴포넌트 설정
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </Suspense>
  </Router>
);
profile
JANG EUN JI | 장은지

0개의 댓글