나만 어렵나? 복습이 필요한 시기!
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 적용하기
DOM, Document Object Model, 문서 객체 모델
-정의
브라우저가 html 태그들을 트리 구조로 만든 객체 모델
-목적
스크립팅 언어(JavaScrpt)가 html 태그들에 접근하고 조작할 수 있도록 함
빠른 데이터 탐색(Tree 구조의 장점) -> 빠른 DOM 변경 -> 잦은 브라우저 리플로우, 리페인트 -> 잦은 재연산
대부분의 JavaScript 프레임워크는 변경된 요소 뿐만 아니라 나머지 요소도 리렌더링 함 -> “바뀐 부분만 비교해서 그 부분만 렌더링을 할 수는 없을까?“라는 생각에 도달
-정의
UI의 이상적인 또는 “가상”적인 표현을 메모리에 저장하고 ReactDOM과 같은 라이브러리에 의해 “실제” DOM과 동기화하는 프로그래밍 개념
-목적
Real DOM을 조작하는 것보다 더 빠르게 렌더링하기 위해
Virual DOM 변경 사항 발생
-> 이전 Virtual DOM과 현재 Virtual DOM의 차이 비교
-> Real DOM에 변경을 수행할 수 있는 최상의 방법 계산
-> Virual DOM 부분적 리렌더링
-> Real DOM에서 한번에 업데이트(최소의 Real 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을 건드리지 않고 자유롭게 조작 가능
두 개의 트리를 비교할 때, 두 엘리먼트의 루트 엘리먼트부터 비교하는 알고리즘
React가 Virtual DOM을 변경 시 효율적인 작업을 위해 사용
-각기 서로 다른 두 요소는 다른 트리를 구축할 것이다.
-개발자가 제공하는 key 프로퍼티를 가지고, 여러 번 렌더링을 거쳐도 변경되지 말아야 하는 자식 요소가 무엇인지 알아낼 수 있을 것이다.
이전 트리를 버리고 완전히 새로운 트리를 구축
이전 트리와 연관된 모든 state 사라짐
// 이전 Counter는 사라지고 새로운 Counter가 remount
// 이전 Counter와 연관된 모든 state 사라짐
<div>
<Counter />
</div>
<span>
<Counter />
</span>
두 엘리먼트의 속성을 확인하여, 변경된 속성들만 갱신
하나의 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'}} />
동시에 두 리스트 순차적으로(위 -> 아래) 비교하고 차이점이 있으면 변경
<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>
이러한 문제를 해결하기 위해, 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 값은 형제 사이에서만 유일하면 됨)
-React에서 Function Component 이전에 사용한 컴포넌트
-문제점
이러한 문제를 해결하기 위해, Hook은 Class없이 React 기능들을 사용하는 방법(Function Component)을 제시
-Hook 사용 가능(Class Component에서 Hook 호출 불가)
-Class Component에 비해 직관적
-역할
class를 작성하지 않고도 state와 다른 React의 기능들을 사용할 수 있게 해줌(React 16.8에 새로 추가된 기능).
-사용 규칙
React에서는 아래의 규칙들을 자동으로 강제하기 위한 linter 플러그인을 제공
최상위에서만 Hook을 호출해야 함
❌ 반복문, 조건문, 중첩된 함수 내부에서 Hook 호출
React는 Hook을 호출되는 순서대로 저장. 그런데 조건문, 반복문 안에서 Hook을 호출하게 되면 호출되는 순서대로 저장을 하기 어려워지고, 결국 예기치 못한 버그를 초래하게 될 수 있음.
오직 React 함수 내에서 Hook을 호출 함
❌ 일반적인 JavaScript 함수에서 호출
✅ React Function Component에서 호출
✅ Custom Hook에서 호출
memoization을 통해 특정 값을 재사용하여 렌더링 최적화, 값을 리턴
// 의존성 배열([a, b])이 변경되었을 때만 생성 함수(() => computeExpensiveValue(a, b)) 호출
import { useMemo } from "react";
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
// 함수 안에서 참조되는 모든 값은 의존성 배열에 나타나야 함
기존에 수행한 연산의 결과값을 메모리에 저장을 해두고, 동일한 입력이 들어오면 재활용하는 프로그래밍 기법. 중복 연산을 할 필요가 없기 때문에 앱의 성능을 최적화. useMemo는 memoizeion 이용하여 복잡한 연산의 중복을 피하고 React 앱의 성능을 최적화.
memoization을 통해 함수를 재사용하여 렌더링 최적화, 함수를 리턴
// 의존성 배열([a, b])이 변경되었을 때만 콜백 함수(() => doSomething(a, b)) 호출
import React, { useCallback } from "react";
const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]);
// 콜백 안에서 참조되는 모든 값은 의존성 배열에 나타나야 함
// useCallback(fn, deps) === useMemo(() => fn, deps)
-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으로 넘길 때 예상치 못한 성능 문제를 막을 수 있음.
개발자가 스스로 커스텀한 훅
반복되는 로직을 함수로 뽑아내어 재사용
-상태관리 로직의 재활용이 가능
-클래스 컴포넌트보다 적은 양의 코드로 동일한 로직을 구현 가능
-함수형으로 작성하기 때문에 보다 명료
-Custom Hook을 정의할 때는 함수 이름 앞에 use를 붙임.
-대개의 경우 프로젝트 내의 hooks 디렉토리에 Custom Hook을 위치 시킴.
-Custom Hook으로 만들 때 함수는 조건부 함수가 아니어야 함. 즉 return 하는 값은 조건부여서는 안 됨.
-일반 함수 내부에서는 React 내장 Hook을 불러 사용할 수 없지만 Custom Hook은 Hook 내부에 useState와 같은 React 내장 Hook을 사용하여 작성 가능.
-같은 Hook을 사용하는 두 개의 컴포넌트는 state를 공유하지 않음. 그저 로직만 공유할 뿐, state는 컴포넌트 내에서 독립적으로 정의 되어 있음.
-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>
);
}
여러 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;
여러 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>
);
}
: 서드파티 라이브러리 부분만 불러오기
번들링 되는 파일에는 앱을 만들면서 npm을 통해 다운받는 서드파티(Third Party) 라이브러리도 포함. 그러므로 서드파티의 용량을 줄이면 번들의 크기도 줄어듬.
/* 이렇게 lodash 라이브러리를 전체를 불러와서 그 안에 들은 메소드를 꺼내 쓰는 것은 비효율적입니다.*/
import _ from 'lodash';
...
_.find([]);
/* 이렇게 lodash의 메소드 중 하나를 불러와 쓰는 것이 앱의 성능에 더 좋습니다.*/
import find from 'lodash/find';
find([]);
-모던 웹 발전 이전: Webpack, Rollup과 같은 툴로 번들링하여 JavaScript를 한 파일로 만든 후 HTML에 링크 태그로 추가
-모던 웹 발전: JavaScript 코드가 많아져 특정 지점에서 코드를 해석하고 실행하는 정도가 느려지게 되면서 '번들을 나눈 뒤 필요한 코드만 불러오면 어떨까?'라는 생각을 함
-모던 웹 발전 이후: 코드 분할
-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를 여기서 사용합니다. */
}
-정의
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>
);
useParams: 현재 url에서 dynamic params를 객체로 반환해주는 hook
useNavigate: 인자의 주소로 이동하는 함수를 반환해주는 hook
참고 사이트: https://reactrouter.com/en/main/hooks/use-params
방법1. window.scrollTo(0,0)
방법2. history(npm 라이브러리) history.action === 'POP'
웹 콘솔에 에러 메시지를 출력
// 게시글 삭제, 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);// 콘솔 에러 메세지 설정
});
};