https://taejinii.tistory.com/65
컴포넌트가 짤리는 문제 발견
해당 달력을 캘린더 라이브러리 없이 직접 구현을 했었습니다. 듀얼모니터로 작업을 했었는데 듀얼모니터는 화면이 커서 문제가 없었지만 작은 맥북화면으로 옮겨보니 캘린더의 밑부분이 잘리는 현상이 발생했습니다.
이 UI 문제점은 치명적이기 때문에 바로 수정하는 작업을 진행하였습니다.
개선을 어떻게 하였나?
// 기존코드
<div className="relative w-full bg-white" ref={ref}>
... 나머지코드 생략
{pickerType !== '' && (
<div className="absolute rounded-[10px] p-3 z-50 mt-2 w-72 bg-white shadow-chatCard caption2 animate-dropdown"
ref={datepickerRef}
>
{renderPickerByType(pickerType)}
</div>
)}
</div>
// 개선코드
<div className="relative w-full bg-white" ref={ref}>
...나머지 코드 생략
{pickerType !== '' && (
<DatePickerWrapper>{renderPickerByType(pickerType)}</DatePickerWrapper>
)}
</div>
부모컴포넌트에서 Datepicker를 감싸는 태그에 ref 객체를 연결하여 한 개의 컴포넌트에서 모든 것을 관리하려 했습니다. 하지만 renderPickerByType에 의해 DatePicker가 랜더링 될 시점에는 datepickerRef 에 값이 할당 되지않았고 DatePicker 컴포넌트가 언마운트 되는시점에 값이 할당되었습니다
따라서 ref 객체와 다이나믹렌더링 관련 로직을 관리하기위해 DatePickerWrapper 라는 컴포넌트를 따로 만들어주었습니다.
'use client';
import { ReactNode, useRef, useEffect, useState } from 'react';
export default function DatePickerWrapper({
children,
}: {
children: ReactNode;
}) {
const ref = useRef<HTMLDivElement>(null);
const [isOverFloating, setIsOverFloating] = useState(false);
const parentHeight =
(ref.current?.parentElement?.offsetHeight as number) + 10;
useEffect(() => {
const currentRectBottom = ref.current?.getBoundingClientRect()
.bottom as number;
const { innerHeight } = window;
setIsOverFloating(currentRectBottom > innerHeight);
}, []);
return (
<div
className="absolute rounded-[10px] p-3 z-50 mt-2 w-72 bg-white shadow-chatCard caption2 animate-dropdown"
ref={ref}
style={isOverFloating ? { bottom: parentHeight } : {}}
>
{children}
</div>
);
}
렌더링 되어야할 컴포넌트가 화면 어느 곳이 위치해 있는지 알아야 하기 때문에 Web API에서 제공하는 getBoundingClientRect를 사용하였습니다.
컴포넌트가 위로 가야 할지 아래로 가야 할지는 상태로 관리하였습니다.
조건은 이와 같습니다.
viewport height의 크기보다 렌더링 하고자 하는 컴포넌트의 bottom부분이 크다면 isOverFloating 상태가 true로 설정됩니다.
마지막으로 렌더링 하고하자하는 컴포넌트의 부모컴포넌트의 height를 구한 후 bottom스타일에 부모컴포넌트 height를 추가하면
부모컴포넌트를 가리지않고 위에 렌더링 되게 됩니다.
Datepicker 컴포넌트 깜빡이는 문제 발견
처음엔 Datepicker 가 mount 되면 상태를 갱신해 주기 위해 useEffect를 사용하였습니다. 물론 동작은 잘되었습니다. 하지만 DatePicker를 계속 껐다켰다를 반복하는 순간 DatePicker가 아래에서 위로빠르게 올라가는 깜빡임 현상이 발생했습니다.
물론 유저가 DatePicker를 이렇게 빠른 속도로 껐다 켰다 하지도 않을 테고 찰나의 순간이라 그냥 넘어가도 되지만... 사소한 문제점 하나도 놓치고 싶지 않았습니다.
useLayoutEffect를 사용하여 해결
해당 문제가 발생하는 원인은 렌더링이 된 이후에 DOM을 조작하여 스타일을 바꾸기 때문에 생기는 문제라고 판단하였습니다.
// DatePickerWrapper.tsx
useLayoutEffect(() => {
const currentRectBottom = ref.current?.getBoundingClientRect()
.bottom as number;
const { innerHeight } = window;
setIsOverFloating(currentRectBottom > innerHeight);
}, []);
위와 같이 useEffect를 useLayoutEffect로 수정하여 문제를 해결하였습니다.
useEffect는 랜더링 이후에 작업을 수행합니다. 컴포넌트가 render와 paint 된 후 실행이 되며 비동기적으로 실행됩니다. paint가 된 이후에 실행되기 때문에 useEffect 내부에 dom에 영향을 주는 코드가 있을 경우 잠깐 깜빡이는 현상을 보이게 됩니다.
반면 useLayoutEffect의 경우 동기적으로 실행되며 paint가 되기 전 즉, 랜더링전에 실행되기 때문에 내부에서 dom을 조작하더라도 깜빡 임문제가 일어나지 않습니다.
useLayoutEffect는 동기적으로 실행되기 때문에 내부의 코드가 모두 실행된 이후 paint 작업이 진행됩니다. 따라서 내부 로직이 복잡할 경우 사용자는 레이아웃을 보기까지 오래 걸릴 수 있다는 단점이 있기 때문에 기본적으로는 useEffect를 사용하는 것이 좋습니다.
useLayoutEffect는 위와 같이 화면이 깜빡거리고 내부에서 상태와 dom을 조작하는 특수한 경우에 사용하는 것이 좋습니다.
결과물!
개선하며 느낀 점.
해당 기능을 구현하면서 DatePicker 뿐만 아니라 Dropdown, Select 등등 여러 가지 커스텀 컴포넌트에서도 사용할 수 있도록 커스텀훅을 만들어 리팩토링 가능할 것 같다는 느낌이 들었다. 만약 커스텀훅을 만들어 여기저기 사용하게 되도록 만든다면 해당 작업내용을 블로그에 올리도록 하겠습니다.
https://github.com/puppypawpaw/pawpaw-client/tree/main/components/ui/DatePicker
전체코드는 위의 깃허브에서 확인가능합니다.
'problem-solving' 카테고리의 다른 글
React-query 서버데이터 커스텀 (useQuery with select) (1) | 2023.05.14 |
---|---|
백엔드없이 사이드프로젝트 로그인,회원가입 기능 구현 feat.json-server-auth (0) | 2023.04.01 |
사이드 프로젝트 React-query 도입 계기 및 사용 후기 (0) | 2023.03.21 |
백엔드없이 사이드 프로젝트 + 배포 with Vercel,json-server,glitch (2) | 2023.03.15 |