[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에 적용하여 컬럼 정의를 추상화함.
설계 원칙
- BaseColumnFactory: 공통 컬럼 생성 메서드를 제공하는 추상 클래스
- 도메인별 Factory: 각 도메인(예: 자산, 서비스 요청)에 맞는 Factory 구현
- 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의 타입 시스템을 활용하여 오타나 잘못된 타입 사용 방지.
createBadgeColumn의type파라미터는'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.