SpreadJS 엑셀 로딩 성능 최적화2

SpreadJS 엑셀 로딩 성능 최적화2

- 실행 시간 측정을 통해 불필요한 데이터 바인딩 작업을 찾아 개선한 경험 -

1. 문제 상황

프로젝트에서 React와 TypeScript를 기반으로 Excel 업무 화면을 구현하고 있으며, Excel 파일을 웹에서 조회하고 수정할 수 있도록 SpreadJS(v17.0.1)를 사용하고 있습니다. 사용자가 업로드한 Excel 파일을 템플릿으로 저장하고, 상세 화면에서는 해당 템플릿에 실제 데이터를 연결하여 화면에 표시하는 구조입니다.

문제는 Excel 상세 화면에 진입할 때 초기 로딩 시간이 지나치게 오래 걸린다는 것이었습니다. 화면에 진입하면 바로 Excel이 표시되지 않고 먼저 빈 화면이 나타난 뒤 로딩 화면이 표시되고, 다시 일정 시간이 지난 후 Excel 화면이 나타났습니다. 개발 환경에서 측정한 결과 전체 로딩 시간은 약 25~30초 정도였습니다. 사용하는 Excel 파일은 총 28개의 시트로 구성되어 있었고, 시트 간 참조와 수식 등이 포함되어 있어 구조가 복잡한 편이었습니다.

이처럼 복잡한 로딩 과정에서 어느 구간이 실제로 시간을 잡아먹는지 단계별로 측정하며 병목을 찾아 개선한 과정을 공유하고자 합니다.

2. Excel 파일 및 행·열 범위 확인

처음에는 Excel 파일의 크기와 시트의 행·열 범위를 확인했습니다. 이전에 SpreadJS를 사용하면서 실제 데이터보다 불필요하게 큰 행·열 범위가 로딩 성능에 영향을 주었던 문제를 개선한 경험이 있었기 때문에, 이번에도 같은 원인일 가능성을 먼저 확인했습니다.

