메인 스레드가 멈추는 이유부터 짚기
대용량 수식이나 사용자 정의 함수(Custom Functions)를 스프레드시트에서 실행하면 화면 스크롤, 클릭, 입력이 순간적으로 멈추는 경험이 생깁니다. 문제의 핵심은 계산 로직이 메인 스레드에서 오래 실행돼 UI 이벤트 처리를 가로막는다는 점입니다.
실무에서는 여러 시트에 걸친 반복 계산, 외부 데이터 참조, 복잡한 사용자 정의 함수가 동시에 호출되며 ‘프리즈’ 현상이 길어집니다. 이 현상은 사용성 저하뿐 아니라 입력 실수와 이탈률 증가로 이어지기 쉽습니다.
이 글은 Calc Worker로 계산을 분리해 메인 스레드를 비워 두는 접근을 설명합니다. 즉, UI는 그대로 반응하고 계산은 백그라운드에서 수행되도록 하는 흐름입니다: 사용자 입력 -> 메인 스레드 이벤트 처리 -> 계산은 워커로 위임 -> 결과만 반영.
Calc Worker와 Custom Functions, 무엇이 다를까
Calc Worker는 계산만 전담하는 별도 스레드입니다. 메인 스레드는 UI, 워커는 수식을 처리하므로 긴 계산이 있어도 스크롤과 입력이 끊기지 않습니다.
Custom Functions는 SUM 같은 기본 함수로 표현하기 어려운 로직을 직접 만든 함수입니다. 예를 들어 API 호출 결과를 가공하거나, 대량의 통계 계산을 하나의 이름으로 감싼 형태입니다.
두 요소를 결합하면 흐름은 간단합니다: 사용자 입력 -> UI 반응(메인) -> 수식/커스텀 함수 평가(워커) -> 완료 신호와 결과 반영. 결과는 셀에만 반영되고, 계산 중 화면은 계속 동작합니다.
실무 예로, “고객ID 목록 -> 고객별 외부 지표 조회 -> 가중 평균” 같은 커스텀 함수를 수백 번 호출해도, 워커가 병렬로 평가를 진행해 체감 지연이 줄어듭니다. 다만 워커-메인 간 메시지 전송에 작은 오버헤드가 생길 수 있습니다.
워커로 계산 떼어내기
UI는 가볍게 두고 계산은 워커가 맡으면 멈춤이 줄어듭니다. 핵심은 “UI 초기화”와 “워커 등록”을 분리하는 것입니다.
아래 순서대로 적용하세요.
- calc-worker.js 파일을 만들고 워커 엔트리로 지정
- 메인 스레드에서 스프레드시트(UI) 먼저 초기화
- 워커를 생성해 계산 엔진과 연결
- 커스텀 함수 이름·인자 형식을 동일하게 정의
- 긴 계산은 청크 처리나 배치 크기 제한
- 오류·타임아웃·취소 플래그 준비
아래 최소 연결 예시는 구조를 보여주기 위한 형태입니다. 코드 위치만 이해해 두면 됩니다.
<!-- index.html -->
<script type="module">
import { Spreadsheet, CalcEngine } from 'spread-like-lib';
const ss = new Spreadsheet('#host');
const worker = new Worker('./calc-worker.js', { type: 'module' });
// 워커 연결
CalcEngine.useWorker(worker);
// 커스텀 함수 등록(워커 위임)
CalcEngine.registerFunction('WEIGHTED_AVG', (values, weights) => {
return { __deferToWorker: true, name: 'WEIGHTED_AVG', args: [values, weights] };
});
</script>
// calc-worker.js
import { WorkerCalcEngine } from 'spread-like-lib/worker';
WorkerCalcEngine.onFunction('WEIGHTED_AVG', ({ values, weights }) => { let sum = 0, wsum = 0; for (let i = 0; i < values.length; i++) { sum += (values[i] || 0) * (weights[i] || 0); wsum += (weights[i] || 0);
} return wsum ?
sum / wsum : 0;
});
메인은 화면만, calc-worker.js는 계산만 담당합니다. 함수 이름과 인자 규격은 두 곳에서 같아야 동작합니다.
실무 체크포인트입니다.
- 외부 API 호출은 워커 안에서 fetch 사용
- 큰 배열은 Float64Array 등으로 보내 복사·전송 비용 절감
- 진행 표시와 취소 버튼은 단순한 스피너+토글로 관리
> 안티패턴: 메인과 워커가 같은 대용량 데이터를 동시에 보관하지 마세요. 공용 캐시 키를 두고 워커만 캐시하며, 메인에는 결과만 반영하는 편이 안전합니다.
흔한 실수와 선택 기준 한눈에 정리
워커로 옮겨도 커스텀 함수가 동기 I/O나 무거운 루프를 그대로 쓰면 이점이 줄어듭니다. 선택 기준은 “메시지 수를 줄이고, 계산을 덩어리로 보내는가”입니다.
다음 체크리스트를 먼저 확인하세요.
- 커스텀 함수는 순수 함수로 작성(전역 상태·DOM 접근 금지)
- 대량 데이터는 한 번에 전달(셀 단위 다중 포스트메시지 금지)
- 외부 요청은 워커 내부에서 배치·캐싱 적용
- 타임아웃·취소 토큰·재시도 간격 명시
- 에러는 메시지로 직렬화해 원인만 전달(스택 전송 과다 금지)
비교 관점에서, 메인 스레드 비동기 호출로 “느낌상 덜 멈춤”을 노리기보다 워커로 완전히 분리하는 편이 안정적입니다. 메인에서 프라미스만 늘리면 이벤트 루프 대기열이 길어져 입력 지연이 남습니다.
> 안티패턴: 셀마다 워커를 새로 생성하거나, 셀 변경 때마다 전체 데이터를 재전송하는 방식은 피하세요. 워커는 재사용하고, 범위 변경 시 변경분만 보내는 증분 전략이 전송 비용을 크게 줄입니다.
트레이드오프도 있습니다. 워커 통신에는 직렬화 비용이 생기므로, 작은 계산은 메인 기본 함수로 처리하고 중·대형 커스텀 계산만 워커로 보내는 혼합 구성이 효율적입니다.
지금 할 일: 워커로 계산 분리 마무리
결론은 “UI는 메인, 계산은 워커”로 역할을 나누면 프리즈를 크게 줄일 수 있습니다. 커스텀 함수는 순수 함수로 정리하고, 데이터는 덩어리로 보내며, 에러·취소 흐름을 미리 준비하세요.
바로 적용하려면 아래 순서로 점검하세요.
- 워커 엔트리(calc-worker.js) 생성 및 메인 초기화 분리
- 커스텀 함수 시그니처(이름·인자) 메인/워커 동일화
- 대량 데이터는 범위 단위 전송, 변경분만 증분 반영
- 타임아웃·취소 토큰·배치 크기 설정 및 로깅 추가
처음에는 작은 시트로 흐름을 검증하고, 배치 크기와 메시지 빈도를 줄이는 방향으로 튜닝하세요. 네트워크나 I/O가 있는 커스텀 함수라면 워커 내부 캐싱과 재시도 간격을 함께 설정하는 편이 안전합니다.
> 실무 팁: 성능 프로파일러로 “메시지 수”와 “워커 처리 시간”을 따로 기록하면, 어디서 대기열이 길어지는지 빠르게 찾을 수 있습니다.
'프로그래밍 > spreadjs' 카테고리의 다른 글
| SpreadJS 계산 성능 튜닝: 재계산 모드 설정과 CalculationProgress 이벤트로 대용량 시트 최적화 (0) | 2026.06.22 |
|---|---|
| SpreadJS 종속 드롭다운 데이터 유효성 검사 구현 가이드: 초보자도 따라하는 단계별 예제 (1) | 2026.06.06 |
| SpreadJS AI 엑셀 함수 – 스프레드시트에 AI 어시스턴트를 적용하는 방법 (0) | 2026.04.03 |
| SpreadJS AI 수식 편집 – 자연어로 엑셀 수식 만드는 방법 (0) | 2026.03.25 |
| SpreadJS AI Pivot Panel – 자연어로 피벗 테이블 만드는 방법 (0) | 2026.03.24 |