"로컬 처리"가 뜻하는 것과 뜻하지 않는 것
로컬 처리란 내가 고른 파일의 바이트를 내 브라우저 탭 안에서 읽고, JavaScript나 WebAssembly로 변환하고, 내용을 서버로 보내지 않은 채 결과를 돌려주는 것입니다. "네트워크 요청이 하나도 없다"는 말보다 좁은 뜻입니다. 페이지 자체는 서버에서 내려받았고, 글꼴·스크립트·분석 도구·광고를 다른 곳에서 불러올 수도 있습니다. 그것들은 파일이 나가는 것이 아닙니다.
그래서 던져야 할 질문은 Network 탭이 비어 있는지가 아니라, 내가 고른 파일의 내용이나 거기서 파생된 데이터를 실은 요청이 있는지입니다. 아래 테스트는 그 둘을 구분하고, 마지막 절은 이 방법으로 증명할 수 없는 것을 다룹니다.
테스트 1: 파일을 처리하는 동안 Network 탭 보기
시험용 파일을 쓰세요. 여권 사본이나 계약서 같은 실제 문서는 피하는 편이 좋습니다. 잃어도 괜찮고 크기를 기억할 수 있는 사진이 이상적입니다. 2.4MB짜리 사진이라면 2.4MB 크기의 요청이 뜰 때 바로 눈에 띕니다.
- 도구 페이지에서 F12(macOS는 Cmd+Option+I)를 눌러 Chrome이나 Edge의 개발자 도구를 열고 Network 탭을 선택합니다.
- Preserve log를 켜서 이동이나 새로고침 뒤에도 기록이 남게 하고, Disable cache를 켜서 캐시에서 조용히 불러오는 리소스도 목록에 다시 나타나게 합니다.
- 기록 지우기 버튼(선이 그어진 원)으로 지금까지의 기록을 비웁니다. 이것이 기준선입니다.
- 이제 파일을 선택하고 미리보기, 변환, 압축, 다운로드를 단계별로 실행합니다. 단계마다 목록을 확인하세요.
- 처리하는 순간에 새로 생기는 행을 봅니다. Size나 Time 열로 정렬하거나 필터 칸에 larger-than:100k를 입력해 작은 요청을 숨기면 쉽습니다.
진짜 업로드는 이렇게 생겼습니다
전형적인 파일 업로드는 콘텐츠 타입이 multipart/form-data인 HTTP POST입니다. 본문에는 경계선 사이에 파일의 바이트가 그대로 들어 있어서, 요청 크기는 파일 크기에 수백 바이트의 틀이 더해진 값입니다. Node에서 1,048,576바이트(1MiB) 파일로 직접 만들어 보니 본문이 1,048,750바이트였고 부가분은 174바이트였습니다. 브라우저는 경계 문자열이 달라서 부가분이 몇 바이트 다르지만 양상은 같습니다.
POST /api/convert HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryX
Content-Length: 1048750
------WebKitFormBoundaryX
Content-Disposition: form-data; name="file"; filename="IMG_2043.jpg"
Content-Type: image/jpeg
(파일의 1,048,576바이트)
------WebKitFormBoundaryX--업로드처럼 보이지 않는 업로드
업로드가 모두 multipart인 것은 아닙니다. 서명된 저장소 URL로 원본 바이트를 PUT으로 보내거나, JSON POST 안에 base64 문자열로 넣어 보내거나(파일보다 약 3분의 1 커집니다), WebSocket으로 스트리밍하거나, navigator.sendBeacon을 쓸 수 있습니다. 모두 개발자 도구에 나타나지만 필터가 서로 다릅니다.
- 먼저 Fetch/XHR로 거릅니다. 업로드 코드 대부분이 fetch나 XMLHttpRequest를 씁니다.
- 다음으로 WS와 Other를 봅니다. 필터를 All로 되돌리고 Size로 정렬하면 나머지도 걸립니다.
- 필터에 method:POST를 쓰면 본문을 보내는 요청만 추려 볼 수 있습니다.
| 업로드 방식 | 나타나는 위치 | 알아보는 단서 |
|---|---|---|
| multipart/form-data POST | Fetch/XHR, 폼 전송이면 Doc | 요청 크기가 파일 크기에 가깝고 Payload에 파일명이 보임 |
| 바이트 그대로 PUT/POST | Fetch/XHR | image/jpeg, application/octet-stream 같은 파일의 Content-Type |
| JSON 안의 base64 | Fetch/XHR | 요청이 파일의 약 1.33배이고 Payload가 긴 텍스트 |
| WebSocket 프레임 | WS, 이어서 Messages 탭 | 파일을 고르는 순간 연결이 열리고 큰 바이너리 프레임이 오감 |
| 비콘 | Other 또는 Fetch/XHR | 페이지 이벤트에서 응답 없이 나가는 POST |
테스트 2: 오프라인 테스트
오프라인 테스트는 목록에서 올바른 행을 찾아내야 하는 부담이 없어서 목록을 읽는 것보다 강력합니다. 도구 페이지를 완전히 불러온 뒤 Network 탭의 스로틀링을 Offline으로 바꾸고(또는 Wi-Fi를 끄고) 파일을 처리합니다. 결과가 나오고 다운로드까지 되면 서버가 필요 없는 작업이라는 뜻입니다.
탭은 그대로 두세요. 오프라인에서 새로고침하면 페이지 자체를 다시 요청하다 실패하므로 파일 처리에 대해서는 아무것도 알 수 없습니다. 또한 도구가 아직 받지 않은 무거운 엔진처럼, 처음 쓸 때 내려받는 리소스가 정당하게 필요할 수도 있습니다. 오프라인에서 실패하면 결론을 내리기 전에 Console과 Network 목록에서 실패한 요청이 무엇을 받으려 했는지 확인하세요.
내 파일이 아닌 요청들
Network 탭이 북적이는 것은 정상이고 대부분 무해합니다. 개수에 놀라기보다 종류를 알아보는 것이 요령입니다.
| 보이는 것 | 있는 이유 | 내 파일인가 |
|---|---|---|
| 페이지, JS·CSS 번들, 글꼴 파일 | 도구 자체를 불러오는 것으로, 보통 파일을 고르기 전에 발생 | 아닙니다. 로드 시점의 요청이지 처리 시점이 아닙니다 |
| 분석·광고 요청(예: googlesyndication.com 호스트) | 페이지 단위의 측정이나 광고 | 파일 내용이 아닙니다. Payload와 처리 시점에 발생하는지로 판단합니다 |
| CDN의 큰 .wasm 또는 .js 파일 | 처음 쓸 때 내려받는 처리 엔진(영상, PDF, OCR) | 아닙니다. 코드를 내려받는 GET이며 파일 정보가 들어 있지 않습니다 |
| 서비스 워커 요청 | 재방문을 빠르게 하거나 오프라인을 가능하게 하는 캐시 계층 | 아닙니다. Application, Service Workers에서 확인합니다 |
| blob:, data: URL | 브라우저 메모리에 있는 내 데이터 | 네트워크 요청이 아닙니다. 아래 참고 |
Blob URL, WebAssembly 엔진, 서비스 워커
blob:https://example.com/3f1c2e1a-... 같은 blob URL은 브라우저 메모리에 있는 객체를 가리킵니다. 로컬 도구는 서버를 거치지 않고 미리보기를 보여 주거나 다운로드를 제공하려고 URL.createObjectURL(file)을 씁니다. Network 탭의 blob: 항목이나 blob: 주소가 출처인 다운로드는 업로드가 아니라 로컬 처리의 증거입니다.
WebAssembly 엔진은 사람들이 가장 자주 오해하는 경우입니다. 영상이나 PDF 도구는 처음 쓸 때 수 MB짜리 .wasm 파일을 내려받을 수 있습니다. 이것은 GET 요청으로, 브라우저가 코드를 요청해 받는 것입니다. 요청에는 파일 내용이 없고, 파일은 그 뒤에 탭 안에서 그 코드로 처리됩니다. 업로드와 구별하는 방법은 데이터의 방향과 크기입니다. 다운로드는 응답이 크고 요청이 작으며, 업로드는 요청이 큽니다.
서비스 워커는 캐시된 페이지와 엔진을 대신 제공해서 재방문 때 요청이 적어 보이게 만듭니다. Application, Service Workers에서 등록 여부를 볼 수 있습니다. 워커가 보낸 요청은 페이지가 아니라 워커의 것으로 분류됩니다. 깨끗한 화면을 보려면 Disable cache를 켜거나 시크릿 창에서 시험하세요.
이 사이트의 파일 도구는 파일을 어떻게 읽는가
이 가이드는 사이트의 세 파일 도구 코드를 읽고 작성했습니다. 파일 내용은 로컬에서 처리됩니다. 워터마크 제거기는 정적 마스크와 엔진을 불러올 때 fetch를 쓰며 내 파일을 보내지는 않습니다. 처리 코드에는 XMLHttpRequest, sendBeacon, WebSocket으로 파일을 업로드하는 호출이 없습니다.
- 이 도구들의 코드에서 createImageBitmap은 영상 모드에서 한 번만 쓰입니다. 같은 출처에서 받은 사이트 자체의 마스크 이미지를 디코딩하는 용도이고 내 파일은 받지 않습니다.
- PDF는 같은 출처의 /tools/clearframe/runtime/에서 pdf.js 워커를 불러옵니다. PDF Pro·PPTX·인포그래픽의 질감 있는 배경 처리는 같은 경로의 OpenCV를 추가로 불러올 수 있으며, 매끄러운 배경 채우기에는 OpenCV가 필요 없습니다. 페이지 리소스를 받는 것이지 업로드가 아닙니다.
| 도구 | 파일을 읽는 방식 | 출력 |
|---|---|---|
| 사진 EXIF 메타데이터 제거 | file.arrayBuffer()로 원본 바이트에서 메타데이터를 검사하고, URL.createObjectURL(file)과 Image 요소로 디코딩 | 캔버스에 그려 품질 0.95의 canvas.toBlob으로 다시 인코딩하고, 새 바이트를 한 번 더 검사해 남은 것을 확인합니다. 다운로드는 blob URL |
| 이미지 용량 줄이기 | URL.createObjectURL(file)과 Image 요소. PNG, JPEG, WebP를 20MiB까지 받으며 arrayBuffer나 FileReader는 쓰지 않음 | 목표 용량에 맞을 때까지 품질과 배율을 바꿔 canvas.toBlob을 반복하고, 결과는 blob URL |
| 워터마크 제거 | 이미지는 object URL과 Image.decode. PDF는 file.arrayBuffer()를 pdf.js에 전달. PPTX는 file.arrayBuffer()를 JSZip에 전달. 영상은 FileReader를 쓰는 @ffmpeg/util의 fetchFile로 FFmpeg의 메모리 파일 시스템에 기록 | 캔버스, jsPDF, JSZip, FFmpeg의 출력이며 모두 blob으로 보관되고 blob URL로 제공 |
워터마크 제거 도구의 다운로드 하나
영상 모드는 WebAssembly로 컴파일된 FFmpeg가 필요합니다. 도구의 코드 주석에 따르면 약 32MB의 코어가 Cloudflare Pages에 올리기엔 커서, 페이지가 ffmpeg-core.js와 ffmpeg-core.wasm을 cdn.jsdelivr.net에서 버전 0.12.6으로 고정해 가져옵니다. 실행하기 전에 내려받은 각 파일의 SHA-256을 계산해 소스에 적힌 값과 다르면 로드를 거부합니다. 엔진은 페이지를 연 뒤 한 번만 불러와 재사용합니다.
앞에서 설명한 바로 그 경우이고 직접 재현할 수 있습니다. 영상을 고르고 Network 탭을 보면 인코딩이 시작되기 전에 jsDelivr로 가는 GET 요청 둘이 보이고, 큰 쪽이 .wasm 파일입니다. 내 영상은 그 안에 없습니다. 로드가 끝난 뒤 네트워크를 끄고 두 번째 영상을 돌려 보면 인코딩이 계속됩니다. 엔진이 이미 탭 안에 있기 때문입니다. 이미지, PDF, PPTX 모드에는 이런 별도의 다운로드가 없습니다.
이 방법으로 증명할 수 없는 것
개발자 도구는 내가 보고 있는 탭의, 내가 지켜본 시간 동안의 트래픽만 보여 줍니다. 내일의 다른 버전이 무엇을 할지는 보여 주지 않으므로, 민감한 파일이라면 큰 업데이트 뒤에 테스트를 반복하세요. 내가 실행하지 않은 코드 경로도 보이지 않습니다. 큰 파일일 때만, 혹은 특정 옵션을 켰을 때만 업로드하는 도구가 있을 수 있으니 실제로 쓰는 옵션을 모두 실행해 보세요.
브라우저 확장 프로그램은 페이지를 읽고 스스로 데이터를 보낼 수 있고, 그 트래픽이 페이지의 Network 탭에 항상 나타나지는 않습니다. 결과가 이상하면 확장 프로그램을 끈 새 브라우저 프로필에서 재현하세요. 더 강한 보증이 필요하면 프록시나 운영체제 방화벽으로 브라우저 바깥에서 트래픽을 보거나, 네트워크를 끈 기기에서 도구를 실행하세요.
마지막으로 로컬 처리가 곧 올바른 처리는 아닙니다. 사진을 기기 안에 두고도 출력에 메타데이터가 남거나, 해석하지 못한 파일을 손상시키는 도구가 있을 수 있습니다. 출력을 원본과 비교하고 원본은 보관하며, 개인정보 주장과 결과의 품질을 따로 판단하세요.