Trouble_Shooting_4

데브월드 트러블 슈팅 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>
  );
}