일본 급여 계산기에 급여명세서를 XLSX로 내려받는 기능을 추가했다. 파일 생성까지는 별문제가 없었는데, 실제로 내려받은 파일을 Windows Excel에서 열자 Excel이 파일을 복구했다.
복구 로그에는 이 문장이 남았다.
Removed Records: Cell information from /xl/worksheets/sheet1.xml
로그가 가리키는 곳은 sheet1.xml의 셀 정보였다.
코드에서는 E17, F17이 먼저 들어갈 수 있었다
이번 급여명세서는 기존 엑셀 템플릿에 값을 채우는 방식이 아니다. JavaScript에서 셀과 스타일, 수식 등을 구성한 뒤 XLSX를 직접 생성한다.
워크시트 XML은 대략 이런 구조다.
<worksheet>
<sheetData>
<row r="17">
<c r="B17">...</c>
<c r="C17">...</c>
<c r="E17">...</c>
<c r="F17">...</c>
</row>
</sheetData>
</worksheet>
그런데 실제 생성 코드는 왼쪽 셀부터 차례대로 만드는 구조가 아니었다. 일부 행에서는 오른쪽의 E17, F17을 먼저 추가하고 나중에 B17, C17을 추가했다.
정렬 과정이 없었기 때문에 개념상 다음과 같은 순서가 만들어질 수 있었다.
<row r="17">
<c r="E17">...</c>
<c r="F17">...</c>
<c r="B17">...</c>
<c r="C17">...</c>
</row>
이 XML은 문제가 발생한 XLSX에서 그대로 추출한 내용이 아니다. 당시 생성 코드에서 확인한 셀 추가 순서를 단순화한 예다.
Microsoft의 Open XML 예제에서도 같은 행에 셀을 넣을 때 CellReference 순서에 맞는 위치를 찾아 넣고 있었다. 생성 코드에서 빠져 있던 것도 이 순서 처리였다.
같은 행의 셀을 주소 순서로 정렬했다
기존에는 셀 XML 문자열 자체를 배열에 넣었다.
list.push('<c r="' + address + '" ...');
셀 주소를 별도로 보관하지 않으니 나중에 정렬하기도 어려웠다. 그래서 주소와 XML을 함께 저장하도록 바꿨다.
list.push({
address: address,
markup: '<c r="' + address + '" ...'
});
그리고 sheetData를 만들기 직전에 같은 행의 셀을 열 번호 기준으로 정렬했다.
문제가 있던 형태를 예로 들면,
E17 → F17 → B17 → C17
순서로 들어갈 수 있던 셀이 최종 XML에서는
B17 → C17 → E17 → F17
순서가 된다.
생성 테스트에도 행별 셀 주소가 오름차순인지 확인하는 검사를 추가했다. 당시 문제가 있었던 형태를 포함해 B17, C17, E17, F17 순서도 검사 대상에 넣었다.
마지막 확인은 Windows Excel에서 했다
코드 수정 후 별도의 테스트 환경에서 생성한 XLSX는 셀 주소가 순서대로 나왔고 기존 계산 결과와 수식도 유지됐다.
처음 문제가 발생한 곳은 Windows Excel이었으니 마지막 확인도 Excel에서 했다. 수정된 코드로 XLSX를 새로 생성해서 다시 열어봤고, 이전에 나오던 복구 메시지는 더 이상 나타나지 않았다.