-
큰 CSV를 처리할 때 웹 화면이 멈춘다면: Web Worker로 옮길 일Programming 2026. 9. 21. 12:28728x90반응형

화면 응답과 무거운 처리를 분리하는 AI 생성 개념 이미지입니다. 브라우저가 무거운 계산으로 멈춘다면 계산을 Web Worker로 분리하고 주고받을 입력·결과를 작게 설계하세요.
큰 CSV를 선택한 뒤 화면의 버튼과 스크롤이 함께 멈춘다면 파일 크기만 볼 일이 아닙니다. 파일을 읽는 단계, CSV를 해석하는 단계, 집계하는 단계, 표를 그리는 단계 중 어디에서 메인 스레드를 오래 점유하는지 나눠 봐야 합니다.
Web Worker는 이 중 화면과 분리할 수 있는 계산을 다른 스레드에서 수행하게 하는 도구입니다. 파일 처리 전체가 자동으로 빨라지거나 메모리 한도가 사라지는 기능은 아닙니다. 아래는 2026년 9월 13일 공식 문서 기준의 설계 설명이며 브라우저 성능 측정이나 대용량 파일 처리는 실행하지 않았습니다.
옮길 후보는 네트워크 대기보다 긴 계산입니다
메인 스레드에는 JavaScript 실행뿐 아니라 사용자 입력과 화면 갱신에 필요한 일이 모입니다. 이곳에서 큰 데이터의 변환·집계를 오래 수행하면 사용자가 누른 버튼에 반응할 기회가 줄어듭니다. 계산을 워커로 옮기면 이런 경쟁을 줄일 수 있습니다. 메인 스레드 밖으로 계산 옮기기
반면 서버 응답을 기다리는 fetch 자체가 느린 것이라면 워커만 추가해 서버 처리 시간이 줄지는 않습니다. 결과를 받은 뒤 수십만 행을 DOM으로 만드는 일이 병목이라면 그 렌더링도 따로 줄여야 합니다.
워커는 페이지의 DOM을 직접 수정할 수 없습니다. 계산 결과를 메시지로 돌려주고 메인 스레드가 필요한 부분을 그리는 방식으로 경계를 정합니다. 입력 파일 선택과 오류 안내는 화면 쪽, 파싱과 집계는 워커 쪽처럼 책임을 나누는 이유입니다. 워커의 실행 환경과 DOM 제한
목표도 구분합니다. 전체 처리 시간은 같거나 통신 비용 때문에 늘어날 수 있지만 그동안 취소 버튼을 누를 수 있다면 반응성은 개선될 수 있습니다. 결과가 몇 초 빨라졌다는 주장과 화면이 덜 멈춘다는 주장을 같은 측정으로 대신하지 않습니다.
큰 문자열을 왕복시키기 전에 데이터 경계를 정합니다
postMessage로 보내는 일반적인 데이터는 구조화된 복제 방식으로 전달됩니다. 큰 CSV 문자열과 그 문자열을 해석한 거대한 객체 배열을 양쪽에 모두 유지하면 계산을 옮겨도 메모리와 메시지 처리 비용이 커질 수 있습니다. Worker.postMessage
ArrayBuffer 같은 전송 가능한 자원은 소유권을 다른 쪽으로 넘길 수 있습니다. 이때 원래 쪽의 버퍼는 분리되므로 이전과 같이 읽고 쓸 수 없습니다. TypedArray 자체가 아니라 그 아래의 ArrayBuffer가 전송 대상이라는 점도 구분합니다. 전송 가능한 객체와 버퍼 소유권
다만 전송 목록에 버퍼만 적는 것으로 수신 메시지에 그 값이 생기는 것은 아닙니다. 메시지에도 수신 측이 접근할 수 있게 포함해야 합니다. 아래 예시에서는 values를 메시지에 넣고 values.buffer를 전송 목록으로 지정합니다.
여기서 다룰 계산은 CSV 전체 파서가 아니라 이미 숫자로 해석된 열의 요약입니다. 실제 CSV에는 따옴표 안의 쉼표와 줄바꿈, 인코딩, 누락값 같은 조건이 있으므로 문자열을 쉼표로 split하는 코드를 완전한 파서처럼 제시하지 않습니다.
워커는 입력을 검사하고 작은 요약만 돌려줍니다
같은 디렉터리에 main.js와 summary-worker.js를 두고 HTTP 환경에서 모듈 스크립트로 불러온다고 하겠습니다. 예시 입력은 Float64Array의 2, 4, 6입니다. 합계 12와 평균 4는 이 입력의 산술적 예상값이며 실행 로그가 아닙니다.
summary-worker.js는 다음과 같습니다. 값이 없으면 평균을 0으로 만들지 않고 null로 구분합니다. 유한한 숫자가 아닌 값이나 합계의 범위 초과도 성공 결과로 숨기지 않습니다.
self.onmessage = ({ data }) => { const { id, values } = data; try { if (!(values instanceof Float64Array)) { throw new Error("Float64Array 입력이 필요합니다."); } let sum = 0; for (const value of values) { if (!Number.isFinite(value)) { throw new Error("유한한 숫자가 아닌 값이 있습니다."); } sum += value; } if (!Number.isFinite(sum)) { throw new Error("합계가 표현 가능한 범위를 벗어났습니다."); } self.postMessage({ id, ok: true, count: values.length, sum, mean: values.length === 0 ? null : sum / values.length, }); } catch (error) { self.postMessage({ id, ok: false, error: error.message }); } };요약이 목적이면 원본 배열을 다시 보내지 않아도 됩니다. 건수·합계·평균과 오류 여부만 반환하므로 결과 메시지는 작게 유지됩니다. 표의 모든 행이 필요하다면 이 구조와 별개로 페이지 단위 결과나 화면에 보이는 범위를 설계해야 합니다.
이 합산은 JavaScript 부동소수점 계산입니다. 금융 금액의 정확한 십진 연산이나 수치 안정성이 중요한 통계 처리를 보장하는 예시는 아닙니다. 워커로 옮겼다고 데이터 형식이나 계산 방식의 한계가 바뀌지는 않습니다.
화면에서는 작업 번호와 종료 처리를 연결합니다
다음 main.js에는 결과를 표시할 #summary 요소가 이미 준비돼 있다고 가정합니다. 새 작업을 시작하면 이전 워커를 종료하고, 결과의 작업 번호가 현재 번호와 맞을 때만 표시합니다. 예시를 단순하게 유지하기 위해 한 번에 한 작업을 처리하는 구성입니다.
const output = document.querySelector("#summary"); if (!output) throw new Error("결과를 표시할 요소가 없습니다."); let active = null; let latestId = 0; export function cancelSummary() { latestId += 1; active?.terminate(); active = null; output.textContent = "작업을 취소했습니다."; } export function startSummary(values) { cancelSummary(); const id = latestId; output.textContent = "계산 중입니다."; let worker; try { worker = new Worker( new URL("./summary-worker.js", import.meta.url), { type: "module" }, ); active = worker; const finish = (text) => { if (id !== latestId || active !== worker) return; output.textContent = text; worker.terminate(); active = null; }; worker.onmessage = ({ data }) => { if (data.id !== id) return; finish(data.ok ? JSON.stringify({ count: data.count, sum: data.sum, mean: data.mean }) : "계산 실패: " + data.error); }; worker.onerror = () => finish("워커를 불러오거나 실행하지 못했습니다."); worker.onmessageerror = () => finish("결과 메시지를 읽지 못했습니다."); worker.postMessage({ id, values }, [values.buffer]); } catch (error) { worker?.terminate(); active = null; output.textContent = "작업 시작 실패: " + error.message; } } startSummary(new Float64Array([2, 4, 6]));Worker 생성 시 현재 모듈 주소를 기준으로 파일 위치를 계산했습니다. 빌드 도구를 쓴다면 워커 파일이 배포 결과에 포함되는지와 실제 URL을 확인해야 합니다. 스크립트 경로, CSP, 로딩 실패가 계산 오류와 별개로 발생할 수 있습니다. 워커 생성과 경로
전송 뒤에는 호출한 쪽에서 values의 버퍼를 재사용하지 않는 계약입니다. 화면에서도 원본이 필요하다면 별도 복사본을 둘지, 워커가 보관하고 필요한 결과만 줄지 결정해야 합니다. 복사본을 만든다면 그 메모리 비용도 계산에 포함합니다.
취소 버튼은 cancelSummary에 연결할 수 있습니다. 예시에는 HTML 버튼의 이벤트 연결이나 파일 선택 UI까지 포함하지 않았습니다. 페이지나 컴포넌트를 정리할 때도 진행 중 워커의 수명을 함께 정리해야 합니다.
종료는 계산을 되돌리는 기능이 아닙니다
terminate는 워커를 즉시 멈추며 남은 작업을 마칠 기회를 보장하지 않습니다. 파일·DB 쓰기나 서버 요청 같은 부수 효과가 있는 워커에 적용했다고 이미 일어난 작업이 취소되는 것은 아닙니다. 이 예시가 외부 저장 없는 요약 계산으로 범위를 한정한 이유입니다. Worker.terminate의 동작
반복문이 오래 도는 워커에 “취소” 메시지만 보내면 그 메시지를 처리할 기회가 늦어질 수도 있습니다. 협력적으로 중단하려면 작업을 나누어 메시지를 처리할 틈을 주는 설계가 필요합니다. 즉시 종료와 진행 상태를 보존하는 중단은 서로 다른 요구입니다.
작업 번호는 취소의 대체물이 아닙니다. 예전 결과를 표시하지 않는 장치일 뿐, 그 계산이 자원을 쓰는 것을 멈추지는 않습니다. 반대로 종료를 요청했어도 화면 상태와 오래된 결과 처리는 명확히 정리해 두는 편이 좋습니다.
매 작업마다 워커를 새로 만드는 이 예시는 생성 비용을 감수하고 수명을 단순하게 했습니다. 자주 반복되는 작은 작업이라면 워커를 재사용하는 구조가 나을 수 있지만, 그때는 작업 대기열과 취소·오류 복구를 별도로 관리해야 합니다.
큰 CSV의 최종 설계에는 파일과 화면도 남습니다
이 예시를 붙였다고 CSV 파싱 단계까지 워커로 옮긴 것은 아닙니다. 메인 스레드에서 전체 파일을 해석하고 숫자 배열을 만든 뒤 마지막 집계만 옮기면 앞부분의 멈춤이 그대로 남을 수 있습니다. 성능 기록에서 실제로 오래 걸린 단계를 옮겨야 합니다.
파일 전체를 한 번에 메모리에 올릴 수 없는 규모라면 청크 단위 읽기와 파서 상태 유지, 점진적 집계를 검토합니다. 이때 청크 경계가 CSV 행이나 따옴표 경계와 같다고 가정하지 않습니다. 파일 크기에 맞는 파싱 설계가 워커 배치보다 먼저 필요한 경우도 있습니다.
검증에서는 같은 입력의 계산 결과가 기존 구현과 같은지, 빈 입력과 잘못된 값을 구분하는지, 처리 중 스크롤·취소가 반응하는지, 큰 메시지 전송이나 결과 렌더링에서 다시 멈추지 않는지를 봅니다. 총시간과 메인 스레드 점유, 메모리는 각각 기록해야 합니다.
Web Worker를 쓸 기준은 코드가 복잡해 보이느냐가 아닙니다. 화면을 막는 계산이 확인됐고 그 계산의 입력·결과·오류·수명을 분리할 수 있느냐입니다. 이 경계를 먼저 정하면 워커를 추가한 뒤에도 느린 이유를 찾을 수 있고, 사용자는 계산이 끝날 때까지 아무 반응 없는 화면을 바라보지 않아도 됩니다.
728x90반응형'Programming' 카테고리의 다른 글
SSE 연결이 다시 붙을 때 메시지가 중복되는 이유 (0) 2026.09.21 Web Worker에 ArrayBuffer를 보냈더니 원본을 못 쓰는 이유 (0) 2026.09.21 뒤로 가기했더니 예전 화면이 그대로 나타나는 이유: bfcache (0) 2026.09.21 304 Not Modified는 오류일까: ETag로 갱신 여부 읽기 (1) 2026.09.20 Cache-Control: no-cache인데 왜 캐시에 저장될까 (0) 2026.09.20