제목 날짜 내용 발행일 23.03.22
해당 포스트는
Custom 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는 컴포넌트 내에서 독립적으로 정의되어 있다.
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;
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;
import "./styles.css";
import { useEffect, useState } from "react";
export default function App() {
const [data, setData] = useState();
useEffect(() => {
fetch("data.json", {
headers: {
"Content-Type": "application/json",
Accept: "application/json"
}
})
.then((response) => {
return response.json();
})
.then((myJson) => {
setData(myJson);
})
.catch((error) => {
console.log(error);
});
}, []);
return (
<div className="App">
<h1>To do List</h1>
<div className="todo-list">
{data &&
data.todo.map((el) => {
return <li key={el.id}>{el.todo}</li>;
})}
</div>
</div>
);
}
이 코드에서 useEffect 부분을 따로 분리해서 custom hook 처럼 만들어 보았다.
//App.js
import "./styles.css";
import useFetch from "./util/useFetch";
export default function App() {
const [data, setData] = useFetch(null);
return (
<div className="App">
<h1>To do List</h1>
<div className="todo-list">
{data &&
data.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(fetchUrl);
useEffect(() => {
fetch("data.json", {
headers: {
"Content-Type": "application/json",
Accept: "application/json"
}
})
.then((response) => {
return response.json();
})
.then((myJson) => {
setData(myJson);
})
.catch((error) => {
console.log(error);
});
}, [data]);
return [data, setData];
};
export default useFetch;

import { useState } from "react";
import useInput from "./util/useInput";
import Input from "./component/Input";
import "./styles.css";
export default function App() {
const [firstNameValue, setFirstNameValue] = useState("");
const [lastNameValue, setLastNameValue] = useState("");
const [nameArr, setNameArr] = useState([]);
const handleSubmit = (e) => {
e.preventDefault();
setNameArr([...nameArr, `${firstNameValue} ${lastNameValue}`]);
};
return (
<div className="App">
<h1>Name List</h1>
<div className="name-form">
<form onSubmit={handleSubmit}>
<div className="name-input">
<label>성</label>
<input
value={firstNameValue}
onChange={(e) => setFirstNameValue(e.target.value)}
type="text"
/>
</div>
<div className="name-input">
<label>이름</label>
<input
value={lastNameValue}
onChange={(e) => setLastNameValue(e.target.value)}
type="text"
/>
</div>
<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>
);
}
이 코드를 이용해서 custom hook 과 Input컴포넌트를 완성해야 한다.
import { useState } from "react";
import useInput from "./util/useInput";
import Input from "./component/Input";
import "./styles.css";
export default function App() {
const [fistValue, firstBind, firstReset] = useInput("");
const [lastValue, lastBind, lastReset] = useInput("");
const [nameArr, setNameArr] = useState([]);
const handleSubmit = (e) => {
e.preventDefault();
setNameArr([...nameArr, `${fistValue} ${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>
);
}
// .component/Input.js
function Input({ labelText, value }) {
return (
<div className="name-input">
<label>{labelText}</label>
<input {...value} type="text" />
</div>
);
}
export default Input;
// .util/useInput.js
import { useState, useCallback } from "react";
function useInput(initialValue) {
const [value, setValue] = useState(initialValue);
const bind = useCallback({
value,
onChange: (e) => {
setValue(e.target.value);
}
});
const reset = useCallback(() => {
setValue(initialValue);
});
//return 해야 하는 값은 배열 형태의 값
return [value, bind, reset];
}
export default useInput;
