Files
Gnuboard7/docs/frontend/responsive-layout.md
T
HeuJung 8328b1db77 docs(core,extensions): 확장 개발자 문서 9세트 집필과 생성기 정합 보강
S1 파일럿 3종(sirsoft-board · sirsoft-gdpr · sirsoft-admin_basic)에 이어
결제·본인인증 동형군 6종의 AGENTS.md · README.md · docs/ 5문서를 집필했다.
확장을 고치려는 쪽이 매번 src/ 를 훑어 구조를 재발견하지 않도록, 설계 의도와
확장점(발행·구독 훅)·수정 시 동반 의무·금지 패턴을 코드 근거로 서술했다.

생성기(ExtensionDocScaffolder)에서 표를 무의미하게 만들던 세 결함을 함께 고쳤다.
getLayoutExtensions 기본 구현이 돌려주는 절대경로가 파일 목록과 중복돼 로컬
머신 경로가 커밋 문서에 실리던 문제, getNotificationDefinitions 를 'key'/'event'
로 읽어 모든 행이 '-' 로 찍히던 문제, getSettingsLayout 절대경로가 정규화 없이
노출되던 문제다. 셋 다 예외를 남기지 않고 표만 조용히 망가뜨리므로
ExtensionDocContractTest 에 각각의 되돌림 red 를 확인한 단언을 두었다.
템플릿의 extensions/{id}/ 는 모듈·플러그인의 발행과 반대 방향(오버라이드)이라
별도 블록(template-overrides)으로 분리했다.

sirsoft-admin_basic 의 컴포넌트·핸들러·레이아웃 문서를 코어 docs/ 에서 그 템플릿
소유로 이관하고, 남은 참조 6축을 재는 가드를 추가했다. 이관 후 남은 옛 경로는
오류가 아니라 헛걸음으로만 나타나 드러나지 않는다. 의 상대 링크가
한 단계 얕아 공유 docs/ 대신 를 가리키던 문제도 함께 고쳤다.

README 상단의 확장명 이미지 배지를 평문 H1 로 바꿨다( 지시 2026-08-31).
루트 README.md · README.ko.md 도 같은 기준을 적용했다. 정보 배지는 유지한다.

sirsoft-gdpr: 회원탈퇴로 자동 철회된 동의가 관리자 동의 이력 화면의 출처 필터로
걸러지지 않던 문제를 고쳤다. Repository 가 'withdraw' 리터럴을 직접 UPDATE 에
싣는데 그 값이 ConsentSource enum 에 없어, 화면 필터 옵션·라벨 어느 쪽에도
도달하지 못했다. 어휘를 enum 단일 출처로 모으고 ko·en·ja 라벨과 필터 옵션을
함께 채웠으며, 어휘 대조 테스트의 모집단에 Repository 를 편입했다.
2026-08-31 15:57:44 +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 컨텐츠 기본값

관련 문서