시작하기에 앞서
이 포스트를 통해 커스텀 훅의 사용 예시와 더불어 실제 프로젝트의 적용 코드 및 사용 주의사항 등을 공유할 예정입니다.
먼저 Hooks 란?
리액트(React)의 훅(Hooks)은 함수형 컴포넌트에서 상태 관리, 라이프 사이클 이벤트 처리, 그리고 React의 다른 기능들을 사용할 수 있게 해주는 기능들입니다. 훅을 사용하면 클래스 기반 컴포넌트에서만 가능했던 여러 기능들을 함수형 컴포넌트 에서도 쉽게 사용할 수 있습니다. 우리가 자주 쓰는 useEffect
, useState
를 포함해 정의된 Hooks를 통해 React의 생명주기를 다룰 수 있습니다.
Hooks의 종류
[Built-in React Hooks – React
The library for web and native user interfaces
react.dev](https://react.dev/reference/react/hooks)
- 상태 훅(State Hooks): 컴포넌트가 사용자 입력과 같은 정보를 "기억"하게 해줍니다. 예를 들어,
useState
와useReducer
가 이에 해당합니다. - 컨텍스트 훅(Context Hooks): 컴포넌트가 멀리 떨어진 부모로부터 정보를 props 없이 받을 수 있게 해줍니다.
useContext
가 이에 해당합니다. - 참조 훅(Ref Hooks): 컴포넌트가 렌더링에 사용되지 않는 정보(예: DOM 노드, 타임아웃 ID)를 유지할 수 있도록 합니다.
useRef
와useImperativeHandle
이 여기에 속합니다. - 효과 훅(Effect Hooks): 컴포넌트가 외부 시스템과 연결되고 동기화할 수 있도록 합니다.
useEffect
,useLayoutEffect
,useInsertionEffect
가 이에 해당합니다. - 성능 훅(Performance Hooks): 재렌더링 성능을 최적화하기 위해 사용됩니다.
useMemo
와useCallback
이 여기에 해당합니다. - 리소스 훅(Resource Hooks): 컴포넌트가 상태의 일부로 갖지 않고도 리소스에 접근할 수 있게 해줍니다.
use
가 이에 해당합니다. - 기타 훅(Other Hooks): 주로 라이브러리 작성자에게 유용하며, 일반적인 애플리케이션 코드에서는 자주 사용되지 않습니다.
useDebugValue
,useId
,useSyncExternalStore
가 여기에 속합니다. - 사용자 정의 훅(Your own Hooks): 사용자가 자신의 JavaScript 함수로 커스텀 훅을 정의할 수 있습니다.
8가지의 Hooks로 나눌 수 있으며 이 중 우리가 알아볼 것은 사용자 정의 훅 즉 Custom Hook이다.
Custom Hook이란?
Custom Hooks: Sharing logic between components
커스텀 훅: 컴포넌트간의 로직 공유
커스텀 훅의 경우 여러 곳에서 반복적으로 사용되는 로직을 분리하여 재사용성을 높이고 가독성을 향상시킬 수 있습니다.
예시 1) 간단한 적용 예
다음을 통해 간단한 사례로 커스텀 훅을 사용하는 예시에 대해 설명하겠습니다.
function InputComponent({ inputRef }) {
return <input ref={inputRef} />;
}
function ButtonComponent({ onClick }) {
return <button onClick={onClick}>Focus on Input</button>;
}
function ParentComponent() {
const inputRef = useRef();
const focusInput = () => {
inputRef.current.focus();
};
return (
<>
<InputComponent inputRef={inputRef} />
<ButtonComponent onClick={focusInput} />
</>
);
}
위와 같이 인풋과 버튼 그리고 그 두 가지를 합친 컴포넌트가 있다고 가정하겠습니다. 단순하게 버튼을 클릭 시 input에 포커스를 주는 기능을 하고 있습니다. 이와 같은 기능을 커스텀 훅을 통해 분리한다면 다음과 같아집니다.
// 분리된 커스텀 훅
function useInputFocus() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current && inputRef.current.focus();
};
return [inputRef, focusInput];
}
//실제 사용 예시
function InputComponent({ inputRef }) {
return <input ref={inputRef} />;
}
function ButtonComponent({ onClick }) {
return <button onClick={onClick}>Focus on Input</button>;
}
function ParentComponent() {
const [inputRef, focusInput] = useInputFocus();
return (
<>
<InputComponent inputRef={inputRef} />
<ButtonComponent onClick={focusInput} />
</>
);
}
이와 같이 useRef, useEffect 등 여러 훅을 사용하는 로직을 분리하는데 사용합니다. 앞으로 useInputFocus를 통해 간편하게 인풋과 포커스를 한줄에 처리할 수 있게 되었습니다.
예시 2) 실제 적용한 useStickyTab 함수
먼저 훅으로 분리되지 않은 함수의 예시를 보여드리겠습니다. 다음과 같이 스크롤 시 특정 높이에 붙을 경우 자동으로 탭의 형식을 변경해주는 방식입니다.
(프로젝트의 특성상 CSS의 제작을 퍼블리셔에게 위임했는데 overflow등의 속성값의 남발로 인해 CSS의 Sticky의 옵션이 적용되지 않아 직접 작성해야 되는 상황 이였습니다.)
// 작성되었던 코드
const LifestyleTabSwiper = () => {
// 하단부터 탭이 LIFESTYLE_SCROLL_HOLD 와 겹치는 부분을 IntersectionObserver로 감지하여 클래스를 변경함
const LIFESTYLE_SCROLL_HOLD = 50;
const [isTabSticky, setIsTabSticky] = useState(false);
const tabRef = useRef(null);
useEffect(() => {
if (!tabRef?.current) return;
const tabElement = tabRef.current;
const observerCallback = (entries) => {
entries.forEach((entry) => {
const shouldStick = !entry.isIntersecting;
if (shouldStick !== isTabSticky) {
tabElement.classList.toggle("is--scroll", shouldStick);
setIsTabSticky(shouldStick);
}
});
};
const observerOptions = {
root: null,
rootMargin: `-${LIFESTYLE_SCROLL_HOLD}px 0px 0px 0px`,
threshold: 1,
};
const observer = new IntersectionObserver(
observerCallback,
observerOptions
);
observer.observe(tabElement);
return () => observer.unobserve(tabElement);
}, [isTabSticky, tabRef]);
return (
<article className="life--styling--tabs">
<section>
<div className="tab--wrapper type--2">
<div className="tab--header--wrapper" ref={tabRef}>
<LifestyleTabList
data={data}
currentTab={currentTab}
goToTab={goToTab}
/>
</div>
... 이하 줄임
작성하고 정상 기능하지만 혹여나 다른 탭에도 같은 기능이 필요하면 긴 코드를 다시 써야하고 너무 길어져 흐름을 읽기 어려워졌습니다. 이제 이 sticky 기능을 훅으로 분리해보겠습니다.
// 분리된 커스텀 훅
export function useStickyTab(LIFESTYLE_SCROLL_HOLD) {
const [isTabSticky, setIsTabSticky] = useState(false);
const tabRef = useRef(null);
useEffect(() => {
if (!tabRef?.current) return;
const tabElement = tabRef.current;
const observerCallback = (entries) => {
entries.forEach((entry) => {
const shouldStick = !entry.isIntersecting;
if (shouldStick !== isTabSticky) {
tabElement.classList.toggle("is--scroll", shouldStick);
setIsTabSticky(shouldStick);
}
});
};
const observerOptions = {
root: null,
rootMargin: `-${LIFESTYLE_SCROLL_HOLD}px 0px 0px 0px`,
threshold: 1,
};
const observer = new IntersectionObserver(
observerCallback,
observerOptions
);
observer.observe(tabElement);
return () => observer.unobserve(tabElement);
}, [isTabSticky, tabRef]);
return tabRef;
}
// 기존 코드에서 사용하기
const LifestyleTabSwiper = () => {
const tabRef = useStickyTab(50);
return (
<article className="life--styling--tabs">
<section>
<div className="tab--wrapper type--2">
<div className="tab--header--wrapper" ref={tabRef}>
<LifestyleTabList
data={data}
currentTab={currentTab}
goToTab={goToTab}
/>
</div>
...이하 줄임
이제 어느 함수에서도 고정을 원하는 tabRef가 생기면const tabRef = useStickyTab(높이);
를 통해 편리하게 사용할 수 있게 되었습니다.
사용 시 주의해야 할 점
일반적인 훅의 사용시 주의해야 될 점 및 커스텀 훅을 사용시 주의사항을 정리해 보았습니다.
- 함수 컴포넌트의 최상위에서만 훅 사용: 훅은 함수 컴포넌트의 최상위 레벨에서만 호출해야 합니다. 반복문, 조건문, 중첩된 함수 내부에서 훅을 호출하면 안 됩니다. 이 규칙은 훅의 호출 순서가 보장되도록 합니다.
- 리액트 함수 컴포넌트 내에서만 훅 사용: 훅은 리액트 함수 컴포넌트나 커스텀 훅 내에서만 호출해야 합니다. 일반 JavaScript 함수에서는 훅을 사용할 수 없습니다.
- 의존성 배열 관리에 주의:
useEffect
,useMemo
,useCallback
등의 훅에서는 의존성 배열(dependency array)을 정확하게 관리해야 합니다. 의존성 배열에 포함된 값이 변경될 때만 훅이 실행됩니다. 배열을 잘못 관리하면 메모리 누수나 불필요한 렌더링이 발생할 수 있습니다. - 훅의 규칙 준수: 리액트 팀은 훅을 올바르게 사용하기 위해 두 가지 주요 규칙을 제시했습니다: "훅은 항상 최상위에서만 호출해야 한다"와 "훅은 리액트 함수 컴포넌트나 커스텀 훅 내에서만 호출해야 한다".
- 상태 업데이트에 주의: 상태 업데이트 함수는 비동기적으로 작동합니다. 따라서, 최신 상태 값을 필요로 하는 경우에는 주의가 필요합니다. 이럴 때는 함수형 업데이트를 사용할 수 있습니다.
- 메모리 누수 방지: 사이드 이펙트를 관리하는
useEffect
훅에서는 컴포넌트가 언마운트될 때 정리(clean-up) 작업을 해야 합니다. 예를 들어, 구독(subscriptions), 타이머, 진행 중인 요청 등을 정리해야 메모리 누수를 방지할 수 있습니다. - 사용자 정의 훅의 네이밍: 커스텀 훅을 만들 때는
use
로 시작하는 이름을 사용하는 것이 좋습니다. 이는 훅의 사용법을 명확하게 하고, 리액트의 린트 규칙과도 일치합니다.
'IT 학습 > 프레임워크' 카테고리의 다른 글
Docker를 통한 문서 배포 자동화 (1) | 2024.03.05 |
---|---|
리액트 쿼리와 주스텐드 그리고 우아콘 (0) | 2024.02.08 |
React에서 스크롤 기반 헤더 최적화 하기 (0) | 2023.10.24 |
Redux란? (리덕스) (0) | 2023.08.30 |
JEST란? (0) | 2023.08.29 |