제목 날짜 내용 발행일 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;