ERP-node/docs/DDD1542/COMPONENT_URL_ZOD_ARCHITECT...

15 KiB

방안 1: 컴포넌트 URL 참조 + Zod 스키마 관리

1. 현재 문제점 정리

1.1 JSON 구조 불일치

현재 상태:
┌─────────────────────────────────────────────────────────────┐
│  v2-table-list 컴포넌트                                      │
│  화면 A: { pageSize: 20, showCheckbox: true }               │
│  화면 B: { pagination: { size: 20 }, checkbox: true }       │
│  화면 C: { paging: { pageSize: 20 }, hasCheckbox: true }    │
│                                                             │
│  → 같은 설정인데 키 이름이 다름                              │
│  → 타입 검증 없음 (런타임 에러 발생)                         │
└─────────────────────────────────────────────────────────────┘

1.2 컴포넌트 수정 시 마이그레이션 필요

컴포넌트 구조 변경:
pageSize → pagination.pageSize 로 변경하면?

→ 100개 화면의 JSON 전부 마이그레이션 필요
→ 테스트 공수 발생
→ 누락 시 런타임 에러

2. 방안 1 + Zod 아키텍처

2.1 전체 구조

┌─────────────────────────────────────────────────────────────┐
│  1. 컴포넌트 코드 + Zod 스키마 (프론트엔드)                   │
│                                                             │
│  @/lib/registry/components/v2-table-list/                   │
│  ├── index.ts           # 컴포넌트 등록                      │
│  ├── TableListRenderer.tsx  # 렌더링 로직                   │
│  ├── schema.ts          # ⭐ Zod 스키마 정의                 │
│  └── defaults.ts        # ⭐ 기본값 정의                     │
│                                                             │
│  코드 수정 → 빌드 → 전 회사 즉시 적용                        │
└─────────────────────────────────────────────────────────────┘
                              │
                              │ URL로 참조
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  2. DB (최소한의 차이점만 저장)                              │
│                                                             │
│  screen_layouts.properties = {                              │
│    "componentUrl": "@/registry/v2-table-list",              │
│    "config": {                                              │
│      "pageSize": 50    ← 기본값(20)과 다른 것만             │
│    }                                                        │
│  }                                                          │
└─────────────────────────────────────────────────────────────┘
                              │
                              │ 설정 병합
                              ▼
┌─────────────────────────────────────────────────────────────┐
│  3. 런타임: 기본값 + 오버라이드 병합 + Zod 검증              │
│                                                             │
│  최종 설정 = deepMerge(기본값, 오버라이드)                   │
│  검증된 설정 = schema.parse(최종 설정)                       │
└─────────────────────────────────────────────────────────────┘

2.2 Zod 스키마 예시

// @/lib/registry/components/v2-table-list/schema.ts
import { z } from "zod";

// 컬럼 설정 스키마
const columnSchema = z.object({
  columnName: z.string(),
  displayName: z.string(),
  visible: z.boolean().default(true),
  sortable: z.boolean().default(true),
  width: z.number().optional(),
  align: z.enum(["left", "center", "right"]).default("left"),
  format: z.enum(["text", "number", "date", "currency"]).default("text"),
  order: z.number().default(0),
});

// 페이지네이션 스키마
const paginationSchema = z.object({
  enabled: z.boolean().default(true),
  pageSize: z.number().default(20),
  showSizeSelector: z.boolean().default(true),
  pageSizeOptions: z.array(z.number()).default([10, 20, 50, 100]),
});

// 체크박스 스키마
const checkboxSchema = z.object({
  enabled: z.boolean().default(true),
  multiple: z.boolean().default(true),
  position: z.enum(["left", "right"]).default("left"),
});

// 테이블 리스트 전체 스키마
export const tableListSchema = z.object({
  tableName: z.string(),
  columns: z.array(columnSchema).default([]),
  pagination: paginationSchema.default({}),
  checkbox: checkboxSchema.default({}),
  showHeader: z.boolean().default(true),
  autoLoad: z.boolean().default(true),
});

// 타입 자동 추론
export type TableListConfig = z.infer<typeof tableListSchema>;

2.3 기본값 정의

// @/lib/registry/components/v2-table-list/defaults.ts
import { TableListConfig } from "./schema";

export const defaultConfig: Partial<TableListConfig> = {
  pagination: {
    enabled: true,
    pageSize: 20,
    showSizeSelector: true,
    pageSizeOptions: [10, 20, 50, 100],
  },
  checkbox: {
    enabled: true,
    multiple: true,
    position: "left",
  },
  showHeader: true,
  autoLoad: true,
};

2.4 설정 로드 로직

