Files
Gnuboard7/docs/frontend/responsive-layout.md
T
HeuJung 6c63536f81 docs(core,extensions): 확장 20개 개발자 문서 완비와 문서 소유 이관
번들 확장 20개 전부에 AGENTS.md · README.md · docs/ 를 채우고, 코어가 들고
있던 확장 소유 문서 두 갈래를 그 확장으로 옮긴다. 번들 템플릿의 컴포넌트·
핸들러·레이아웃 상세와 확장이 구독하는 활동 로그 훅 목록이 그 대상이며,
코어에는 총계와 링크만 남아 확장이 기능을 늘릴 때 코어 문서를 고쳐야 하던
역방향 의존이 사라진다.

전수 완비를 확인하고 강제를 조인다 — 문서 동반 룰을 대상 목록 없는 error 로
승격하고, 검사 스크립트가 문서 미보유를 실패로 올리며, 미채움 마커 baseline 을
0 으로 기록한다. 한쪽만 조이면 "새 확장이 문서 없이 들어와도 초록" 인 상태가
남는데 그 결과는 이상 0건과 구분되지 않는다.

집필 과정에서 드러난 생성기 결함 셋을 함께 고친다. 스케줄 주기 열이 계약 키를
읽지 않아 모든 확장에서 '-' 였고, 네임스페이스를 붙인 핸들러 등록 키가 수집에서
통째로 빠졌으며, README 골격이 폐기된 히어로 배지를 계속 찍어내고 있었다.
셋 다 산출물이 아니라 원천이 틀린 것이라, 가드의 모집단에 생성기 출력 자체를
넣어 다음 확장이 같은 상태로 태어나는 경로를 막는다.
2026-08-31 22:57:36 +09:00

13 KiB

반응형 레이아웃 개발 (engine-v1.1.0+)

그누보드7 템플릿 시스템의 반응형 UI 구현 가이드


TL;DR (5초 요약)

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, sirsoft-basic


목차

  1. 개요
  2. responsive 속성 (권장)
  3. Tailwind Breakpoint 활용
  4. 전역 상태 기반 동적 스타일
  5. 조건부 렌더링
  6. 모범 사례
  7. 완전한 반응형 레이아웃 예시
  8. 임의의 breakpoint 사용
  9. 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 로 띄우는 안내와 같은 값을 같은 방법으로 읽어야 한다.

// ✅ 엔진과 동일 — 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을 합친 범위입니다. 다음 규칙을 준수하세요:

✅ 사용: portable만 단독 사용 (mobile+tablet 동일 처리)
❌ 금지: portable과 mobile/tablet 혼용

올바른 사용 예시:

{
  "responsive": {
    "portable": {
      "props": { "className": "flex-col" }
    }
  }
}

잘못된 사용 예시:

{
  "responsive": {
    "portable": { "props": { "className": "hidden" } },
    "tablet": { "props": { "className": "flex" } }
  }
}

mobile과 tablet을 구분해야 하는 경우:

{
  "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 이하)

기본 사용법

{
  "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는 유지

{
  "props": {
    "className": "base-class",
    "variant": "primary",
    "size": "large"
  },
  "responsive": {
    "mobile": {
      "props": {
        "className": "mobile-class"
      }
    }
  }
}

모바일에서 적용되는 props:

  • className: "mobile-class" (대체됨)
  • variant: "primary" (유지)
  • size: "large" (유지)

children 완전 교체

{
  "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 조건 오버라이드

{
  "id": "sidebar",
  "type": "basic",
  "name": "Div",
  "if": "{{true}}",
  "responsive": {
    "mobile": {
      "if": "{{_global.sidebarOpen}}"
    }
  }
}

커스텀 범위 사용

{
  "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. 좁은 범위 > 넓은 범위: 여러 범위에 매칭되면 좁은 범위가 우선
{
  "responsive": {
    "mobile": { "text": "모바일 프리셋" },
    "0-480": { "text": "작은 모바일" }
  }
}

400px에서: "작은 모바일" (커스텀 범위 우선) 600px에서: "모바일 프리셋" (프리셋만 매칭)


2. Tailwind Breakpoint 활용

기본 Breakpoint

Breakpoint 최소 너비
sm 640px
md 768px
lg 1024px
xl 1280px
2xl 1536px

사용 예시

{
  "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

{
  "props": {
    "className": "w-full sm:w-1/2 md:w-1/3 lg:w-1/4 xl:w-1/6"
  }
}

3. 전역 상태 기반 동적 스타일

모바일 사이드바 예시

{
  "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. 조건부 렌더링

오버레이 예시

{
  "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. 완전한 반응형 레이아웃 예시

모바일 사이드바 토글이 있는 관리자 레이아웃:

{
  "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부터는 임의의 값을 직접 지정할 수 있습니다:

{
  "props": {
    "className": "hidden min-[900px]:flex max-[1200px]:grid"
  }
}

8. z-index 계층 관리

반응형 레이아웃에서 z-index 계층 구조:

z-index 용도 설명
z-50 사이드바 최상위
z-40 오버레이 사이드바 아래, 컨텐츠 위
z-auto 컨텐츠 기본값

관련 문서