문제
한 프로젝트 안에서 엑셀을 내보내는 경로가 두 개였고, 각각 다른 라이브러리를 사용 중.
| 경로 | 라이브러리 | 헤더 스타일 |
|---|---|---|
| AG Grid export | xlsx |
불가 |
| 리포트 export | exceljs |
적용됨 |
-
xlsx(SheetJS) 커뮤니티 버전의 제약 — 셀 스타일 미지원 - 결과 — AG Grid에서 내보낸 파일만 헤더가 밋밋
사용자 입장에서는 같은 제품에서 받은 두 엑셀 파일의 생김새가 다른 셈이다.
경계를 정하는 게 먼저였다
-
excelExport.ts확인 결과: 코드가 두 종류로 분리 가능
순수 로직
-
resolveCellValue— 셀 값 해석 -
buildSheetAoa— AG Grid의 컬럼 정의와 노드에서 2차원 배열 생성 - 담긴 도메인 규칙 — “headerName이 있는 컬럼만 포함”, “valueFormatter 반영”, “isHeader 밴드행 제외”
렌더부
-
하는 일 — 2차원 배열을 실제 워크북으로 만들고 파일로 출력
-
라이브러리에 묶인 범위: 렌더부뿐
그래서 순수 로직은 그대로 두고 렌더부만 교체하기로 범위를 확정했다.
- 이 경계의 효과: 도메인 규칙 재검증 불필요, 기존 테스트 절반 이상 그대로 유효
교체
export async function exportGridsAsExcel(
sheets: ExcelSheetSpec[],
options: { fileName?: string } = {},
): Promise<void> {
const { fileName = 'export.xlsx' } = options;
const ExcelJS = (await import('exceljs')).default;
const wb = new ExcelJS.Workbook();
for (const { api, columnDefs, sheetName } of sheets) {
const { headers, rows } = buildSheetAoa(api, columnDefs);
await writeSheet(wb, sheetName, headers, rows);
}
// ...
}
-
buildSheetAoa호출 — 그대로 유지 - 변경 범위 — 그 아래 워크북 생성 부분
-
헤더 스타일 — 리포트 export 쪽이 이미 쓰던
excelStyle.ts헬퍼(addSheet,drawHeader,drawDataRow,freezeAt) 재사용 - 두 경로가 같은 헬퍼 사용 → 생김새 자동 통일
- 컬럼 폭 — 헤더 텍스트 길이 기반 계산 함수 추가
function computeColumnWidths(headers: string[]): number[] {
return headers.map((h) => Math.min(40, Math.max(10, h.length * 2 + 4)));
}
예상 못 한 파급: 함수가 async가 됐다
-
exceljs의writeBuffer()— Promise 반환 -
xlsx의XLSX.write()— 동기 - 결과 — export 함수 시그니처가
void에서Promise<void>로 변경 - 파급 지점 — 호출부. 이 함수들은 대부분 버튼 클릭 핸들러에서 fire-and-forget으로 호출 중
onClick={() => void exportGridsAsExcel(...)}
- 걸리는 규칙 — TypeScript
no-floating-promises - 조치 — 호출부 8곳에
void표기 추가 -
의미 —
await하지 않는 게 의도임을 명시 - 여기서 판단 하나 발생
- 대안: 호출부를 전부
async핸들러로 전환해await+ 로딩 상태 노출 - 기각 사유: 작업 범위 초과 — 8개 화면의 UX를 동시 변경하는 일
- 판단: 라이브러리 교체와 로딩 UX 개선은 별개의 변경이므로 미혼합
테스트
-
excelExport.test.ts— 140줄 갱신 - 순수 로직 테스트(
resolveCellValue,buildSheetAoa) — 대부분 무변경 - 바뀐 부분 — 워크북 생성 결과 검증
- 변경 성격 — 테스트도 async가 되면서 assertion 앞에
await가 붙는 기계적 변경이 상당수
남는 교훈
라이브러리 교체 작업에서 가장 중요한 건 어디까지가 라이브러리에 묶인 코드인지 선을 긋는 것이었다.
- 선을 그은 뒤 실제 재작성 범위:
excelExport.ts한 파일의 절반 정도 -
나머지 9개 파일의 변경: 전부
void한 글자 -
가정:
buildSheetAoa같은 도메인 규칙이 라이브러리 호출과 뒤엉킨 경우 — 예컨대XLSX.utils객체를 셀 단위로 조작하며 필터링 규칙까지 그 안에서 처리 — 교체 비용 몇 배
이건 결과적으로 원래 코드가 잘 짜여 있었다는 얘기이기도 하다. 라이브러리를 바꿀 계획이 없더라도 순수 로직과 I/O를 분리해두면 이럴 때 값을 한다. - 기억할 점: 동기 함수가 async가 되며 생기는 파급
- 라이브러리 API의 동기·비동기 성질은 함수 시그니처를 타고 호출부까지 전파
- 사전 점검 항목: “이 라이브러리의 대응 API가 async인가” → 작업량 사전 가늠 가능