이제 리사이징 컴포넌트 기능 구현을 해보려고 한다.
합성컴포넌트로 만들었으니 서브 컴포넌트에서 데이터 핸들링을 해보도록 하자.
해당 라이브러리의 리사이즈 기능을 메인컴포넌트와 서브컴포넌트, 어디서 구현하는게 더 나을지 고민을 좀 했다. 일단 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은 어떻게 할 것인가의 예외처리까지 필요로 하게 된다. 그 모든 처리를 메인 컴포넌트가 떠안게 된다면 합성컴포넌트를 사용하는 의미가 없을 것이라 판단됐다.
그래서 메인컴포넌트는 설계 때 만들어둔 {고유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>
)
}
이제 서브 컴포넌트가 리사이징을 담당하고, 그 상태를 메인 컴포넌트에 저장하는 구조로 구현했다.
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>
)
}
서브 컴포넌트가 리사이징의 주체를 가져가면서 메인 컴포넌트에서 직접 레이아웃을 생성했을 때와 비교했을 때의 장점을 정리했다.
- 메인컴포넌트가 모든 children마다 서브컴포넌트로 감싸져 있는지 확인 할 필요가 없다.
- 서브컴포넌트 하나가 메인컴포넌트 안에서 다른 서브컴포넌트와는 별개로 단독으로 리사이징 한다.
- 메인컴포넌트에 children에 서브컴포넌트로 이뤄지지 않은 컴포넌트는 리사이징에서 알아서 리사이징 기능이 부여되지 않는다.
여기서 children에 height를 props로 강제로 넘긴 이유는 단순히 height 100%만 해줘도 key를 들고 있는 Box에 따라 크기조절이 되기야 하겠지만, 서브컴포넌트 children오는 컴포넌트가 높이값을 가지고 계산해야되는 부분들이 있기 때문이다.
<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>
</>
)
}
일단 확장성을 느끼지 않을 수 없다. 서브컴포넌트가 단독으로 하위 컴포넌트들의 높이값을 핸들링하고 있으니 아마 고정된 높이값을 기준으로 레이아웃을 비율로 늘리거나 줄이는 방식에는 꽤 제한이 따를 것 같다. defaultHeight라는 props값을 컴포넌트를 가져다 쓰는곳에서 계산하는 방식으로 이루어져야 할 수도 있다.
그런 요구사항은 없다.
라고 말은 하지만 또 모르는거니까..
그래서 추후에는 레이아웃 생성 기능을 메인 컴포넌트로 이관하는 방향도 고려해볼 예정이다.