// @/lib/registry/utils/configLoader.ts
import { deepMerge } from "@/lib/utils";

export function loadComponentConfig<T>(
  componentUrl: string,
  overrideConfig: Partial<T>
): T {
  // 1. 컴포넌트 모듈에서 스키마와 기본값 가져오기
  const { schema, defaultConfig } = getComponentModule(componentUrl);
  
  // 2. 기본값 + 오버라이드 병합
  const mergedConfig = deepMerge(defaultConfig, overrideConfig);
  
  // 3. Zod 스키마로 검증 + 기본값 자동 적용
  const validatedConfig = schema.parse(mergedConfig);
  
  return validatedConfig;
}

3. 현재 시스템 적응도 분석

3.1 변경이 필요한 부분

영역 현재 변경 후 공수
컴포넌트 폴더 구조 types.ts만 있음 schema.ts, defaults.ts 추가 중간
screen_layouts 모든 설정 저장 URL + 차이점만 저장 중간
화면 저장 로직 JSON 통째로 저장 차이점 추출 후 저장 중간
화면 로드 로직 JSON 그대로 사용 기본값 병합 + Zod 검증 낮음
기존 데이터 - 마이그레이션 필요 높음

3.2 기존 코드와의 호환성

현재 Zod 사용 현황:
✅ zod v4.1.5 이미 설치됨
✅ @hookform/resolvers 설치됨 (react-hook-form + Zod 연동)
✅ 공통코드 관리에 Zod 스키마 사용 중 (lib/schemas/commonCode.ts)

→ Zod 패턴이 이미 프로젝트에 존재함
→ 동일한 패턴으로 컴포넌트 스키마 추가 가능

3.3 점진적 마이그레이션 가능 여부

Phase 1: 새 컴포넌트만 적용
- 신규 컴포넌트는 schema.ts + defaults.ts 구조로 생성
- 기존 컴포넌트는 그대로 유지

Phase 2: 핵심 컴포넌트 마이그레이션
- v2-table-list, v2-button-primary 등 자주 사용하는 것 먼저
- 기존 JSON 데이터 → 차이점만 남기고 정리

Phase 3: 전체 마이그레이션
- 나머지 컴포넌트 순차 적용

→ 점진적 적용 가능 ✅

4. 향후 장점

4.1 컴포넌트 수정 시

변경 전:
컴포넌트 수정 → 100개 화면 JSON 마이그레이션 → 테스트 → 배포

변경 후:
컴포넌트 수정 → 빌드 → 배포 → 끝

왜?
- 기본값/로직은 코드에 있음
- DB에는 "다른 것만" 저장되어 있음
- 코드 변경이 자동으로 모든 화면에 적용됨

4.2 새 설정 추가 시

변경 전:
1. types.ts 수정
2. 100개 화면 JSON에 새 필드 추가 (마이그레이션)
3. 기본값 없으면 에러 발생

변경 후:
1. schema.ts에 필드 추가 + .default() 설정
2. 끝. 기존 데이터는 자동으로 기본값 적용됨

// 예시
const schema = z.object({
  // 기존 필드
  pageSize: z.number().default(20),
  
  // 🆕 새 필드 추가 - 기본값 있으면 마이그레이션 불필요
  showRowNumber: z.boolean().default(false),
});

4.3 타입 안정성

// 현재: 타입 검증 없음
const config = component.componentConfig; // any 타입
config.pageSize; // 있을 수도, 없을 수도...
config.pagination.pageSize; // 구조가 다를 수도...

// 변경 후: Zod로 검증 + TypeScript 타입 추론
const config = tableListSchema.parse(rawConfig);
config.pagination.pageSize; // ✅ 타입 보장
config.unknownField; // ❌ 컴파일 에러

4.4 런타임 에러 방지

// Zod 검증 실패 시 명확한 에러 메시지
try {
  const config = tableListSchema.parse(rawConfig);
} catch (error) {
  if (error instanceof z.ZodError) {
    console.error("설정 오류:", error.errors);
    // [
    //   { path: ["pagination", "pageSize"], message: "Expected number, received string" },
    //   { path: ["columns", 0, "align"], message: "Invalid enum value" }
    // ]
  }
}

4.5 문서화 자동화

// Zod 스키마에서 자동으로 문서 생성 가능
import { zodToJsonSchema } from "zod-to-json-schema";

const jsonSchema = zodToJsonSchema(tableListSchema);
// → JSON Schema 형식으로 변환 → 문서화 도구에서 사용

5. 유지보수 측면

5.1 컴포넌트 개발자 입장

