Post

[Design Pattern] Factory Pattern으로 AG-Grid 컬럼 정의 재사용하기

반복되는 AG-Grid 컬럼 정의 코드를 Factory Pattern으로 추상화하여 재사용성 향상

문제 상황

AG-Grid를 사용하는 여러 페이지에서 컬럼 정의(ColDef[])를 작성하다 보니 다음과 같은 문제가 발생함:

1. 코드 중복

1
2
3
4
5
6
7
8
9
10
11
12
13
// 페이지 A
const columnDefs = [
  { field: 'id', headerName: 'ID', width: 100 },
  { field: 'status', headerName: '상태', width: 120, cellRenderer: BadgeCellRenderer },
  { field: 'createdAt', headerName: '생성일', width: 150, valueFormatter: dateFormatter },
];

// 페이지 B
const columnDefs = [
  { field: 'userId', headerName: 'ID', width: 100 },
  { field: 'state', headerName: '상태', width: 120, cellRenderer: BadgeCellRenderer },
  { field: 'timestamp', headerName: '생성일', width: 150, valueFormatter: dateFormatter },
];

상태(Badge), 날짜, 액션 버튼 등 패턴이 반복되지만 매번 수동으로 작성해야 함.

2. 유지보수 어려움

  • Badge 렌더러 변경 시 모든 파일을 찾아서 수정해야 함.
  • 날짜 포맷 변경 시 여러 곳을 수정해야 함.
  • 실수로 일부 컬럼만 수정하면 일관성이 깨짐.

3. 타입 안정성 부족

  • valueFormatter, cellRenderer 등을 매번 수동으로 지정하다 보면 오타나 잘못된 타입 사용 가능성이 높음.

Factory Pattern 도입

Java/Spring에서 자주 사용하는 Factory Pattern을 TypeScript에 적용하여 컬럼 정의를 추상화함.

설계 원칙

  1. BaseColumnFactory: 공통 컬럼 생성 메서드를 제공하는 추상 클래스
  2. 도메인별 Factory: 각 도메인(예: 자산, 서비스 요청)에 맞는 Factory 구현
  3. Helper 메서드: Badge, Date, Action 등 자주 사용하는 컬럼 타입을 메서드화

BaseColumnFactory 구현

1. 기본 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// BaseColumnFactory.ts
import type { ColDef } from 'ag-grid-community';
import { BadgeCellRenderer } from '../cellRenderers/BadgeCellRenderer';

export abstract class BaseColumnFactory {
  /**
   * 텍스트 컬럼 생성
   */
  protected createTextColumn(
    field: string,
    headerName: string,
    options?: Partial<ColDef>
  ): ColDef {
    return {
      field,
      headerName,
      sortable: true,
      filter: true,
      resizable: true,
      ...options,
    };
  }

  /**
   * Badge 컬럼 생성 (상태, 우선순위 등)
   */
  protected createBadgeColumn(
    field: string,
    headerName: string,
    type: 'priority' | 'status' | 'severity',
    options?: Partial<ColDef>
  ): ColDef {
    return {
      field,
      headerName,
      cellRenderer: BadgeCellRenderer,
      cellRendererParams: { type },
      sortable: true,
      filter: true,
      resizable: true,
      minWidth: 100,
      maxWidth: 120,
      ...options,
    };
  }

  /**
   * 날짜 컬럼 생성
   */
  protected createDateColumn(
    field: string,
    headerName: string,
    options?: Partial<ColDef>
  ): ColDef {
    return {
      field,
      headerName,
      valueFormatter: (params) => {
        if (!params.value) return '-';
        return new Date(params.value).toLocaleDateString('ko-KR');
      },
      sortable: true,
      filter: 'agDateColumnFilter',
      resizable: true,
      minWidth: 120,
      maxWidth: 150,
      ...options,
    };
  }

  /**
   * 액션 컬럼 생성 (편집/삭제 버튼)
   */
  protected createActionColumn(
    onEdit?: (data: any) => void,
    onDelete?: (data: any) => void
  ): ColDef {
    return {
      field: 'actions',
      headerName: '',
      cellRenderer: ActionCellRenderer,
      cellRendererParams: { onEdit, onDelete },
      sortable: false,
      filter: false,
      resizable: false,
      minWidth: 100,
      maxWidth: 100,
      pinned: 'right',
    };
  }
}

2. 도메인별 Factory 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// AssetColumnFactory.ts
import { BaseColumnFactory } from './BaseColumnFactory';
import type { ColDef } from 'ag-grid-community';

