Custom Hook을 쓰는 이유
Custom Hook은 개발자가 스스로 커스텀한 훅을 의미하며 이를 이용해 반복되는 로직을 함수로 뽑아내어 재사용할 수 있다.
- 여러 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을 사용한다면 가능하다.
두 컴포넌트에서 사용하기 위해 동일하게 사용되고 있는 로직을 분리하여 함수 useFriendStatus로 만든다.
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;
}
이렇게 Custom Hook을 정의할 때는 일종의 규칙이 필요하다.
- Custom Hook을 정의할 때는 함수 이름 앞에 use를 붙이는 것이 규칙이다.
- Custom Hook을 정의할 때는 함수 이름 앞에 use를 붙이는 것이 규칙이다.
- use가 붙지 않은 함수는 그저 일반 함수로 취급하기 때문에 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는 컴포넌트 내에서 독립적으로 정의되어 있다.
~ 실습 ~
useFetch Hooks
useEffect hook을 이용한 로직은 반복되는 로직이 많다.
- 특히 API를 통해 데이터를 받아와 처리하는 로직은 반복적일 수밖에 없다.
- 이러한 로직을 custom hook으로 만들어 분리하고 필요한 컴포넌트마다 적용을 한다면 컴포넌트들을 좀 더 직관적으로 관리할 수 있을 것이다.
여러 url을 fetch할 때 쓸 수 있는 useFetch 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;
useEffect를 사용하여 컴포넌트의 생명주기와 관련된 작업을 처리할 수 있으며,
- 이를 통해 컴포넌트의 상태 변화와 외부 동작을 연결할 수 있다.
- 이를 통해 리액트 컴포넌트를 좀 더 동적이고 상호작용적으로 만들 수 있다.
useFetch Hook : 주석 설명 참조
useInputs Hooks 1
여러 input에 의한 상태 변경을 할 때 쓸 수 있는 useInputs Hooks
import { useState, useCallback } from 'react';
function useInputs(initialForm) {
const [form, setForm] = useState(initialForm);
// change
const onChange = useCallback(e => { // 🟣 설명(1)
const { name, value } = e.target;
setForm(form => ({ ...form, [name]: value })); // 🟣 설명(2)
}, []);
const reset = useCallback(() => setForm(initialForm), [initialForm]); // 🟣 설명(3)
return [form, onChange, reset];
}
export default useInputs;
🟣 설명(1) onChange 함수에 useCallback을 쓰는 이유
[useCallback hook]
useCallback hook은 콜백 함수를 메모이제이션하는 데 사용된다. 즉, 성능 최적화를 위해 콜백 함수를 이전에 생성된 것과 동일한 것으로 유지하여 불필요한 재생성을 방지한다. useCallback 훅의 두 번째 매개변수는 의존성 배열(dependency array)으로, 해당 배열에 포함된 값들이 변경되지 않는 한 콜백 함수가 새로 생성되지 않음을 의미한다. 만약 의존성 배열이 비어있지 않고, 배열 안에 특정 값들이 포함되어 있다면 해당 값들 중 하나라도 변경될 때마다 새로운 콜백 함수가 생성된다. 이는 메모리 사용량을 줄이고 불필요한 렌더링을 방지하기 위한 최적화 기법이다.
onChange 함수가 useInputs 커스텀 훅의 반환 값으로 사용될 때, useCallback을 사용하여 이전에 생성된 콜백 함수를 재사용할 수 있다.
- 위의 코드에서 빈 배열([])을 의존성 배열로 전달한 이유는 해당 콜백 함수가 어떤 외부의 값에 의존하지 않고, 오로지 내부의 상태(form, setForm)만을 사용하기 때문이다.
- 따라서 이 콜백 함수는 오로지 한 번 생성되며, 컴포넌트가 다시 렌더링되어도 새로운 콜백 함수가 생성되지 않는다.
- 이렇게 하면 매번 새로운 콜백 함수를 생성하는 오버헤드가 발생하지 않으며, 성능 향상을 기대할 수 있다.
🟣 설명(2) 화살표 함수의 축약 문법
setForm(form => ({ ...form, [name]: value }));
- form => ({ ...form, [name]: value })는 화살표 함수의 축약 문법이다.
- 이것은 객체 리터럴을 반환하는데, form 객체의 모든 속성을 복사한 후, [name] 프로퍼티를 value 값으로 업데이트한 새로운 객체를 반환한다.
- name은 동적으로 변경될 수 있는 프로퍼티이다. 대괄호([])를 사용하여 변수 값을 프로퍼티 이름으로 사용할 수 있게 한다. 따라서 setForm 함수 내부에서 name 변수의 값을 동적으로 사용하기 위해 대괄호([])를 사용한다.
- 화살표 함수 내에서 중괄호({})를 사용하는 경우, 함수의 본문이 여러 줄로 구성될 때 사용됩니다. 그러나 이 경우에는 소괄호(())로 감싸는 것은 객체 리터럴을 반환하기 위한 문법적인 규칙이다.
- 이는 중괄호를 함수의 본문 구분을 위해 사용하는 것과 구분하기 위함이다.
- 화살표 함수가 소괄호로 감싸진 객체 리터럴을 반환하면, 해당 객체가 반환 값으로 취급된다.
예시
const getKey = () => 'dynamicKey';
const getValue = () => 'dynamicValue';
const getObject = () => ({
[getKey()]: getValue()
});
🟣 설명(3) reset 함수에 useCallback을 쓰는 이유
- reset 함수가 initialForm 값을 참조하고 있으므로, useCallback을 사용하여 이전에 생성된 함수를 재사용할 수 있다.
- 의존성 배열 [initialForm]을 설정함으로써, reset 함수는 initialForm 값이 변경될 때마다 새로운 함수가 생성되도록 보장된다.
- 이렇게 함으로써, useInputs 커스텀 훅이 호출될 때마다 항상 최신의 initialForm 값을 사용하여 reset 함수가 생성된다.
- 즉 form의 값이 변경되어 컴포넌트가 재렌더링되었을 때도 onChange와 reset 함수의 주소값이 변경되지 않는다.
🟣 설명(4) 만든 useInput Hooks 사용하기
import React from 'react';
import useInputs from './useInputs';
export default function SimpleForm () {
const [form, onChange, reset] = useInputs({ username: '', email: '' });
const handleSubmit = e => {
e.preventDefault();
console.log(form);
// 여기서 폼 데이터를 처리하거나 다른 작업을 수행할 수 있다.
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>
Username:
<input
type="text"
name="username"
value={form.username}
onChange={onChange}
/>
</label>
</div>
<div>
<label>
Email:
<input
type="email"
name="email"
value={form.email}
onChange={onChange}
/>
</label>
</div>
<div>
<button type="submit">Submit</button>
<button type="button" onClick={reset}>Reset</button>
</div>
</form>
);
};
useInputs Hooks 2
input 또한 반복적으로 사용되는 로직을 가지고 있는 컴포넌트이다.
- 이런 식으로 앱 내에 반복적으로 사용되고 관리되는 로직은 많다. (input, form, button 등)
- input도 지금은 2개 정도 있어 관리가 크게 요구되지 않는다.
- 그러나 후에 회원가입 컴포넌트를 만들거나, 앱의 볼륨이 커지게 된다면 input은 굉장히 많이 사용되는 UI 중 하나이기 때문에 반복되는 로직을 관리해야 하는 필요성이 생긴다.
- 이런 컴포넌트 또한 custom hook을 이용하여 로직을 분리하여 관리할 수 있다.
- 컴포넌트 내에 반복되는 로직을 분리를 해 관리를 하면 컴포넌트들을 좀 더 깔끔하게 관리할 수 있다.
before
after에서 App 컴포넌트에 들어 있는 input을 Input 컴포넌트로 바꾼 후, custom hook을 이용하여 input의 로직을 분리해 볼 예정이다.
after
- util > useInput.js
- useInput 커스텀 훅을 따로 만들었다.
- src > component > Input.js
- input 컴포넌트도 따로 만들어줬다.
useFetch Hook : 코드 주석 설명 참조
1. 하다가 생긴 문제
useInput hook에서 Component를 useCallback을 적용하여 함수를 작성했다.
- 문제는 -> Input의 value 값이 바뀔 때마다 Component 함수가 재호출되어 focus out이 되어 원활한 입력이 되지 않는 문제가 발생했다.
- 예를 들어 '성'을 입력할 때 'L'을 누른 순간, 콘솔창에 "called!"가 찍히며 인풋창이 focus out이 되었고, 그 다음 'e'를 누르기 위해서는 다시 인풋창을 클릭해야만 했다.
문제의 코드
// useInput.js
import { useState, useCallback } from "react";
import Input from "../component/Input";
export default function useInput(initialValue) {
const [value, setValue] = useState(initialValue);
const reset = () => setValue(initialValue);
const Component = useCallback(() => {
console.log("called!");
return <Input value={value} setValue={setValue} />;
}, [value]); <!-- 🟣 원인은 value 값이 바뀔 때마다 리렌더링되어 Input이 새로 return 되기 때문이었다. -->
return [Component, value, reset];
}
// Input.js
export default function Input({ value, setValue }) {
const handleKeyUp = (event) => {
setValue(event.target.value);
};
return (
<input
value={value}
type="text"
onChange={handleKeyUp}
/>
);
}
1-1. 1차 해결
중요한 것은 value 값이 변함에 따라 Input 컴포넌트가 다시 return 되는 문제이기 때문에 value 값이 '입력이 끝날 때' 바뀌게 하는 것이다. 따라서 event의 key가 Enter이거나, event의 type이 blur(focus out)될 때, value 값이 바뀌도록 설정해주었다.
문제 ) 하지만, 에러가 발생했고 문제의 원인은 <input> 요소에 value prop을 제공했는데, 해당 요소에 onChange 핸들러가 제공되지 않았기 때문이었다. ( 또는 readOnly가 설정되지 않았기 때문에 발생한다고 한다.)
- 따라서 value = {value} 대신, defaultValue prop을 사용했다.
// Input.js
export default function Input({ value, setValue }) {
const handleKeyUp = (event) => {
if (event.key === "Enter" || event.type === "blur") {
setValue(event.target.value);
}
};
return (
<input
defaultValue={value}
type="text"
onKeyUp={handleKeyUp}
onBlur={handleKeyUp}
/>
);
}
1-2. 2차 해결
1차 해결에서 찜찜했던 것은, onChange 대신 onKeyUp 이벤트를 사용하여 상태 관리를 할 수는 있지만, 이 방식은 일부 제약사항이 있을 수 있다는 것이다.
- onKeyUp 이벤트는 사용자가 키를 누르고 놓을 때마다 발생하므로, 입력 필드의 값이 변경되는 방식이다.
- onChange 이벤트는 입력 필드의 값이 변경될 때마다 즉시 발생하므로, 사용자의 모든 입력을 캡처하여 상태를 업데이트할 수 있다.
- 이는 입력 중간에도 상태가 즉시 업데이트되어 실시간으로 상태를 반영할 수 있다는 장점을 가지고 있다.
물론 해당 컴포넌트에서는 실시간 변경이 크게 중요하지는 않지만, 범용적으로 보았을 때는 onChange로 구현하는 것이 효율적인 방법일 수 있기 때문에 onChange를 사용하여 인풋 컴포넌트를 제어하고자 했다.
bind 객체를 사용한 방법
onChange 함수를 만들어 반환하는 방법
function useInput(initialValue) {
const [value, setValue] = useState(initialValue);
const onChange = useCallback((e) => {
const { value } = e.target;
setValue(value);
}, []);
const reset = useCallback(() => setValue(initialValue), [initialValue]);
return [value, onChange, reset];
}
export default useInput;
const [firstValue, firstOnChange, firstReset] = useInput("");
const [secondValue, secondOnChange, secondReset] = useInput("");
const [nameArr, setNameArr] = useState([]);
const handleSubmit = (e) => {
e.preventDefault();
setNameArr([...nameArr, `${firstValue} ${secondValue}`]);
firstReset();
secondReset();
};
return (
<div className="App">
<h1>Name List</h1>
<div className="name-form">
<form onSubmit={handleSubmit}>
<div className="name-input">
<label>성</label>
<input value={firstValue} onChange={firstOnChange} type="text" />
</div>
<div className="name-input">
<label>이름</label>
<input value={secondValue} onChange={secondOnChange} type="text" />
.
.
.
** Input 컴포넌트를 따로 만들지 않는 경우 아래와 같이 간단하게 작성할 수 있겠다.
'FE > React' 카테고리의 다른 글
React.lazy()와 React.Suspense (0) | 2023.05.22 |
---|---|
React의 렌더링 최적화를 도와주는 useCallback (0) | 2023.05.19 |
React의 렌더링 최적화를 도와주는 useMemo (0) | 2023.05.19 |
React의 Hook의 개념과 사용 규칙 (0) | 2023.05.19 |
React Diffing Algorithm - 상태변화를 감지할 수 있도록 하는 React의 비교 알고리즘 (0) | 2023.05.19 |