서문
현대 웹 개발에서는 다양한 기기의 화면 크기에 적응하는 반응형 인터페이스를 만드는 것이 필수적입니다.
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에 컴포넌트를 전달하는것 만으로 쉽게 화면 구성이 가능합니다.
해당 레이아웃 시스템을 잘 활용하면 위의 예시 처럼 더 복잡한 반응형 구조도 쉽게 관리 가능합니다.
결론
해당 구조를 통해 개발자는 레이아웃에 대한 가독성을 높여 유지보수를 쉽게 만들고, 각 컴포넌트는 독립적인 기능과 스타일을 유지하면서도 상위 레이아웃과 원활하게 통합됩니다. 이 글을 보신 여러분도 한번 프로젝트에 적용해 편리하게 반응형 레이아웃을 관리해 보는 것도 좋을 것 같습니다.
'개발일지' 카테고리의 다른 글
React 프로젝트 설계 2: 컴포넌트 유형 나누기 (0) | 2024.07.03 |
---|---|
Firefox에서 Grid 영역에 Fixed 불가능한 크로스 브라우징 이슈 해결 (0) | 2024.07.02 |
UX를 고려한 개발: react-beautiful-dnd 에서 버튼 클릭 방식으로의 전환 (1) | 2024.06.28 |
React 프로젝트 설계 1: 나만의 폴더 구조 도입 (0) | 2024.06.27 |
useReducer의 initialState와 props 문제 (0) | 2024.02.05 |