export class AssetColumnFactory extends BaseColumnFactory {
  createColumns(
    onEdit?: (data: any) => void,
    onDelete?: (data: any) => void
  ): ColDef[] {
    return [
      this.createTextColumn('assetId', '자산 ID', { minWidth: 120, maxWidth: 150 }),
      this.createTextColumn('assetName', '자산명', { flex: 1, minWidth: 200 }),
      this.createBadgeColumn('status', '상태', 'status'),
      this.createTextColumn('owner', '담당자', { minWidth: 100 }),
      this.createDateColumn('createdAt', '등록일'),
      this.createActionColumn(onEdit, onDelete),
    ];
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ServiceRequestColumnFactory.ts
import { BaseColumnFactory } from './BaseColumnFactory';
import type { ColDef } from 'ag-grid-community';

export class ServiceRequestColumnFactory extends BaseColumnFactory {
  createColumns(
    onEdit?: (data: any) => void,
    onDelete?: (data: any) => void
  ): ColDef[] {
    return [
      this.createTextColumn('requestId', '요청 ID', { minWidth: 140 }),
      this.createTextColumn('requester', '요청자', { minWidth: 120 }),
      this.createBadgeColumn('priority', '우선순위', 'priority'),
      this.createBadgeColumn('state', '상태', 'status'),
      this.createTextColumn('category', '카테고리', { minWidth: 150 }),
      this.createDateColumn('requestedAt', '요청일'),
      this.createActionColumn(onEdit, onDelete),
    ];
  }
}

실제 사용 예시

Before (Factory 패턴 적용 전)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// AssetListPage.tsx
const columnDefs: ColDef[] = [
  {
    field: 'assetId',
    headerName: '자산 ID',
    sortable: true,
    filter: true,
    resizable: true,
    minWidth: 120,
    maxWidth: 150,
  },
  {
    field: 'assetName',
    headerName: '자산명',
    sortable: true,
    filter: true,
    resizable: true,
    flex: 1,
    minWidth: 200,
  },
  {
    field: 'status',
    headerName: '상태',
    cellRenderer: BadgeCellRenderer,
    cellRendererParams: { type: 'status' },
    sortable: true,
    filter: true,
    resizable: true,
    minWidth: 100,
    maxWidth: 120,
  },
  // ... 이하 생략
];

return <AGGridWrapper columnDefs={columnDefs} rowData={data} />;

After (Factory 패턴 적용 후)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// AssetListPage.tsx
import { AssetColumnFactory } from '@/factories/columnDefs/AssetColumnFactory';

const handleEdit = (data: any) => {
  console.log('Edit:', data);
};

const handleDelete = (data: any) => {
  console.log('Delete:', data);
};

const columnFactory = new AssetColumnFactory();
const columnDefs = columnFactory.createColumns(handleEdit, handleDelete);

return <AGGridWrapper columnDefs={columnDefs} rowData={data} />;

코드가 15줄 → 5줄로 단축되고, 가독성과 유지보수성이 크게 향상됨.


결과

1. 코드 재사용성 향상

  • 동일한 패턴의 컬럼을 여러 곳에서 쉽게 재사용 가능.
  • 새로운 페이지에 테이블을 추가할 때 Factory만 호출하면 됨.

2. 유지보수 간편화

  • Badge 렌더러 변경 시 BaseColumnFactory만 수정하면 모든 곳에 적용됨.
  • 날짜 포맷 변경도 한 곳만 수정하면 됨.

3. 타입 안정성 확보

  • TypeScript의 타입 시스템을 활용하여 오타나 잘못된 타입 사용 방지.
  • createBadgeColumntype 파라미터는 'priority' | 'status' | 'severity'로 제한됨.

4. 일관성 유지

  • 모든 테이블이 동일한 패턴으로 구성되어 사용자 경험이 일관됨.
  • 컬럼 너비, 정렬, 필터 설정이 통일됨.

주의할 점

1. createColumns()는 public 메서드여야 함

1
2
3
4
5
6
7
8
9
// ❌ 잘못된 예
protected createColumns(): ColDef[] {  // 외부에서 접근 불가
  return [...];
}

// ✅ 올바른 예
public createColumns(): ColDef[] {  // 외부에서 접근 가능
  return [...];
}

2. 헬퍼 메서드는 protected로 선언

  • createTextColumn, createBadgeColumn 등은 protected로 선언하여 하위 클래스에서만 사용 가능하게 함.
  • 외부에서 직접 호출하지 못하도록 캡슐화.

3. options 파라미터로 유연성 확보

1
2
3
4
5
6
7
8
9
// 기본 설정 사용
this.createTextColumn('name', '이름')

// 커스텀 설정 추가
this.createTextColumn('name', '이름', {
  flex: 2,
  minWidth: 300,
  cellStyle: { fontWeight: 'bold' },
})

Partial<ColDef>을 사용하여 필요한 경우 기본 설정을 오버라이드할 수 있게 함.

4. 도메인별 Factory는 용도에 맞게 분리

  • 자산 관리: AssetColumnFactory
  • 서비스 요청: ServiceRequestColumnFactory
  • 사용자 관리: UserColumnFactory

각 도메인의 특성에 맞게 Factory를 분리하여 관리.


추가 개선 사항

1. 긴 텍스트 컬럼 (Ellipsis 처리)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected createLongTextColumn(
  field: string,
  headerName: string,
  options?: Partial<ColDef>
): ColDef {
  return {
    field,
    headerName,
    sortable: true,
    filter: true,
    resizable: true,
    cellStyle: {
      overflow: 'hidden',
      textOverflow: 'ellipsis',
      whiteSpace: 'nowrap',
    },
    tooltipField: field,  // 전체 텍스트를 툴팁으로 표시
    ...options,
  };
}

2. 자동 줄바꿈 컬럼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected createWrappedTextColumn(
  field: string,
  headerName: string,
  options?: Partial<ColDef>
): ColDef {
  return {
    field,
    headerName,
    sortable: true,
    filter: true,
    resizable: true,
    wrapText: true,
    autoHeight: true,
    ...options,
  };
}

Factory Pattern을 도입하여 AG-Grid 컬럼 정의 코드를 크게 줄이고 재사용성을 높였음. Java/Spring의 객체 지향 패턴을 프론트엔드에도 적용하여 유지보수성이 향상됨.

도움이 되셨길 바랍니다! 😀

This post is licensed under CC BY 4.0 by the author.