DB데이터를 XLSX 파일로 내보내는 기능을 PHP로 만드는 방법은 관리자 페이지를 만들다 보면 거의 한 번은 꼭 만나게 됩니다.
회원 목록, 주문 내역, 예약 현황처럼 화면에서만 보는 데이터는 결국 엑셀로 내려받아야 하는 경우가 많기 때문입니다.
저도 실제로 관리자 페이지를 만들 때 처음에는 CSV로 처리했다가, 한글 깨짐과 서식 문제 때문에 결국 PHP에서 XLSX 파일을 직접 생성하는 방식으로 바꾼 적이 있습니다.
특히 실무에서는 단순히 “엑셀 저장”이 아니라, DB 조회 → 컬럼명 정리 → 헤더 출력 → 파일 다운로드 처리까지 한 번에 안정적으로 돌아가야 합니다.
이번 글에서는 그 흐름을 기준으로, PHP에서 가장 많이 쓰는 방식으로 정리해보겠습니다.
왜 CSV보다 XLSX가 편한가
처음에는 CSV가 더 간단해 보여서 많이 선택합니다.
그런데 실제 운영에서는 아래 문제가 자주 생깁니다.
- 한글이 깨진다
- 셀 너비나 숫자/날짜 형식을 제어하기 어렵다
- 여러 시트 구성이 어렵다
- 사용자가 “엑셀 파일처럼” 기대하는 형태와 다르다
그래서 관리자 기능에서는 보통 XLSX 다운로드가 더 만족도가 높습니다.
PHP에서 XLSX를 만들 때 가장 많이 쓰는 라이브러리
예전에는 PHPExcel을 많이 썼지만, 이 프로젝트는 오래전에 중단됐고 지금은 후속 라이브러리인 PhpSpreadsheet 사용이 권장됩니다.
Packagist 기준으로 PHPExcel은 2015년 마지막 버전 이후 deprecated 되었고, 공식 문서도 PhpSpreadsheet를 사용하도록 안내합니다.
또 현재 PhpSpreadsheet 최신 패키지는 최소 PHP 8.1을 요구합니다.
즉, 지금 새로 만드는 기능이라면 기준은 이렇습니다.
- 신규 개발: PhpSpreadsheet
- 기존 레거시 유지보수: PHP 버전에 맞는 구버전 검토
- PHPExcel 신규 사용: 비추천
진행 흐름은 이렇게 잡으면 된다
실무에서는 보통 아래 순서로 만듭니다.
- DB에서 데이터 조회
- 엑셀 헤더 행 작성
- 반복문으로 데이터 입력
- 파일명 지정
- 브라우저 다운로드 헤더 설정
- XLSX로 출력
구조만 잡아두면 회원관리, 예약관리, 문의관리 등 거의 모든 관리자 페이지에 재사용할 수 있습니다.
PhpSpreadsheet 설치하기
composer require phpoffice/phpspreadsheet
PhpSpreadsheet는 다양한 스프레드시트 포맷을 읽고 쓸 수 있고, Xlsx Writer로 파일 저장이 가능합니다.
공식 문서에서도 Xlsx writer를 사용해 파일로 저장하는 방식을 안내하고 있습니다.
가장 기본적인 XLSX 다운로드 예제
아래 예제는 MySQL/MariaDB에서 회원 목록을 조회해서 엑셀로 다운로드하는 가장 기본적인 형태입니다.
<?php
require __DIR__ . '/vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
$mysqli = new mysqli('localhost', 'db_user', 'db_pass', 'db_name');
if ($mysqli->connect_error) {
exit('DB 연결 실패: ' . $mysqli->connect_error);
}
$mysqli->set_charset('utf8mb4');
$sql = "
SELECT
userid,
username,
email,
phone,
regdate
FROM member
ORDER BY regdate DESC
";
$result = $mysqli->query($sql);
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setTitle('회원목록');
/* 헤더 */
$sheet->setCellValue('A1', '아이디');
$sheet->setCellValue('B1', '이름');
$sheet->setCellValue('C1', '이메일');
$sheet->setCellValue('D1', '연락처');
$sheet->setCellValue('E1', '가입일');
/* 데이터 */
$rowNum = 2;
while ($row = $result->fetch_assoc()) {
$sheet->setCellValue('A' . $rowNum, $row['userid']);
$sheet->setCellValue('B' . $rowNum, $row['username']);
$sheet->setCellValue('C' . $rowNum, $row['email']);
$sheet->setCellValue('D' . $rowNum, $row['phone']);
$sheet->setCellValue('E' . $rowNum, $row['regdate']);
$rowNum++;
}
/* 컬럼 너비 자동 조정 */
foreach (range('A', 'E') as $col) {
$sheet->getColumnDimension($col)->setAutoSize(true);
}
/* 파일명 */
$fileName = 'member_list_' . date('Ymd_His') . '.xlsx';
/* 다운로드 헤더 */
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment;filename="' . $fileName . '"');
header('Cache-Control: max-age=0');
$writer = new Xlsx($spreadsheet);
$writer->save('php://output');
exit;
이 코드만으로도 기본 다운로드는 바로 구현할 수 있습니다.
내가 실제로 먼저 막혔던 부분
처음 만들 때 가장 많이 헷갈리는 건 보통 이 3가지입니다.
- 브라우저에서 파일이 깨지는 문제
출력 전에 공백이나 BOM, warning 문구가 먼저 나오면 엑셀 파일이 깨질 수 있습니다.
예를 들면 이런 경우입니다.- PHP 시작 태그 전에 공백이 있음
echo,print_r,var_dump가 먼저 실행됨- include 파일에서 warning 발생
- UTF-8 BOM이 포함된 PHP 파일 사용
- 숫자가 이상하게 바뀌는 문제
전화번호, 우편번호, 긴 주문번호는 엑셀에서 숫자로 인식되면 값이 변형될 수 있습니다.
예를 들어:01012341234→ 앞자리 0이 사라짐- 긴 숫자 → 지수 표기
- 날짜처럼 생긴 값 → 날짜 포맷으로 자동 변환
- 데이터가 많을 때 느려지는 문제
회원 수가 몇 만 건을 넘어가면 브라우저 다운로드 방식이 버거워질 수 있습니다.- 몇 천 건 수준: 바로 다운로드
- 수만 건 이상: 조건 검색 후 생성
- 아주 큰 데이터: 배치 파일로 생성 후 다운로드 링크 제공
스타일을 조금만 넣어도 훨씬 보기 좋아진다
실무에서 엑셀은 내용도 중요하지만 첫 줄 제목 스타일만 넣어도 훨씬 정리된 느낌이 납니다.
<?php
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Fill;
$sheet->getStyle('A1:E1')->getFont()->setBold(true);
$sheet->getStyle('A1:E1')->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
$sheet->getStyle('A1:E1')->getFill()->setFillType(Fill::FILL_SOLID);
$sheet->getStyle('A1:E1')->getFill()->getStartColor()->setARGB('FFEFEFEF');
보통 여기까지만 적용해도 관리자 다운로드 파일 품질이 꽤 올라갑니다.
날짜와 금액은 형식을 지정해두면 편하다
조회 데이터에 예약일, 결제금액, 정산금액이 들어가면 서식을 지정하는 편이 좋습니다.
예를 들어 금액 컬럼이 F열이라면:
$sheet->getStyle('F2:F' . ($rowNum - 1))
->getNumberFormat()
->setFormatCode('#,##0');
날짜도 문자열로만 넣지 말고 상황에 따라 형식을 맞춰주면, 사용자가 엑셀에서 다시 가공하기 편해집니다.
조건 검색과 함께 엑셀 다운로드를 붙이면 실무형이 된다
실제 관리자에서는 전체 데이터를 다 받기보다, 검색 결과 그대로 다운로드하는 기능이 더 많이 쓰입니다.
예를 들면 이런 흐름입니다.
- 이름 검색
- 가입일 기간 검색
- 상태값 검색
- 정렬 적용
- 현재 결과만 엑셀 다운로드
즉, 리스트 화면에 사용한 WHERE 조건을 그대로 재사용하면 됩니다.
$where = " WHERE 1=1 ";
if (!empty($_GET['s_keyword'])) {
$keyword = $mysqli->real_escape_string($_GET['s_keyword']);
$where .= " AND username LIKE '%{$keyword}%'";
}
if (!empty($_GET['s_date_start'])) {
$startDate = $mysqli->real_escape_string($_GET['s_date_start']);
$where .= " AND regdate >= '{$startDate} 00:00:00'";
}
if (!empty($_GET['s_date_end'])) {
$endDate = $mysqli->real_escape_string($_GET['s_date_end']);
$where .= " AND regdate <= '{$endDate} 23:59:59'";
}
$sql = "
SELECT userid, username, email, phone, regdate
FROM member
{$where}
ORDER BY regdate DESC
";
이렇게 하면 사용자가 화면에서 보고 있는 데이터와 다운로드 파일 내용이 맞아떨어집니다.
함수로 빼두면 재사용하기 좋다
이 기능은 한 번 만들고 끝나는 경우가 거의 없습니다.
- 회원 목록
- 주문 목록
- 예약 목록
- 상담 신청 목록
- 게시판 신청 내역
이런 식으로 계속 늘어나기 때문에, 공통 함수로 빼두면 편합니다.
예를 들면 이런 구조입니다.
<?php
function download_xlsx(array $headers, array $rows, string $sheetTitle, string $fileName): void
{
require __DIR__ . '/vendor/autoload.php';
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setTitle($sheetTitle);
$colIndex = 1;
foreach ($headers as $header) {
$sheet->setCellValueByColumnAndRow($colIndex, 1, $header);
$colIndex++;
}
$rowNum = 2;
foreach ($rows as $row) {
$colIndex = 1;
foreach ($row as $value) {
$sheet->setCellValueByColumnAndRow($colIndex, $rowNum, $value);
$colIndex++;
}
$rowNum++;
}
foreach (range(1, count($headers)) as $i) {
$column = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($i);
$sheet->getColumnDimension($column)->setAutoSize(true);
}
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment;filename="' . $fileName . '"');
header('Cache-Control: max-age=0');
$writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet);
$writer->save('php://output');
exit;
}
이렇게 만들어두면 다른 관리자 메뉴에서도 쉽게 재사용할 수 있습니다.
레거시 PHP 사이트라면 꼭 확인할 점
여기서 중요한 포인트가 하나 있습니다.
최신 PhpSpreadsheet는 현재 PHP 8.1 이상이 필요합니다. 그래서 PHP 7.x 또는 그 이하 환경이라면 최신 버전을 바로 넣기 어렵습니다.
레거시 프로젝트라면 보통 다음 중 하나로 갑니다.
- 서버 PHP 버전 업그레이드
- 호환 가능한 구버전 검토
- 기존 CSV 방식 유지
- 별도 엑셀 생성 서버 분리
예전에 PHP 5.x, 7.4 사이트 유지보수할 때 이 부분 때문에 설치부터 막히는 경우가 많았습니다.
그래서 기능 구현 전에 서버 PHP 버전부터 확인하는 게 가장 먼저입니다.