쿼리 결과를 Excel 보고서로 바꾸는 일은 사실 한 코트를 걸친 세 가지 문제입니다. Delphi의 각 필드 타입은 올바른 Excel 타입으로 셀에 안착해야 하고, 헤더 행은 스키마 덤프가 아니라 보고서처럼 읽혀야 하며, 숫자와 날짜와 금액은 변환 과정에서 살아남는 서식을 지녀야 합니다. 이 중 하나라도 건너뛰면 파일은 여전히 열리고 그럴듯해 보이지만, 재무 담당자가 열을 선택하고 합계를 기다리는 순간 나타나지 않는 값 때문에 실패합니다. 값이 텍스트로 기록되었기 때문에 Excel은 이를 레이블로 취급하고, 이를 경고하는 예외는 어디에서도 발생하지 않습니다
HotXLS는 Delphi와 C++Builder에서 XLS 및 XLSX 파일을 직접 작성하는 네이티브 Object Pascal 스프레드시트 라이브러리로, Excel 자동화가 전혀 관여하지 않습니다. TDataset에서 통합 문서로 가는 두 가지 경로를 제공합니다: 바로 꽂아 쓰는 TDataToXLS 컴포넌트와, 워크북 API를 직접 호출하는 수작업 루프입니다. 이 둘은 서로 바꿔 쓸 수 있는 관계가 아닙니다. 이 컴포넌트는 XLS 파사드 위에 구축된 VCL 시민이므로, 어느 쪽을 선택해야 하는지는 코드가 어디서 실행되는지와 소비자가 어떤 파일 형식을 기대하는지에 달려 있습니다. 이어서 두 경로 모두와, 컴포넌트가 더 이상 적절한 도구가 아니게 되는 지점, 그리고 어느 쪽을 선택하든 필드 타입을 온전히 유지하는 방법을 다룹니다
필드 타입이야말로 진짜 내보내기 계약입니다
API를 호출하기 전에, 각 Delphi 필드 타입이 셀에 어떻게 안착할지부터 정해야 합니다. Delphi 문자열을 받은 셀은 문자열로 남습니다. HotXLS는 '1,234.50'이 숫자를 의도한 것이라고 추측하지 않으며, 그래서는 안 됩니다. 로케일에 의존하는 재파싱이야말로 독일식 소수점 콤마가 영어 서버에서 천 단위 구분자로 둔갑하는 정확한 원인이기 때문입니다. 신뢰할 수 있는 패턴은 타입이 지정된 접근자를 통해 값을 대입하는 것입니다. 숫자 필드에는 AsFloat 또는 AsCurrency를, 날짜에는 서식화된 문자열이 아니라 진짜 Excel 날짜 일련번호를 셀에 담기 위해 AsDateTime을, 그리고 실제로 텍스트인 필드에만 AsString을 사용합니다
NULL 처리는 기본값에 맡길 것이 아니라 명시적으로 결정해야 합니다. VarToStr로 필드 값을 변환하면 SQL NULL이 빈 문자열이 되어 텍스트 셀이 되지만, 대입 자체를 건너뛰면 셀이 진짜로 비게 되며 이것이 바로 AVERAGE, COUNT, 피벗 테이블 소비자가 기대하는 바입니다. 금액 열의 경우, 루프를 작성하기 전에 NULL이 0을 의미하는지 알 수 없음을 의미하는지 결정해야 합니다. 누군가 해당 열에 서식을 적용하고 나면 이 둘은 겉보기에 똑같아지지만, 그 차이는 이후 계산되는 모든 집계 값을 바꿔 놓습니다
컴포넌트 경로: VCL 애플리케이션의 TDataToXLS
이미 데이터 모듈에 쿼리가 연결되어 있는 전형적인 VCL 애플리케이션이라면, TDataToXLS가 한 번의 호출로 끝나는 경로입니다. FireDAC이든 ADO든 IBX든, 추상 데이터셋 인터페이스를 구현하는 것이라면 무엇이든 TDataset 후손을 순회하며, 헤더 캡션과 글꼴, 테두리, 선택적인 그룹 소계, 대규모 결과 집합에 대한 자동 시트 분할을 갖춘 스타일이 적용된 워크시트를 생성합니다
var
Exporter: TDataToXLS;
begin
Exporter := TDataToXLS.Create(nil);
try
Exporter.Dataset := OrdersQuery; // 임의의 TDataset 후손
Exporter.WorksheetName := 'Orders';
Exporter.HeaderSource := hsDisplayLabel; // 원본 열 이름이 아니라 캡션
Exporter.GroupFields.Add('CustomerID'); // 고객별 소계 블록
Exporter.RowsPerSheet := 50000; // BIFF8 행 상한 아래로 유지
Exporter.VisibleFieldsOnly := True; // Field.Visible을 존중
Exporter.SaveDatasetAs('orders.xls');
finally
Exporter.Free;
end;
end;
실제 운영에서 무게를 지탱하는 것은 대부분 두 속성입니다. HeaderSource := hsDisplayLabel은 원본 SQL 열 이름 대신 각 필드의 DisplayLabel을 기록하므로, 통합 문서에는 CUST_NM 대신 "Customer Name"이 표시됩니다. RowsPerSheet가 존재하는 이유는 이 컴포넌트가 그리드가 65,536행 × 256열에서 멈추는 BIFF8을 기록하기 때문입니다. 이를 50,000으로 설정하면 형식 상한이 데이터를 잘라내기 전에 큰 결과 집합을 여러 시트로 나눕니다. 외관은 HeaderFont, DetailFont, GroupColor와 테두리 스타일 속성들이 담당하며, 소비자가 순수한 셀을 원할 때는 DisableFormat 집합이 서식 범주 전체를 끌 수 있습니다. 그 밖의 맞춤 처리가 필요하다면 AfterCell과 AfterRow 이벤트가 방금 기록된 범위를 후처리할 수 있도록 넘겨줍니다
컴포넌트의 한계
TDataToXLS에는 세 가지 제약이 설계 단계부터 내재되어 있으며, 이를 미리 알아두면 두 스프린트 뒤에 어색한 재설계를 하지 않아도 됩니다
- 온전한 의미에서의 VCL 컴포넌트입니다. 이 유닛은
Forms,Controls,Dialogs를 끌어들이므로, 콘솔 작업이나 Windows 서비스에 연결하면 바이너리에 VCL 전체가 딸려 옵니다. 핵심 워크북 유닛에는 그런 의존성이 없습니다.Windows,Classes,SysUtils,Variants만 있으면 되며, 그래서 서버 측 코드는 아래에 나오는 루프를 사용해야 합니다 - XLS 파사드 위에 구축되어 있습니다. 이 컴포넌트는
IXLSWorkbook을 채워서 .xls(BIFF8)를 기록합니다. OOXML 출력으로 전환하는 속성은 존재하지 않습니다 - 이벤트는 XLS 방언으로 말합니다.
AfterCell의Cell: IXLSRange매개변수는 XLS 객체 모델에 속하므로, 그곳에 작성한 셀 단위 맞춤 코드는 파일이 나중에 .xlsx로 변환되더라도 XLS 스타일 코드로 남습니다
컴포넌트의 출력에서 .xlsx를 생성하는 방법
소비자가 .xlsx를 요구하지만 내보내기 로직이 이미 TDataToXLS에 자리 잡고 있다면, lxXlsxExport 유닛의 브리지 함수가 채워진 워크북을 한 번의 호출로 변환합니다:
uses lxXlsxExport;
Exporter.SaveDatasetAs('orders.xls');
// 컴포넌트가 채운 IXLSWorkbook을 노출합니다
SaveXLSWorkbookAsXLSX(Exporter.Workbook, 'orders.xlsx');
이 브리지는 완전한 충실도의 변환기가 아니라 표 형태 데이터의 운반체로 취급해야 합니다. 값, 수식, 숫자 서식, 채우기 색상, 글꼴 속성, 열 너비, 보기 설정을 복사합니다. 테두리, 병합된 범위, 코멘트, 차트, 조건부 서식은 의도적으로 복사하지 않습니다. 헤더와 행으로 이루어진 평범한 그리드라면 이것으로 충분합니다. 스타일이 적용된 보고서라면 충분하지 않으며, 정직한 해결책은 변환된 파일을 손보는 대신 XLSX를 처음부터 직접 생성하는 것입니다
서비스와 배치 작업을 위한 수작업 루프
서버 측 코드는 TXLSXWorkbook을 직접 대상으로 삼아야 합니다. 어떤 샘플이든 복사하기 전에 두 파사드 사이의 수명 주기 차이를 알아두어야 합니다. XLS 쪽의 TXLSWorkbook은 참조 카운트가 매겨진 인터페이스로 유지되며 수동으로 해제해서는 안 되는 반면, TXLSXWorkbook은 try..finally Free가 필요한 일반 클래스입니다. 두 관례를 뒤섞는 것은 누수나 이중 해제를 만들어내는 확실한 방법입니다
procedure ExportOrders(Q: TDataSet; const FileName: string);
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Row: Integer;
begin
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Orders');
Sheet.Cells[1, 1].Value := 'Order No';
Sheet.Cells[1, 2].Value := 'Customer';
Sheet.Cells[1, 3].Value := 'Ordered';
Sheet.Cells[1, 4].Value := 'Amount';
Row := 2;
Q.First;
while not Q.Eof do
begin
Sheet.Cells[Row, 1].Value := Q.FieldByName('OrderNo').AsInteger;
Sheet.Cells[Row, 2].Value := Q.FieldByName('Customer').AsString;
if not Q.FieldByName('Ordered').IsNull then
Sheet.Cells[Row, 3].Value := Q.FieldByName('Ordered').AsDateTime;
Sheet.Cells[Row, 4].Value := Q.FieldByName('Amount').AsFloat;
Inc(Row);
Q.Next;
end;
Book.StreamingWrite := True; // 시트 XML을 즉시 zip으로 스트리밍합니다
Book.SaveAs(FileName);
finally
Book.Free;
end;
end;
여기서 중요한 것은 타입이 지정된 대입문과 IsNull 가드입니다. 날짜는 날짜 일련번호로, 금액은 배정밀도 실수로 도착하며, NULL인 주문 날짜는 빈 문자열이 되는 대신 진짜로 비어 있는 채로 남습니다. StreamingWrite := True는 저장 경로만 바꿉니다. 워크시트 XML은 먼저 하나의 거대한 문자열로 조립되는 대신 zip 컨테이너로 곧바로 스트리밍되며, 이는 수십만 행 규모에서 SaveAs 시점의 메모리 스파이크를 완화합니다. 모든 저장 메서드에는 TStream 오버로드도 있으므로, 워크북을 디스크에 닿지 않고 곧바로 HTTP 응답으로 보낼 수 있습니다. 스트리밍 쓰기와 배치 작업 문서는 이러한 배포 패턴을 자세히 다루며, 대규모 워크북 성능 문서는 행 수가 더 늘어날 때 취해야 할 조치를 다룹니다
이 루프는 스레드 전반으로 확장되는 경로이기도 합니다. 두 엔진 모두 네이티브 Object Pascal 작성기입니다. 한쪽은 BIFF8 레코드 스트림, 다른 쪽은 OOXML zip과 XML이므로, 내보내기의 어떤 부분도 COM 자동화를 건드리거나 서버에 Excel 라이선스를 필요로 하지 않습니다. 이것이 가져다주는 것은 각 스레드가 자신의 워크북을 구축하기만 한다면 단일 인스턴스 병목 없는 병렬성입니다. 워크북 객체는 공유 사용에 대해 스레드 안전하지 않으므로, 규칙은 내보내기당 하나의 인스턴스이며, 락으로 보호되는 공유 인스턴스는 결코 사용하지 않는 것입니다
이를 중심으로 설계하기 전에 알아둘 가치가 있는 한계가 하나 있습니다. XLSX 그리드는 1,048,576행 × 16,384열에서 멈추므로, XLS 쪽에서 RowsPerSheet가 처리하는 시트 분할이 여기서는 거의 필요하지 않습니다. 백만 행짜리 워크북은 사람이 소비하고 싶어하는 대상도 아닌 경우가 많습니다. 결과 집합이 정말로 그 정도 규모라면 구분자로 나눈 파일이 대체로 더 나은 계약이며, CSV와 TSV 내보내기 문서는 그 경우에 적용되는 구분자, BOM 동작, 수식 평가 관련 주의사항을 다룹니다
시작점 고르기
내보내기가 VCL 데스크톱 도구 안에 있고 .xls 출력으로 충분하다면, TDataToXLS와 그룹화 지원부터 시작하십시오. 코드량이 가장 적으며, 이미 설명한 충실도 한계를 받아들일 수 있는 한 나중에 누군가 .xlsx를 요구할 때 SaveXLSWorkbookAsXLSX를 통한 브리지가 준비되어 있습니다. 코드가 무인으로 실행되거나 소비자가 처음부터 .xlsx를 요구한다면 루프를 작성하십시오. 두 경로 모두 작동하는 데모 프로젝트와 함께 제공되며 HotXLS Delphi Component 패키지의 일부입니다