Chapter1. React 심화
1-1. Virtual DOM
1-2. React Diffing AlgorithmChapter2. React Hooks
2-1. Component와 Hook
2-2. useMemo
2-3. useCallback
2-4. Custom HooksChapter3. React 기능
3-1. 코드 분할(Code Spliting)
3-2. React.lazy()와 Suspense
가상의 DOM 객체로 실제 DOM의 사본 같은 개념
<html>
,<head>
,<body>
와 같은 태그들에 접근 및 조작할 수 있도록 태그들을 트리구조로 객체화 시킨 것.DOM 구조 : 계층적 구조
DOM 변경 및 업데이트 = 브라우저의 렌더링 엔진 또한 화면을 리플로우(Reflow)한다는 것
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"
}
]
]
}
]
}
2개의 가정을 갖고 시간 복잡도 O(n)의 새로운 휴리스틱 알고리즘을 구현해낸다.
1. 각기 서로 다른 두 요소는 다른 트리를 구축할 것이다.
2. 개발자가 제공하는 key
프로퍼티로, 여러 번 렌더링을 거쳐도 변경되지 말아야 하는 자식 요소가 무엇인지 알아낼 수 있을 것이다.
<ul>
태그 밑엔 <li>
태그만 와야함, <p>
태그 안에 <p>
태그 중복사용 불가)<ul>
과 새로운 <ul>
을 비교할 때 자식 노드를 순차적으로 위에서부터 아래로 비교하면서 바뀐 점을 찾는다. key
속성을 지원key
: React는 key
를 이용해 기존 트리의 자식과 새로운 트리의 자식이 일치하는지 아닌지 확인할 수 있다. key
속성에는 유일한Id
를 부여해주면 된다.<ul>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
//key가 2014인 자식 엘리먼트를 처음에 추가
<ul>
<li key="2014">Connecticut</li>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
key
속성을 통해 2014
라는 자식 엘리먼트가 새롭게 생겼고, 나머지 2015
,2016
키를 가진 엘리먼트는 위치만 이동했다는 걸 파악한다. 그래서 추가된 엘리먼트만 변경한다. React 16.8 버전부터 추가된 기능
클래스 컴포넌트와 생명주기 메서드를 이용하여 작업을 하던 기존 방식에서 벗어나 함수형 컴포넌트에서도 더 직관적인 함수를 이용하여 작업할 수 있게 만든 기능
// <Counter /> 컴포넌트를 클래스로 작성한 예
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>You clicked {this.state.counter} times</p>
<button onClick={this.handleIncrease}>
Click me
</button>
</div>
)
}
}
// <Counter /> 컴포넌트를 함수형 컴포넌트로 작성
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>
)
}
Counter
컴포넌트에서 숫자를 올리기 위해 상태값을 저장하고 사용할 수 있게 해주는 useState()
가 있는데, 이 메서드가 바로 Hook이다.Counter
컴포넌트에서 useState()
Hook을 호출해 함수 컴포넌트(function component) 안에 state를 추가한 형태다.Hook은 React 16.8에 새로 추가된 기능입니다. Hook은 class를 작성하지 않고도 state와 다른 React의 기능들을 사용할 수 있게 해줍니다.
- 함수형 컴포넌트에서 상태 값 및 다른 여러 기능을 사용하기 편리하게 해주는 메소드
- function으로만 React를 사용할 수 있으므로 클래스형 컴포넌트에선 동작 X
렌더링 최적화를 위한 Hook
특정 값(value)를 재사용하고자 할 때 사용하는 Hook
/* useMemo를 사용하기 전에는 꼭 import하기. */
import { useMemo } from "react";
function Calculator({value}){
const result = useMemo(() => calculate(value), [value]);
return <>
<div>
{result}
</div>
</>;
}
value
값이 계속 바뀌는 게 아니고 어딘가에 저장을 해뒀다가 다시 꺼내서 쓸 수만 있다면 굳이 calculate
함수를 호출할 필요도 없을 것이다. 여기서 useMemo
Hook을 사용할 수 있다.useMemo
를 호출하여 해당 함수를 감싸주면 전과 후의 렌더링을 비교해 value
값이 동일할 경우 이전 렌더링의 value
값을 그대로 재활용하게 된다.const memoizedValue = useMemo(() => computeExpensiveValue(a,b), [a,b]);
이제 이름을 입력해도 add 함수는 호출되지 않는다.
렌더링 최적화를 위한 Hook
useMemo와 마찬가지로 메모이제이션 기법을 이용한 Hook
useMemo는 결과값을 재사용하는 Hook이고,
useCallback은 함수의 재사용을 위해 사용하는 Hook
함수의 불필요한 재렌더링을 방지
useCallback
Hook을 사용하면 그 함수가 의존하는 값들이 바뀌지 않는 한 기존 함수를 계속해서 반환한다.
즉 x와 y값이 동일하다면 다음 렌더링 때 이 함수를 재사용한다.
/* useCallback를 사용하기 전에는 꼭 import하기 */
import React, { useCallback } from "react";
function Calculator({x, y}){
const add = useCallback(() => x + y, [x, y]);
return <>
<div>
{add()}
</div>
</>;
}
const memoizedCallback = useCallback(() => {
doSomething(a, b);
},[a, b]);
useCallback
은 참조 동등성에 의존한다.
React는 JS언어로 만들어진 오픈소스 라이브러리이기 때문에 기본적으로 JS문법을 따른다. JS에서 함수는 객체다.
객체는 값의 주소를 저장하기 때문에 반환하는 값이 같아도 일치연산자로 비교하면 false
가 출력된다.
function doubleFactory(){
return (a) => 2 * a;
}
const double1 = doubleFactory();
const double2 = doubleFactory();
double1(8); // 16
double2(8); // 16
double1 === double2; // false
double1 === double1; // true
double1
과 double2
는 같은 함수를 할당했음에도 메모리 주소 값이 다르기 때문에, 메모리 주소에 의한 참조 비교 시 다른 함수로 봄
React도 이와 같다. React는 리렌더링 시 함수를 새로이 만들어서 호출을 한다. (새롭게 만들어 호출된 함수 ≠ 기존 함수)
useCallback
을 이용해 함수 자체를 저장해서 재사용하면, 함수 메모리 주소 값을 저장했다가 재사용하는 것과 같다. 재선언을 막는다.개발자가 스스로 커스텀한 훅
이를 이용해 반복되는 로직을 함수로 뽑아내어 재사용할 수 있다.
useSomething
)use
붙이기⭐️ 같은 Custom Hook 사용 ≠ 같은 state 공유
⭐️ 그저 로직만 공유할 뿐
⭐️ state는 컴포넌트 내에서 독립적으로 정의되어 있다.
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;
// [코드] 여러 input에 의한 상태 변경을 할 때 쓸 수 있는 useInputs Hooks
ReactDOM.render
지원 xconst rootElement = document.getElementById("root");
ReactDOM.render(<AppTest />, rootElement);
// React 18이전의 index.js
import { createRoot } from "react-dom/client";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
<App />
);
// 바뀐 index.js
“어느 페이지에서 코드를 해석하고 실행하는 정도가 느려졌는지 파악해서 번들을 나눈 뒤에 지금 필요한 코드만 불러오고 나중에 필요한 코드는 나중에 불러올 수 있지 않을까??”
코드 분할 : 런타임 시 여러 번들을 동적으로 만들고 불러오는 것으로, Webpack, Rollup과 같은 번들러가 지원하는 기능.
/* lodash 라이브러리 전체를 불러와서 그 안에 들은 메소드를 꺼내 쓰는 것은 비효율적.*/
import _ from 'lodash';
...
_.find([]);
/* 이렇게 lodash의 메소드 중 하나를 불러와 쓰는 것이 앱의 성능에 더 좋다.*/
import find from 'lodash/find';
find([]);
import
지시자를 사용해 파일을 불러오는 방법 (기존 방법)/* 최상위 import 지시자로 라이브러리 및 파일을 불러오고 */
import moduleA from "library";
form.addEventListener("submit", e => {
e.preventDefault();
someFunction();
});
const someFunction = () => {
/* 코드 중간에서 불러온 파일을 사용. */
}
form.addEventListener("submit", e => {
e.preventDefault();
/* 동적 불러오기는 이런 식으로 코드의 중간에 불러올 수 있게 됨. */
import('library.moduleA')
.then(module => module.default)
.then(someFunction())
.catch(handleError());
});
const someFunction = () => {
/* moduleA를 여기서 사용. */
}
then
함수를 사용해 필요한 코드만 가져온다.React.lazy
와 함께 사용할 수 있다.
React.lazy
를 통해 컴포넌트를 동적으로 import할 수 있다.
- 이를 사용하면 초기 렌더링 지연시간을 어느정도 줄일 수 있게 된다
- Router로 분기가 나눠진 컴포넌트들을 lazy를 통해 import하게 되면 해당 path로 이동할때 컴포넌트를 불러오게 된다.
React.lazy
로 감싼 컴포넌트는 단독 사용 XReact.suspense
컴포넌트 하위에서 렌더링 해야함import Component from './Component';
/* React.lazy로 dynamic import를 감싸준다. */
const Component = React.lazy(() => import('./Component'));
렌더링이 준비되지 않은 컴포넌트가 있을 때 로딩 화면을 보여주고 로딩이 완료되면 렌더링이 준비된 컴포넌트를 보여주는 기능
/* 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>
);
}
Route
에 두 기능을 적용시키는 게 좋다.import { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
// 1. 라우터가 분기되는 컴포넌트에서 각 컴포넌트에 React.lazy를 사용하여 import
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const App = () => (
<Router>
// Route 컴포넌트들을 Suspense로 감싼 후 로딩 화면으로 사용할 컴포넌트를 fallback 속성으로 설정
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
</Router>
);