26 KiB
26 KiB
반응형 그리드 시스템 아키텍처
최종 업데이트: 2026-01-30
1. 개요
1.1 현재 문제
컴포넌트 위치/크기가 픽셀 단위로 고정되어 반응형 미지원
// 현재 DB 저장 방식 (screen_layouts_v2.layout_data)
{
"position": { "x": 1753, "y": 88 },
"size": { "width": 158, "height": 40 }
}
| 화면 크기 | 결과 |
|---|---|
| 1920px (디자인 기준) | 정상 |
| 1280px (노트북) | 오른쪽 버튼 잘림 |
| 768px (태블릿) | 레이아웃 완전히 깨짐 |
| 375px (모바일) | 사용 불가 |
1.2 목표
| 목표 | 설명 |
|---|---|
| PC 대응 | 1280px ~ 1920px |
| 태블릿 대응 | 768px ~ 1024px |
| 모바일 대응 | 320px ~ 767px |
1.3 해결 방향
현재: 픽셀 좌표 → position: absolute → 고정 레이아웃
변경: 그리드 셀 번호 → CSS Grid + ResizeObserver → 반응형 레이아웃
2. 현재 시스템 분석
2.1 데이터 현황
총 레이아웃: 1,250개
총 컴포넌트: 5,236개
회사 수: 14개
테이블 크기: 약 3MB
2.2 컴포넌트 타입별 분포
| 컴포넌트 | 수량 | shadcn 사용 |
|---|---|---|
| v2-input | 1,914 | ✅ @/components/ui/input |
| v2-button-primary | 1,549 | ✅ @/components/ui/button |
| v2-table-search-widget | 355 | ✅ shadcn 기반 |
| v2-select | 327 | ✅ @/components/ui/select |
| v2-table-list | 285 | ✅ @/components/ui/table |
| v2-media | 181 | ✅ shadcn 기반 |
| v2-date | 132 | ✅ @/components/ui/calendar |
| v2-split-panel-layout | 131 | ✅ shadcn 기반 (반응형 필요) |
| v2-tabs-widget | 75 | ✅ shadcn 기반 |
| 기타 | 287 | ✅ shadcn 기반 |
| 합계 | 5,236 | 전부 shadcn |
2.3 현재 렌더링 방식
// frontend/lib/registry/layouts/flexbox/FlexboxLayout.tsx (라인 234-248)
{components.map((child) => (
<div
style={{
position: "absolute", // 절대 위치
left: child.position.x, // 픽셀 고정
top: child.position.y, // 픽셀 고정
width: child.size.width, // 픽셀 고정
height: child.size.height, // 픽셀 고정
}}
>
{renderer.renderChild(child)}
</div>
))}
2.4 핵심 발견
✅ 이미 있는 것:
- 12컬럼 그리드 설정 (gridSettings.columns: 12)
- 그리드 스냅 기능 (snapToGrid: true)
- shadcn/ui 기반 컴포넌트 (전체)
❌ 없는 것:
- 그리드 셀 번호 저장 (현재 픽셀 저장)
- 반응형 브레이크포인트 설정
- CSS Grid 기반 렌더링
- 분할 패널 반응형 처리
2.5 레이아웃 시스템 구조
현재 시스템에는 두 가지 레벨의 레이아웃이 존재합니다:
2.5.1 화면 레이아웃 (screen_layouts_v2)
화면 전체의 컴포넌트 배치를 담당합니다.
// DB 구조
{
"version": "2.0",
"components": [
{ "id": "comp_1", "position": { "x": 100, "y": 50 }, ... },
{ "id": "comp_2", "position": { "x": 500, "y": 50 }, ... },
{ "id": "GridLayout_1", "position": { "x": 100, "y": 200 }, ... }
]
}
현재: absolute 포지션으로 컴포넌트 배치 → 반응형 불가
2.5.2 컴포넌트 레이아웃 (GridLayout, FlexboxLayout 등)
개별 레이아웃 컴포넌트 내부의 zone 배치를 담당합니다.
| 컴포넌트 | 위치 | 내부 구조 | CSS Grid 사용 |
|---|---|---|---|
GridLayout |
layouts/grid/ |
zones 배열 | ✅ 이미 사용 |
FlexboxLayout |
layouts/flexbox/ |
zones 배열 | ❌ absolute |
SplitLayout |
layouts/split/ |
left/right | ❌ flex |
TabsLayout |
layouts/ |
tabs 배열 | ❌ 탭 구조 |
CardLayout |
layouts/card-layout/ |
zones 배열 | ❌ flex |
AccordionLayout |
layouts/accordion/ |
items 배열 | ❌ 아코디언 |
2.5.3 구조 다이어그램
┌─────────────────────────────────────────────────────────────────┐
│ screen_layouts_v2 (화면 전체) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 현재: absolute 포지션 → 반응형 불가 │ │
│ │ 변경: ResponsiveGridLayout (CSS Grid) → 반응형 가능 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌─────────────────────────────┐ │
│ │ v2-button │ │ v2-input │ │ GridLayout (컴포넌트) │ │
│ │ (shadcn) │ │ (shadcn) │ │ ┌─────────┬─────────────┐ │ │
│ └──────────┘ └──────────┘ │ │ zone1 │ zone2 │ │ │
│ │ │ (이미 │ (이미 │ │ │
│ │ │ CSS Grid│ CSS Grid) │ │ │
│ │ └─────────┴─────────────┘ │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
2.6 기존 레이아웃 컴포넌트 호환성
2.6.1 GridLayout (기존 커스텀 그리드)
// frontend/lib/registry/layouts/grid/GridLayout.tsx
// 이미 CSS Grid를 사용하고 있음!
const gridStyle: React.CSSProperties = {
display: "grid",
gridTemplateRows: `repeat(${gridConfig.rows}, 1fr)`,
gridTemplateColumns: `repeat(${gridConfig.columns}, 1fr)`,
gap: `${gridConfig.gap || 16}px`,
};
호환성: ✅ 완전 호환
- GridLayout은 화면 내 하나의 컴포넌트로 취급됨
- ResponsiveGridLayout이 GridLayout의 위치만 관리
- GridLayout 내부는 기존 방식 그대로 동작
2.6.2 FlexboxLayout
// frontend/lib/registry/layouts/flexbox/FlexboxLayout.tsx
// zone 내부에서 컴포넌트를 absolute로 배치
{zoneChildren.map((child) => (
<div style={{
position: "absolute",
left: child.position?.x || 0,
top: child.position?.y || 0,
}}>
{renderer.renderChild(child)}
</div>
))}
호환성: ✅ 호환 (내부는 기존 방식 유지)
- FlexboxLayout 컴포넌트 자체의 위치는 ResponsiveGridLayout이 관리
- 내부 zone의 컴포넌트 배치는 기존 absolute 방식 유지
2.6.3 SplitPanelLayout (분할 패널)
호환성: ⚠️ 별도 수정 필요
- 외부 위치: ResponsiveGridLayout이 관리 ✅
- 내부 반응형: 별도 수정 필요 (모바일에서 상하 분할)
2.6.4 호환성 요약
| 컴포넌트 | 외부 배치 | 내부 동작 | 추가 수정 |
|---|---|---|---|
| v2-button, v2-input 등 | ✅ 반응형 | ✅ shadcn 그대로 | ❌ 불필요 |
| GridLayout | ✅ 반응형 | ✅ CSS Grid 그대로 | ❌ 불필요 |
| FlexboxLayout | ✅ 반응형 | ⚠️ absolute 유지 | ❌ 불필요 |
| SplitPanelLayout | ✅ 반응형 | ❌ 좌우 고정 | ⚠️ 내부 반응형 추가 |
| TabsLayout | ✅ 반응형 | ✅ 탭 그대로 | ❌ 불필요 |
2.7 동작 방식 비교
변경 전
화면 로드
↓
screen_layouts_v2에서 components 조회
↓
각 컴포넌트를 position.x, position.y로 absolute 배치
↓
GridLayout 컴포넌트도 absolute로 배치됨
↓
GridLayout 내부는 CSS Grid로 zone 배치
↓
결과: 화면 크기 변해도 모든 컴포넌트 위치 고정
변경 후
화면 로드
↓
screen_layouts_v2에서 components 조회
↓
layoutMode === "grid" 확인
↓
ResponsiveGridLayout으로 렌더링 (CSS Grid)
↓
각 컴포넌트를 grid.col, grid.colSpan으로 배치
↓
화면 크기 감지 (ResizeObserver)
↓
breakpoint에 따라 responsive.sm/md/lg 적용
↓
GridLayout 컴포넌트도 반응형으로 배치됨
↓
GridLayout 내부는 기존 CSS Grid로 zone 배치 (변경 없음)
↓
결과: 화면 크기에 따라 컴포넌트 재배치
3. 기술 결정
3.1 왜 Tailwind 동적 클래스가 아닌 CSS Grid + Inline Style인가?
Tailwind 동적 클래스의 한계:
// ❌ 이건 안 됨 - Tailwind가 빌드 타임에 인식 못함
className={`col-start-${col} md:col-start-${mdCol}`}
// ✅ 이것만 됨 - 정적 클래스
className="col-start-1 md:col-start-3"
Tailwind는 빌드 타임에 클래스를 스캔하므로, 런타임에 동적으로 생성되는 클래스는 인식하지 못합니다.
해결책: CSS Grid + Inline Style + ResizeObserver:
// ✅ 올바른 방법
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(12, 1fr)',
}}>
<div style={{
gridColumn: `${col} / span ${colSpan}`, // 동적 값 가능
}}>
{component}
</div>
</div>
3.2 역할 분담
| 영역 | 기술 | 설명 |
|---|---|---|
| UI 컴포넌트 | shadcn/ui | 버튼, 인풋, 테이블 등 (이미 적용됨) |
| 레이아웃 배치 | CSS Grid + Inline Style | 컴포넌트 위치, 크기, 반응형 |
| 반응형 감지 | ResizeObserver | 화면 크기 감지 및 브레이크포인트 변경 |
┌─────────────────────────────────────────────────────────┐
│ ResponsiveGridLayout (CSS Grid) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ shadcn │ │ shadcn │ │ shadcn │ │
│ │ Button │ │ Input │ │ Select │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ ┌─────────────────────────────────────────────┐ │
│ │ shadcn Table │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
4. 데이터 구조 변경
4.1 현재 구조 (V2)
{
"version": "2.0",
"components": [{
"id": "comp_xxx",
"url": "@/lib/registry/components/v2-button-primary",
"position": { "x": 1753, "y": 88, "z": 1 },
"size": { "width": 158, "height": 40 },
"overrides": { ... }
}]
}
4.2 변경 후 구조 (V2 + 그리드)
{
"version": "2.0",
"layoutMode": "grid",
"components": [{
"id": "comp_xxx",
"url": "@/lib/registry/components/v2-button-primary",
"position": { "x": 1753, "y": 88, "z": 1 },
"size": { "width": 158, "height": 40 },
"grid": {
"col": 11,
"row": 2,
"colSpan": 1,
"rowSpan": 1
},
"responsive": {
"sm": { "col": 1, "colSpan": 12 },
"md": { "col": 7, "colSpan": 6 },
"lg": { "col": 11, "colSpan": 1 }
},
"overrides": { ... }
}],
"gridSettings": {
"columns": 12,
"rowHeight": 80,
"gap": 16
}
}
4.3 필드 설명
| 필드 | 타입 | 설명 |
|---|---|---|
layoutMode |
string | "grid" (반응형 그리드 사용) |
grid.col |
number | 시작 컬럼 (1-12) |
grid.row |
number | 시작 행 (1부터) |
grid.colSpan |
number | 차지하는 컬럼 수 |
grid.rowSpan |
number | 차지하는 행 수 |
responsive.sm |
object | 모바일 (< 768px) 설정 |
responsive.md |
object | 태블릿 (768px ~ 1024px) 설정 |
responsive.lg |
object | 데스크톱 (> 1024px) 설정 |
4.4 호환성
position,size필드는 유지 (디자인 모드 + 폴백용)layoutMode가 없으면 기존 방식(absolute) 사용- 마이그레이션 후에도 기존 화면 정상 동작
5. 구현 상세
5.1 그리드 변환 유틸리티
// frontend/lib/utils/gridConverter.ts
const DESIGN_WIDTH = 1920;
const COLUMNS = 12;
const COLUMN_WIDTH = DESIGN_WIDTH / COLUMNS; // 160px
const ROW_HEIGHT = 80;
/**
* 픽셀 좌표를 그리드 셀 번호로 변환
*/
export function pixelToGrid(
position: { x: number; y: number },
size: { width: number; height: number }
): GridPosition {
return {
col: Math.max(1, Math.min(12, Math.round(position.x / COLUMN_WIDTH) + 1)),
row: Math.max(1, Math.round(position.y / ROW_HEIGHT) + 1),
colSpan: Math.max(1, Math.round(size.width / COLUMN_WIDTH)),
rowSpan: Math.max(1, Math.round(size.height / ROW_HEIGHT)),
};
}
/**
* 기본 반응형 설정 생성
*/
export function getDefaultResponsive(grid: GridPosition): ResponsiveConfig {
return {
sm: { col: 1, colSpan: 12 }, // 모바일: 전체 너비
md: {
col: Math.max(1, Math.round(grid.col / 2)),
colSpan: Math.min(grid.colSpan * 2, 12)
}, // 태블릿: 2배 확장
lg: { col: grid.col, colSpan: grid.colSpan }, // 데스크톱: 원본
};
}
5.2 반응형 그리드 레이아웃 컴포넌트
// frontend/lib/registry/layouts/responsive-grid/ResponsiveGridLayout.tsx
import React, { useRef, useState, useEffect } from "react";
type Breakpoint = "sm" | "md" | "lg";
interface ResponsiveGridLayoutProps {
layout: LayoutData;
isDesignMode: boolean;
renderer: ComponentRenderer;
}
export function ResponsiveGridLayout({
layout,
isDesignMode,
renderer,
}: ResponsiveGridLayoutProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [breakpoint, setBreakpoint] = useState<Breakpoint>("lg");
// 화면 크기 감지
useEffect(() => {
if (!containerRef.current) return;
const observer = new ResizeObserver((entries) => {
const width = entries[0].contentRect.width;
if (width < 768) setBreakpoint("sm");
else if (width < 1024) setBreakpoint("md");
else setBreakpoint("lg");
});
observer.observe(containerRef.current);
return () => observer.disconnect();
}, []);
const gridSettings = layout.gridSettings || { columns: 12, rowHeight: 80, gap: 16 };
return (
<div
ref={containerRef}
style={{
display: "grid",
gridTemplateColumns: `repeat(${gridSettings.columns}, 1fr)`,
gridAutoRows: `${gridSettings.rowHeight}px`,
gap: `${gridSettings.gap}px`,
minHeight: isDesignMode ? "600px" : "auto",
}}
>
{layout.components
.sort((a, b) => (a.grid?.row || 0) - (b.grid?.row || 0))
.map((component) => {
// 반응형 설정 가져오기
const gridConfig = component.responsive?.[breakpoint] || component.grid;
const { col, colSpan } = gridConfig;
const rowSpan = component.grid?.rowSpan || 1;
return (
<div
key={component.id}
style={{
gridColumn: `${col} / span ${colSpan}`,
gridRow: `span ${rowSpan}`,
}}
>
{renderer.renderChild(component)}
</div>
);
})}
</div>
);
}
5.3 브레이크포인트 훅
// frontend/lib/registry/layouts/responsive-grid/useBreakpoint.ts
import { useState, useEffect, RefObject } from "react";
type Breakpoint = "sm" | "md" | "lg";
export function useBreakpoint(containerRef: RefObject<HTMLElement>): Breakpoint {
const [breakpoint, setBreakpoint] = useState<Breakpoint>("lg");
useEffect(() => {
if (!containerRef.current) return;
const observer = new ResizeObserver((entries) => {
const width = entries[0].contentRect.width;
if (width < 768) setBreakpoint("sm");
else if (width < 1024) setBreakpoint("md");
else setBreakpoint("lg");
});
observer.observe(containerRef.current);
return () => observer.disconnect();
}, [containerRef]);
return breakpoint;
}
5.4 분할 패널 반응형 수정
// frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx
// 추가할 코드
const containerRef = useRef<HTMLDivElement>(null);
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
if (!containerRef.current) return;
const observer = new ResizeObserver((entries) => {
const width = entries[0].contentRect.width;
setIsMobile(width < 768);
});
observer.observe(containerRef.current);
return () => observer.disconnect();
}, []);
// 렌더링 부분 수정
return (
<div
ref={containerRef}
className={cn(
"flex h-full",
isMobile ? "flex-col" : "flex-row" // 모바일: 상하, 데스크톱: 좌우
)}
>
<div style={{
width: isMobile ? "100%" : `${leftWidth}%`,
minHeight: isMobile ? "300px" : "auto"
}}>
{/* 좌측/상단 패널 */}
</div>
<div style={{
width: isMobile ? "100%" : `${100 - leftWidth}%`,
minHeight: isMobile ? "300px" : "auto"
}}>
{/* 우측/하단 패널 */}
</div>
</div>
);
6. 렌더링 분기 처리
// frontend/lib/registry/DynamicComponentRenderer.tsx
function renderLayout(layout: LayoutData) {
// layoutMode에 따라 분기
if (layout.layoutMode === "grid") {
return <ResponsiveGridLayout layout={layout} renderer={this} />;
}
// 기존 방식 (폴백)
return <FlexboxLayout layout={layout} renderer={this} />;
}
7. 마이그레이션
7.1 백업
-- 마이그레이션 전 백업
CREATE TABLE screen_layouts_v2_backup_20260130 AS
SELECT * FROM screen_layouts_v2;
7.2 마이그레이션 스크립트
-- grid, responsive 필드 추가
UPDATE screen_layouts_v2
SET layout_data = (
SELECT jsonb_set(
jsonb_set(
layout_data,
'{layoutMode}',
'"grid"'
),
'{components}',
(
SELECT jsonb_agg(
comp || jsonb_build_object(
'grid', jsonb_build_object(
'col', GREATEST(1, LEAST(12, ROUND((comp->'position'->>'x')::NUMERIC / 160) + 1)),
'row', GREATEST(1, ROUND((comp->'position'->>'y')::NUMERIC / 80) + 1),
'colSpan', GREATEST(1, ROUND((comp->'size'->>'width')::NUMERIC / 160)),
'rowSpan', GREATEST(1, ROUND((comp->'size'->>'height')::NUMERIC / 80))
),
'responsive', jsonb_build_object(
'sm', jsonb_build_object('col', 1, 'colSpan', 12),
'md', jsonb_build_object(
'col', GREATEST(1, ROUND((ROUND((comp->'position'->>'x')::NUMERIC / 160) + 1) / 2.0)),
'colSpan', LEAST(ROUND((comp->'size'->>'width')::NUMERIC / 160) * 2, 12)
),
'lg', jsonb_build_object(
'col', GREATEST(1, LEAST(12, ROUND((comp->'position'->>'x')::NUMERIC / 160) + 1)),
'colSpan', GREATEST(1, ROUND((comp->'size'->>'width')::NUMERIC / 160))
)
)
)
)
FROM jsonb_array_elements(layout_data->'components') as comp
)
)
);
7.3 롤백
-- 문제 발생 시 롤백
DROP TABLE screen_layouts_v2;
ALTER TABLE screen_layouts_v2_backup_20260130 RENAME TO screen_layouts_v2;
8. 동작 흐름
8.1 데스크톱 (> 1024px)
┌────────────────────────────────────────────────────────────┐
│ 1 2 3 4 5 6 7 8 9 10 │ 11 12 │ │
│ │ [버튼] │ │
├────────────────────────────────────────────────────────────┤
│ │
│ 테이블 (12컬럼) │
│ │
└────────────────────────────────────────────────────────────┘
8.2 태블릿 (768px ~ 1024px)
┌─────────────────────────────────────┐
│ 1 2 3 4 5 6 │ 7 8 9 10 11 12 │
│ │ [버튼] │
├─────────────────────────────────────┤
│ │
│ 테이블 (12컬럼) │
│ │
└─────────────────────────────────────┘
8.3 모바일 (< 768px)
┌──────────────────┐
│ [버튼] │ ← 12컬럼 (전체 너비)
├──────────────────┤
│ │
│ 테이블 (스크롤) │ ← 12컬럼 (전체 너비)
│ │
└──────────────────┘
8.4 분할 패널 (반응형)
데스크톱:
┌─────────────────────────┬─────────────────────────┐
│ 좌측 패널 (60%) │ 우측 패널 (40%) │
└─────────────────────────┴─────────────────────────┘
모바일:
┌─────────────────────────┐
│ 상단 패널 (이전 좌측) │
├─────────────────────────┤
│ 하단 패널 (이전 우측) │
└─────────────────────────┘
9. 수정 파일 목록
9.1 새로 생성
| 파일 | 설명 |
|---|---|
lib/utils/gridConverter.ts |
픽셀 → 그리드 변환 유틸리티 |
lib/registry/layouts/responsive-grid/ResponsiveGridLayout.tsx |
CSS Grid 레이아웃 |
lib/registry/layouts/responsive-grid/useBreakpoint.ts |
ResizeObserver 훅 |
lib/registry/layouts/responsive-grid/index.ts |
모듈 export |
9.2 수정
| 파일 | 수정 내용 |
|---|---|
lib/registry/DynamicComponentRenderer.tsx |
layoutMode 분기 추가 |
components/screen/ScreenDesigner.tsx |
저장 시 grid/responsive 생성 |
v2-split-panel-layout/SplitPanelLayoutComponent.tsx |
반응형 처리 추가 |
9.3 수정 없음
| 파일 | 이유 |
|---|---|
v2-input/* |
레이아웃과 무관 (shadcn 그대로) |
v2-button-primary/* |
레이아웃과 무관 (shadcn 그대로) |
v2-table-list/* |
레이아웃과 무관 (shadcn 그대로) |
v2-select/* |
레이아웃과 무관 (shadcn 그대로) |
| ...모든 v2 컴포넌트 | 수정 불필요 |
10. 작업 일정
| Phase | 작업 | 파일 | 시간 |
|---|---|---|---|
| 1 | 그리드 변환 유틸리티 | gridConverter.ts |
2시간 |
| 1 | 브레이크포인트 훅 | useBreakpoint.ts |
1시간 |
| 2 | ResponsiveGridLayout | ResponsiveGridLayout.tsx |
4시간 |
| 2 | 렌더링 분기 처리 | DynamicComponentRenderer.tsx |
1시간 |
| 3 | 저장 로직 수정 | ScreenDesigner.tsx |
2시간 |
| 3 | 분할 패널 반응형 | SplitPanelLayoutComponent.tsx |
3시간 |
| 4 | 마이그레이션 스크립트 | SQL | 2시간 |
| 4 | 마이그레이션 실행 | - | 1시간 |
| 5 | 테스트 및 버그 수정 | - | 4시간 |
| 합계 | 약 2.5일 |
11. 체크리스트
개발 전
- screen_layouts_v2 백업 완료
- 개발 환경에서 테스트 데이터 준비
Phase 1: 유틸리티
gridConverter.ts생성useBreakpoint.ts생성- 단위 테스트 작성
Phase 2: 레이아웃
ResponsiveGridLayout.tsx생성DynamicComponentRenderer.tsx분기 추가- 기존 화면 정상 동작 확인
Phase 3: 저장/수정
ScreenDesigner.tsx저장 로직 수정SplitPanelLayoutComponent.tsx반응형 추가- 디자인 모드 테스트
Phase 4: 마이그레이션
- 마이그레이션 스크립트 테스트 (개발 DB)
- 운영 DB 백업
- 마이그레이션 실행
- 검증
Phase 5: 테스트
- PC (1920px, 1280px) 테스트
- 태블릿 (768px, 1024px) 테스트
- 모바일 (375px, 414px) 테스트
- 분할 패널 화면 테스트
- GridLayout 컴포넌트 포함 화면 테스트
- FlexboxLayout 컴포넌트 포함 화면 테스트
- TabsLayout 컴포넌트 포함 화면 테스트
- 중첩 레이아웃 (GridLayout 안에 컴포넌트) 테스트
12. 리스크 및 대응
| 리스크 | 영향 | 대응 |
|---|---|---|
| 마이그레이션 실패 | 높음 | 백업 테이블에서 즉시 롤백 |
| 기존 화면 깨짐 | 중간 | layoutMode 없으면 기존 방식 사용 (폴백) |
| 디자인 모드 혼란 | 낮음 | position/size 필드 유지 |
| GridLayout 내부 깨짐 | 낮음 | 내부는 기존 방식 유지, 외부 배치만 변경 |
| 중첩 레이아웃 문제 | 낮음 | 각 레이아웃 컴포넌트는 독립적으로 동작 |
13. 참고
- COMPONENT_LAYOUT_V2_ARCHITECTURE.md - V2 아키텍처
- CSS Grid Layout - MDN
- ResizeObserver - MDN
- shadcn/ui - 컴포넌트 라이브러리