일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- Web
- 회고
- CustomHook
- Webworker
- 자바스크립트
- 리팩토링
- radixui
- virtaullist
- 프론트엔드
- MFA
- sharedworker
- CRA
- 리액트
- server component
- 이것저것
- provider 패턴
- 티스토리챌린지
- vite
- react
- frontend
- context.api
- 아키텍처
- 웹워커
- 클린코드
- TypeScript
- MicroFrontEnd
- 합성 컴포넌트
- 오블완
- JavaScript
- 에세이
- Today
- Total
Lighthouse of FE biginner
[React] 사례로 살펴보는 커스텀 훅 - 2 본문
들어가며
저번 포스팅에서 사례로 살펴보는 커스텀 훅이라는 글로 여러 케이스의 커스텀 훅을 소개드렸습니다.
이번 포스팅에서는 소개하지 못했던 사례인 특정 훅을 사용하면서도 여러 컴포넌트에서 사용 가능해 추상화가 가능할 경우를 실제로 구현했던 과정에서 왜 이런 설계를 했는지 고민했던 과정을 남겨보겠습니다.
과정
문제 제기
먼저 이번 사례는 프로젝트 개선 과정에서 도출된 사례입니다. 진행중인 프로젝트의 대시보드에서는 툴팁을 직접 구현해서 사용하고 있습니다.
툴팁의 위치를 계산하기 위해 마우스 커서의 위치를 반환하고 해당 x, y축의 좌표값을 컴포넌트의 top, left로 사용하고 있습니다.
이 과정에서 툴팁 컴포넌트가 Viewport의 끝단에 마주칠경우 툴팁이 화면에 보이지 않는 영역에 위치해버려 스크롤이 생기는 케이스가 발생했습니다.
이는 브라우저 리랜더링을 발생시키고 화면을 리플로우 시켜 성능에도 좋지 못하며 사용자에게는 버그로 인식될 가능성이 농후합니다. (실제로 버그기도 하구요)
살펴 보기
먼저 위치 좌표를 계산해 반환하는 훅을 살펴봤습니다.
export const useCursorPosition = (props?: UseCursorPositionProps): UseCursorPosition => {
const [cursorPosition, setCursorPosition] = React.useState<CursorPosition>(null);
const onMouseMove = React.useCallback(
(e: React.MouseEvent<Element>) => {
const { clientX: x, clientY: y } = e;
const scrollTop = document?.fullscreenElement?.scrollTop || 0;
setCursorPosition({
x: x + (props?.gap ?? CURSOR_GAP),
y: y + (props?.gap ?? CURSOR_GAP) + scrollTop,
});
},
[props?.gap]
);
const onMouseLeave = React.useCallback(() => {
setCursorPosition(null);
}, []);
return { cursorPosition, onMouseMove, onMouseLeave };
};
커서의 위치와 마우스 핸들링 이벤트를 반환하는 훅 입니다. 위치 좌표는 Gap이라는 약간의 조미료를 첨가해 위치를 수정하고 있습니다.
개선 과정
그럼 개선 과정을 살펴봅시다.
논리적으로 생각해보면 커서가 현재 Viewport의 하단과 우측에 위치한다면 컴포넌트의 위치가 계산된 CursorPosition에서 컴포넌트의 width, height 값 만큼 뺀 값을 넣어주면 됩니다.
이때 상단과 좌측을 고려하지 않은 이유는 현재 대시보드 레이아웃과 툴팁 컴포넌트의 특징 상 툴팁이 커서의 좌측에 위치하지 않으며 툴팁이 커서의 아래에 위치하고 있기 때문입니다.
위 과정을 구현해봅시다.
먼저 툴팁의 가장 최상단 엘리먼트에 바인딩 할 ref가 필요합니다. 그리고 ref의 width, height와 window 객체의 innerWidth, innerHeight를 가지고 실제 위치를 계산할 로직도 필요합니다.
현재 개선해야 할 컴포넌트는 20개가 넘습니다. 20개가 넘는 컴포넌트에서 모두 ref와 위 로직을 포함하고 있다면 코드 중복이 발생하고 관리 포인트가 늘어나게 되어 유지보수에 어려움이 생길거라고 판단됩니다.
그럼 원래 사용했던 커스텀 훅을 수정할까 라는 생각을 할 수 있겠지만, 이는 또 다음과 같은 제약사항이 있습니다.
1. useCursorPosition에 특정 비즈니스 로직이 섞이게 된다면 기존 훅의 기능을 잃어버린다.
2. SOLID 원칙 중 Open Close Principle인 "확장에는 열려있고 수정에는 닫혀있어야 한다" 에 위배된다.
3. 만약 툴팁이 아닌 다른 컴포넌트에서 커서의 위치가 필요한 경우에는 해당 비즈니스 로직으로 인해 예기치 못한 버그가 발생할 수 있다.
그럼 어떻게 하는 것이 좋을까요?
저는 useCursorPosition을 래핑하는 커스텀 훅을 제작하기로 했습니다.
새로운 훅 설계
새로운 훅을 설계해봅시다.
먼저 ref 객체는 훅에서 선언하고 반환되는 객체의 프로퍼티로 내려줄 예정입니다. 이때 훅 입장에서 ref로 바인딩 될 엘리먼트가 어떤 태그인지 모르기 때문에 ref의 타입을 제네릭으로 받아서 타입 바인딩을 할 수 있도록 만들어줍니다.
훅은 useCursorPosition 훅을 래핑하고 새롭게 계산된 position과 마우스 이벤트를 함께 반환합니다.
계산되어 반환할 값은 랜더링 최적화를 위해 useMemo로 메모이제이션 합니다.
import { RefObject, useMemo, useRef } from 'react';
import { UseCursorPosition, useCursorPosition } from '@/hooks/useCursorPosition.ts';
type Coordinate = {
x: number;
y: number;
};
type UseCalculateCoordinateResult = {
ref: RefObject<Element | null>;
coordinate: Coordinate | null;
} & Omit<UseCursorPosition, 'cursorPosition'>;
const CURSOR_GAP = 10;
export const useCalculateTooltipCoordinate = <
Element extends HTMLElement
>(): UseCalculateCoordinateResult => {
const componentRef = useRef<Element | null>(null);
const { cursorPosition, onMouseMove, onMouseLeave } = useCursorPosition({ gap: CURSOR_GAP });
const coordinate = useMemo(() => {
const tooltip = componentRef.current;
const coordinate = cursorPosition;
const { innerWidth, innerHeight } = window;
if (tooltip && coordinate) {
const { width: tooltipWidth, height: tooltipHeight } = tooltip.getBoundingClientRect();
if (tooltipWidth + coordinate.x > innerWidth) {
coordinate.x -= tooltipWidth + CURSOR_GAP;
}
if (tooltipHeight + coordinate.y > innerHeight) {
coordinate.y -= tooltipHeight + CURSOR_GAP;
}
}
return coordinate;
}, [cursorPosition, componentRef]);
return {
ref: componentRef,
onMouseMove,
onMouseLeave,
coordinate,
};
};
이렇게 제작된 커스텀 훅을 위젯 컴포넌트에서 사용합니다. 마우스 이벤트를 특정 엘리먼트에 걸어주고 (아래 코드에는 존재하지 않습니다.) 계산된 좌표를 툴팁 컴포넌트에 바인딩 합니다.
const { ref, onMouseMove, onMouseLeave, coordinate } = useCalculateTooltipCoordinate<HTMLDivElement>();
const Tooltip = () => (
<div
ref={ref as RefObject<HTMLDivElement>}
style={{ left: x, top: y, visibility: isOpen ? 'visible' : 'hidden' }}
>
{...중략}
</div>
);
결과
결과를 살펴봅시다. 개선 사항이 잘 반영된 것을 확인할 수 있습니다.
위 과정을 진행하고 새로운 커스텀 훅을 사용함으로써 우리는 여러 이점을 누릴 수 있습니다.
1. 본래의 커스텀 훅에 특정 비즈니스 로직을 섞지 않아서 역할을 지켰다.
2. 특정 과정을 위한 비즈니스 로직을 묶어 관리 포인트를 일원화 했고 유지보수에 용이하게 만들었다.
3. 컴포넌트에서 비즈니스 로직을 캡슐화
마치며
이번 포스팅을 통해서 커스텀 훅을 사용해야 하는 사례를 자세하게 살펴봤습니다.
커스텀 훅을 잘 사용하면 프로젝트에서 엄청난 이점을 누릴 수 있습니다.
논리적으로 잘 설계된 프로젝트는 오랫동안 잘 유지될 수 있습니다.
읽어봐주셔서 감사합니다.
'[WEB] 프론트엔드' 카테고리의 다른 글
Hello Zustand! (0) | 2024.08.07 |
---|---|
[React] Radix UI를 활용해 MultiSelect 컴포넌트 구현하기 (1) | 2024.08.01 |
[React] 사례로 살펴보는 커스텀 훅 (1) | 2024.07.26 |
ResizeObserver 활용하기 (0) | 2024.07.24 |
라이브러리 배포 및 패킹 프로세스 자동화 (0) | 2024.07.18 |