DB데이터를 XLSX 파일로 내보내는 기능을 PHP로 만드는 방법

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 신규 사용: 비추천

진행 흐름은 이렇게 잡으면 된다

실무에서는 보통 아래 순서로 만듭니다.

  1. DB에서 데이터 조회
  2. 엑셀 헤더 행 작성
  3. 반복문으로 데이터 입력
  4. 파일명 지정
  5. 브라우저 다운로드 헤더 설정
  6. 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가지입니다.

  1. 브라우저에서 파일이 깨지는 문제
    출력 전에 공백이나 BOM, warning 문구가 먼저 나오면 엑셀 파일이 깨질 수 있습니다.
    예를 들면 이런 경우입니다.
    • PHP 시작 태그 전에 공백이 있음
    • echo, print_r, var_dump가 먼저 실행됨
    • include 파일에서 warning 발생
    • UTF-8 BOM이 포함된 PHP 파일 사용
  2. 숫자가 이상하게 바뀌는 문제
    전화번호, 우편번호, 긴 주문번호는 엑셀에서 숫자로 인식되면 값이 변형될 수 있습니다.
    예를 들어:
    • 01012341234 → 앞자리 0이 사라짐
    • 긴 숫자 → 지수 표기
    • 날짜처럼 생긴 값 → 날짜 포맷으로 자동 변환
  3. 데이터가 많을 때 느려지는 문제
    회원 수가 몇 만 건을 넘어가면 브라우저 다운로드 방식이 버거워질 수 있습니다.
    • 몇 천 건 수준: 바로 다운로드
    • 수만 건 이상: 조건 검색 후 생성
    • 아주 큰 데이터: 배치 파일로 생성 후 다운로드 링크 제공

스타일을 조금만 넣어도 훨씬 보기 좋아진다

실무에서 엑셀은 내용도 중요하지만 첫 줄 제목 스타일만 넣어도 훨씬 정리된 느낌이 납니다.

<?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 버전부터 확인하는 게 가장 먼저입니다.