[React] Progress Bar (일반 / streaming)

iberis2·2023년 10월 20일
1

React 리액트

목록 보기
19/20

사용 라이브러리 : react, axios, fetch, antd

1. 일반적인 파일 업로드 / 다운로드 시

다운로드 할 용량을 알고, 서버에서 response.header 에 content-length 를 넘겨줄 때

api.ts

  • axios 의 config 옵션 중 onUploadProgress, 와 onDownloadProgress 옵션을 활용할 수 있다.
  • onUploadProgress 의 파라미터인 progressEventloadedtotal 속성을 사용하여 progress bar 에 넘겨줄 percent 를 계산하면 된다.
  • loaded는 지금까지 업로드(다운로드) 된 용량을 나타내고, total은 전체 용량을 나타낸다.
  • download 할 때 서버에서 response.header 에 content-length 를 넘겨주어야 percentage를 계산할 수 있다.

App.tsx

css 라이브러리인 antd 의 Upload , Progress 등의 컴포넌트를 활용해 구현

🔗 antd 공식 문서 - Components

  • <Progress />percent props 에 onUploadProgress 에서 만든 percentage 를 넘겨주면 된다.

2. content-length 를 알 수 없을 때 (대용량 파일 다운로드)

Chunked transfer encoding
Chunked transfer encoding 은 응답 데이터의 크기를 미리 알 수 없는 상황에서 유용하며, 스트리밍 서비스나 대용량 파일 다운로드와 같이 데이터가 동적으로 생성되는 경우에 자주 사용됩니다. 이 방식을 사용하면 서버는 데이터를 조각으로 나누어 전송할 수 있어, 데이터 전송이 완료될 때까지 기다리지 않고도 클라이언트에게 빠르게 데이터를 보낼 수 있습니다.


서버와 정한 개발 환경

일정시간 이상 서버에서 아무 응답을 보내지 않으면 시간 초과로 요청이 취소 되기 때문에 대용량 파일을 보낼 때 1% 가 완성될 때마다. 을 보내주기로 했다.
그리고 다운로드가 완료되면 다운로드 파일에 대한 정보를 넘겨준다.

  • 즉, response 가 ..............................(점 약100개)....{ "다운로드한 파일 이름": "파일 이름", "생성 날짜": "2023-10-20" } 일정시간 이후 이런식으로 오게 된다,

axios 의 config 속성 중에도 responseType: 'stream' 이 있지만, node.js 가 아닌 브라우저 환경에서는 axios 로 스트리밍 다운로드를 구현할 수 없었다.

참고 : 🔗 stream으로 데이터 전송 방법 및 axios 사용 문제점

에러 상황 1

처음 GPT에게 물어본 결과 config 속성에 responseType: 'stream' 를 추가하고 response.data.on 매서드를 통해 progress 를 계산하는 코드를 짜줬다.

  const downloadFile = () => {
    const source = axios.CancelToken.source();
    
    axios
      .get(`url`, {
        responseType: 'stream',
        cancelToken: source.token,
      })
      .then(response => {
        const contentLength = response.headers['content-length'];
        let receivedBytes = 0;

        // 스트림을 통해 데이터를 받음
        response.data.on('data', chunk => {
          receivedBytes += chunk.length;

          // 받은 데이터 양을 기반으로 프로그레스를 계산
          const newProgress = Math.floor((receivedBytes / contentLength) * 100);
          setProgress(newProgress);
        });

        // 서버에서 전송이 끝났을 때
        response.data.on('end', () => {
          // 받은 데이터 양이 contentLength와 같을 때, 다운로드 완료
          if (receivedBytes === contentLength) {
            console.log('다운로드 완료');
          } else {
            // 받은 데이터 양과 contentLength가 다를 때, 다운로드 오류 처리
            console.error('다운로드 오류: 받은 데이터 양과 서버 응답 크기가 다릅니다.');
          }
        });
      })
      .catch(error => {
        // 요청이 취소되었을 때는 에러를 무시
        if (axios.isCancel(error)) {
          console.log('요청이 취소되었습니다.');
        } else {
          // 다운로드 중 오류가 발생한 경우 처리
          console.error('파일 다운로드 오류:', error);
        }
      });

    // 10초 후에 요청을 취소하도록 설정 (서버로부터 지속적으로 응답이 오지 않는 경우 대비)
    setTimeout(() => {
      source.cancel('Request canceled after 10 seconds');
    }, 10000); // 10초 (10000 밀리초)
  };

문제점

  1. 여기에도 content-length 가 있어야 percentage 를 계산할 수 있는 코드였다.
  2. response.data.on 메서드를 리액트(vite 환경) + axios 에서 찾을 수 없었다.

