본문 바로가기

프로그래밍/spreadjs

SpreadJS Excel 대용량 Import/Export 실전: 메모리 한계, 진행률 UI, 에러 복구 전략

반응형

왜 대용량 Excel Import/Export가 자주 실패할까

 

업무에서 수십만 행의 Excel을 다루면 브라우저가 멈추거나 메모리 부족 경고가 뜨기 쉽습니다. 대용량 Import/Export는 성능뿐 아니라 사용자 경험과 복구 전략까지 함께 설계해야 합니다.

 

SpreadJS를 쓰면 화면에서 바로 Excel을 불러오고 저장할 수 있지만, 파일 크기와 시트 수가 늘면 처리 시간이 길어집니다. 이때 진행률 표시가 없으면 사용자는 오작동으로 오해하고 이탈할 수 있습니다.

 

또한 변형된 서식, 수식 오류, 병합 셀 같은 요소가 예상치 못한 예외를 만들 수 있습니다. 실무에서는 중간 저장, 에러 위치 안내, 재시도 경로가 준비되어야 업무 중단을 줄일 수 있습니다.

 

이 글은 메모리 한계에 부딪히지 않도록 분할 처리 관점으로 접근하고, 간단한 진행률 UI와 오류 복구 흐름을 예제로 정리합니다. 지금 필요한 것은 “빠르게 불러오기”가 아니라 “끝까지 안전하게 완료하기”입니다.

 

실패를 부르는 패턴과 성공 사례로 보는 핵심 개념

 

브라우저에서 Excel을 한 번에 모두 올리면, 파일 해제 -> 시트 파싱 -> 셀 생성이 연속으로 메모리를 잡아먹습니다. 대용량에서는 “일괄 처리” 대신 “분할 처리+중간 저장”이 기본입니다.

 

실패 사례는 보통 다음 흐름에서 발생합니다: 큰 파일 업로드 -> UI 프리즈 -> 사용자가 새로고침 -> 임포트 중단. 반대로 성공 사례는 작은 청크로 읽고, 진행률을 보여주며, 에러가 나면 해당 범위만 건너뛰고 계속 진행합니다.

 

적용 순서는 단순합니다. - 파일을 시트/범위 단위로 나누어 읽습니다.

 

- 진행률 바·취소 버튼을 함께 둡니다. - 변형된 서식·병합 셀은 로그를 남기고 스킵 후 계속합니다.

 

- 중간 스냅샷(예: 1만 행 단위)을 저장해 재시작 시간을 줄입니다.

 

> 안티패턴: “메모리 넉넉하니 한 번에 처리” 접근은 브라우저 탭 크래시로 이어지기 쉽습니다. 분할 처리로 네트워크·메모리 피크를 낮추는 편이 안전합니다.

 

Import/Export 빠른 체크리스트

 

대용량은 단계를 쪼개고 진행률·취소·복구를 함께 둬야 중단 위험이 낮습니다. 아래 목록을 맞추면 실패 지점을 줄일 수 있습니다.

 

- 파일 확인: 확장자·크기·시트 수 점검, 허용 상한 안내
- 분할 처리: 시트 → 범위(예: 1만 행) → 청크 순서
- 진행 UI: 진행률·남은 시간·취소 버튼
- 메모리: 처리 끝난 범위 즉시 해제, 이미지/스타일 최소
- 오류 대응: 병합 셀·깨진 수식은 로그 후 건너뛰기, 좌표 기록
- 중간 저장: n행마다 스냅샷, 재시작 오프셋
- 내보내기: 스트림 기반, 배치 플러시, 완료 후 무결성 검사

 

진행률과 취소는 사용자가 통제감을 갖게 합니다. Import는 청크로, Export는 배치로 나눠 네트워크와 메모리 피크를 낮추세요.

 

아래 코드는 범위 단위로 Import하고 UI를 주기적으로 갱신하는 구조 예시입니다.

 

// 예시는 구조 이해용입니다.
async function importByChunks(file, spread, chunkSize = 10000) {
  const workbook = new GC.Spread.Sheets.Workbook(spread.host());
  const io = new GC.Spread.Excel.IO();
  const { sheets } = await io.open(file); // 필요 시 스타일/이미지 로드 제한
  let canceled = false;

const cancelBtn = document.getElementById('btn-cancel');
  cancelBtn.onclick = () => (canceled = true);

for (const s of sheets) {
    const ws = workbook.getActiveSheet();
    const totalRows = s.getRowCount();
    for (let start = 0; start < totalRows; start += chunkSize) {
      if (canceled) return { status: 'canceled', offset: start };
      const end = Math.min(start + chunkSize, totalRows);
      try {
        const range = new GC.Spread.Sheets.Range(start, 0, end - start, s.getColumnCount());
        ws.copyTo(range, { formulas: false }); // 수식 이슈는 값으로 로드
        updateProgress((end / totalRows) * 100);
        await new Promise(r => requestIdleCallback ? requestIdleCallback(r) : setTimeout(r));
      } catch (e) {
        logError({ sheet: s.name, start, end, error: e.message });
        continue;
      }
    }
  }
  return { status: 'done' };
}

 

- 포인트: 시트 → 범위 → 청크 반복, UI는 주기 갱신. requestIdleCallback이 없으면 setTimeout으로 멈춤을 줄입니다.

 

