훅?! 많이 들어봤지만 좀 어렵기도 하고... 이걸 왜 쓰는지... 함수랑 뭐가 다른지... 어렵죠?^^
그럼 훅에 대해서 먼저 알아볼게요
훅(Hook)과 일반 함수(Function)의 가장 큰 차이는 리액트의 기능을 사용할 수 있는지 여부입니다.
컴포넌트가 렌더링되는 동안에만 사용할 수 있는 특별한 함수
특징
1. 반드시 이름이 use로 시작해야 한다. (예: useState, useEffect, useGeolocation)
2. 리액트의 내부 기능(React State, Lifecycle features)을 사용할 수 있습니다.
3. 오직 함수형 리액트 컴포넌트 내부나 다른 커스텀 훅 내부에서만 호출할 수 있습니다.
훅은 컴포넌트의 렌더링과는 별개로 로직과 상태 관리를 담당합니다. 데이터를 가져오거나, 상태를 저장하거나, DOM에 직접 접근하는 등의 복잡한 작업을 수행합니다.
함수는 JavaScript의 기본적인 기능으로, 입력(인자)을 받아 특정 작업을 수행하고 값을 반환하는 단순한 코드 블록
특징
일반 함수는 데이터 계산, 문자열 처리, 외부 라이브러리 함수 호출 등 순수하게 JS, TS 영역에 속하는 작업을 수행합니다.
다들 커스텀 훅에 대해서 한번씩은 들어보신 적 있죠?
훅도 잘 모르는데 커스텀 훅은 뭐지...
이번에는 커스텀 훅에 대해서 알아보겠습니다.
커스텀 훅은 말 그대로 개발자가 직접 만들어 사용하는 리액트 훅으로,
리액트의 내장 훅(useState, useEffect 등)을 이용해 특정 로직을 재사용 가능하게 만드는 함수입니다.
우선 제가 생각하는 커스텀 훅을 사용하는 가장 큰 이유는 장점 중 하나인 코드의 재사용성 때문입니다.
지금 제가 프로젝트에서 구현하고 있는 기능 중 하나인 GeoLocation API를 통해 사용자의 위치를 가져오는 기능을 구현하고 있는데 처음에는 하나의 컴포넌트에서만 사용하기 위해 커스텀 훅이 아닌 컴포넌트로 구현하였지만 개발이 진행되면서 유저의 위치를 가져오는 로직이 다른 컴포넌트에서도 동일하게 사용되는 경우가 있어 해당 로직을 커스텀 훅으로 변경하여 필요한 컴포넌트에서 함수 호출을 하여 사용하는 방식으로 변경하였습니다.
기존 컴포넌트 로직
[BEFORE] 위치 가져오기 로직 (LocationFetcher 컴포넌트)
import { useEffect } from 'react'
const LocationFetcher = () => {
useEffect(() => {
if(navigator.geolocation) {
console.log("GeoLocation API 지원 확인");
navigator.geolocation.getCurrentPosition (
// 위치 요청 성공 여부에 따른 콜백 함수
(position) => {
const lat = position.coords.latitude // 위도
const lng = position.coords.longitude // 경도
console.log("사용자 현재 위치 가져오기 성공!");
console.log(`위도 (Latitude): ${lat}`);
console.log(`경도 (Longitude): ${lng}`);
},
(error) => {
console.error("현재 위치 가져오기 실패", error.message);
if (error.code === error.PERMISSION_DENIED) {
console.warn("이유: 사용자가 위치 정보 접근을 거부했습니다.");
} else if (error.code === error.POSITION_UNAVAILABLE) {
console.warn("이유: 위치 정보를 사용할 수 없습니다.");
} else if (error.code === error.TIMEOUT) {
console.warn("이유: 위치 요청 시간이 초과되었습니다.");
}
}
)
}
}, [])
return null;
}
export default LocationFetcher
커스텀 훅으로 변경한 로직
[AFTER] 위치 가져오기 로직 (useGeolocation 커스텀 훅)
import { useState, useEffect } from 'react'
interface GeoLocationState {
lat: number | null;
lng: number | null;
error: string | null;
}
const useGeolocation = () => {
const [locationState, setLocationState] = useState<GeoLocationState>({
lat: null,
lng: null,
error: null,
})
useEffect(() => {
if(!navigator.geolocation) {
console.error("현재 브라우저는 GeoLocation API를 지원하지 않습니다.");
setLocationState(prev => ({
...prev,
error: "현재 브라우저는 GeoLocation API를 지원하지 않습니다."
}));
}
// 위치 요청 성공시 실행되는 콜백함수
const successCallback = (postion: GeolocationPosition) => {
const lat = postion.coords.latitude;
const lng = postion.coords.longitude;
console.log("사용자의 현재 위치 가져오기 성공");
console.log(`위도 (Latitude): ${lat}, 경도 (Longitude): ${lng}`);
setLocationState({lat,lng, error: null});
};
const errorCallback = (error: GeolocationPositionError) => {
let errorMessage = `ERROR(${error.code}): ${error.message}`;
if (error.code === error.PERMISSION_DENIED) {
errorMessage = "사용자가 위치 정보 접근을 거부했습니다.";
} else if (error.code === error.POSITION_UNAVAILABLE) {
errorMessage = "위치 정보를 사용할 수 없습니다.";
} else if (error.code === error.TIMEOUT) {
errorMessage = "위치 요청 시간이 초과되었습니다.";
}
console.error(`useGeolocation: ${errorMessage}`);
setLocationState(prev => ({
...prev,
error: errorMessage,
}));
};
// navigator.geolocation.getCurrentPosition(successCallback, errorCallback);
navigator.geolocation.watchPosition(successCallback, errorCallback);
}, []);
return locationState;
};
export default useGeolocation;
이처럼 컴포넌트가 아닌 커스텀 훅으로 해당 로직을 변경하면서
이 기능을 사용하기 위해 다른 컴포넌트에서는 로직을 복사하는것이 아닌
import { useEffect, useRef, useState } from 'react'
import { loadNaverMapScript } from '../../utils/naverMapLoader'
import useGeolocation from "../../hooks/useLocation";
한줄로 import 하여 사용할 수 있습니다.