이번 유닛에서는 리액트의 동작 방식과, 리액트 Hooks에 대해 배우며 리액트를 좀 더 심도 있게 학습하였다.
React에는 Virtual DOM이라고 하는 가상의 DOM 객체가 있다. 이는 실제 DOM의 사본 같은 개념으로, React는 실제 DOM 객체에 접근하여 조작하는 대신 이 가상의 DOM 객체에 접근하여 변화 전과 변화 후를 비교하고 바뀐 부분을 적용한다.
결론부터 말하면
- 속도적인 부분과 많은 일을 수행하다 버그가 발생하거나 브라우저가 죽는 일 등등의 일을 개선하고자 Virtual DOM이 나왔다.
그리고 virtual dom은 메모리 상에 존재하는 하나의 객체이다. 이제 리액트는 특정 state에 변화가 생겼다는 알림을 받으면 real dom 전체를 렌더링 시켜주는 것이 아니라, virtual dom을 렌더링 시킨다는 것이다.
즉, 브라우저를 새롭게 렌더링 시키는 비용보다, 객체를 새로 만드는 비용이 훨씬 더 저렴하기 때문에 virtual dom을 이용하는게 효율적이다는 것이다.
React Diffing Algorithm이란?
하나의 트리를 다른 트리로 변형을 시키는 가장 작은 조작 방식을 알아내야만 했는데, 알아낸 조작 방식 알고리즘은 O(n^3)의 복잡도를 가지고 있다.
이 알고리즘을 그대로 React에 적용한다면 너무 비싸기 때문에 React는 두 가지의 가정을 가지고 시간 복잡도 O(n)의 새로운 휴리스틱 알고리즘(Heuristic Algorithm)을 구현해냈다.
두가지 가정
React가 DOM 트리를 탐색하는 방법
React는 기존의 가상 DOM 트리와 새롭게 변경된 가상 DOM 트리를 비교할 때, 트리의 레벨 순서대로 순회하는 방식으로 탐색한다.(너비 우선 탐색(BFS))
React는 이런 식으로 동일 선상에 있는 노드를 파악한 뒤 다음 자식 세대의 노드를 순차적으로 파악해 나간다.
React Hooks는 React 16.8 버전부터 추가된 기능으로, 클래스 컴포넌트와 생명주기 메서드를 이용하여 작업을 하던 기존 방식에서 벗어나 함수형 컴포넌트에서도 더 직관적인 함수를 이용하여 작업할 수 있게 만든 기능이다.
Hook이란?
Hook은 class를 작성하지 않고도 state와 다른 React의 기능들을 사용할 수 있게 해준다.
function Counter () {
const [counter, setCounter] = useState(0);
const handIncrease = () => {
setCounter(counter + 5)
}
return (
<div>
<p>You clicked {counter} times</p>
<button onClick={handIncrease}>
Click
</button>
</div>
)
}
위의 예시와 같이 useState()가 바로 Hook이다.
Hook 사용 규칙
리액트 함수의 최상위에서만 호출해야 한다.
오직 리액트 함수 내에서만 사용되어야 한다.
useMemo은 특정 값(value)를 재사용하고자 할 때 사용하는 Hook이다.
import { useMemo } from "react";
function Calculator({inex}){
const result = useMemo(() => calculate(index), [index]);
return <>
<div>
{result}
</div>
</>;
}
여기 index 를 인자로 받는 Calculator 컴포넌트가 있다고 생각하자.
index 는 일종의 값으로, 이 값이 계속 바뀌는 경우라면 어쩔 수 없겠지만, 렌더링을 할 때마다 이 index 계속 바뀌는 게 아니라고 생각해보자.
그럼 이 값을 어딘가에 저장을 해뒀다가 다시 꺼내서 쓸 수만 있다면 굳이 calculate 함수를 호출할 필요도 없을 것이다.
여기서 useMemo Hook을 사용하면 효율적일 것이다.(Memoization)
개발자가 스스로 커스텀한 훅을 의미하며 이를 이용해 반복되는 로직을 함수로 뽑아내어 재사용할 수 있다.
장점
상태관리 로직의 재활용이 가능
클래스 컴포넌트보다 적은 양의 코드로 동일한 로직을 구현할 수 있다
함수형으로 작성하기 때문에 보다 명료하다는 장점이 있다.
Custom Hooks정의 시 규칙
- Custom Hook을 정의할 때는 함수 이름 앞에 use를 붙이는 것이 규칙이다.
- 대개의 경우 프로젝트 내의 hooks 디렉토리에 Custom Hook을 위치 시킨다.
- Custom Hook으로 만들 때 함수는 조건부 함수가 아니어야 한다.
코드 분할은 런타임 시 여러 번들을 동적으로 만들고 불러오는 것으로, Webpack, Rollup과 같은 번들러가 지원하는 기능이다.
즉, 지금 당장 필요한 코드가 아니라면 따로 분리를 시키고, 나중에 필요할 때 불러와서 사용하도록 하자는 것이다.
React는 SPA(Single-Page-Application)인데, 사용하지 않는 모든 컴포넌트까지 한 번에 불러오기 때문에 첫 화면이 렌더링 될때까지의 시간이 오래걸리기 때문에 사용하지 않는 컴포넌트는 나중에 불러오기 위해 코드 분할을 도입하였다.
그 방법은 dynamic import(동적 불러오기)이다.
dynamic import
dynamic import는 then 함수를 사용해 필요한 코드만 가져온다. 가져온 코드에 대한 모든 호출은 해당 함수 내부에 있어야 한다.
form.addEventListener("submit", el => {
el.preventDefault();
import('library.moduleA')
.then(module => module.default)
.then(someFunction())
.catch(handleError());
});
const someFunction = () => {
}
React.lazy 함수를 사용하면 dynamic import를 사용해 컴포넌트를 렌더링할 수 있다. React.lazy를 통해 컴포넌트를 동적으로 import를 할 수 있기 때문에 이를 사용하면 초기 렌더링 지연시간을 어느정도 줄일 수 있다.
Suspense는 아직 렌더링이 준비되지 않은 컴포넌트가 있을 때 로딩 화면을 보여주고, 로딩이 완료되면 렌더링이 준비된 컴포넌트를 보여주는 기능이다.
React.lazy와 Suspense의 적용
import { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./routes/nav'));
const About = lazy(() => import('./routes/footer'));
const App = () => (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<nav />} />
<Route path="/about" element={<footer />} />
</Routes>
</Suspense>
</Router>
);
여기서 Suspense, lazy를 꼭 import해 주어야 하고, Route 컴포넌트들을 Suspense로 감싼 후 로딩 화면으로 사용할 컴포넌트를 fallback 속성으로 설정해 주면 된다.