(※ 이전 행·열 범위 관련 성능 개선 사례는 https://www.nextree.io/spreadjs-egsel-roding-seongneung-coejeoghwa/ 에서 확인할 수 있습니다.)

이전 사례에서는 실제 데이터가 약 100행에 불과했지만 Excel 내부 시트의 행 범위가 최대 행인 1,048,576행까지 설정되어 있었습니다. 실제 데이터와 관계없이 큰 범위를 처리하게 되면서 로딩 시간이 증가했고, 불필요하게 설정된 범위를 줄이는 방식으로 개선했습니다.

이번에도 Excel의 행·열 범위를 확인한 결과 일부 시트의 범위가 실제 데이터에 비해 크게 설정되어 있었습니다. 프로젝트에서는 Excel 입력 범위를 최대 1,000행, 1,000열까지 사용하도록 정해져 있었기 때문에 해당 범위로 제한하여 저장하는 테스트를 진행했습니다. 그 결과 SJS 데이터의 용량은 다음과 같이 감소했습니다.

- 수정 전: 약 5.35 MB (5,357,864 bytes)

- 수정 후: 약 4.67 MB (4,675,857 bytes) 

하지만 파일 크기가 감소했음에도 로딩 시간은 크게 줄어들지 않았습니다. 따라서 이번 문제의 핵심 원인은 단순히 Excel 파일 크기나 행·열 범위 때문은 아니라고 판단했습니다. 이전에는 Excel 자체의 불필요한 범위를 줄여 성능을 개선했다면, 이번에는 Excel을 불러온 이후 화면에서 수행되는 작업을 확인할 필요가 있었습니다.

3. 로딩 과정별 실행 시간 측정

파일 자체가 원인이 아니라면 실제로 어느 작업에서 시간이 발생하는지 확인할 필요가 있었습니다. 그래서 Excel을 화면에 표시하는 과정을 여러 단계로 나누어 실행 시간을 측정했습니다. 대표적인 측정 결과는 다음과 같았습니다.

작업

소요 시간

workbook.open

약 2.3초

BindingPath 정보 구성

약 25ms

BindingPath 데이터 가공

약 1ms

CellBindingSource 생성

약 2ms

setDataSource

약 18초

결과는 예상과 달리 Excel 파일을 실제로 읽어오는 `workbook.open()`에는 약 2.3초가 걸렸지만, 이후 BindingPath를 확인하거나 데이터를 가공하는 과정은 대부분 수십 밀리초 이하였습니다. 반면 `setDataSource()`를 실행하는 구간에서는 약 18초가 소요되었습니다. 이를 통해 이번 로딩 지연의 주요 원인이 Excel 파일을 읽는 과정이 아니라 Excel을 읽은 이후 데이터를 시트에 연결하는 과정에 있다는 것을 확인했습니다.

4. 전체 시트의 데이터 바인딩 처리 확인

원인을 더 구체적으로 확인하기 위해 기존 코드를 살펴봤습니다. 기존 구현은 모든 시트를 대상으로 `setDataSource()`를 실행하고 있었습니다.

const dataSource = new GC.Spread.Sheets.Bindings.CellBindingSource(data);

for (let i = 0; i < workbook.getSheetCount(); i++) {
    const sheet = workbook.getSheet(i);
    sheet.setDataSource(dataSource);
}

이번 Excel은 총 28개의 시트로 구성되어 있었지만, 실제로 데이터 바인딩이 필요한 시트는 일부였습니다.

[기존 구조] 28개 시트 전체 순회 -> 모든 시트에 setDataSource() 실행

여기서 시트별 실행 시간을 측정해 보았습니다.

- Sheet 1 : 약 1,021ms

- Sheet 2 : 약 836ms

- Sheet 3 : 약 805ms

- Sheet 4 : 약 812ms

- Sheet 5 : 약 783ms

- Sheet 6 : 약 771ms

시트 하나를 처리하는 데 약 0.7~0.9초 정도가 걸렸습니다. 더 중요한 점은 바인딩할 경로(BindingPath)가 없는 시트에서도 `setDataSource()` 호출에 상당한 시간이 소요되고 있었다는 것입니다. 따라서 이번 문제는 바인딩이 필요하지 않은 시트까지 동일한 작업을 반복 수행하고 있었던 것이 주요 원인이라고 판단했습니다.

다만 왜 바인딩 경로가 없는 시트에서도 setDataSource() 호출 자체에 시간이 소요되는지, SpreadJS 내부 동작은 추후 별도로 검증해 볼 예정입니다.

5. 데이터 바인딩 대상 시트 선별

기존에는 모든 시트에 `setDataSource()`를 실행했지만, 실제로 데이터 바인딩 대상이 되는 시트만 선별하여 처리하도록 구조를 변경했습니다.

const dataSource = new GC.Spread.Sheets.Bindings.CellBindingSource(data);

for (let i = 0; i < workbook.getSheetCount(); i++) {
    const sheet = workbook.getSheet(i);

    if (!sheet.visible()) {
        continue;
    }

    if (!isBindingTargetSheet(sheet)) {
        continue;
    }

    sheet.setDataSource(dataSource);
}

- 다른 시트의 수식/참조용으로만 사용되는 숨김(Hidden) 시트 역시 데이터 바인딩 대상에서 제외하여 불필요한 연산을 방지했습니다.

- `isBindingTargetSheet()`는 해당 시트에 BindingPath가 존재하는지 확인하는 로직입니다. 하나라도 BindingPath가 발견되면 데이터 바인딩 대상 시트로 판단합니다.

구조를 정리하면 다음과 같습니다.

[개선 후 구조]

모든 시트 순회 -> 데이터 바인딩 필요 여부 확인 -> 필요한 시트에만 setDataSource() 실행

6. 로딩 시간 개선 결과

기존 구현에서 상세 화면의 전체 로딩 시간은 테스트 환경 기준 약 25~30초였습니다. 데이터 바인딩이 필요한 시트만 선별 처리하도록 변경한 후에는 약 15~20초 수준으로 감소했습니다.

[개선 전] 약 25~30초

           ↓

[개선 후] 약 15~20초 (약 40~50% 단축)

테스트 환경 기준 약 40~50%의 로딩 시간 감소를 확인할 수 있었습니다. (※ 사용자 PC 사양, 브라우저 환경, Excel 구조에 따라 차이가 있을 수 있습니다.)

파일 용량을 줄이는 것만으로는 크게 개선되지 않았던 로딩 지연을, 로딩 이후 불필요하게 수행되던 작업의 범위를 줄여 상당 부분 개선할 수 있었습니다.

7. 마무리

이번 성능 개선은 파일 크기나 Excel 구조만을 원인으로 가정하지 않고, 실제 로딩 과정을 단계별로 측정하여 병목을 확인한 사례입니다.

처음에는 이전 경험을 바탕으로 Excel의 행·열 범위를 원인으로 예상했지만, 파일 크기를 줄인 후에도 로딩 시간이 크게 개선되지 않았습니다. 이후 로딩 과정별 실행 시간을 측정하면서 `workbook.open()`보다 `setDataSource()`에 훨씬 많은 시간이 소요되는 것을 확인했고, 시트별 측정을 통해 데이터 바인딩이 필요하지 않은 시트에서도 해당 작업이 수행되고 있음을 발견했습니다.

이번 개선으로 테스트 환경에서 초기 로딩 시간을 약 25~30초에서 15~20초로 단축할 수 있었습니다. 다만 최종 로딩 시간 역시 추가 개선의 여지가 있다고 판단하여, 이후에는 Excel 로딩 이후 수행되는 다른 작업까지 범위를 넓혀 최적화할 예정입니다.

복잡한 Excel 화면의 성능 문제를 확인할 때는 파일 용량뿐만 아니라 실제 로딩 과정에서 어떤 작업이 얼마나 많은 시간을 사용하는지 측정하고, 필요한 작업만 수행하도록 범위를 줄이는 것이 중요하다는 점을 확인할 수 있었습니다.

참고 문서

[SpreadJS Data Binding 공식 문서]

https://developer.mescius.com/spreadjs/docs/v19/features/binding

JY

Site footer