Custum Hooks
개발자가 스스로 커스텀한 훅을 의미하며 이를 이용해 반복되는 로직을 함수로 뽑아내어 재사용할 수 있다.
여러 url을 fetch할 때, 여러 input에 의한 상태 변경 등 반복되는 로직을 동일한 함수에서 작동하게 하고 싶을 때 커스텀 훅을 주로 사용한다. 이를 이용하면
- 상태관리 로직의 재활용이 가능하고
- 클래스 컴포넌트보다 적은 양의 코드로 동일한 로직을 구현할 수 있으며
- 함수형으로 작성하기 때문에 보다 명료하다는 장점이 있다. (e.g. useSomething)
예를 들어 이런 컴포넌트가 있다고 보자. 해당 컴포넌트는 실제 React 공식 문서에 있는 컴포넌트이다.
//FriendStatus : 친구가 online인지 offline인지 return하는 컴포넌트
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일 때 초록색으로 표시하는 컴포넌트
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>
);
}
FriendStatus 컴포넌트는 사용자들이 온라인인지 오프라인인지 확인하고, FriendListItem 컴포넌트는 사용자들의 상태에 따라 온라인이라면 초록색으로 표시하는 컴포넌트이다. 이 두 컴포넌트는 정확하게 똑같이 쓰이는 로직이 존재하고 있다. Custom Hook을 사용한다면 이 로직을 빼내서 두 컴포넌트에서 공유할 수 있다.
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;
}
두 컴포넌트에서 사용하기 위해 동일하게 사용되고 있는 로직을 분리하여 함수 useFriendStatus로 만든다. 이렇게 Custom Hook을 정의할 때는 일종의 규칙이 필요하다.
- Custom Hook을 정의할 때는 함수 이름 앞에 use를 붙이는 것이 규칙이다.
- 대개의 경우 프로젝트 내의 hooks 디렉토리에 Custom Hook을 위치 시킨다.
- Custom Hook으로 만들 때 함수는 조건부 함수가 아니어야 한다. 즉 return 하는 값은 조건부여서는 안 된다. 그렇기 때문에 위의 이 useFriendStatus Hook은 온라인 상태의 여부를 boolean 타입으로 반환하고 있다.
이렇게 만들어진 Custom Hook은 Hook 내부에 useState와 같은 React 내장 Hook을 사용하여 작성할 수 있다. 일반 함수 내부에서는 React 내장 Hook을 불러 사용할 수 없지만 Custom Hook 에서는 가능하다는 것 또한 알아두면 좋다.
이제 이 useFriendStatus Hook을 두 컴포넌트에 적용해보자.
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
const isOnline = useFriendStatus(props.friend.id);
return (
<li style={{ color: isOnline ? 'green' : 'black' }}>
{props.friend.name}
</li>
);
}
로직을 분리해 Custom Hook으로 만들었기 때문에 두 컴포넌트는 더 직관적으로 확인이 가능해진다.
그러나 같은 Custom Hook을 사용했다고 해서 두 개의 컴포넌트가 같은 state를 공유하는 것은 아니다. 그저 로직만 공유할 뿐, state는 컴포넌트 내에서 독립적으로 정의 되어 있다.
레퍼런스: https://ko.reactjs.org/docs/hooks-custom.html
Custom 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;
[코드] 여러 url을 fetch할 때 쓸 수 있는 useFetch Hook
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
실습
1. custom hook을 이용하여 useEffect 로직 분리하기
API를 통해 데이터를 받아와 처리하는 로직은 반복적일 수밖에 없다. 이러한 로직을 custom hook으로 만들어 분리하고 필요한 컴포넌트마다 적용을 한다면 컴포넌트들을 좀 더 직관적으로 관리할 수 있을 것이다.
// App.js
import useFetch from "./util/useFetch";
import "./styles.css";
export default function App() {
const fetchData = useFetch("data.json");
return (
<div className="App">
<h1>To do List</h1>
<div className="todo-list">
{fetchData &&
fetchData.todo.map((el) => {
return <li key={el.id}>{el.todo}</li>;
})}
</div>
</div>
);
}
// useFetch.js
import { useEffect, useState } from "react";
const useFetch = (fetchUrl) => {
const [data, setData] = useState();
useEffect(() => {
fetch(fetchUrl, {
headers: {
"Content-Type": "application/json",
Accept: "application/json"
}
})
.then((response) => {
return response.json();
})
.then((myJson) => {
setData(myJson);
})
.catch((error) => {
console.log(error);
});
}, [fetchUrl]);
return data;
};
export default useFetch;
2. custom hook을 이용하여 input 로직 분리하기
input 또한 반복적으로 사용되는 로직을 가지고 있는 컴포넌트이다. 이런 식으로 앱 내에 반복적으로 사용 되고 관리되는 로직은 많다(input, form, button 등). input도 지금은 2개 정도 있어 관리가 크게 요구되지 않지만, 후에 회원가입 컴포넌트를 만들거나, 앱의 볼륨이 커지게 된다면 input은 굉장히 많이 사용되는 UI 중 하나이기 때문에 반복되는 로직을 관리 해야 하는 필요성이 생긴다. 이런 컴포넌트 또한 custom hook을 이용하여 로직을 분리하여 관리할 수 있다. 컴포넌트 내에 반복되는 로직을 분리를 해 관리를 하면 컴포넌트들을 좀 더 깔끔하게 관리할 수 있다.
// App.js
import { useState } from "react";
import useInput from "./util/useInput";
import Input from "./component/Input";
import "./styles.css";
export default function App() {
const [firstValue, firstBind, firstReset] = useInput("");
const [lastValue, lastBind, lastReset] = useInput("");
const [nameArr, setNameArr] = useState([]);
const handleSubmit = (e) => {
e.preventDefault();
setNameArr([...nameArr, `${firstValue} ${lastValue}`]);
firstReset();
lastReset();
};
return (
<div className="App">
<h1>Name List</h1>
<div className="name-form">
<form onSubmit={handleSubmit}>
<Input labelText={"성"} value={firstBind} />
<Input labelText={"이름"} value={lastBind} />
<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>
);
}
// Input.js
function Input({ labelText, value }) {
return (
<div className="name-input">
<label>{labelText}</label>
{* 구조분해할당. 알아서 인식해서 분리해줌 *}
{* <input value = {value} onChange={(e) => setValue(e.target.value} type="text" /> *}
<input {...value} type="text" />
</div>
);
}
export default Input;
// useInput.js
import { useState } from "react";
function useInput(initialValue) {
const [value, setValue] = useState(initialValue);
// 구조분해할당. 아래와 같음
/* const bind = {
value: value,
onChange: (e) => {
setValue(e.target.value);
}
};*/
const bind = {
value,
onChange: (e) => {
setValue(e.target.value);
}
};
const reset = () => {
setValue(initialValue);
};
//return 해야 하는 값은 배열 형태의 값
return [value, bind, reset];
}
export default useInput;
React의 주목해야 할 기능
React는 현재도 계속해서 업데이트가 되고 있는 오픈소스 라이브러리이다. 이번에 React가 버전 18로 업데이트가 되면서 많은 부분이 바뀌었는데, 가장 빠르게 알아볼 수 있는 변화는 콘솔 창에 이전에 보이지 않던 경고문이 보인다는 점이다.
해당 경고문은 이제 React 18 버전은 더이상 ReactDOM.render를 지원하지 않는다는 내용이다. 물론 버전이 18보다 낮은 앱에서는 해당 경고문은 보이지 않는다. 그러나 계속해서 업데이트가 되고 있는 라이브러리이니만큼 해당 이슈는 알아둬야 할 것이다. 언제까지 18 버전 이하만 사용할 일은 없을 것이고, 18버전을 쓰는데도 지원하지 않는 이전의 문법을 사용해서는 안 되기 때문이다.
const rootElement = document.getElementById("root");
ReactDOM.render(<AppTest />, rootElement);
[코드] React 18이전의 index.js
저런 경고문이 보이는 이유는 React 18에서는 이제 createRoot API를 사용하기 때문이기 때문이다. React 18에서 생긴 자동 배칭 또한 이 createRoot API를 사용해야 한다.
import { createRoot } from "react-dom/client";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
<App />
);
[코드] 바뀐 index.js
그 중 주목할 부분은 Concurrent Feature이다. 지금까지는 Concurrent Mode라고 명명되었지만 이번에 Concurrent Feature로 바뀌면서 하나의 기능으로 들어오게 되다. 그 중에는 Suspense 기능이 있는데, React 18 버전부터는 이 Suspense 기능을 이용해 독립적으로 렌더링을 할 수 있도록 했다.
코드 분할
대부분 React 앱들은 Webpack, Rollup과 같은 툴을 사용해 번들링(Bundling)한다. 이렇게 하면 HTML 웹 페이지에 JavaScript를 쉽게 추가할 수 있기 때문이다. 번들된 앱은 모든 JavaScript가 한 곳에 있기 때문에 페이지를 설정하는 데 필요한 호출 수가 적은 링크 태그 하나만 필요하게 된다. 과거에는 이렇게 해도 무리가 없었다. 모던 웹 이전의 웹 JavaScript 코드는 최소한의 수준으로 작성되었기 때문이다.
그러나 이제는 번들링하게 되면 특정 지점에서 코드를 해석하고 실행하는 정도가 느려지게 되었다. 모던 웹으로 발전하면서 점점 DOM을 다루는 정도가 정교해지며 JavaScript 코드 자체가 방대해지고 무거워졌기 때문이다.
그러나 이제는 번들링하게 되면 특정 지점에서 코드를 해석하고 실행하는 정도가 느려지게 되었다. 모던 웹으로 발전하면서 점점 DOM을 다루는 정도가 정교해지며 JavaScript 코드 자체가 방대해지고 무거워졌기 때문이다.
“그렇다면 어느 페이지에서 코드를 해석하고 실행하는 정도가 느려졌는지 파악해서 번들을 나눈 뒤에 지금 필요한 코드만 불러오고 나중에 필요한 코드는 나중에 불러올 수 있지 않을까??”
이것이 코드 분할의 핵심 아이디어이다. 번들이 거대해지는 것을 방지하기 위한 좋은 해결 방법은 번들을 물리적으로 나누는 것이다. 코드 분할은 런타임 시 여러 번들을 동적으로 만들고 불러오는 것으로, Webpack, Rollup과 같은 번들러가 지원하는 기능입니다.
따라서 코드 분할을 하게 되면 지금 당장 필요한 코드가 아니라면 따로 분리를 시키고, 나중에 필요할 때 불러와서 사용할 수 있다. 이를 통하여 대규모 프로젝트의 앱인 경우에도 페이지의 로딩 속도를 개선할 수 있게 된다.
번들 분할 혹은 줄이는 법
번들링 되는 파일에는 앱을 만들면서 npm을 통해 다운받는 서드파티(Third Party) 라이브러리도 포함이 된다. 서드파티 라이브러리는 개인 개발자나 프로젝트 팀, 혹은 업체등에서 개발하는 라이브러리로, 즉 제 3자 라이브러리이다. 서드파티 라이브러리는 플러그인이나 라이브러리 또는 프레임워크 등이 존재하며, 이 라이브러리를 잘 사용하면 편하고 효율적인 개발을 할 수 있다.
그러나 서드파티 라이브러리는 사용자에게 다양한 메소드를 제공하기 때문에 코드의 양이 많고, 번들링 시 많은 공간을 차지한다. 따라서 사용 중인 라이브러리의 전부를 불러와서 사용하는 것보다 따로 따로 불러와서 사용할 수 있다면 많은 공간을 차지하지 않을 수 있게 된다.
/* 이렇게 lodash 라이브러리를 전체를 불러와서 그 안에 들은 메소드를 꺼내 쓰는 것은 비효율적입니다.*/
import _ from 'lodash';
...
_.find([]);
/* 이렇게 lodash의 메소드 중 하나를 불러와 쓰는 것이 앱의 성능에 더 좋습니다.*/
import find from 'lodash/find';
find([]);
해당 코드는 lodash라는 라이브러리를 예시로 하고 있다. (섹션2에서 했던 underbar 과제가 lodash 라이브러리와 비슷하게 구현하는 과제였다) lodash는 배열, 숫자, 객체, 문자열을 사용할 때 반복적인 작업 같은 것을 할 시 사용하기에 좋은 라이브러리이다.
lodash라는 라이브러리는 하나의 폴더와 같고, 그 폴더 안에는 개발 시 다양한 상황에 쓰기 좋은 메소드들, 즉 함수 코드들이 들어 있다. 이 함수 코드들의 양이 상당하기 때문에 전부 가져올 시, 정말로 필요한 것 한두 개만 쓰인다면 나머지는 그냥 쓰이지 않는 코드 뭉치로 앱 내부에 남게 된다. 이는 앱의 성능을 저하시킬 요지가 있기 때문에, 필요한 것 한두 개만 가져다 쓰는 식으로 개발하는 것이 훨씬 좋다.
React에서의 코드 분할
React는 SPA(Single-Page-Application)인데, 사용하지 않는 모든 컴포넌트까지 한 번에 불러오기 때문에 첫 화면이 렌더링 될때까지의 시간이 오래걸린다. 그래서 사용하지 않는 컴포넌트는 나중에 불러오기 위해 코드 분할 개념을 도입했다.
React에서 코드 분할하는 방법은 dynamic import(동적 불러오기)를 사용하는 것이다. 그 전까지는 코드 파일의 가장 최상위에서 import 지시자를 사용해 사용하고자 하는 라이브러리 및 파일을 불러오는 방법을 사용했었다. 이를 static import(정적 불러오기)라고 한다.
static import(정적 불러오기)
/* 기존에는 파일의 최상위에서 import 지시자를 이용해 라이브러리 및 파일을 불러왔습니다. */
import moduleA from "library";
form.addEventListener("submit", e => {
e.preventDefault();
someFunction();
});
const someFunction = () => {
/* 그리고 코드 중간에서 불러온 파일을 사용했습니다. */
}
기존에는 항상 import 구문은 문서의 상위에 위치해야 했고, 블록문 안에서는 위치할 수 없는 제약 사항이 있었다. 왜냐하면 번들링 시 코드 구조를 분석해 모듈을 한 데 모으고 사용하지 않는 모듈은 제거하는 등의 작업을 하는데, 코드 구조가 간단하고 고정이 되어 있을 때에야만 이 작업이 가능해지기 때문이었다.
그러나 이제는 구문 분석 및 컴파일해야 하는 스크립트의 양을 최소화 시키기 위해 dynamic import 구문을 지원합니다.
dynamic import(동적 불러오기)
form.addEventListener("submit", e => {
e.preventDefault();
/* 동적 불러오기는 이런 식으로 코드의 중간에 불러올 수 있게 됩니다. */
import('library.moduleA')
.then(module => module.default)
.then(someFunction())
.catch(handleError());
});
const someFunction = () => {
/* moduleA를 여기서 사용합니다. */
}
dynamic import를 사용하게 되면 불러온 moduleA 가 다른 곳에서 사용되지 않는 경우, 사용자가 form을 통해 양식을 제출한 경우에만 가져오도록 할 수 있다.
dynamic import는 then 함수를 사용해 필요한 코드만 가져온다. 가져온 코드에 대한 모든 호출은 해당 함수 내부에 있어야 한다. 이 방식을 사용하면 번들링 시 분할된 코드(청크)를 지연 로딩시키거나 요청 시에 로딩할 수 있다.
이 dynamic import는 React.lazy 와 함께 사용할 수 있다.
React.lazy()
React.lazy 함수를 사용하면 dynamic import를 사용해 컴포넌트를 렌더링할 수 있다. React는 SPA(Single-Page-Application)이므로 한 번에 사용하지 않는 컴포넌트까지 불러오는 단점이 있다. React는 React.lazy를 통해 컴포넌트를 동적으로 import를 할 수 있기 때문에 이를 사용하면 초기 렌더링 지연시간을 어느정도 줄일 수 있게 된다.
import Component from './Component';
/* React.lazy로 dynamic import를 감쌉니다. */
const Component = React.lazy(() => import('./Component'));
이 React.lazy로 감싼 컴포넌트는 단독으로 쓰일 수는 없고, React.suspense 컴포넌트의 하위에서 렌더링을 해야 한다.
React.Suspense
Router로 분기가 나누어진 컴포넌트들을 위 코드처럼 lazy를 통해 import하면 해당 path로 이동할때 컴포넌트를 불러오게 되는데 이 과정에서 로딩하는 시간이 생기게 된다. 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 컴포넌트의 fallback prop은 컴포넌트가 로드될 때까지 기다리는 동안 로딩 화면으로 보여줄 React 엘리먼트를 받아들인다. Suspense 컴포넌트 하나로 여러 개의 lazy 컴포넌트를 보여줄 수도 있다.
React.lazy와 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>
);
라우터에 Suspense를 적용하는 것은 간단한 편이다. 라우터가 분기되는 컴포넌트에서 각 컴포넌트에 React.lazy를 사용하여 import한다. 그리고 Route 컴포넌트들을 Suspense로 감싼 후 로딩 화면으로 사용할 컴포넌트를 fallback 속성으로 설정해주면 된다. 초기 렌더링 시간이 줄어드는 분명한 장점이 있으나 페이지를 이동하는 과정마다 로딩 화면이 보여지기 때문에 서비스에 따라서 적용 여부를 결정해야 한다.
'코드스테이츠 SEB FE 41기 > Section 별 내용 정리' 카테고리의 다른 글
section4/Unit5/[컴퓨터 공학] 기초(11/30) (0) | 2022.11.30 |
---|---|
section4/Unit4/[React] 심화(11/29) (0) | 2022.11.29 |
section4/Unit4/[React] 심화(11/25) (0) | 2022.11.24 |
section4/Unit3/[HTML/CSS] 심화(11/24) (0) | 2022.11.24 |
section4/Unit3/[HTML/CSS] 심화(11/23) (0) | 2022.11.23 |