[#삽질] React Resize Component - 설계

jung moon chai·2025년 4월 17일
0

React 삽질기록

목록 보기
3/6
post-thumbnail

프로젝트를 진행하던 중 추가요건이 하나 추가됐다.
레이아웃의 높이를 조절하게 해달라는 요건이었다.
마침 위젯기능을 구현해야해서 react-grid-layout을 사용중이었는데,
어차피 해당 라이브러리에 Resizing기능이 있으니 이걸 래핑해서 쓰기로 결정했다.


1. 컴포넌트 설계

일단 어떠한 상태들을 들고 있어야할지 정의를 내렸다.

  1. 레이아웃에 렌더링할 컨텐츠들의 높이
  2. 레이아웃이 1개가 아닌데 배열로 들고 있을까? 객체로 들고있을까?

처음엔 당연히 각 레이아웃의 분기가 있을테니 배열로 만들려 했다.
그런데 문제는 단순히 하나의 컴포넌트로 래핑해서는 각 레이아웃의 분기마다에 고유키값을 생성하는데 문제가 생겼다.
레이아웃별로 react-grid-layout에서 노출할 레이아웃의 고유키값과 항상 매핑이 되었어야 했기 때문이다. 단순히 index로 하기엔 레이아웃의 추가 여부등에서 명확하게 맞아 떨어지지 않거나, 안맞아 떨어질 경우에 싱크를 맞춰줘야하는 추가 리소스가 필요할 것이라고 생각했다.

그래서 높이값들을 레이아웃이 그려질때 고유키값을 생성해서 그 고유키값을 key값으로 지정해서 state에 저장했다.

const ResizeGridLayout = ({ children }) => {
  const [heights, setHeights] = useState({})

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

  return (
    <Box>
      {children}
    </Box>
  )
}
export default ResizeGridLayout

여기서 또 하나의 고민이 생겼다. children을 배열 형태로 단순히 나누어 관리할 경우, 레이아웃을 정의하는 과정에서 개발자가 의도치 않은 형태로 사용할 가능성이 있었기 때문이다.

예를 들어,

<ResizeGridLayout>
	<Box>레이아웃1</Box>
    <Box>레이아웃1 안에 들어가야할 컨텐츠</Box>
    <Box>레이아웃2</Box>
</ResizeGridLayout>

위와 같은 형태로 사용될 경우, 명확하게 내가 어디부터 어디까지 사이즈 조절을 할 것이다. 라고 분기가 되어있지 않기때문이다.

이런 상황을 배재하고자 children들을 명확히 분기 해줄 컴포넌트가 필요해졌다.


2. 합성 컴포넌트(Compound Component)

컴포넌트를 사용하는 방식에 제한을 두고 명확한 구조를 제공하기 위해 합성 컴포넌트 패턴을 도입했다. 내가 이해한 합성 컴포넌트의 핵심은 다음과 같다.

  • 메인 컴포넌트와 서브 컴포넌트로 나뉘며, 서브 컴포넌트는 메인 컴포넌트의 상태와 로직을 공유하고 관리한다.
  • 사용하는 쪽에서는 서브 컴포넌트로 명확하게 조합하여 사용한다.

그래서 내가 리사이즈 할 레이아웃 영역입니다. 를 ResizeGridLayout에 알려줄 합성 컴포넌트를 만들었다.

const ResizeGridLayout = ({ children }) => {
  const [heights, setHeights] = useState({})

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

  return (
    <Box>
      {children}
    </Box>
  )
}

const Layout = ({ children }) => {
	return <>{children}</>
}

ResizeGridLayout.Layout  = Layout
export default ResizeGridLayout

그런데 프로퍼티 할당하는 부분에서 약간의 고민이 생겼다. 우리는 현재 프로젝트가 타입스크립트가 아니라서 저렇게 할당해도 문제가 생기는 부분은 없을 것이다.
근데 타입스크립트의 경우엔 각 컴포넌트들을 생성하면 이미 컴포넌트에 대한 타이핑이 지정되기 때문에 저런식으로 서브컴포넌트를 붙일 경우 타입에러가 발생한다.

그래서 프로퍼티 객체를 복사해서 넣는 방식으로 교체했다.(혹시 나중에 타입스크립트로 교체 될 수도 있기때문.)

const ResizeGrid = Object.assign(ResizeGridLayout, {
  Layout,
})
export default ResizeGrid

이제 대략적인 컴포넌트의 구조는 잡힌것 같다.

<ResizeGrid>
    <ResizeGrid.Layout>
    	<Box>컨텐츠1</Box>
    </ResizeGrid.Layout>
	<ResizeGrid.Layout>
    	<Box>컨텐츠1</Box>
	</ResizeGrid.Layout>
</ResizeGrid>

이렇게 사용하는 곳에서 Layout분기를 서브컴포넌트로 해주면 컴포넌트를 사용하는 개발자입장에서도 명확히 내가 어느 컨텐츠들을 레이아웃으로 분기하겠다. 명확하기 때문이다.


3. 느낀점

라이브러리에서 제공하거나 다른 사람들이 만든 컴포넌트를 처음 사용할 때면, 가끔씩 "이렇게까지 복잡하게 써야 하나?" 라는 생각이 들곤 했다.

그런데 이번에 합성 컴포넌트 패턴을 직접 도입하면서, 그런 복잡한 구조나 엄격한 사용 방식 뒤에는 항상 그럴 만한 이유가 있다는 걸 깨달았다. 사용자 입장에선 보이지 않는 내부 로직과, 우리가 쉽게 놓칠 수 있는 다양한 상황에 대한 고민이 담겨 있었던 것이다.

물론 지금 내가 설계한 컴포넌트가 가장 좋은 방법인지는 아직 확신이 없다. 앞으로 더 다양한 상황을 만나거나, 다른 사람들이 더 깔끔하게 해결한 코드를 접하게 되면 내 생각도 다시 바뀔지 모른다.

더 좋은 설계 방법도 있을 거고, 시간이 지나면 생각이 달라질 수도 있다.
하지만 지금의 고민과 삽질도 결국 다 의미가 있을 거라 믿고, 이제 대략적인 설계 방향은 잡았으니, 다음 글에서는 이 구조를 바탕으로 본격적으로 리사이징 기능을 구현해보려고 한다.

profile
고급개발자되기

0개의 댓글