React Hook
Hook은 함수형 컴포넌트에서 상태 값 및 다른 여러 기능을 사용하기 편리하게 해주는 메서드를 의미합니다. Hook은 class가 아닌 function으로만 React를 사용할 수 있게 해주는 것이기 때문에 클래스형 컴포넌트에서는 동작하지 않습니다.
Hook 사용 규칙
- 리액트 함수의 최상위에만 호출해야 합니다.
- 반복문, 조건문, 중첩된 함수 내에서 Hook을 실행하면 예상한 대로 동작하지 않을 우려가 있습니다.
- 오직 리액트 함수 내에서만 사용되어야 한다.
- 이는 리액트 함수형 컴포넌트나 커스텀 Hook이 아닌 다른 일반 JavaScript 함수 안에서 호출해서는 안 된다는 의미입니다.
useMemo
렌더링 최적화를 위한 Hook으로, 특정 값을 재사용하고자 할 때 사용합니다.
function Calculator({value}){
const result = calculate(value);
return <div>{result}</div>;
}
만약 위 코드에서 calculate 가 값을 반환하는데 시간이 몇 초 이상 걸린다면 해당 컴포넌트는 렌더링을 할 때마다 이 함수를 계속해서 호출할 것이고, 그때마다 시간이 몇 초 이상 소요가 될 것입니다,
import { useMemo } from "react";
function Calculator({value}){
const result = useMemo(() => calculate(value), [value]);
return <div>{result}</div>;
}
만약 value가 계속 바뀌지 않으면, 이 값을 어딘가에 저장을 해뒀다가 다시 꺼내서 쓰면 해결되기 때문에 여기서 useMemo Hook을 사용할 수 있습니다. 이 방법을 통해 value 값이 동일한 경우에는 이전 렌더링의 value 값을 그대로 재활용할 수 있게 됩니다. 이는 기존에 수행한 연산의 결과값을 메모리에 저장을 해두는 프로그래밍 기법인 Memoization 개념과 긴밀한 관계가 있습니다.
useCallback
렌더링 최적화를 위한 Hook으로, useMemo와 같이 메모이제이션 기법을 활용하지만 useCallback는 함수의 재사용을 하고 싶을때 사용합니다.
function Calculator({x,y}) {
const add = () => x+y;
return <div>{add()}</div>;
}
이 함수는 해당 컴포넌트가 렌더링 될 때마다 새롭게 만들어질 것입니다. useMemo와 마찬가지로, 해당 컴포넌트가 리렌더링 되더라도 그 함수가 의존하고 있는 값인 x,y가 바뀌지 않는다고 생각하면 이 함수 또한 메모리에 저장해 뒀다가 다시 꺼내서 쓸 수 있을 것입니다.
이때 useCallback Hook을 사용하면 그 함수가 의존하는 값들이 바뀌지 않는 한 기존 함수를 계속해서 반환, 즉 x,y 값이 동일하다면 다음 렌더링 때도 이 함수를 다시 사용합니다.
import React, { useCallback } from 'react';
function Calculator({x,y}) {
const add = () => useCallback(() => x+y,[x,y]);
return <div>{add()}</div>;
}
useCallback은 함수를 호출을 하지 않는 Hook이 아니라, 그저 메모리에서 함수를 꺼내서 호출하는 Hook이기 때문에 단순히 컴포넌트 내에서 함수를 반복해서 생성하지 않기 위해서 사용하는 것은 큰 의미가 없거나 오히려 손해인 경우도 있습니다. 그래서 자식 컴포넌트의 props로 함수를 전달해 줄 때 사용하는 것이 좋습니다.
Custom Hooks
개발자가 스스로 커스텀한 훅을 의미하며 이를 이용해 반복되는 로직을 함수로 만들어 재사용할 수 있습니다.
여러 url을 fetch할 때, 여러 input에 의한 상태 변경 등 반복되는 로직을 동일한 함수에서 작동하게 하고 싶을 때 커스텀 훅을 주로 사용합니다.
커스텀 훅의 장점
- 상태관리 로직의 재활용이 가능합니다.
- 클래스 컴포넌트보다 적은 양의 코드로 동일한 로직을 구현할 수 있습니다.
- 함수형으로 작성하기 때문에 보다 명료하다는 장점이 있습니다.
커스텀 훅 규칙
- 커스텀 훅을 정의할 때는 함수 이름 앞에 use를 붙여야 합니다
- 프로젝트 내의 hooks 디렉토리에 위치시킵니다.
- 커스텀 훅으로 만들 때 함수는 조건부 함수가 아니어야 합니다. 즉 return 하는 값은 조건부여서는 안 됩니다.
여러 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;
코드 분할(Code Spliting)
대부분 React 앱들은 Webpack, Rollup과 같은 툴을 사용해 번들링(Bundling)합니다. 이렇게 하면 HTML 웹 페이지에 JavaScript를 쉽게 추가할 수 있기 때문입니다. 번들된 앱은 모든 JavaScript가 한 곳에 있기 때문에 페이지를 설정하는 데 필요한 호출 수가 적은 링크 태그 하나만 필요하게 됩니다.
모던 웹 이전에는 무리가 없었지만, 모던 웹으로 발전하면서 DOM 다루는 정도가 정교해지며 JavaScript 코드 자체가 받애해지고 무거워져서 번들링하게 되면 특정 지점에서 코드를 해석하고 실행하는 정도가 느려지게 되었습니다.
'그렇다면 느려진 페이지를 파악해 번들을 나눈 뒤에 지금 필요한 코드만 불러오는건 어떨까? 라는 아이디어에서 나온게 코드 분할의 핵심 아이디어입니다. 번들이 거대해지는 것을 방지하기 위한 좋은 해결 방법은 번들을 물리적으로 나누는 것입니다. 코드 분할은 런타임 시 여러 번들을 동적으로 만들고 불러오는 것으로, 지금 당장 필요한 코드가 아니라면 따로 분리를 시키고, 나중에 필요할 때 불러와서 사용할 수 있습니다. 이를 통해 대규모 프로젝트의 앱의 경우에도 페이지의 로딩 속도를 개선할 수 있습니다.
React 에서의 코드 분할
React는 SPA인데, 사용하지 않는 모든 컴포넌트를 한 번에 불러오기 때문에 첫 화면이 렌더링 될 때까지의 시간이 오래 걸립니다. 그래서 사용하지 않는 컴포넌트는 나중에 불러오기 위해 코드 분할 개념을 도입했습니다.
Static Import
코드 파일의 가장 최상위에서 import 지시자를 이용해 사용하고자 하는 라이브러리 및 파일을 불러오는 방법입니다.
import moduleA from 'library';
Dynamic Import
기존의 Static import는 항상 import 구문은 문서의 상위에 위치해야 했고, 블록문 안에서는 위치할 수 없었습니다.
그러나 이제는 구문 분석 및 컴파일해야 하는 스크립트의 양을 최소하기 위해 Dynamic Import 구문을 지원합니다.
import('library.moduleA')
.then(module => module.default)
.then(function()) // 실행하고자 하는 함수 등
.catch(handleError());
불러온 모듈이 다른 곳에서 사용되지 않는 경우, 사용자가 form을 통해 양식을 제출한 경우에만 가져오도록 할 수 있습니다. dynamic import는 then 함수를 사용해 필요한 코드만 가져옵니다. 가져온 코드에 대한 모든 호출은 해당 함수 내부에 있어야 하며, 이 방식을 사용하면 번들링 시 분할된 코드(청크)를 지연 로딩시키거나 요청시에 로딩할 수 있습니다.
React.lazy()
React.lazy 함수를 사용하면 dynamic import 를 사용해 컴포넌트를 렌더링할 수 있습니다. React.lazy를 통해 컴포넌트를 동적으로 import를 할 수 있기 때문에 이를 사용해 초기 렌더링 지연시간을 어느 정도 줄일 수 있게 됩니다.
/* React.lazy를 사용하지 않은 경우 */
import Component from './Component';
/* React.lazy를 사용한 경우 */
/* React.lazy로 dynamic import를 감쌉니다. */
const Component = React.lazy(() => import('./Component'));
이 React.lazy로 감싼 컴포넌트는 단독으로 쓰일 수는 없고, React.suspense 컴포넌트의 하위에서 렌더링해야 합니다.
React.suspense
Router로 분기가 나누어진 컴포넌트들을 lazy를 통해 import하면 해당 path로 이동할 때 컴포넌트를 불러오게 되는데 이 과정에서 로딩하는 시간이 생기게 됩니다. Suspense는 아직 렌더링이 준비되지 않은 컴포넌트가 있을 때 로딩 화면을 보여주고, 로딩이 완료되면 렌더링이 준비된 컴포넌트를 보여주는 기능입니다.
import { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function App() {
return (
{/* 이런 식으로 React.lazy로 감싼 컴포넌트를 Suspense 컴포넌트의 하위에 렌더링합니다. */}
{/* Suspense 컴포넌트 하위에 여러 개의 lazy 컴포넌트를 렌더링시킬 수 있습니다. */}
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
);
}
앱에 코드 분할을 도입할 곳을 결정하는 것은 까다로울 수 있기 때문에, 중간에 적용시키는 것보다는 웹 페이지를 불러오고 진입하는 단계인 Route에 두 기능을 적용시키는 것이 좋습니다.
초기 렌더링 시간이 줄더드는 장점이 있지만, 페이지를 이동하는 과정마다 로딩화면이 보이기 때문에 서비스에 따라서 적용 여부를 결정해야 합니다.
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={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
</Router>
);
'프론트엔드 > Section4' 카테고리의 다른 글
| Proxy (3) | 2023.06.07 |
|---|---|
| TypeScript (Enum,Interface,Type,Class) (2) | 2023.05.30 |
| TypeScript (타입, 함수, 연산자:유니온/인터섹션) (3) | 2023.05.30 |
| UI/UX 디자인 시스템 (2) | 2023.05.24 |
| Virtual DOM (2) | 2023.05.19 |