> 실무 팁: Export에 스타일·이미지·수식 재계산을 모두 넣으면 시간이 급증합니다. 꼭 필요한 요소만 포함하고, 마지막에 열 수·헤더·샘플 셀로 자동 무결성 점검을 두세요.

 

자세한 옵션과 최신 사양은 SpreadJS 공식 제품 페이지에서 확인하세요.

 

흔한 실수와 선택 기준 한눈에 정리

 

대용량 처리에서 가장 흔한 실수는 “파일을 한 번에 읽고 한 번에 쓰는” 접근입니다. 분할 처리 없이 서식·이미지까지 모두 유지하려 하면 메모리 피크가 급격히 올라갑니다.

 

실무에서는 요구사항을 기능 보존 vs 안정성 중 어디에 둘지 먼저 결정해야 합니다. 보존을 높일수록 처리 시간과 실패 위험이 커집니다.

 

- Import 우선순위: 값/필수 수식 -> 기본 스타일 -> 고급 서식(조건부, 이미지)
- Export 전략: 배치 플러시(예: 5천~1만 행) -> 완료 전 무결성 검사 -> 최종 압축
- 진행 UI: 퍼센트 + 취소 + 에러 건수 표시, 재시작 오프셋 유지
- 메모리 대응: 시트 단위 로드/해제, 이미지·병합 최소화, 완료 청크 즉시 dispose
- 에러 처리: 병합 충돌 스킵, 깨진 수식 좌표 로그, 해당 범위 재시도 옵션

 

> 안티패턴: “사용자 불편하니 취소 버튼은 빼자”는 장애 시 전량 재작업을 유발합니다. 최소한 취소와 재시작 오프셋은 함께 두는 편이 낫습니다.

 

비교 관점에서 값 위주 Export는 가장 안정적이며, 수식·서식 보존은 가시성은 좋지만 실패 지점이 늘어납니다. 요구가 섞여 있다면 값은 전량, 고급 서식은 상한선(예: 2만 행까지만)처럼 단계적 정책을 권장합니다.

 

지금 적용할 실행 안내

 

대용량 Import/Export는 값을 먼저 확보하고 진행률·취소·복구를 함께 설계할 때 성공 확률이 높습니다. 한 번에 처리 대신 시트·범위·청크로 나누는 것이 기본입니다.

 

바로 시작하려면 아래 순서대로 체크하세요.

 

- 상한 설정: 허용 파일 크기·시트 수 공지, 초과 시 안내
- 분할 처리: 시트 → 1만 행 범위 → 청크 처리 계획 수립
- 진행 UI: 퍼센트·남은 시간·취소 버튼 배치
- 메모리 관리: 처리 완료 청크 즉시 해제, 이미지·병합 최소
- 오류 복구: 에러 좌표 로그, 스킵 후 계속, 재시작 오프셋 저장

 

실행 흐름은 업로드 -> 시트별 파싱 -> 범위 단위 적용 -> UI 갱신 -> 스냅샷 저장 -> 완료 검증입니다. 필요 시 Export는 배치 플러시로 동일하게 나눠 처리합니다.

 

> 팁: 오류가 난 범위만 재시도할 수 있게 오프셋과 에러 목록을 별도 저장하세요. 전체 재처리를 줄여 체감 속도가 크게 개선됩니다.

 

진행률·취소·재시작이 있는 Import UI

 

대용량 Import는 현재 상태를 보여줘야 불안이 줄어듭니다. 퍼센트·취소·재시작 오프셋을 함께 두면 중단 후에도 이어서 처리할 수 있습니다.

 

핵심은 파일을 청크(예: 1만 행)로 나눠 순서대로 읽는 것입니다. 청크마다 UI를 갱신하고, 마지막 성공 지점을 스냅샷으로 저장합니다.

 

- 진행 표시: 전체 대비 퍼센트, 예상 남은 시간
- 사용자 제어: 취소 버튼, 일시정지/재개
- 복구 데이터: 마지막 오프셋, 에러 위치 목록, 스킵 범위

 

아래 코드는 구조 예시입니다. 청크 Import와 진행률·취소·재시작 흐름을 한눈에 보여줍니다.

 

// 구조 예시: 청크 Import + 진행률 + 취소 + 재시작
async function importLargeExcel(file, startOffset = 0, chunkSize = 10000, signal) {
  const wb = await GC.Spread.Sheets.Workbook.open(file); // SpreadJS 로드
  const sheet = wb.getActiveSheet();
  const total = sheet.getRowCount();
  let offset = startOffset;

while (offset < total) {
    if (signal?.aborted) break;
    const end = Math.min(offset + chunkSize, total);
    const values = sheet.getArray(offset, 0, end - offset, sheet.getColumnCount());
    applyValuesToGrid(values, offset); // 화면/데이터 적용 (사용자 구현)
    offset = end;

updateProgress(Math.round((offset / total) * 100)); // 진행률 UI
    await nextFrame();       // UI 프리즈 방지: requestAnimationFrame 래퍼
    saveSnapshot({ offset }); // 재시작 지점 저장 (로컬/서버)
  }
  return { completed: offset >= total, offset };
}

 

포인트는 한 번에 처리하지 말고, 청크마다 UI를 깨워주는 것입니다. 오프셋 스냅샷이 있어야 새로고침이나 취소 후에도 이어 진행됩니다.

 

> 안티패턴: 진행 표시 없이 긴 대기가 이어지면 사용자가 새로고침해 작업이 끊깁니다. 최소한 퍼센트와 취소 버튼을 넣으세요.

반응형