[TIL] Custom Hooks, 코드분할

ㅜㅜ·2022년 11월 28일
1

Today I learn

목록 보기
61/77
post-thumbnail

🪝 Custom Hooks

개발자가 스스로 커스텀한 훅을 의미하며, 이를 사용하면 반복되는 로직을 함수로 뽑아내 재사용 가능.

Custom Hook은 컴포넌트와 만드는 방법이 비슷하지만 UI 컴포넌트는 UI를 반환하는 반면에 Custom Hook은 원하는 데이터들을 반환할수 있다.

일반 함수 내부에서는 리액트 내장 Hook 불러 사용할 수 없지만, Custom Hook에서는 리액트 내장 Hook을 불러 사용할 수 있음.

어떤 각각의 컴포넌트들이 동일한 커스텀 훅(이 커스텀 훅이 useState를 사용하고 있을 때)을 사용했다고 해서 같은 state를 공유하는 것은 아님. 로직만 공유할 뿐, state는 컴포넌트 내에서 독립적으로 정의되어 있다.


Custom Hook 정의 규칙

  • 함수 이름 앞에 use 붙임
  • 프로젝트 Hooks 디렉토리에 Custom Hook 위치시킴
  • 함수는 조건부 함수가 아니어야 함 = 리턴하는 값은 조건부여서는 안 됨.

Custom Hook 예시

fetch나 input 요소는 반복해서 사용하는 경우가 많으므로 Custom Hook으로 만들어 볼 수 있다.

//여러 Url을 Fetch할 때 쓸 수 있는 UseFetch
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
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;



Custom Hook 실습

useFetchData

useFetchData는 fetchUrl이라는 인자를 받아 해당 Url로 fetch를 이용해 데이터를 받아온다.
받아온 데이터는 useState를 이용해 data에 담기고 이 data를 반환한다.

useInput

useInput은 Input 컴포넌트에 전달해줄 value, onChange 함수, reset 함수를 담은 배열을 반환한다.

fetch 까지는 할만했는데...
요소에 대한 이벤트까지 생각해주려니까 조금 어렵게 느껴진다.

스터디원은 입력창뿐 아니라 폼 전체 (h1부터 버튼까지 포함)를 Input 컴포넌트에 포함되도록 만들었고, useInput에서도 위 예시 속의 useInputs 훅을 조금 변형해서 실습했다고 했다.





🆕 React 18 버전 업데이트

createRootAPI

//React 18이전의 index.js
const 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 />
);

React 18 버전은 더이상 ReactDOM.render 지원하지 않음.

대신 createRoot API 사용.

React 18에서 생긴 자동 배칭 또한 createRoot API를 사용함.


concurrent mode => concurrent Feature : Suspense 기능 ?!

자바스크립트는 싱글 스레드 언어이기 때문에 원래 하나의 작업을 수행할 때 다른 작업 동시에 수행할 수 없으나, concurrent mode를 사용하면 여러 작업을 동시에 처리할 수 있었다.

React 18 버전부터는 Concurrent Feature로 바뀌어 하나의 기능으로 들어오게 되었고, 해당 기능 중 하나가 Suspense 기능.





✂️ 코드분할

코드 분할은 런타임 시 여러 번들을 동적으로 만들고 불러오는 것으로 Webpack, Rollup과 같은 번들러가 지원하는 기능.


React에서 코드 분할

React는 SPA(Single-Page-Application)이므로 한 번에 사용하지 않는 컴포넌트까지 불러오는 단점이 있다.
dynamic import (동적 불러오기) 사용하면 리액트에서 코드 분할 가능.
여태 사용하던 방식은 파일 최상위에서 import 지시자를 사용해 불러오는 static import (정적 불러오기) 방식이었음.


static import

//static import : 파일의 최상위에서 import 지시자를 이용해 라이브러리 및 파일을 불러옴
import moduleA from "library";

form.addEventListener("submit", e => {
  e.preventDefault();
  someFunction();
});

const someFunction = () => {
  //import로 불러온 moduleA를 코드 중간에서 사용
}

import 구문은 문서의 상위에 위치해야 했고, 블록문 안에서는 위치할 수 없었는데, 번들링 시 코드 구조를 분석해 모듈을 한 데 모으고 사용하지 않는 모듈은 제거하는 등의 작업을 하는데, 코드 구조가 간단하고 고정이 되어 있을 때에야만 이 작업이 가능해지기 때문이다.


dynamic import

form.addEventListener("submit", e => {
  e.preventDefault();
//dynamic import는 아래와 같이 코드의 중간에 import 해올 수 있음. 
  import('library.moduleA')
    .then(module => module.default)
    .then(someFunction())
    .catch(handleError());
});

const someFunction = () => {
//moduleA 여기서 사용 할 수 있음.
}

이제는 구문 분석 및 컴파일해야 하는 스크립트의 양을 최소화 시키기 위해 dynamic import 구문을 지원한다.

dynamic import를 사용하게 되면 불러온 moduleA 가 다른 곳에서 사용되지 않는 경우, 사용자가 form을 통해 양식을 제출한 경우에만 가져오도록 할 수 있음.

dynamic import는 then 함수를 사용해 필요한 코드만 가져오고 가져온 코드에 대한 모든 호출은 해당 함수 내부에 있어야 함.

이 방식을 사용하면 번들링 시 분할된 코드(청크)를 지연 로딩시키거나 요청 시에 로딩할 수 있다.
이 dynamic import는 React.lazy 와 함께 사용할 수 있습니다.



React.lazy()

import Component from './Component';

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

React.lazy()를 이용하면 동적 불러오기를 할 수 있고, 이를 이용해 초기 렌더링 지연시간을 어느정도 줄일 수 있게 됨.

React.lazy()로 감싼 컴포넌트는 단독으로 쓰일 수 없고, React.suspense 컴포넌트의 하위에서 렌더링을 해야함.



React.Suspense()

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

Supense컴포넌트의 fallbackprop은 컴포넌트가 로드될 때까지 기다리는 동안 로딩 화면으로 보여줄 React 엘리먼트를 받아들인다.

(Suspense컴포넌트 하나로 여러 개의 lazy 컴포넌트를 보여줄 수도 있음)



적용

앱이 코드 분할을 도입할 곳을 결정하는 것은 까다롭기 때문에 중간에 적용시키는 것보다는 웹 페이지를 불러오고 진입하는 단계인 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>
);

라우터에 Suspense를 적용하는 것은 간단한 편인데

라우터가 분기되는 컴포넌트에서 각 컴포넌트에 React.lazy
를 사용하여 import 하고 Route 컴포넌트들을 Suspense
로 감싼 후 로딩 화면으로 사용할 컴포넌트를 fallback속성으로 설정해준다.

초기 렌더링 시간이 줄어드는 분명한 장점이 있으나 페이지를 이동하는 과정마다 로딩 화면이 보여지기 때문에 서비스에 따라서 적용 여부를 결정해야 함.

profile
다시 일어나는 중

0개의 댓글