개발자가 스스로 커스텀한 훅을 의미하며 이를 이용해 반복되는 로직을 함수로 뽑아내어 재사용할 수 있습니다.
여러 url을 fetch할 때, 여러 input에 의한 상태 변경 등 반복되는 로직을 동일한 함수에서 작동하게 하고 싶을 때 커스텀 훅을 주로 사용합니다.
이를 이용하면상태관리 로직의 재활용이 가능하고 클래스 컴포넌트보다 적은 양의 코드로 동일한 로직을 구현할 수 있으며 함수형으로 작성하기 때문에 보다 명료하다는 장점이 있습니다. (e.g. useSomething)
예를 들어 이런 컴포넌트가 있다고 봅시다. 해당 컴포넌트는 실제 React 공식 문서에 있는 컴포넌트입니다.
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는 컴포넌트 내에서 독립적으로 정의 되어 있습니다.
<[코드] 여러 url을 fetch할 때 쓸 수 있는 useFetch Hook>
const useFetch = ( initialUrl:string ) => { // useFetch 함수를 정의하고 initialUrl을 매개변수로 받는다.
const [url, setUrl] = useState(initialUrl); // url과 setUrl을 useState를 이용해 초기값으로 initialUrl을 가진다.
const [value, setValue] = useState(''); // value와 setValue를 useState를 이용해 초기값으로 빈 문자열을 가진다.
const fetchData = () => axios.get(url).then(({data}) => setValue(data)); // fetchData 함수를 정의하고 axios를 이용해 url에서 데이터를 가져와서 setValue로 값을 설정한다.
useEffect(() => { // useEffect를 이용해 렌더링 후 fetchData 함수를 실행한다.
fetchData();
},[url]);
return [value]; // value를 반환한다.
};
export default useFetch; // useFetch 함수를 내보낸다.
<[코드] 여러 input에 의한 상태 변경을 할 때 쓸 수 있는 useInputs Hooks>
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;
-useInput
라는 커스텀 룩을 만드는 함수
initialForm
이라는 초기 입력값을 받아들이는 함수를 선언한다.fetch/util/useFetch.js
import { useState, useEffect } from 'react'
const useFetch = (initialUrl) => {
const [url, setUrl] = useState(initialUrl);
const [value, setValue] = useState('');
const fetchData = () => //axios.get(url).then(({ data }) => setValue(data));
fetch('data.json', {
headers: {
"Content-Type": "application/json",
Accept: "application/json"
}
})
.then((response) => {
return response.json();
})
.then((myJson) => {
setValue(myJson);
})
useEffect(() => {
fetchData();
}, [url]);
return [value];
};
이 Hook은 initialUrl 매개 변수를 받아 초기 URL 값을 설정하고, 그 URL에서 데이터를 가져옵니다.
이 때, useState Hook을 사용하여 URL 및 데이터 값을 유지하고, useEffect Hook을 사용하여 URL 값이 변경될 때마다 데이터를 가져옵니다.
fetchData 함수는 fetch API를 사용하여 데이터를 가져옵니다.
이 코드에서는 로컬 JSON 파일을 가져오는 예시로 작성되어 있습니다.
이를 수정하여 실제 API의 주소로 변경할 수 있습니다. 이때, setValue 함수를 호출하여 데이터 값을 설정합니다.
-마지막으로, useEffect Hook에서 fetchData 함수를 호출하여 URL 값이 변경될 때마다 데이터를 가져오도록 설정합니다. return 문에서는 데이터 값을 반환합니다.
CustomFetchExcercise.js
import { useEffect, useState } from "react";
import './fetch.css';
import useFetch from "./util/useFetch";
const CustomFetchExcercise = () => {
const [data] = useFetch('data.json')
return (
<div className="todo-wrap">
<h1 className="todo-title">To do List</h1>
<div className="todo-list">
{data &&
data.todo.map((el) => {
return <li key={el.id}>{el.todo}</li>;
})}
</div>
</div>
);
}
export default CustomFetchExcercise;
이 코드는 To-Do List를 화면에 렌더링하기 위해 데이터를 가져오는 컴포넌트입니다.
useFetch라는 커스텀 훅을 사용하여 data.json에서 데이터를 가져온다.
useFetch 훅은 초기 URL을 인자로 받고, 이 URL을 사용하여 데이터를 가져온다.
그리고 useState를 사용하여 데이터를 저장한다.
useEffect를 사용하여 컴포넌트가 마운트될 때 fetchData() 함수를 실행한다.
이 때, fetchData() 함수는 fetch() 메소드를 사용하여 data.json에서 데이터를 가져와 setValue() 함수를 통해 데이터를 저장합니다. 마지막으로, useFetch 훅은 데이터를 반환한다.
CustomFetchExcercise 컴포넌트는 useFetch 훅으로부터 반환된 data를 사용하여 To-Do List를 화면에 렌더링한다.
data가 존재하면, data.todo 배열을 순회하며 각각의 객체를 키로 사용하여 리스트 아이템을 생성한다.
이 리스트 아이템은 el.todo 값으로 구성되어 있다.
input.jsx
function Input({ name, value, onChange }) {
// TODO : input의 로직을 유의하며 컴포넌트로 분리합니다.
return <input
name={name}
value={value}
onChange={onChange}
type='text'
/>
}
export default Input;
Input 컴포넌트는 name, value, onChange 속성을 받아 input 요소를 반환합니다.
name 속성은 input 요소의 name 속성으로 사용되고, value 속성은 input 요소의 현재 값을 설정합니다.
onChange 속성은 input 요소의 값이 변경될 때 실행되는 함수로, 상위 컴포넌트에서 onChange 함수를 전달하여 Input 컴포넌트를 제어할 수 있습니다.
이 컴포넌트는 Input 요소의 로직을 분리하고, 재사용성을 높이기 위해 만들어졌습니다.
이 컴포넌트를 사용하면 여러 곳에서 input 요소를 쉽게 생성하고 재사용할 수 있습니다.
CustomInputExcercise.js
import { useState } from "react";
import useInputs from "./util/useInput";
import Input from "./components/Input";
import shortid from "shortid";
import "./input.css";
const CustomInputExcercise = () => {
//TODO : input에 들어가는 상태값 및 로직을 custom hook으로 구현합니다.
//until 폴더에 useInput.js 파일이 만들어져 있습니다.
const [form, onChange, reset] = useInputs({ first: '', last: '' })
const [nameArr, setNameArr] = useState([]);
const handleSubmit = (e) => {
e.preventDefault();
let userName = {
id: shortid.generate(),
first: form.first,
last: form.last
}
setNameArr([...nameArr, userName]);
reset()
};
return (
<div className="Input-wrap">
<h1>Name List</h1>
<div className="name-form">
<form onSubmit={handleSubmit}>
<div className="name-input">
<label>성</label>
<input
name='first'
value={form.first}
onChange={(e) => onChange(e)}
type="text"
/>
</div>
<div className="name-input">
<label>이름</label>
<input
name='last'
value={form.last}
onChange={(e) => onChange(e)}
type="text"
/>
</div>
<button>제출</button>
</form>
</div>
<div className="name-list-wrap">
<div className="name-list">
{nameArr.map((el) => {
return <p key={el.id}>{el.first} {el.last}</p>;
})}
</div>
</div>
</div>
);
}
export default CustomInputExcercise;
이 코드는 React로 작성된 이름을 입력받아 리스트에 추가하는 기능을 구현한 컴포넌트입니다.
useState Hook을 사용하여 nameArr 상태값을 생성하고, setNameArr 함수를 사용하여 상태값을 업데이트 합니다. - --form 상태값은 useInputs 커스텀 훅을 사용하여 생성하며, 이 커스텀 훅은 first와 last라는 두 개의 상태값을 갖고 있으며, 이 상태값들은 onChange 함수를 통해 업데이트 됩니다. reset 함수는 입력값을 초기화하는 함수입니다.
handleSubmit 함수는 폼을 제출할 때 실행되며, shortid 패키지를 사용하여 유일한 id 값을 생성한 후 nameArr 배열에 userName 객체를 추가하고, reset 함수를 호출하여 입력값을 초기화합니다.
컴포넌트는 이름을 입력받는 폼과 입력한 이름을 출력하는 리스트로 구성됩니다.
폼은 onSubmit 핸들러를 통해 handleSubmit 함수를 실행하며, 이름을 입력하는 input 요소는 form.first와 form.last 값을 사용하여 업데이트합니다.
이름 리스트는 nameArr 배열을 .map() 메소드를 사용하여 출력합니다.
각 요소에는 id, first, last 값을 사용하여 키를 부여합니다.