작업 현재 변경 후
새 컴포넌트 생성 types.ts 작성 (선택) schema.ts + defaults.ts 작성 (필수)
설정 구조 변경 마이그레이션 스크립트 작성 schema 수정 + 기본값 설정
타입 체크 수동 검증 Zod가 자동 검증
디버깅 console.log로 추적 Zod 에러 메시지로 바로 파악

5.2 화면 개발자 입장

작업 현재 변경 후
화면 생성 모든 설정 직접 지정 필요한 것만 오버라이드
설정 실수 런타임 에러 저장 시 Zod 검증 에러
기본값 확인 코드 뒤져보기 defaults.ts 확인

5.3 운영자 입장

작업 현재 변경 후
일괄 설정 변경 100개 JSON 수정 defaults.ts 수정 → 전체 적용
회사별 기본값 불가능 회사별 defaults 테이블 추가 가능
오류 추적 어려움 Zod 검증 로그 확인

6. 데이터 마이그레이션 계획

6.1 차이점 추출 스크립트

// 기존 JSON에서 기본값과 다른 것만 추출
async function extractDiff(componentUrl: string, fullConfig: any): Promise<any> {
  const { defaultConfig } = getComponentModule(componentUrl);
  
  function getDiff(defaults: any, current: any): any {
    const diff: any = {};
    
    for (const key of Object.keys(current)) {
      if (defaults[key] === undefined) {
        // 기본값에 없는 키 = 그대로 유지
        diff[key] = current[key];
      } else if (typeof current[key] === 'object' && !Array.isArray(current[key])) {
        // 중첩 객체 = 재귀 비교
        const nestedDiff = getDiff(defaults[key], current[key]);
        if (Object.keys(nestedDiff).length > 0) {
          diff[key] = nestedDiff;
        }
      } else if (JSON.stringify(defaults[key]) !== JSON.stringify(current[key])) {
        // 값이 다름 = 저장
        diff[key] = current[key];
      }
      // 값이 같음 = 저장 안 함 (기본값 사용)
    }
    
    return diff;
  }
  
  return getDiff(defaultConfig, fullConfig);
}

6.2 마이그레이션 순서

1. 컴포넌트별 schema.ts, defaults.ts 작성
2. 기존 데이터 분석 (어떤 설정이 자주 사용되는지)
3. 가장 많이 사용되는 값을 기본값으로 설정
4. 차이점 추출 스크립트 실행
5. 새 구조로 데이터 업데이트
6. 테스트

7. 예상 공수

단계 작업 예상 공수
Phase 1 아키텍처 설계 + 유틸리티 함수 1주
Phase 2 핵심 컴포넌트 5개 스키마 작성 1주
Phase 3 데이터 마이그레이션 스크립트 1주
Phase 4 테스트 + 버그 수정 1주
Phase 5 나머지 컴포넌트 순차 적용 2-3주
총계 6-7주

8. 위험 요소 및 대응

8.1 위험 요소

위험 영향 대응
기존 데이터 손실 높음 마이그레이션 전 백업 필수
스키마 설계 실수 중간 충분한 리뷰 + 테스트
런타임 성능 저하 낮음 Zod는 충분히 빠름
개발자 학습 비용 낮음 Zod는 직관적, 이미 사용 중

8.2 롤백 계획

문제 발생 시:
1. 기존 JSON 구조로 데이터 복원 (백업에서)
2. 새 로직 비활성화 (feature flag)
3. 원인 분석 후 재시도

9. 결론

9.1 방안 1 + Zod 조합의 평가

항목 점수 이유
현재 시스템 적응도 ★★★★☆ Zod 이미 사용 중, 점진적 적용 가능
향후 확장성 ★★★★★ 새 설정 추가 용이, 타입 안정성
유지보수성 ★★★★★ 코드 수정 → 전 회사 적용, 명확한 에러
마이그레이션 공수 ★★★☆☆ 6-7주 소요, 점진적 적용으로 리스크 분산
안정성 ★★★★☆ Zod 검증으로 런타임 에러 방지

9.2 최종 권장

✅ 방안 1 (URL 참조 + Zod 스키마) 적용 권장

이유:
1. 컴포넌트 수정 → 코드만 변경 → 전 회사 자동 적용
2. Zod로 JSON 구조 일관성 보장
3. 타입 안정성 + 런타임 검증
4. 기존 시스템과 호환 (Zod 이미 사용 중)
5. 점진적 마이그레이션 가능

9.3 다음 단계

  1. 핵심 컴포넌트 1개로 PoC (Proof of Concept)
  2. 팀 리뷰 및 피드백
  3. 표준 패턴 확정
  4. 순차적 적용