에러 상황 2

TypeError: response.data.on is not a function

type error 에 대해 GPT 에게 다시 물어보니,

응답 스트림에 이벤트 리스너를 추가하는 부분에서 타입 오류가 발생하고 있습니다. 이 문제를 해결하려면 response.data가 Readable 스트림 객체로 올바르게 인식되도록 하여 이벤트 리스너를 추가해야 합니다.
대부분의 브라우저에서는 기본적으로 스트림을 지원하지만, Node.js에서는 스트림 처리에 관련된 모듈을 명시적으로 불러와야 합니다.

라고 하며 다음과 같은 코드를 를 짜주었다.

import { Readable } from 'stream'; // Node.js의 Readable 스트림 모듈을 불러옵니다.

/* ... 중략 ... */

axios.get('url', {

}).then(response => {
	/* ... 중략 ... */

  const stream = Readable.from(response.data);
  stream.on('data', chunk => { /* ...중략... */ })

})

하지만 역시나 Uncaught Error: Module "stream" has been externalized for browser compatibility. Cannot access "stream.Readable" in client code. See http://vitejs.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility for more details. 에러가 발생했고, GPT 에게 다시 물어본 결과 다음과 같은 답변을 받았다.

Vite의 현재 버전에서는 브라우저 호환성을 위해 Node.js의 stream 모듈을 직접 사용할 수 없습니다. 이런 경우에는 브라우저에서도 사용 가능한 라이브러리를 사용하거나 브라우저 호환성이 있는 방법으로 스트림 처리를 해야 합니다.

일반적으로 브라우저 환경에서는 fetch() 함수를 사용하여 데이터를 받아오는 것이 일반적입니다. fetch()를 사용하여 스트림을 받아오고 진행률을 계산할 수 있습니다.

해결

그렇게해서 axios를 포기하고 fetch를 통해 만든 작동하는 코드

import { Button, message, Progress } from 'antd';
import { useEffect, useState } from 'react';

const DownloadComponent = () => {
  const [progress, setProgress] = useState(0);
  const [messageText, setMessageText] = useState('');

  const downloadFile = () => {
    const url = `https://url 주소`;

    // fetch() 함수를 사용하여 스트림 데이터를 받아옵니다.
    fetch(url, {
      method: 'post',
      headers: {
        'Content-Type': 'application/json',
      },
    })
      .then(response => {
        // 스트림을 받아올 때마다 프로그레스를 계산하여 업데이트합니다.
        const reader = response?.body?.getReader();

        function read() {
          return reader?.read().then(({ done, value }): Promise<undefined> | undefined => {
            if (done) { // 다운로드가 끝나면 Progress를 100% 로 바꿔줍니다. ('.'이 정확히 100개가 오지 않아도 응답이 전부 와서 끝나면 percentage를 100으로 만듦
              setProgress(100);
              return;
            }

            if (value.byteLength === 1) { // '.' 은 1byte 이기 때문에 '.' 한 개당 1% 씩 증가시켜줍니다.
              setProgress(prev => prev + 1);
            } else {
              // 마지막에 '.' 이 아닌 문자열이 오면 완료 메시지에 보여주기 위해 1byte가 아닌 값이면 내용을 읽어서 messageText state에 저장했다
              // if(done) 안에 작성하면 이마 완료 되었기 때문에 읽을 text 가 없었다.
              
              const textDecoder = new TextDecoder('utf-8'); // value 를 text로 디코딩해서 text로 만든 후 메시지에 저장해준다.
              const decodedText = textDecoder.decode(value);
              setMessageText(decodedText.slice(1)); // 결과가 .{ 어쩌구 저쩌구 내용} 으로 와서 앞의 .은 slice()로 잘라줬다.
            }

            // 다음 데이터 블록을 읽어옵니다.
            return read();
          });
        }

        // 스트림 데이터를 읽어옵니다.
        read();
      })
      .catch(error => {
        console.error('파일 다운로드 오류:', error);
      });
  };

  useEffect(() => {
    if (progress === 100) {
      // 다운로드가 완료 되어 100% 가 되면 메시지를 보여줍니다.
      message.success(messageText); // antd 라이브러리의 message 문법
    }
  }, [progress]);

  return (
    <div>
      <Button onClick={downloadFile} disabled={progress !== 0}>
        다운
      </Button>{' '}
      {progress}% 다운로드 중
      <Progress status="active" percent={progress} />
    </div>
  );
};

export default DownloadComponent;

profile
React, Next.js, TypeScript 로 개발 중인 프론트엔드 개발자

0개의 댓글