custom Hook
또한 결국은 함수이다. 함수인데, 안에서 상태 설정을 할 수 있는 로직을 포함한 함수이다. 또한 일반 자바스크립트 함수와 다르게 내부에서 리액트 내장 Hook이나 다른 custom hook을 사용할 수 있다.
숫자를 1씩 더하면서 세는 컴포넌트가 있고, 숫자를 1씩 빼면서 세는 컴포넌트가 있다고 가정해보자. 이때 두 컴포넌트의 로직은 덧셈과 뺄셈을 제외하면 모두 같은 코드를 가질 것이다. 프로그래밍을 할 때 만약 동일한 코드가 중복된다면 하나의 함수로 빼서 사용하는 것이 일반적이다. 이 두 컴포넌트의 내부에서는 리액트 훅 (useState, useEffect)를 사용하여 로직이 구성되어있다. 이 경우는 일반 자바스크립트 함수로 로직을 빼서 사용할 수 없다. 왜냐하면 리액트 Hook은 일반 자바스크립트 함수 내부에서 사용할 수 없기 때문이다. 리액트 Hook은 custom Hook
이나 리액트 함수 내부에서만 사용 가능하다.
따라서, 별도의 함수에 이러한 공통 로직을 아웃소싱하려면
custom Hook
을 만들어야 한다.
1️⃣ custom Hook 정의하기
일반 컴포넌트를
component 파일
내부에 저장하는 것과 같이custom Hook 파일
을 별도로 만들어서 내부에 저장한다.![]()
내부에 저장하는
custom Hook
들의 규칙은, 함수 이름이 무조건 use로 시작하여야 한다는 점이다. use로 시작하여 만들어진 함수들은 리액트에게 해당 함수가custom Hook
임을 알려주는 징표이기 때문이다.이렇게 함수 이름을 선언하고, 내부에는 재사용 하려고 하는 코드를 넣으면 된다.
// 파일 구조 : src > hooks > use-counter.js
// 함수 이름 use로 시작하기
const useCounter = () => {
const [counter, setCounter] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCounter((prevCounter) => prevCounter + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return counter;
};
export default useCounter;
2️⃣ custom Hook 사용하기
해당
custom Hook
을 다른 컴포넌트에서 사용할 때는, import하고 일반 함수 혹은 컴포넌트를 사용하듯 사용하면 된다. 만약custom Hook
컴포넌트 안에서useState
나useEffect
를 사용하여 상태를 정의한다면, 이 state과 effect는 해당 커스텀 훅을 사용하고 있는 컴포넌트의 코드들에 묶이게 된다.즉
custom Hook
을 부르는 컴포넌트들은custom Hook
에서 정의한 state와 effect를 각자의 state과 effect로 가지게 된다.custom Hook
을 사용했다고 하여 해당custom Hook
을 사용하는 컴포넌트들에서 동일한 상태를 공유하는 것이 아니다!
🧐
그렇다면
custom Hook
에서 정의된 state를 다른 컴포넌트에서는 어떻게 사용해야할까?
custom Hook
은 결국 함수인 것을 기억할 것이다. 즉custom Hook
은 return 값을 가질 수 있다. 외부의 컴포넌트들에서custom Hook
을 호출함으로써 사용하고자 하는 값들을custom Hook
의 return 값으로 가지면 된다. 이때 return 값은 배열이든 숫자든 객체든 어떤 것이든 return가능하다.
// ForwardCounter.js
import Card from './Card';
import useCounter from '../hooks/use-counter';
const ForwardCounter = () => {
// custom-hook을 호출, custom Hook의 return값 사용하기
const counter = useCounter();
return <Card>{counter}</Card>;
};
export default ForwardCounter;
http request
를 보내는 작업은 보통useEffect
와useState
를 사용하여 로직이 작성된다. 이 경우http request
를 보내는 작업의 코드는 동일한 경우가 많으니 따로 로직을 분리하는게 필요한데, 내부에서 react hook을 사용하기 때문에 일반 자바스크립트 함수를 사용하여 분리할 수 없다. 따라서custom hook
을 사용하여http request
를 보내는 작업을 분리하면 된다!
이 훅은 어떤 type의 요청이든 받아서 모든 종류의 URL로 보내야하고 어떤 데이터도 변환 가능하도록 작성하여 재사용성을 높이려고 한다. 또한 로딩과 에러에 관한 상태도 관리할 것이다.
이렇게 재사용성을 높이기 위해서는 지금 만드는 hook의 매개변수로 request를 보내는 URL과 내부에서 사용할 메소드, 그리고 헤더, 본문등을 받아와야 유연하게 사용 가능한 custom hook
을 만들 수 있다. 따라서 이런 정보를 담고있는 객체를 매개변수로 받아오도록 구현해보자.
// http Hook (custom Hoom)
import {useState, useEffect} from 'react';
// 객체랑 메소드를 매개변수로 받아옴
const useHttp = (requestConfig, applyData) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
// http request 보내는 로직
const sendRequest = async () => {
setIsLoading(true);
setError(null);
try {
// fetch를 더 유연하게 하기 위해 custom Hook을 사용하는 컴포넌트로부터 객체를 매개변수로 받아옴
const response = await fetch(
requestConfig.url, {
method: requestConfig.method ? requestConfig.method : 'GET',
headers: requestConfig.headers ? requestConfig.headers : {},
body: requestConfig.body ? JSON.stringify(requestConfig.body) : null,
}
);
// fetch API는 직접 에러 헨들링 필요
if (!response.ok) {
throw new Error('Request failed!');
}
const data = await response.json();
// 상황에 따른 메소드를 custom hook을 사용하는 컴포넌트에서 정의하여 넘겨줌
applyData(data);
} catch (err) {
setError(err.message || 'Something went wrong!');
}
setIsLoading(false);
};
// custom Hook을 부르는 component에서 활용할 custom hook 내부의 state 값들을 return하기
return {
isLoading,
error,
sendRequest,
};
};
export default useHttp;
리액트는 기본 자바스크립트 base이기 때문에 컴포넌트가 재실행됨에 따라 내부에 있는 state값들이나 함수들이 갱신되게 된다. 이전 섹션에서 알아봤듯이 원시값의 경우 갱신되어도 값이 같으면 같은 값으로 판단하지만 객체나 함수의 경우 코드가 변하지 않았더라도 메모리 상에 다른 위치에 존재하게 되므로 다른 값으로 인지한다. 이에 따라 컴포넌트가 계속해서 재실행되게 되는 문제가 발생한다. 이를 방지하기 위해서는 useCallback
을 긴밀하게 사용하여야 한다!
우리가 만든 http Hook
에서 만든 sendRequest
함수는 App.js
에서 사용될 때 useEffect
내부에서 사용되게 된다. 이때 useEffect
의 내부에서 사용하는 모든 값은 dependencies로 넣어야 한다는 일종의 리액트 훅 규칙에 따라 dependencies에 sendRequest
를 추가하게 되면 위에서 설명한 부분 때문에 무한루프가 발생한다.
// App.js
...생략
// 객체분해할당으로 useHttp의 return값 받아오기
// sendRequest는 alias를 통해서 다른 이름으로 받아와서 사용!
const {isLoading, error, sendRequest: fetchTasks} = useHttp(
{url: 'https://react-http-ab392-default-rtdb.firebaseio.com/tasks.json'},
transformTasks
);
// sendRequest 실행
useEffect(() => {
fetchTasks();
}, [fetchTasks]);
해결방안 1️⃣ - useCallback, useMemo
따라서, useHttp
에서 sendRequest
를 useCallback
으로 감싸서 선언해야한다.
// useHttp.js
const useHttp = (requestConfig, applyData) => {
...생략
const sendRequest = useCallback (async () => {
...생략
}, [requestConfig, applyData]);
return {
isLoading,
error,
sendRequest,
};
};
하지만 이 경우에도 문제가 생긴다. sendRequest
의 매개변수로 받아와서 사용하는 값들인 requestConfig
와 applyData
는 모두 객체와 함수값이다. 따라서 컴포넌트가 재실행되면서 다시 만들어지는 참조값들이고 마찬가지로 무한루프를 일으킨다. 때문에 App.js
에서 매개변수로 useHttp
에 넘기는 해당 값들 또한 useCallback
으로 감싸서 선언해주어야 한다.
// App.js
// 매개변수로 넘겨줄 메소드 useCallback으로 감싸기!
const transformTasks = useCallback(tasksObj => {
const loadedTasks = [];
for (const taskKey in tasksObj) {
loadedTasks.push({ id: taskKey, text: tasksObj[taskKey].text });
}
setTasks(loadedTasks);
}, [])
이때 requestConfig
는 함수가 아닌 객체값이기 때문에 useMemo()
와 같은 것을 사용해서 변하지 않게 해주어야 한다.
해결방안 2️⃣
또는 useHttp
를 만들 때 매개변수로 외부값을 받아오도록 하지 않고 useHttp
내부에서 선언한 함수인 sendRequest
의 매개변수로 받아오도록 변경할 수 있다.
이렇게 하면 받아온 매개변수 (requestConfig
와 상황에 따라 사용할 메소드)가 useCallback
의 내부에서 사용하는 값이기 때문에 dependencies에 requestconfig
객체값을 포함하지 않아도 된다.
// useHttp.js
// 매개변수로 아무것도 받지 않음
const useHttp = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
// useCallback으로 선언하는 함수에 매개변수로 넣어주기
const sendRequest = useCallback (async (requestConfig, applyData) => {
... 생략
// 모두 useHttp component의 내부에서 사용되는 값이므로 dependencies추가 안해도 됨
}, []);
return (...생략);
};
export default useHttp;
// App.js
function App() {
// useHttp() 자체에는 아무런 매개변수 전달 안함
const {isLoading, error, sendRequest: fetchTasks} = useHttp();
// useHttp 내부의 함수를 useEffect를 통해서 실행시키기
useEffect(() => {
...
// useHttp 내부 함수에 매개변수로 값 전달해주기
fetchTasks(
{url: 'https://react-http-ab392-default-rtdb.firebaseio.com/tasks.json'},
transformTasks
);
}, [fetchTasks]);
return (
...생략
);
}
export default App;