본문 바로가기

개발일지

Compound Component 패턴으로 반응형 레이아웃 관리하기

서문

현대 웹 개발에서는 다양한 기기의 화면 크기에 적응하는 반응형 인터페이스를 만드는 것이 필수적입니다.

React와 컴파운드 컴포넌트 패턴을 활용하면 유연하고 유지 관리가 쉬운 레이아웃을 구현할 수 있습니다.

이 글에서는 제가 프로젝트를 진행하면 구상한 컴파운드 컴포넌트 패턴을 통한 반응형 레이아웃 관리에 대해 예시를 들며 설명합니다.

컴파운드 컴포넌트 패턴이란?

//Example.tsx
const ExampleContext = createContext()
 
function Example(props) {
  const [open, toggle] = useState(false)
 
  return (
    <ExampletContext.Provider value={{ open, toggle }}>
      {props.children}
    </ExampleContext.Provider>
  )
}
 
function Toggle() {
  const { open, toggle } = useContext(ExampleContext)
 
  return (
    <div onClick={() => toggle(!open)}>
      <Icon />
    </div>
  )
}

function List({ children }) {
  const { open } = React.useContext(ExampleContext)
  return open && <ul>{ children }</ul>
}

function Item({ children }) {
  return <li>{ children }</li>
}
 
Example.Toggle = Toggle
Example.List = List
Example.Item = Item


// App.tsx
import React from "react"
import { FlyOut } from "./FlyOut"
 
export default function ExampleMenu() {
  return (
    <Example>
      <Example.Toggle />
      <Example.List>
        <Example.Item>Edit</Example.Item>
        <Example.Item>Delete</Example.Item>
      </Example.List>
    </Example>
  )
}

 

여러 개의 작은 컴포넌트들을 조립해 하나의 큰 컴포넌트를 만드는 패턴으로, 위 예시처럼 큰 컴포넌트의 attribute에 작은 컴포넌트 할당해 구현하는 패턴입니다. 일반적으로 레이아웃 보다는 연관된 컴포넌트들의 응집도를 높이고, 관련 요소끼리 편하게 상태를 관리하기 위해 사용하는 패턴입니다.

반응형 레이아웃 관리하기

import { ReactNode } from 'react';
import styled from 'styled-components';

import { media } from '@/theme/theme';

//각각 데스크탑, 데스크탑 + 태블릿 뷰에서만 children을 출력하는 커스텀 컴포넌트
import { Desktop, Expanded } from './Responsive';

interface LayoutProps {
  children?: ReactNode;
}

function Layout({ children }: LayoutProps) {
  return <LayoutContainer>{children}</LayoutContainer>;
}

function Left({ children }: LayoutProps) {
  return (
    <Expanded>
      <LeftContainer>{children}</LeftContainer>
    </Expanded>
  );
}

function Right({ children }: LayoutProps) {
  return (
    <Desktop>
      <RightContainer>{children}</RightContainer>
    </Desktop>
  );
}

function Main({ children }: LayoutProps) {
  return <MainContainer>{children}</MainContainer>;
}

function Center({ children }: LayoutProps) {
  return <CenterContainer>{children}</CenterContainer>;
}

const LayoutContainer = styled.div`
  display: grid;
  position: relative;

  width: fit-content;
  min-height: 100vh;
  margin: 0 auto;

  @media ${media.mobile} {
    width: 100%;
    grid-template:
      'header' 61px
      '   .  ' minmax(calc(100vh - 65px), auto)
      'footer' 0/100%;
  }

  @media ${media.tablet} {
    grid-template:
      'header header' 90px
      '   .     .   ' minmax(calc(100vh - 90px), auto)
      'footer footer' 0 / 280px minmax(400px, 700px);
  }

  @media ${media.desktop} {
    grid-template:
      'header header header' 90px
      '   .     .      .   ' minmax(calc(100vh - 90px), auto)
      'footer footer footer' 0 / 280px minmax(400px, 700px) 280px;
  }
`;

const LeftContainer = styled.aside`
  position: fixed;

  width: 280px;
  padding: 10px;

  @media ${media.expanded} {
    grid-column: 1/2;
    grid-row: 2/3;
  }
`;

const RightContainer = styled.aside`
  position: fixed;
  padding: 10px;

  @media ${media.desktop} {
    grid-column: 3/4;
    grid-row: 2/3;
  }
`;

const MainContainer = styled.div`
  padding: 10px;
  
  @media ${media.mobile} {
    grid-row: 2/3;
  }

  @media ${media.expanded} {
    grid-column: 2/3;
    grid-row: 2/3;
  }
`;

const CenterContainer = styled.div`
  position: relative;

  max-width: 800px;
  margin: 0 auto;
  padding: 10px;

  grid-column: 1/4;
  grid-row: 2/3;
`;

Layout.Left = Left;
Layout.Right = Right;
Layout.Main = Main;
Layout.Center = Center;

export default Layout;

 

위의 코드 예제에서는 css grid와 Layout, Left, Right, Main, Center 등의 컴포넌트를 사용하여 복잡한 레이아웃을 구성하고 있습니다.

각각의 컴포넌트는 특정한 미디어 쿼리에 반응하여 적절한 스타일을 적용받습니다.

예를 들어, LeftContainer와 RightContainer는 각각 Expended뷰 및 Desktop뷰에서 화면 좌우에 위치가 고정되며,

MainContainer는 화면 크기에 따라 화면 중앙 또는 중앙 + 우측에 컨텐츠를 고정시키며

CenterContainer는 컨텐츠를 항상 화면 중앙에 고정시킵니다.

실제 페이지 구현

 

export default function JJHTopicPage() {
  const { title } = useQuesryString();
  return (
    <Layout>
      <Header />
      
      <Layout.Left>
        <JJHSideMenu />
      </Layout.Left>
      
      <Layout.Main>
        <Title>정주행 - {title}</Title>
        <ToggleButton />
        <JJHTopicList />
      </Layout.Main>
      
      <Layout.Right>
        <TopicAnchor />
      </Layout.Right>
    </Layout>
  );
}

 

예를 들어 위 컴포넌트는 이 레이아웃 시스템을 사용하여 정주행콘텐츠 페이지를 구성하는데

좌측(Layout.Left)에는 사이드 메뉴

중앙(Layout.Main)에는 제목, 토글 버튼, 주제 목록을 표시하고

우측(Layout.Right)에는 주제 앵커를 표시한다는 사실을 컴포넌트 내부에서 직관적이고 쉽게 알 수 있습니다.

 

또한 개발 시에는 하위의 개별 컴포넌트에서는 레이아웃 위치에 신경 쓸 필요 없이 Layout 컴포넌트의 Attribute 컴포넌트들의 children에 컴포넌트를 전달하는것 만으로 쉽게 화면 구성이 가능합니다.

 

 

 

해당 레이아웃 시스템을 잘 활용하면 위의 예시 처럼 더 복잡한 반응형 구조도 쉽게 관리 가능합니다.

결론

해당 구조를 통해 개발자는 레이아웃에 대한 가독성을 높여 유지보수를 쉽게  만들고, 각 컴포넌트는 독립적인 기능과 스타일을 유지하면서도 상위 레이아웃과 원활하게 통합됩니다. 이 글을 보신 여러분도 한번 프로젝트에 적용해 편리하게 반응형 레이아웃을 관리해 보는 것도 좋을 것 같습니다.