데브월드 트러블 슈팅 4
ResizeObserver API 활용하기
문제 상황
- 메인 페이지에 존재하는 사이드 바의 확장, 축소, 존재 유무와 같은 상태 변화와 뷰포트 크기에 따른 반응형 디자인 요구사항 충족
- 미디어 쿼리의 한계와 특정 DOM에 대한 복잡한 상태 관리
문제해결을 위한 아이디어
- ResizeObserver API를 활용해서 특정 요소를 지정하고 관찰하기
ResizeObserver ?
- intersection observer와 사용방법이 거의 비슷하다.
- 특정요소를 지정하고 관찰
- 콜백함수에 내가 하려는 일을 정의
문제 해결
- useResizeObserver.ts
import { RefObject, useLayoutEffect, useReducer } from "react";
interface Options<T extends HTMLElement = HTMLElement> {
ref: RefObject<T>;
}
interface ResponsiveClassName {
sm: string;
md: string;
lg: string;
xl: string;
"2xl": string;
}
interface Action {
type: keyof typeof responsiveClassNames;
payload: (typeof responsiveClassNames)[keyof typeof responsiveClassNames];
}
interface State {
className: string;
}
const responsiveClassNames: ResponsiveClassName = {
"2xl": "grid-cols-5 gap-6",
//1536
xl: "grid-cols-4 gap-[23px]",
// 1280
lg: "grid-cols-3 gap-4",
//1024
md: "grid-cols-2 gap-6",
//786
sm: "grid-cols-1 gap-6",
//640
};
function getContainerWidth_returnClassName(width: number): {
type: keyof ResponsiveClassName;
payload: (typeof responsiveClassNames)[keyof typeof responsiveClassNames];
} {
if (width >= 1620) {
return { type: "2xl", payload: responsiveClassNames["2xl"] };
} else if (width >= 1240) {
return { type: "xl", payload: responsiveClassNames["xl"] };
} else if (width >= 1024) {
return { type: "lg", payload: responsiveClassNames["lg"] };
} else if (width >= 730) {
return { type: "md", payload: responsiveClassNames["md"] };
} else {
return { type: "sm", payload: responsiveClassNames["sm"] };
}
}
const reducer: React.Reducer<State, Action> = (state, action) => {
if (!action) {
throw new Error("디스패치 함수에 액션이 정의되지 않았습니다??");
}
const { className } = state;
const { type, payload } = action;
switch (type) {
case "2xl":
return { className: payload };
case "xl":
return { className: payload };
case "lg":
return { className: payload };
case "md":
return { className: payload };
case "sm":
return { className: payload };
default:
return { className };
}
};
export function useResizeObserver({ ref }: Options) {
const [state, dispatch] = useReducer(reducer, { className: "" });
useLayoutEffect(() => {
if (!ref.current) return;
if (typeof window === "undefined" || !("ResizeObserver" in window)) return;
const observer = new ResizeObserver(([entry]) => {
const { type, payload } = getContainerWidth_returnClassName(
entry.borderBoxSize[0].inlineSize
);
if (state.className !== payload) {
dispatch({ payload, type });
}
});
observer.observe(ref.current);
return () => {
observer.disconnect();
};
}, [state.className]);
return state;
}
- 컴포넌트
export default function PostFeed() {
const ref = useRef(null);
const { className } = useResizeObserver({ ref });
return (
<div className={clsx(`grid py-5 px-6 ${className}`)} ref={ref}>
{MOCK_DATA.map((post, idx) => (
<PostCard
key={idx}
title={post.title}
subTitle={post.subTitle}
userName={post.userName}
date={post.date}
/>
))}
</div>
);
}