브라우저 대용량 파일 1 - 한줄씩 읽기

composite·2022년 10월 27일
5
post-thumbnail

일단 나는 대용량 업로드, 다운로드, 읽기 업무를 뒤돌아보니 꽤 많이 했었다.
하지만 SI/SM 에서는 최신 기술 더럽게 배척하다 보니 지금같이 뛰어나게 해결 가능한 웹 기술들은...
아니 필요하다면서 최신 기술 못쓰게 브라우저 옛버전으로 박아버리는 클라쓰 어디 안가고.

하지만, 그렇다고 내가 가만히 있지는 않는다,
내가 순수 브라우저로 대용량 파일 처리하는 시리즈를 집필할 테니,
실무에 자스롤 고생하는 여러분들에게 도움이 되기 바란... 다나 뭐래나...

준비물

목표

여러분은 오늘 몇메가짜리 CSV 파일 하나가 주어졌을 때, 만약 이걸 브라우저에서 그대로 다 읽어들이고 split 하고 쉼표 나누기엔 텍스트 파일의 곱절 이상의 메모리를 브라우저에서 쳐먹으므로, 이로 인한 버벅임을 각오해야 한다.
하지만, 이번 Stream 기능을 통해 메모리 걱정 없이, 서버에서 평소 했던 것처럼, 우린 대용량 텍스트 파일을 부드럽~게 읽을 것이다.
이 때, 응용은 여러분에게 맡긴다. 여기서는 아주 간단하게 쉼표로 구분되기만 하고 한줄씩 되어 있는 텍스트 파일을 예러 들 것이다.

시↘작↑

시작은 fetch다. 즉, 대용량 텍스트 파일을 일단 가져와야지. 구시대 산물인 XMLHttpRequest 쓰지 마라.
여기서 예제는 async 구문 안 쓰고 그냥 Promise 방식을 쓰겠다. 따라서 비동기 문법 수정 또한 여러분 몫이다.

fetch('/path/to/target.csv').then(response => {
  // TODO
});

자, 일단 파일을 불러들였으면 Response 객체가 불러와지겠지? 우린 body 속성을 읽겠다.
이 속성에서 ReadableStream 객체가 여러분을 반길 것이다. 우린 이걸 걸고 진행하겠다.

텍스트로 변환

변환하고 스트림을 유지하기 위해 우리는 pipeThrough 메소드를 사용할 것이다.
node.js 스트림을 다뤄봤다면... 구현하는 건 게임 오버네.

fetch('/path/to/target.csv').then(response => {
  response.body
    .pipeThrough(new TextDecoderStream())
});

당연하겠지만 원시 바이트 스트림이기 때문에 텍스트로 변환하는 과정이 필요하다. 다행히도 TextDecoderStream 객체는 브라우저에서 제공하는 표준 API로, 손쉽게 텍스트 스트림으로 변환해줄 것이다. 첫번째 인자에 인코딩 있는데 기본은 UTF-8 이다. 제발 EUC-KR 같은 구닥다리 인코딩 쓸 일은 없기 바란다.

한줄씩 읽기

내가 준비물에 쓴 WHATWG 예시에서 splitStream 함수를 제공한다. 여기서 텍스트 스트림을 한줄 텍스트 스트림으로 변환해준다.
원리는 간단하다. 열나게 읽어대고, 거기서 줄 문자 단위로 나뉘어 마지막만 남기고 뱉어낸 다음 계속 진행한다. 마지막에는 남은 한줄을 읽는 것으로 마무리.

fetch('/path/to/target.csv').then(response => {
  response.body
    .pipeThrough(new TextDecoderStream())
    .pipeThrough(splitStream('\n'))
});

이제 CSV 파일 한줄씩 읽는 것이 해결되었다. 하지만 한줄 스트림으로 만들기 때문에 이 역시 pipeThrough 메소드를 쓴다.

쉼표로 분리 후 마무리

한줄씩 읽는 것만 해도 목적은 대부분 달성했다. 우리가 CSV 읽는 건 행 단위로 나눠 거기에 쉼표 만큼의 열을 나누고 데이터를 표현하는 거다. 이건 pipeTo 메소드를 사용하고 인자에 WritableStream 객체를 생성해 해결할 것이다.
별 거 없다. 인자 속성에 write 메소드를 구현하고, 첫번째 인자에 한줄씩 가져온 텍스트를 쉼표로 분리하고 처리하면 끝이다.

const pre = document.querySelector('pre')
fetch('/path/to/target.csv').then(response => {
  response.body
    .pipeThrough(new TextDecoderStream())
    .pipeThrough(splitStream('\n'))
    .pipeTo(new WritableStream({
      write(row) {
        const cols = row.split(',')
        pre.appendChild(document.createTextNode(cols.join('\t')))
      }
    }))
});

예시는 <pre> 태그에다가 탭으로 구분하는 방식으로 변환한 텍스트를 지속적으로 담을 것이다.
이 때, HTML 에서 렌더링할 땐 역시 appendChild 가 메모리 절약에 큰 도움이 된다.
또한, 텍스트 내용이기 때문에, document.createTextNode 메소드를 통해 텍스트 노드를 만들어 기록해야 한다.
이정도는 프론트엔드라면 충분히 알 내용이고...

끝?!

그래. 끝이다. 간단하지?
돌아가는 예제? 여기에 대령했노라.

이것으로 대용량 파일을 브라우저 뻗지 않고 불러들이고 읽어들이는 방법을 알아보았다.
히제 이걸 실무로 적용하고 싶다면 마음대로 하면 된다. 어자피 나도 예제 살짝 응용한 것 뿐이니.

다음은... 아마 대용량 업로드가 될 지도 모르겠다.
일단 확인해 보고 브라우저 지원이 그나마 활성화된 방향으로 잡아서 다음 시리즈를 내놓도록 하겠다.
그럼,

끗.

profile
지옥에서 온 개발자

0개의 댓글