리액트를 사용하며 훅을 정말 많이 사용하게 되는데 기본적으로 내장되어 있는 훅이 아닌 커스텀훅을 제작해서 사용하는 경우도 많다. 일반적으로 사용하는 유틸함수와 커스텀훅의 차이에 대해서 공부하고, 각각의 상황에서 어떤 함수를 만들어 사용해야 하는지를 알게 되었다.
1. 리액트 훅이란?
먼저, 리액트 훅을 가볍게 설명해보자면 함수형 컴포넌트에서 상태와 라이프사이클의 기능을 사용할 수 있게 해주는 것이라고 할 수 있다. 리액트 16.8 버전부터 새롭게 도입되었으며 대표적인 내장 훅으로는 useState, useEffect, useRef, useMemo, useCallback 등이 있다. 커스텀 훅이 아닌 리액트 훅에 대해서 좀 더 자세하게 알고 싶다면 아래 게시글을 참고.
2. 리액트 커스텀훅이란?
그렇다면 커스텀훅이란 뭘까. 말 그대로 커스텀해서 만든 훅이다. 즉, 내장 훅을 조합하거나 특정 로직을 재사용하기 위해 만들어진 사용자 정의 훅이라고 할 수 있다. 커스텀훅을 만들 때도 몇 가지 정해진 규칙이 있는데 이름은 'use'로 시작해야 하며, 일반 함수와 동일하게 동작하지만 리액트의 훅 규칙을 따른다. 커스텀 훅을 사용하면 로직을 분리하고, 코드의 재사용성을 높이며, 컴포넌트를 더 간결하게 만들 수 있다.
2.1. 커스텀 훅 연습
1) useToggle
토글 기능을 수행하는 훅을 만들어보면 다음과 같이 만들 수 있다. 상태를 관리하는 useState와 함수를 메모이제이션할 때 사용하는 useCallback을 사용해서 만든 커스텀훅이다.
import { useState, useCallback } from 'react';
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => {
setValue(v => !v);
}, []);
return [value, toggle];
}
export default useToggle;
2) useFetch
데이터를 fetch할 때 사용할 수 있는 훅의 예제이다. useState와 useEffect를 조합하여 사용한다.
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchData() {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
}
fetchData();
}, [url]);
return { data, loading, error };
}
export default useFetch;
3) useTimeout
해당 예제는 타이머를 관리하는 커스텀 훅으로 timeout 처리를 할 때, 유용하게 쓰이는 훅이다. 리액트에서 타임아웃 처리를 해줄 때, useTimeout 커스텀훅을 하나 만들어두면 여러 컴포넌트에서 재사용할 수 있게 된다.
import { useState, useEffect } from 'react';
function useTimeout(duration, onTimeout) {
const [timeLeft, setTimeLeft] = useState(duration);
useEffect(() => {
if (timeLeft <= 0) {
onTimeout();
return;
}
const timerId = setInterval(() => {
setTimeLeft(prevTime => prevTime - 1);
}, 1000);
return () => clearInterval(timerId);
}, [timeLeft, onTimeout]);
return timeLeft;
}
export default useTimeout;
4) useWindowSize
마지막으로 윈도우 창 사이즈를 추적하는 커스텀훅도 만들 수 있다.
import { useState, useEffect } from 'react';
function useWindowSize() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowSize;
}
export default useWindowSize;
3. 유틸함수란?
앞서 커스텀훅을 살펴봤는데 그렇다면 유틸함수와의 차이는 어떤 게 있을까. 평소에 utils 폴더에 관리하고 있는 함수들이 유틸함수라고 생각하면 될 것 같은데 유틸함수는 리액트 훅과는 다르게 상태와 컴포넌트에 종속되지 않고 특정한 작업을 수행하기 위해 만들어진 정말 그냥 일반적인 함수이다. 유틸 함수는 주로 데이터 변환, 형식화, 계산 등의 작업을 수행하고 애플리케이션 전반에 걸쳐 재사용될 수 있다. 예를 들어, 날짜를 형식화하는 함수를 만들어놓고 여러 곳에서 재사용할 수 있다.
export function formatDate(date) {
const options = { year: 'numeric', month: 'long', day: 'numeric' };
return new Date(date).toLocaleDateString(undefined, options);
}
해당 함수가 아니더라도 암호화, 단순 형식 변환, 계산 등에서는 유틸함수를 만들어 사용하면 된다.
4. 커스텀훅과 유틸함수의 차이
최종적으로 커스텀훅과 유틸함수의 차이를 정리해보자면 다음과 같다. 크게 의존성, 사용범위, 구현방식에 차이가 있다고 보면 될 것 같다. 결론적으로, 커스텀훅은 리액트 컴포넌트와의 상호작용이 필요한 로직을 재사용하기 위해 만들어진 것이고, 유틸함수는 특정 작업을 수행하는 독립적인 로직을 재사용하기 위해 만들어진 것이라고 볼 수 있다.
커스텀훅 | 유틸함수 | |
의존성 | 리액트의 훅을 사용하여 상태 관리나 사이드 이펙트 처리 | 리액트와 무관하게 독립적으로 동작 |
사용범위 | 리액트 컴포넌트 내부에서 사용되며, 컴포넌트의 상태나 라이프사이클과 밀접관 관련이 있음 | 어디에서나 호출될 수 있으며, 컴포넌트와 상관없이 다양한 로직을 처리할 수 있음 |
구현방식 | use로 시작하는 함수로 구현되며 내부에서 다른 훅을 호출할 수 있음 | 단순한 자바스크립트 함수로 구현 |
5. 커스텀훅과 유틸함수의 폴더구조
처음에는 커스텀훅이 아닌 유틸함수를 주로 쓰다보니 utils 폴더에 유틸함수를 넣어두었는데 커스텀훅을 공부하고 만들어보면서 해당 훅들은 어디에 넣어야 할까를 고민했던 것 같다. 그래서 찾아보면서 hooks라는 폴더로 따로 관리하면 되겠다는 생각이 들었다. 커스텀 훅은 hooks에 유틸함수는 utils 폴더에 각각 넣어두면 될 것 같다.
src/
├── components/ # UI 컴포넌트를 저장하는 폴더
│ ├── Header/
│ ├── Footer/
│ └── ...
├── hooks/ # 커스텀 훅을 저장하는 폴더
│ ├── useFetch.js
│ └── useTimeout.js
├── utils/ # 유틸리티 함수를 저장하는 폴더
│ ├── dateUtils.js
│ └── mathUtils.js
├── pages/ # 페이지 컴포넌트를 저장하는 폴더
│ ├── HomePage/
│ └── AboutPage/
├── styles/ # 스타일 파일(CSS, SCSS 등)을 저장하는 폴더
│ └── ...
├── assets/ # 이미지, 폰트, 아이콘 등의 정적 자산을 저장하는 폴더
│ └── ...
├── context/ # 리액트 컨텍스트를 저장하는 폴더
│ └── AuthContext.js
└── App.js # 메인 앱 컴포넌트
평소 유틸함수는 정말 많이 만들어보고 사용했던 것 같은데, 커스텀훅을 만들고 연습해본 것은 이번이 처음이었다. 그래서 처음에는 두 가지 함수의 차이에 대해서 잘 구분이 가지 않아, 먼저 각각의 차이를 공부해야겠다고 생각했고, 적절한 상황에서 각각의 장점을 발휘할 수 있도록 코드를 구현해야겠다는 생각이 들었다.