Portal Foundation Sidebar 컴포넌트 가이드라인
1. 목적과 결론
Portal Foundation의 Sidebar는 Ant Design Layout.Sider와 Menu를 기본 구조로 사용하고, Portal Foundation은 공통 레이아웃 규칙과 테마 token만 제공해야 한다. 소비 포털은 Sidebar 내부 구조를 다시 구현하거나 개별 CSS로 위치를 보정하지 않는다.
현재 공통 기준값은 다음과 같다.
| 항목 | 공통값 | 소유 계층 |
|---|---|---|
| 펼친 Sidebar 폭 | 248px | PortalShell |
| 접힌 Sidebar 폭 | 64px | PortalShell |
| 상단바 높이 | 64px | PortalShell |
| 메뉴 행 높이 | 42px | Ant Design Menu token |
| 메뉴 아이콘 크기 | 18px | Ant Design Menu token + 아이콘 컴포넌트 |
| 아이콘과 라벨 간격 | 10px | Ant Design Menu token |
| 메뉴 배경 | transparent | Foundation Ant Design theme |
핵심 원칙: 펼침과 접힘에서 아이콘의 기준축을 유지한다. 접힘 상태에서는 라벨만 숨기고, 아이콘은 같은 왼쪽 기준선에 남긴다.
2. 레이아웃 계층
Sidebar를 먼저 좌우 영역으로 분리하고, 오른쪽 Workspace 안에서 Header와 Content를 수직으로 분리한다. Header를 먼저 전체 화면에 배치한 뒤 좌우를 나누는 구조는 Sidebar와 Header의 경계가 섞이고 rail 접힘이 불안정해질 수 있으므로 사용하지 않는다.
PortalShell
├── Sidebar rail
│ ├── Brand
│ ├── Collapse control
│ ├── Optional sidebar header
│ ├── Navigation Menu
│ └── Optional footer / role switch
└── Workspace
├── Top navigation / Header
└── Main content권장 React 구조는 다음과 같다.
<Layout className="pf-shell">
<Sider
className="pf-shell-sidebar"
width={248}
collapsedWidth={64}
collapsed={collapsed}
trigger={null}
>
<div className="pf-shell-sidebar__brand">
<span className="pf-shell-sidebar__brand-mark" aria-hidden="true">
{brandMark}
</span>
<strong className="pf-shell-sidebar__brand-name">
{brandName}
</strong>
</div>
<Button
type="text"
className="pf-shell-sidebar__collapse"
aria-label={collapsed ? "메뉴 펼치기" : "메뉴 접기"}
aria-expanded={!collapsed}
onClick={onToggleCollapse}
>
<PanelLeftIcon aria-hidden="true" />
</Button>
{sidebarHeader ? (
<div className="pf-shell-sidebar__header">{sidebarHeader}</div>
) : null}
<nav className="pf-shell-sidebar__nav" aria-label="서비스 메뉴">
<Menu
mode="inline"
inlineCollapsed={collapsed}
items={items}
selectedKeys={selectedKeys}
defaultOpenKeys={defaultOpenKeys}
onClick={onMenuClick}
/>
</nav>
{sidebarFooter || roleSwitch ? (
<div className="pf-shell-sidebar__footer">
{roleSwitch}
{sidebarFooter}
</div>
) : null}
</Sider>
<Layout className="pf-shell-workspace">
<Header className="pf-shell-topnav">{topNavigation}</Header>
<Content className="pf-shell-main">{children}</Content>
</Layout>
</Layout>3. 메뉴 데이터와 이벤트
메뉴 데이터는 Portal Foundation의 navigation model에서 Ant Design MenuProps["items"]로 변환한다. 소비 포털은 Ant Design의 내부 DOM을 직접 조작하지 않고, 변환 계층에 navigation callback을 전달한다.
const items: MenuProps["items"] = navigationItems.map((item) => ({
key: item.id,
icon: item.icon,
label: item.label,
children: item.children?.map((child) => ({
key: child.id,
icon: child.icon,
label: child.label,
})),
}));
<Menu
mode="inline"
inlineCollapsed={collapsed}
items={items}
selectedKeys={[activeNavigationId]}
onClick={({ key }) => {
const item = findNavigationItem(key);
item?.onSelect?.();
onClose?.();
}}
/>href가 없는 메뉴도 반드시 onClick으로 처리한다. 메뉴 항목이 링크인지 상태 변경용 action인지에 따라 렌더링을 분기하되, 클릭 가능한 항목을 단순 문자열로 반환하지 않는다.
if (!item.href) {
return (
<button type="button" onClick={() => item.onSelect?.()}>
{item.label}
</button>
);
}실제 Foundation adapter에서는 이 동작을 공통으로 처리해야 하며, 소비 포털에서 별도의 메뉴 클릭 보정 코드를 추가하지 않는다.
4. Ant Design theme token
Menu의 색상과 기본 크기는 소비 포털 CSS가 아니라 buildPortalAntdTheme에서 관리한다. Sidebar가 rail 배경을 소유하므로 Menu 표면은 투명해야 한다.
export const portalMenuTokens = {
itemHeight: 42,
iconSize: 18,
collapsedIconSize: 18,
iconMarginInlineEnd: 10,
itemBg: "transparent",
subMenuItemBg: "transparent",
itemHoverBg: "transparent",
itemActiveBg: "transparent",
itemSelectedBg: "transparent",
};<ConfigProvider
theme={{
components: {
Menu: portalMenuTokens,
},
}}
>
{children}
</ConfigProvider>PortalConfigProvider는 PortalThemeProvider 아래에 배치한다. 그래야 Portal theme mode가 Ant Design algorithm과 Menu token에 함께 반영된다.
<PortalThemeProvider storage={themeStorage}>
<PortalConfigProvider mode={theme.mode}>
{children}
</PortalConfigProvider>
</PortalThemeProvider>5. 아이콘 위치 규칙
펼침과 접힘에서 아이콘의 X축 기준을 동일하게 유지한다. 접힌 rail의 폭이 64px이고 아이콘이 18px이면 좌우 여백은 다음과 같이 계산된다.
(64px - 18px) / 2 = 23px따라서 메뉴 아이콘의 공통 시작 위치는 23px로 둔다.
펼침: [23px 여백][18px 아이콘][10px 간격][라벨]
접힘: [23px 여백][18px 아이콘]이 규칙은 아이콘이 펼침/접힘 전환 시 좌우로 이동하지 않도록 한다. Ant Design inlineCollapsed의 기본 중앙 정렬만 그대로 사용하면 펼침 상태의 icon column과 접힘 상태의 rail 중앙이 달라질 수 있으므로, Foundation Shell의 공통 정렬 규칙으로 명시한다.
권장 CSS는 다음과 같다.
.pf-shell-sidebar__nav {
min-width: 0;
}
.pf-shell-sidebar__nav .ant-menu {
width: 100%;
border-inline-end: 0;
background: transparent;
}
.pf-shell-sidebar__nav .ant-menu-item,
.pf-shell-sidebar__nav .ant-menu-submenu-title {
display: flex;
align-items: center;
min-height: 42px;
height: 42px;
margin-inline: 0;
padding-inline-start: 23px;
padding-inline-end: 12px;
border-radius: 0;
}
.pf-shell-sidebar__nav .ant-menu-item .ant-menu-item-icon,
.pf-shell-sidebar__nav .ant-menu-submenu-title .ant-menu-item-icon {
display: inline-flex;
flex: 0 0 18px;
width: 18px;
height: 18px;
align-items: center;
justify-content: center;
margin-inline-end: 10px;
font-size: 18px;
}
.pf-shell-sidebar__nav .ant-menu-item .ant-menu-title-content,
.pf-shell-sidebar__nav .ant-menu-submenu-title .ant-menu-title-content {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pf-shell-sidebar__nav .ant-menu-inline-collapsed {
width: 64px;
}
.pf-shell-sidebar__nav .ant-menu-inline-collapsed > .ant-menu-item,
.pf-shell-sidebar__nav .ant-menu-inline-collapsed > .ant-menu-submenu > .ant-menu-submenu-title {
justify-content: flex-start;
padding-inline: 23px;
}
.pf-shell-sidebar__nav .ant-menu-inline-collapsed .ant-menu-item-icon,
.pf-shell-sidebar__nav .ant-menu-inline-collapsed .ant-menu-submenu-title .ant-menu-item-icon {
margin-inline-end: 0;
}Ant Design token과 Shell CSS가 같은 값을 선언하더라도 역할은 다르다. Token은 Menu의 component behavior를 관리하고, Shell CSS는 공통 icon axis와 rail geometry를 관리한다. 소비 포털은 이 두 계층을 다시 덮어쓰지 않는다.
5.1 접힘 rail의 말풍선(툴팁)
접힘 상태의 항목 이름은 아이콘 위 말풍선으로만 읽을 수 있다. Ant Design 기본 right 정렬은 말풍선을 메뉴 항목 오른쪽 모서리에 붙이고 자체 오프셋(marginXXS + 화살표 반폭)까지 더하므로, 항목 폭이 아이콘보다 넓은 rail에서는 말풍선이 아이콘에서 어색하게 멀어진다. 셸은 이 오프셋을 왼쪽으로 당겨 아이콘 바로 옆에 붙이며, 값은 PortalShell의 PORTAL_SHELL_RAIL_TOOLTIP_ALIGN가 소유한다.
<Menu inlineCollapsed={collapsed} tooltip={{ placement: "right", align: PORTAL_SHELL_RAIL_TOOLTIP_ALIGN }} />소비 포털은 말풍선 위치를 다시 덮어쓰지 않는다.
6. Brand와 접기 버튼
Brand는 Sidebar의 상단 영역에 속한다. Header로 이동시키지 않는다. Sidebar를 접을 때 brand name은 숨기되, brand mark는 유지할 수 있다. 접기 버튼은 Sidebar에 속하며, 접힌 상태에서 아이콘 axis를 방해하지 않도록 별도의 absolute control로 배치한다.
.pf-shell-sidebar {
position: relative;
min-height: 100vh;
}
.pf-shell-sidebar__brand {
display: flex;
min-height: 64px;
align-items: center;
gap: 10px;
padding: 0 23px;
}
.pf-shell-sidebar__brand-mark {
display: inline-flex;
flex: 0 0 18px;
width: 18px;
height: 18px;
align-items: center;
justify-content: center;
}
.pf-shell-sidebar__collapse {
position: absolute;
top: 18px;
right: 10px;
display: grid;
width: 28px;
height: 28px;
place-items: center;
border: 0;
background: transparent;
}
.pf-shell-sidebar__collapse .pf-icon {
width: 18px;
height: 18px;
}
.pf-shell-sidebar.is-collapsed .pf-shell-sidebar__brand {
justify-content: flex-start;
padding-inline: 23px;
}
.pf-shell-sidebar.is-collapsed .pf-shell-sidebar__brand-name {
display: none;
}접기 버튼과 brand mark가 같은 위치를 차지해야 하는 경우에는 hover 시에만 접기 버튼을 노출할 수 있다. 이 동작은 접근성을 위해 :focus-within도 함께 지원해야 한다.
7. 상태별 스타일 규칙
| 상태 | 표시 규칙 | 금지 사항 |
|---|---|---|
| 펼침 | 아이콘은 23px 축, 라벨은 아이콘 오른쪽 | 메뉴별 임의 padding-left 금지 |
| 접힘 | 라벨 숨김, 아이콘은 동일한 23px 축 | justify-content: center로 axis 변경 금지 |
| 선택됨 | Foundation accent와 선택 배경 규칙 사용 | 소비 포털별 선택색 재정의 금지 |
| Hover | 투명 배경을 유지하고 텍스트 대비만 조정 | 흰색 패널 배경 생성 금지 |
| 그룹 | Ant Design submenu indentation 사용 | 그룹별 수동 margin 누적 금지 |
| 모바일 | Sidebar를 drawer/overlay로 표시 | desktop rail 폭을 모바일에 강제 금지 |
8. 배경과 테마 규칙
Sidebar와 Menu 사이에 별도의 흰색 블록이 생기지 않아야 한다. 다음 세 영역의 배경 책임을 분리한다.
| 영역 | 배경 소유자 |
|---|---|
| Sidebar rail | PortalSidebar / Shell surface token |
| Menu root와 item | Ant Design Menu token의 transparent |
| 선택·hover 상태 | Foundation accent 및 상태 token |
| Main content | PortalMain / layout background token |
다음과 같은 소비 포털 CSS는 사용하지 않는다.
/* 금지: Menu가 Sidebar와 다른 표면을 만든다. */
.ant-menu {
background: #fff;
}
/* 금지: Ant Design의 공통 icon axis를 소비 포털에서 덮어쓴다. */
.ant-menu-item {
padding-left: 12px !important;
}9. 접근성 및 동작 체크리스트
Sidebar 구현은 다음 항목을 모두 만족해야 한다.
- Sidebar에는
aria-label="서비스 메뉴"가 있는nav가 존재한다. - 접기 버튼은 현재 상태를
aria-expanded로 전달한다. - 접기 버튼의 label은 상태에 따라
메뉴 접기와메뉴 펼치기로 바뀐다. href가 없는 메뉴도 키보드로 focus하고 Enter로 실행할 수 있다.- 선택된 메뉴는
selectedKeys와 애플리케이션 상태가 일치한다. - 모바일 overlay를 닫을 때 Escape와 backdrop click이 동작한다.
- 아이콘은 장식 요소일 경우
aria-hidden="true"를 사용한다. - 메뉴 label은 아이콘만으로 대체하지 않는다.
- 접힘 상태에서도 icon button의 hit area가 충분히 유지된다.
10. 검증 방법
공통 Foundation 변경 후에는 다음 순서로 검증한다.
cd /tmp/portal-foundation
pnpm test --run
pnpm build소비 포털에서는 다음을 확인한다.
cd /tmp/UiPlatform
pnpm run check
pnpm run test
pnpm run build
pnpm install --frozen-lockfile --prefer-offline브라우저에서 확인할 시나리오는 다음과 같다.
- 펼침 상태에서 첫 번째 메뉴 아이콘의 왼쪽 시작 위치를 측정한다.
- Sidebar를 접고 같은 메뉴 아이콘의 왼쪽 시작 위치를 측정한다.
- 두 측정값이
23px기준으로 일치하는지 확인한다. - 메뉴 label만 사라지고 아이콘의 위치와 크기는 유지되는지 확인한다.
- 선택·hover 시 Sidebar와 Menu 사이에 흰색 면이 생기지 않는지 확인한다.
- 접기 버튼이 메뉴 클릭 영역을 가리지 않는지 확인한다.
- 메뉴 항목을 클릭했을 때 URL 또는 애플리케이션 상태가 변경되는지 확인한다.
- light/dark mode 전환 후 rail과 Menu의 배경이 함께 변경되는지 확인한다.
11. 소비 포털 이식 규칙
새 소비 포털로 이식할 때는 PortalShell, PortalConfigProvider, PortalThemeProvider를 먼저 연결한다. 이후 navigation model을 adapter에 전달하고, 소비 포털에는 업무별 메뉴 데이터와 페이지 콘텐츠만 둔다.
소비 포털에서 허용되는 설정은 다음과 같다.
- 제품명과 brand mark
- 메뉴 데이터와 active key
- 포털별 메뉴 action
- 사용자 프로필과 account action
- Sidebar header/footer 슬롯
- 포털 accent와 허용된 theme option
소비 포털에서 허용되지 않는 설정은 다음과 같다.
- Sidebar 폭 변경
- 접힘 폭 변경
- 공통 icon size 변경
ant-menu-item의 위치 보정- Menu 배경색 직접 지정
!important를 이용한 Ant Design 구조 override- Ant Design 내부 DOM 구조를 전제로 한 selector
12. 변경 시 영향 범위
공통값을 변경할 때는 다음 파일과 패키지를 함께 확인한다.
| 대상 | 확인 내용 |
|---|---|
packages/ui-react/src/PortalShell.tsx | Sider 폭, collapse state, markup hierarchy |
packages/ui-react/src/providers/antdTheme.ts | Menu token과 mode 연동 |
packages/ui-react/styles/styles.css | 공통 icon axis와 Shell geometry |
packages/ui-react/src/providers/antdTheme.test.ts | token contract |
| Foundation tarball | 빌드 후 패키지 내용과 integrity |
UiPlatform vendor/ | 소비 포털에 실제 반영된 패키지 |
UiPlatform pnpm-lock.yaml | tarball integrity |