데이터셋 컬럼 관리: 삭제/치환/연산 모달 컴포넌트 설계¶
개요¶
XGEN 2.0의 데이터 프로세서는 RAG 파이프라인에 투입할 데이터셋을 전처리하는 도구다. CSV/Parquet 파일을 로드한 뒤, 컬럼 삭제, 값 치환, 수학 연산, 문자열 포맷팅, 컬럼 간 계산 등의 조작을 수행할 수 있다. 거기에 PyArrow 콜백으로 임의의 Python 코드도 실행할 수 있다.
이런 다양한 조작을 하나의 거대한 폼으로 만들면 복잡도가 감당이 안 된다. 각 조작별로 독립적인 모달 컴포넌트를 만들어서, 사이드바에서 원하는 기능을 선택하면 해당 모달이 열리는 구조로 설계했다. 최종적으로 13개의 모달 컴포넌트가 만들어졌다.
아키텍처¶
flowchart LR
subgraph Sidebar["DataProcessorSidebar<br/>(3단 네비게이션)"]
A["카테고리<br/>불러오기/분석/편집/저장"]
B["액션<br/>컬럼 편집/추가/정제"]
C["서브액션<br/>삭제/치환/연산/..."]
end
subgraph Modals["modals/ (13개)"]
D[ColumnDeleteModal]
E[ColumnValueReplaceModal]
F[ColumnOperationModal]
G[ColumnFormatModal]
H[ColumnCalculationModal]
I[DatasetCallbackModal]
J[ColumnCopyModal]
K[ColumnRenameModal]
L[StatisticsModal]
M[HuggingFaceUploadModal]
end
subgraph Container["DataProcessor.tsx"]
N["13개 모달 open/close 상태"]
O["핸들러 함수"]
P["데이터 테이블 렌더링"]
end
subgraph API["dataManagerAPI.js"]
Q["27개 API 함수"]
end
A --> B --> C
C --> N
N --> D & E & F & G & H & I & J & K & L & M
O --> Q
파일 구조¶
src/app/main/dataSection/
components/
DataProcessor.tsx -- 컨테이너 (모달 상태 + 데이터 테이블)
DataProcessorSidebar.tsx -- 3단 네비게이션 사이드바
modals/
index.ts -- 배럴 export
ColumnDeleteModal.tsx
ColumnValueReplaceModal.tsx
ColumnOperationModal.tsx
ColumnFormatModal.tsx
ColumnCalculationModal.tsx
DatasetCallbackModal.tsx
ColumnCopyModal.tsx
ColumnRenameModal.tsx
ColumnInfoModal.tsx
StatisticsModal.tsx
SpecificColumnNullRemoveModal.tsx
HuggingFaceUploadModal.tsx
DownloadDialog.tsx
assets/
ColumnDeleteModal.module.scss
ColumnValueReplaceModal.module.scss
... -- 각 모달별 개별 SCSS
핵심 구현¶
모달 컴포넌트 패턴¶
모든 모달이 동일한 패턴을 따른다:
isOpen이 false면 null 반환createPortal로document.body에 렌더링- 폼 상태를 로컬
useState로 관리 - 닫을 때 폼 상태 초기화
- 콜백 함수로 결과를 부모에 전달
ColumnDeleteModal — 다중 선택 삭제¶
체크박스로 여러 컬럼을 선택하고 한 번에 삭제한다.
interface ColumnDeleteModalProps {
isOpen: boolean;
onClose: () => void;
onDeleteMultipleColumns: (columnNames: string[]) => void;
availableColumns: string[];
}
export const ColumnDeleteModal: React.FC<ColumnDeleteModalProps> = ({
isOpen, onClose, onDeleteMultipleColumns, availableColumns
}) => {
const [selectedColumns, setSelectedColumns] = useState<string[]>([]);
if (!isOpen) return null;
const handleToggleAll = () => {
if (selectedColumns.length === availableColumns.length) {
setSelectedColumns([]);
} else {
setSelectedColumns([...availableColumns]);
}
};
const handleToggle = (col: string) => {
setSelectedColumns(prev =>
prev.includes(col)
? prev.filter(c => c !== col)
: [...prev, col]
);
};
const handleSubmit = () => {
onDeleteMultipleColumns(selectedColumns);
setSelectedColumns([]);
onClose();
};
return createPortal(
<div className={styles.overlay}>
<div className={styles.modal}>
<h3>컬럼 삭제</h3>
<div className={styles.selectAll}>
<label>
<input
type="checkbox"
checked={selectedColumns.length === availableColumns.length}
onChange={handleToggleAll}
/>
전체 선택/해제
</label>
</div>
<div className={styles.columnList}>
{availableColumns.map(col => (
<label key={col}>
<input
type="checkbox"
checked={selectedColumns.includes(col)}
onChange={() => handleToggle(col)}
/>
{col}
</label>
))}
</div>
<button
onClick={handleSubmit}
disabled={selectedColumns.length === 0}
>
선택된 {selectedColumns.length}개 컬럼 삭제
</button>
</div>
</div>,
document.body
);
};
삭제 버튼에 선택된 개수를 표시해서 실수로 전체를 삭제하는 것을 방지한다.
ColumnValueReplaceModal — 문자열 치환¶
특정 컬럼에서 oldValue를 newValue로 치환한다.
interface ColumnValueReplaceModalProps {
isOpen: boolean;
onClose: () => void;
onReplaceValues: (columnName: string, oldValue: string, newValue: string) => void;
availableColumns: string[];
}
컬럼 선택 드롭다운, 기존값/새값 입력 필드로 구성된다. 기존값이 비어있으면 실행할 수 없도록 disabled 처리한다.
ColumnOperationModal — 단일 컬럼 수학 연산¶
숫자 컬럼에 수학 연산을 적용한다. +5, *2.5, /3+1 같은 연산식을 입력하면 컬럼의 모든 값에 적용된다.
const handleSubmit = () => {
// 연산식 검증: 숫자와 사칙연산자만 허용
if (!/^[+\-*/\d.]+$/.test(operation)) {
alert('유효한 연산식을 입력해주세요. (예: +5, *2.5, /3)');
return;
}
onApplyOperation(selectedColumn, operation);
setOperation('');
onClose();
};
정규식 /^[+\-*/\d.]+$/로 안전하지 않은 입력을 차단한다. eval() 같은 위험한 코드를 실행하지 않고, 백엔드에서 안전하게 처리한다.
ColumnFormatModal — 다중 컬럼 문자열 결합¶
여러 컬럼의 값을 템플릿으로 결합해서 새 컬럼을 생성한다. 예를 들어 {이름}_님의_{직업} 같은 템플릿으로 "홍길동_님의_개발자" 같은 값을 만들 수 있다.
interface ColumnFormatModalProps {
isOpen: boolean;
onClose: () => void;
onFormatColumns: (
columnNames: string[],
template: string,
newColumnName: string
) => void;
availableColumns: string[];
}
핵심 기능은 동적 컬럼 슬롯이다. "컬럼 추가" 버튼으로 슬롯을 늘리고, 각 슬롯에서 드롭다운으로 컬럼을 선택한다. 선택한 컬럼이 바뀔 때마다 템플릿 예시가 자동으로 업데이트된다.
const [columnSlots, setColumnSlots] = useState<string[]>(['']);
const addColumnSlot = () => {
setColumnSlots(prev => [...prev, '']);
};
const removeColumnSlot = (index: number) => {
setColumnSlots(prev => prev.filter((_, i) => i !== index));
};
// 템플릿 예시 자동 생성
const generateTemplateExample = () => {
const selectedCols = columnSlots.filter(c => c !== '');
if (selectedCols.length === 0) return '';
return selectedCols.map(col => `{${col}}`).join('_');
};
중복 컬럼명 검사도 포함되어 있다. 새 컬럼 이름이 기존 컬럼과 겹치면 경고를 표시한다.
ColumnCalculationModal — 컬럼 간 사칙연산¶
두 컬럼의 값을 사칙연산해서 새 컬럼을 만든다. 가격 * 수량 = 총액 같은 계산이 가능하다.
interface ColumnCalculationModalProps {
isOpen: boolean;
onClose: () => void;
onCalculateColumns: (
column1: string,
column2: string,
operation: string,
newColumnName: string
) => void;
availableColumns: string[];
}
연산자는 버튼 UI로 선택한다: +, -, x, ÷. 선택하면 연산 미리보기가 표시된다 — 예: "가격 x 수량 → 총액".
DatasetCallbackModal — PyArrow 콜백 코드 에디터¶
가장 강력하면서 가장 복잡한 모달이다. 사용자가 PyArrow 코드를 직접 작성해서 데이터셋을 조작할 수 있다.
interface DatasetCallbackModalProps {
isOpen: boolean;
onClose: () => void;
onExecuteCallback: (callbackCode: string) => void;
sampleData?: any[];
columns?: string[];
}
핵심 기능:
- 코드 에디터:
<textarea>에 줄번호 표시, Tab/Shift+Tab 들여쓰기 지원 - 6개 예제 코드: 컬럼 필터링, 행 필터링, 새 컬럼 추가, 문자열 조작, 정렬, 중복 제거
- 샘플 데이터 미리보기: 상위 3행을 테이블로 보여줘서 컬럼명과 값 형태를 확인할 수 있다
- 도움말: 사용 가능한 모듈(
pa,pc,pq,sys)과 보안 경고 표시
// Tab 들여쓰기 처리
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Tab') {
e.preventDefault();
const textarea = e.currentTarget;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
if (e.shiftKey) {
// Shift+Tab: 들여쓰기 제거
const beforeCursor = callbackCode.substring(0, start);
const lastNewline = beforeCursor.lastIndexOf('\n');
const lineStart = lastNewline + 1;
const lineContent = callbackCode.substring(lineStart, start);
if (lineContent.startsWith(' ')) {
setCallbackCode(
callbackCode.substring(0, lineStart)
+ lineContent.substring(4)
+ callbackCode.substring(start)
);
}
} else {
// Tab: 4칸 들여쓰기
setCallbackCode(
callbackCode.substring(0, start)
+ ' '
+ callbackCode.substring(end)
);
}
}
};
예제 코드 중 하나:
# 컬럼 필터링 예시
import pyarrow as pa
import pyarrow.compute as pc
# 특정 컬럼만 선택
selected_columns = ['이름', '나이', '직업']
table = table.select(selected_columns)
보안 관련: 코드는 백엔드의 격리된 환경에서 실행되며 30초 타임아웃이 적용된다.
들여쓰기 제거 로직에서 whitespace 패턴 매칭이 잘못된 버그가 있었다. /^ /(탭 4개)를 /^ {4}/(공백 4개)로 수정했다.
사이드바: 3단 네비게이션¶
DataProcessorSidebar는 카테고리 → 액션 → 서브액션의 3단 구조로 기능을 분류한다.
// 카테고리 정의
const CATEGORIES = [
{ id: 'load', title: '데이터셋 불러오기', icon: IoDownloadOutline },
{ id: 'analysis', title: '분석', icon: IoStatsChart },
{ id: 'edit', title: '편집', icon: IoPencil },
{ id: 'save', title: '저장', icon: IoSave },
];
// 편집 카테고리의 액션
case 'edit':
return [
{ id: 'column-edit', title: '컬럼 편집', subActions: [...] },
{ id: 'column-add', title: '컬럼 추가', subActions: [...] },
{ id: 'data-clean', title: '데이터 정제', subActions: [...] },
];
// 컬럼 편집의 서브액션
subActions: [
{ id: 'column-delete', title: '열 삭제', modal: 'columnDeleteModal' },
{ id: 'column-replace', title: '열 데이터 변경', modal: 'columnValueReplaceModal' },
{ id: 'column-operation', title: '열 데이터 연산', modal: 'columnOperationModal' },
{ id: 'column-rename', title: '열 이름 변경', modal: 'columnRenameModal' },
{ id: 'dataset-callback', title: 'PyArrow 콜백 함수', modal: 'datasetCallbackModal' },
]
서브액션의 modal 프로퍼티가 DataProcessor의 상태 이름과 일치해서, 클릭하면 해당 모달이 열린다.
DataProcessor: 모달 상태 관리¶
13개 모달의 open/close 상태를 모두 DataProcessor가 소유한다. 각 모달별 핸들러 함수도 동일한 패턴을 따른다.
// 모달 상태 (13개)
const [columnDeleteModalOpen, setColumnDeleteModalOpen] = useState(false);
const [columnValueReplaceModalOpen, setColumnValueReplaceModalOpen] = useState(false);
const [columnOperationModalOpen, setColumnOperationModalOpen] = useState(false);
// ... 10개 더
// 핸들러 패턴 (모든 모달에 동일하게 적용)
const handleFormatColumns = async (
columnNames: string[],
template: string,
newColumn: string
) => {
showSuccessToastKo('처리중...');
const result = await formatDatasetColumns(
managerId, columnNames, template, newColumn
);
if (result.success) {
showSuccessToastKo('포맷팅이 적용되었습니다.');
loadDataTableInfo(); // 데이터 리로드
} else {
showErrorToastKo(result.message);
}
};
삭제 작업은 showDeleteConfirmToastKo로 한 번 더 확인한다:
const handleDeleteColumns = async (columnNames: string[]) => {
showDeleteConfirmToastKo({
title: '컬럼 삭제',
message: `${columnNames.length}개 컬럼을 삭제하시겠습니까?`,
onConfirm: async () => {
const result = await dropDatasetColumns(managerId, columnNames);
if (result.success) {
showSuccessToastKo('컬럼이 삭제되었습니다.');
loadDataTableInfo();
}
},
});
};
API 레이어¶
dataManagerAPI.js에 컬럼 관련 API 함수 10개가 정의되어 있다. 모든 함수가 동일한 패턴을 따른다:
export const replaceColumnValues = async (
managerId, columnName, oldValue, newValue
) => {
if (!managerId) throw new Error('Manager ID는 필수입니다.');
if (!columnName) throw new Error('컬럼명은 필수입니다.');
const response = await apiClient(
`${API_BASE_URL}/api/data-manager/processing/replace-values`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
manager_id: managerId,
column_name: columnName,
old_value: oldValue,
new_value: newValue,
}),
}
);
const data = await response.json();
devLog.info(`Column values replaced:`, data);
return data;
};
공통 패턴:
- manager_id 필수 검증
- apiClient 래퍼로 인증 자동 처리
- devLog로 성공/실패 로깅
- REST POST, JSON body
트러블슈팅¶
단일 파일에서 13개 파일로 분리¶
초기에는 DataProcessorModal.tsx 하나에 모든 모달이 들어있었다. 9월 22일의 커밋(18650092)에서 컬럼 삭제/치환/연산 3개 모달을 추가하면서 537줄의 diff가 발생했다. 파일이 너무 커지자 다음 날 바로 13개 개별 파일로 분리했다.
분리의 핵심은 modals/index.ts 배럴 export다:
// modals/index.ts
export { ColumnDeleteModal } from './ColumnDeleteModal';
export { ColumnValueReplaceModal } from './ColumnValueReplaceModal';
export { ColumnOperationModal } from './ColumnOperationModal';
// ... 10개 더
DataProcessor에서는 한 줄로 모든 모달을 import한다:
import {
ColumnDeleteModal,
ColumnValueReplaceModal,
ColumnOperationModal,
ColumnFormatModal,
ColumnCalculationModal,
DatasetCallbackModal,
// ...
} from './modals';
사이드바 관심사 분리¶
같은 커밋에서 사이드바의 불필요한 API 임포트를 정리했다. 사이드바가 직접 dropDatasetColumns, replaceColumnValues 등의 API를 import하고 있었는데, 실제 API 호출은 DataProcessor에서 하므로 사이드바에서는 불필요했다. 사이드바는 순수하게 네비게이션 역할만 담당하도록 리팩토링했다.
결과 및 회고¶
4일간(9/19~9/23) 7개 커밋에 걸쳐 데이터 프로세서의 모달 시스템을 구축했다. 단일 책임 원칙을 모달에 적용해서 각 모달이 하나의 조작에만 집중하도록 설계한 것이 핵심이다. 13개 모달이 많아 보이지만, 각각이 독립적이라 수정이 간단하다. ColumnDeleteModal을 고칠 때 ColumnFormatModal에 영향을 주지 않는다.
createPortal 패턴은 모든 모달에 일관되게 적용했다. z-index 문제를 원천적으로 해결하고, 부모 컴포넌트의 overflow 설정에 영향받지 않는다.
DatasetCallbackModal의 PyArrow 코드 에디터는 가장 도전적인 부분이었다. 전용 코드 에디터 라이브러리(Monaco, CodeMirror)를 도입할 수도 있었지만, 번들 크기 증가가 부담스러워서 <textarea> 기반으로 직접 구현했다. 줄번호와 Tab 들여쓰기 정도면 충분히 사용 가능하다.
관련 글
- 문서 디렉토리 트리 UI: 파일 카운트와 컴팩트 레이아웃
FrontendNext.jsReact - Admin 모델 서빙 매니저: GPU 현황과 모델 배포 UI
FrontendGPUModel Serving - Workflow Execution Panel: 검증과 에러 핸들링 UI 패턴
FrontendReactSSE - MinIO 기반 모델 선택 UI: 로딩 상태와 에러 처리
FrontendMinIOModel Management - 인증 플로우 개선: 토큰 검증과 리프레시 처리
AuthenticationFrontendNext.js