[#삽질] React Resize Component - 구현

jung moon chai·2025년 4월 17일
0

React 삽질기록

목록 보기
4/6
post-thumbnail

이제 리사이징 컴포넌트 기능 구현을 해보려고 한다.
합성컴포넌트로 만들었으니 서브 컴포넌트에서 데이터 핸들링을 해보도록 하자.


1. Resize의 주체

해당 라이브러리의 리사이즈 기능을 메인컴포넌트와 서브컴포넌트, 어디서 구현하는게 더 나을지 고민을 좀 했다. 일단 React-Grid-Layout의 예시 코드를 먼저 살펴보자.

const layout = [
  {
    i: "1",
    x: 0,
    y: 0,
    w: 12,
    h: 150,
    minH: 150,
  },
  {
    i: "2",
    x: 0,
    y: 1,
    w: 12,
    h: 150,
    minH: 150,
  },
]
<ReactGridLayout layout={layout} cols={12} rowHeight={1} margin={[0, 0]}>
  <div key="1">1</div>
  <div key="2">2</div>
</ReactGridLayout>

위 react grid layout 의 예시코드에 참고해서 말하자면 메인컴포넌트에서 레이아웃을 관리할때 children들이 서브컴포넌트로 구성되었는지, 안되었는지도 체크를 해야 할 것이고, 서브컴포넌트로 구성된 children들만 서로 묶어서 매핑하면서 고유키값을 생성하고, x, y축에 대한 값을 설정해줘야 한다. 그리고 서브컴포넌트로 구성되지 않은 children은 어떻게 할 것인가의 예외처리까지 필요로 하게 된다. 그 모든 처리를 메인 컴포넌트가 떠안게 된다면 합성컴포넌트를 사용하는 의미가 없을 것이라 판단됐다.


2. 메인컴포넌트 및 context

그래서 메인컴포넌트는 설계 때 만들어둔 {고유key: height} 의 객체로 상태만 들고 있고, 핸들링할 이벤트만 만들고 context로 발행해주었다.

// /src/components/common/ResizeGridLayout/context.js
import { createContext, useContext } from "react"

const defaultValue = {
  heights: {},
  registerHeight: () => null,
}

export const ResizeLayoutContext = createContext(defaultValue)

export const useResizeContext = () => useContext(ResizeLayoutContext)

// /src/components/common/ResizeGridLayout/index.jsx
const ResizeGridLayout = ({ children }) => {
  const [heights, setHeights] = useState({})

  const registerHeight = useCallback(
    (id, height) => {
      setHeights(prev => ({ ...prev, [id]: height }))
    },
    [heights],
  )

  const memoHeight = useMemo(() => ({ heights, registerHeight }), [heights, registerHeight])

  return (
    <ResizeLayoutContext.Provider value={memoHeight}>
      <LayoutStyles>{children}</LayoutStyles>
    </ResizeLayoutContext.Provider>
  )
}

3. 서브 컴포넌트

이제 서브 컴포넌트가 리사이징을 담당하고, 그 상태를 메인 컴포넌트에 저장하는 구조로 구현했다.

const Layout = ({ children, defaultHeight = 400 }) => {
  // 고유ID 생성
  const id = useId()
  // 메인컴포넌트에서 발행한 context
  const { heights, registerHeight } = useResizeContext()
  // 내 높이값을 메인컴포넌트가 가지고 있거나, 없다면 기본값
  const height = useMemo(() => heights?.[id] || defaultHeight, [heights, id])
  // react-grid-layout의 layout 세팅값
  const layout = [
    {
      i: id,
      x: 0,
      y: 0,
      w: 12,
      h: height,
      minH: 150,
    },
  ]
  // 첫 렌더시 메인컴포넌트에 나의 정보 저장
  useEffect(() => {
    registerHeight(id, defaultHeight)
  }, [])

  return (
    <Box>
      <GridLayout
        layout={layout}
        // colsize
		cols={12} 
		// height 단위
        rowHeight={1} 
		// 리사이즈 옵션
        isResizable={true} 
		// 드래그이동 옵션 제거
        isDraggable={false} 
		// y축만 리사이즈 핸들링
        resizeHandles={["s"]}
		// 리사이즈 콜백함수
        onResizeStop={(_, __, newItem) => registerHeight(id, newItem.h)}
        // 레이아웃간 여백 제거
        margin={[0, 0]}>
        <Box key={id} sx={{ transform: "none !important" }}>
          {children && cloneElement(children, { height })}
        </Box>
      </GridLayout>
      <Dividers spacing={4} />
    </Box>
  )
}

서브 컴포넌트가 리사이징의 주체를 가져가면서 메인 컴포넌트에서 직접 레이아웃을 생성했을 때와 비교했을 때의 장점을 정리했다.

  1. 메인컴포넌트가 모든 children마다 서브컴포넌트로 감싸져 있는지 확인 할 필요가 없다.
  2. 서브컴포넌트 하나가 메인컴포넌트 안에서 다른 서브컴포넌트와는 별개로 단독으로 리사이징 한다.
  3. 메인컴포넌트에 children에 서브컴포넌트로 이뤄지지 않은 컴포넌트는 리사이징에서 알아서 리사이징 기능이 부여되지 않는다.

여기서 children에 height를 props로 강제로 넘긴 이유는 단순히 height 100%만 해줘도 key를 들고 있는 Box에 따라 크기조절이 되기야 하겠지만, 서브컴포넌트 children오는 컴포넌트가 높이값을 가지고 계산해야되는 부분들이 있기 때문이다.


4. 합성컴포넌트 활용

<ResizeGrid>
  <ResizeGrid.Layout defaultHeight={300}>
	<Test />
  </ResizeGrid.Layout>
</ResizeGrid>

const Test = () => {
  return (
    <>
      <p>text</p>
      <div>text 영역을 제외한 div 영역</div>
    </>  
  )
}

Test컴포넌트에서 p태그 영역을 제외한 크기를 갖는 div가 높이 계산해야될 상황이 있기 때문이다.

Test컴포넌트가 곧 Layout 서브컴포넌트로부터 들고 있는 높이값을 props로 전달받게 되니 height의 크기에서 p태그의 높이값만 빼주면 된다.

const Test = ({ height }) => {
  const ref = useRef(null)
  const [divHeight, setDivHeight] = useState(0)
  useEffect(() => {
    if (ref.current) {
      setDivHeight(height - ref.current.offsetHeight)
    }
  }, [height])
  return (
    <>
      <p ref={ref}>text</p>
      <div style={{height: divHeight}}>text 영역을 제외한 div 영역</div>
    </>  
  )
}

5. 느낀점

일단 확장성을 느끼지 않을 수 없다. 서브컴포넌트가 단독으로 하위 컴포넌트들의 높이값을 핸들링하고 있으니 아마 고정된 높이값을 기준으로 레이아웃을 비율로 늘리거나 줄이는 방식에는 꽤 제한이 따를 것 같다. defaultHeight라는 props값을 컴포넌트를 가져다 쓰는곳에서 계산하는 방식으로 이루어져야 할 수도 있다.

그런 요구사항은 없다. 라고 말은 하지만 또 모르는거니까..

그래서 추후에는 레이아웃 생성 기능을 메인 컴포넌트로 이관하는 방향도 고려해볼 예정이다.

profile
고급개발자되기

0개의 댓글