# 반응형 레이아웃 개발 (engine-v1.1.0+) > 그누보드7 템플릿 시스템의 반응형 UI 구현 가이드 --- ## TL;DR (5초 요약) ```text 1. responsive 속성: 컴포넌트 레벨 breakpoint 오버라이드 (권장) 2. Tailwind breakpoint: sm/md/lg/xl/2xl 클래스 3. 전역 상태: _global.isMobile 등으로 조건부 렌더링 4. z-index 계층: modal(50) > dropdown(40) > sidebar(30) 5. 모바일 우선: 기본 스타일 후 md:, lg: 추가 ``` --- ## 템플릿별 지원 현황 | 템플릿 식별자 | features.responsive | 설명 | |--------------|-------------------|------| | `sirsoft-admin_basic` | 미선언 | 데스크톱 중심, Tailwind responsive 클래스는 동작 | | `sirsoft-basic` | `true` | MobileNav 포함, portable preset 지원 | > 상세 컴포넌트 목록: [sirsoft-admin_basic](templates/sirsoft-admin_basic/components.md), [sirsoft-basic](templates/sirsoft-basic/components.md) --- ## 목차 1. [개요](#개요) 2. [responsive 속성 (권장)](#1-responsive-속성-권장) 3. [Tailwind Breakpoint 활용](#2-tailwind-breakpoint-활용) 4. [전역 상태 기반 동적 스타일](#3-전역-상태-기반-동적-스타일) 5. [조건부 렌더링](#4-조건부-렌더링) 6. [모범 사례](#5-모범-사례) 7. [완전한 반응형 레이아웃 예시](#6-완전한-반응형-레이아웃-예시) 8. [임의의 breakpoint 사용](#7-임의의-breakpoint-사용) 9. [z-index 계층 관리](#8-z-index-계층-관리) --- ## 개요 그누보드7 템플릿 시스템은 **`responsive` 속성**, Tailwind CSS breakpoint, 전역 상태를 활용하여 순수 레이아웃 JSON으로 반응형 UI를 구현합니다. ### 반응형 구현 방법 비교 | 방법 | 사용 시점 | 특징 | |------|----------|------| | **`responsive` 속성** | props, children, text 변경 시 | 컴포넌트 레벨 오버라이드, 권장 | | **Tailwind breakpoint** | className만 변경 시 | CSS 레벨, 간단한 스타일 변경 | | **전역 상태** | 사용자 인터랙션 기반 | 토글 버튼 등 상태 제어 | --- ## 1. responsive 속성 (권장) `responsive` 속성을 사용하면 화면 크기에 따라 컴포넌트의 props, children, text, if, iteration을 오버라이드할 수 있습니다. ### Breakpoint 프리셋 | 프리셋 | 화면 너비 | 설명 | |--------|----------|------| | `mobile` | 0 ~ 767px | 모바일 전용 | | `tablet` | 768 ~ 1023px | 태블릿 전용 | | `desktop` | 1024px 이상 | 데스크톱 전용 | | `portable` | 0 ~ 1023px | 모바일 + 태블릿 (비데스크톱) | ### 확장 코드에서 같은 경계를 판정할 때 모듈·플러그인이 화면 폭에 따라 다른 동작을 고르는 경우(팝업 대신 페이지 전환 등), 그 판정은 레이아웃이 `responsive` 로 띄우는 안내와 **같은 값을 같은 방법으로** 읽어야 한다. ```ts // ✅ 엔진과 동일 — ResponsiveManager 가 window.innerWidth 로 breakpoint 를 정한다 const PORTABLE_MAX_WIDTH = 1023; const isPortable = window.innerWidth <= PORTABLE_MAX_WIDTH; // ❌ 경계에서 어긋난다 const isPortable = window.matchMedia('(max-width: 1023px)').matches; ``` `matchMedia` 는 CSS 픽셀 기준이라 `devicePixelRatio` 가 정수가 아닌 환경에서 `window.innerWidth` 와 1px 어긋난다. 실측(Chrome, dPR 1.0000000447)에서 `innerWidth === 1023` 인데 `matchMedia('(max-width: 1023px)')` 가 `false` 였다. 이 어긋남은 딱 경계 폭에서만 나타나므로 개발 중에는 드러나지 않고, 화면은 "페이지로 이동합니다" 라고 안내해 놓고 실제로는 팝업이 열리는 형태로 사용자에게만 보인다. 경계값 자체도 상수로 두고 SSoT(`ResponsiveManager` 의 `portable` 정의)를 주석에 밝힌다 — 프리셋 범위가 바뀌면 확장도 함께 고쳐야 한다는 신호가 코드에 남는다. ### portable 프리셋 사용 규칙 `portable` 프리셋은 `mobile`과 `tablet`을 합친 범위입니다. 다음 규칙을 준수하세요: ```text ✅ 사용: portable만 단독 사용 (mobile+tablet 동일 처리) ❌ 금지: portable과 mobile/tablet 혼용 ``` **올바른 사용 예시:** ```json { "responsive": { "portable": { "props": { "className": "flex-col" } } } } ``` **잘못된 사용 예시:** ```json { "responsive": { "portable": { "props": { "className": "hidden" } }, "tablet": { "props": { "className": "flex" } } } } ``` **mobile과 tablet을 구분해야 하는 경우:** ```json { "responsive": { "mobile": { "props": { "className": "grid-cols-1" } }, "tablet": { "props": { "className": "grid-cols-2" } } } } ``` ### 커스텀 범위 | 형식 | 설명 | 예시 | |------|------|------| | `min-max` | 범위 지정 | `0-599`, `600-899` | | `min-` | 최소값 이상 | `1200-` (1200px 이상) | | `-max` | 최대값 이하 | `-599` (599px 이하) | ### 기본 사용법 ```json { "id": "responsive_button", "type": "basic", "name": "Button", "props": { "className": "px-4 py-2", "variant": "primary", "size": "large" }, "text": "데스크톱 버튼", "responsive": { "mobile": { "props": { "className": "px-2 py-1", "size": "small" }, "text": "모바일 버튼" }, "tablet": { "props": { "size": "medium" }, "text": "태블릿 버튼" } } } ``` ### Props 머지 규칙 **얕은 머지 (Shallow Merge)**: 지정한 props만 대체, 미지정 props는 유지 ```json { "props": { "className": "base-class", "variant": "primary", "size": "large" }, "responsive": { "mobile": { "props": { "className": "mobile-class" } } } } ``` **모바일에서 적용되는 props**: - `className`: `"mobile-class"` (대체됨) - `variant`: `"primary"` (유지) - `size`: `"large"` (유지) ### children 완전 교체 ```json { "id": "card", "type": "composite", "name": "Card", "children": [ { "id": "desktop-content", "type": "basic", "name": "Div", "text": "데스크톱 전용" }, { "id": "desktop-details", "type": "basic", "name": "Div", "text": "상세 정보" } ], "responsive": { "mobile": { "children": [ { "id": "mobile-content", "type": "basic", "name": "Div", "text": "모바일 요약" } ] } } } ``` ### if 조건 오버라이드 ```json { "id": "sidebar", "type": "basic", "name": "Div", "if": "{{true}}", "responsive": { "mobile": { "if": "{{_global.sidebarOpen}}" } } } ``` ### 커스텀 범위 사용 ```json { "id": "grid", "type": "basic", "name": "Div", "props": { "className": "grid grid-cols-4" }, "responsive": { "0-599": { "props": { "className": "grid grid-cols-1" } }, "600-899": { "props": { "className": "grid grid-cols-2" } }, "900-1199": { "props": { "className": "grid grid-cols-3" } }, "1200-": { "props": { "className": "grid grid-cols-4" } } } } ``` ### 우선순위 1. **커스텀 범위 > 프리셋**: 커스텀 범위가 프리셋보다 우선 적용 2. **좁은 범위 > 넓은 범위**: 여러 범위에 매칭되면 좁은 범위가 우선 ```json { "responsive": { "mobile": { "text": "모바일 프리셋" }, "0-480": { "text": "작은 모바일" } } } ``` **400px에서**: `"작은 모바일"` (커스텀 범위 우선) **600px에서**: `"모바일 프리셋"` (프리셋만 매칭) --- ## 2. Tailwind Breakpoint 활용 ### 기본 Breakpoint | Breakpoint | 최소 너비 | |------------|----------| | `sm` | 640px | | `md` | 768px | | `lg` | 1024px | | `xl` | 1280px | | `2xl` | 1536px | ### 사용 예시 ```json { "id": "mobile_header", "type": "basic", "name": "Div", "props": { "className": "flex items-center h-16 border-b px-4 md:hidden" } } ``` **해석**: 기본(모바일)에서는 `flex items-center h-16 border-b px-4`, md(768px) 이상에서는 `hidden` ### 다중 breakpoint ```json { "props": { "className": "w-full sm:w-1/2 md:w-1/3 lg:w-1/4 xl:w-1/6" } } ``` --- ## 3. 전역 상태 기반 동적 스타일 ### 모바일 사이드바 예시 ```json { "id": "sidebar", "type": "basic", "name": "Div", "props": { "className": "{{_global.sidebarOpen ? 'translate-x-0' : '-translate-x-full'}} fixed inset-y-0 left-0 z-50 w-64 transition-transform duration-300 lg:relative lg:translate-x-0 bg-white" } } ``` ### 해석 - **모바일**: `_global.sidebarOpen` 상태에 따라 슬라이드 인/아웃 - `true`: `translate-x-0` (제자리) - `false`: `-translate-x-full` (왼쪽으로 완전히 숨김) - **데스크톱 (lg 이상)**: 항상 표시 (`lg:relative lg:translate-x-0`) - **애니메이션**: `transition-transform duration-300` (300ms) --- ## 4. 조건부 렌더링 ### 오버레이 예시 ```json { "id": "overlay", "type": "basic", "name": "Div", "if": "{{_global.sidebarOpen}}", "props": { "className": "fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden" }, "actions": [ { "event": "onClick", "type": "setState", "target": "global", "payload": { "sidebarOpen": false } } ] } ``` ### 특징 - `if` 속성으로 조건부 렌더링 (사이드바 열릴 때만 DOM에 추가) - 모바일에서만 표시 (`lg:hidden`) - 클릭 시 사이드바 닫기 --- ## 5. 모범 사례 ### DO - Tailwind breakpoint 적극 활용 - 전역 상태로 UI 상태 관리 - 조건부 렌더링으로 불필요한 요소 제거 - CSS transition으로 부드러운 애니메이션 - 레이아웃 JSON만으로 반응형 구현 ### DON'T - 플랫폼별 집합 컴포넌트 생성 금지 (MobileHeader, MobileSidebar 등) - JavaScript로 직접 DOM 조작 금지 - 인라인 스타일 과도한 사용 지양 - 미디어 쿼리 직접 작성 지양 (Tailwind 사용) --- ## 6. 완전한 반응형 레이아웃 예시 모바일 사이드바 토글이 있는 관리자 레이아웃: ```json { "components": [ { "id": "admin_layout_root", "type": "basic", "name": "Div", "props": { "className": "flex h-screen overflow-hidden" }, "children": [ { "id": "mobile_header", "type": "basic", "name": "Div", "props": { "className": "flex items-center justify-between h-16 border-b px-4 md:hidden" }, "children": [ { "id": "mobile_menu_button", "type": "basic", "name": "Button", "props": { "className": "p-2 hover:bg-gray-100 rounded-lg" }, "actions": [ { "event": "onClick", "type": "setState", "target": "global", "payload": { "sidebarOpen": "{{!_global.sidebarOpen}}" } } ], "children": [ { "type": "basic", "name": "Icon", "props": { "name": "bars" } } ] } ] }, { "id": "mobile_overlay", "type": "basic", "name": "Div", "if": "{{_global.sidebarOpen}}", "props": { "className": "fixed inset-0 bg-black bg-opacity-50 z-40 md:hidden" }, "actions": [ { "event": "onClick", "type": "setState", "target": "global", "payload": { "sidebarOpen": false } } ] }, { "id": "sidebar", "type": "basic", "name": "Div", "props": { "className": "{{_global.sidebarOpen ? 'translate-x-0' : '-translate-x-full'}} fixed inset-y-0 left-0 z-50 w-64 transition-transform duration-300 md:relative md:translate-x-0 flex flex-col border-r bg-white" }, "children": [] }, { "id": "content", "type": "basic", "name": "Div", "props": { "className": "flex-1 overflow-auto" }, "children": [] } ] } ] } ``` --- ## 7. 임의의 breakpoint 사용 Tailwind v3부터는 임의의 값을 직접 지정할 수 있습니다: ```json { "props": { "className": "hidden min-[900px]:flex max-[1200px]:grid" } } ``` --- ## 8. z-index 계층 관리 반응형 레이아웃에서 z-index 계층 구조: | z-index | 용도 | 설명 | |---------|------|------| | `z-50` | 사이드바 | 최상위 | | `z-40` | 오버레이 | 사이드바 아래, 컨텐츠 위 | | `z-auto` | 컨텐츠 | 기본값 | --- ## 관련 문서 - [컴포넌트 개발 규칙](./components.md) - [전역 상태 관리](./state-management.md) - [다크 모드 지원](./dark-mode.md) - [레이아웃 JSON 스키마](./layout-json.md)