마이 블로그 프로젝트 04-2 - 첨부파일 업로드/다운로드.

이유승·2023년 7월 17일
0

웹 에디터에서 이미지 파일을 업로드하고, 이를 화면에 출력하려면 어떻게 해야 할까? 이 프로젝트는 나의 개발 블로그를 목적으로 시작한 프로젝트였고 게시글에 이미지를 적절하게 띄우는 것은 반드시 필요한 일이었다.

그런데 웹 에디터 라이브러리를 사용하기로 한 이후, 해당 라이브러리에서 이미지 파일을 에디터에 업로드하면 그냥 화면에 알아서 출력해주고 있어서 이 부분에 대한 고민은 한동안 접어둘 수 있었다.

  • 이게 어떤 원리로 가능한 일인지 당시에는 잘 이해가 가지 않았다. 나중에 다른 프로젝트를 진행하면서 어느 정도 감이 왔는데, 웹 에디터에서 이미지를 붙여넣으면 웹 에디터 라이브러리를 제공하는 회사의 서버에 이미지가 업로드되고 해당 주소값을 통해 이미지를 화면에 출력해주고 있는 것같다.



1. 파일을 업로드 하는 기능.

게시글 내부에서 이미지를 다루는 문제는 해결되었지만, 개인적인 호기심으로 글 작성시 첨부파일 기능을 업로드 하는 기능을 구현하게 되었다. UI의 구성은 간단하다. input 태그에 file 타입을 적용하기만 하면 된다.

<input className={styles.upload} type='file' onChange={handleOnChangeFile}/>   

문제는 파일 데이터를 어떻게 저장해야 하는가. 기존에 사용하던 파이어스토어는 자바스크립트 객체 형태의 데이터만을 다룰 수 있다. 이미지 파일 같은 데이터는 파이어스토어가 아닌 스토리지를 활용해야 한다.

파일 데이터를 그냥 스토리지쪽 기능을 이용하여 업로드하면 그만이라고 생각했는데.. 파일의 이름을 지정해주어야 했다. 콘솔을 이용하여 파일 데이터를 뜯어본 결과 사용자가 업로드한 파일은 File 객체 형태로 다음과 같은 정보들을 key-value 형태로 갖는다는 것을 알게 되었다.

name
파일 이름.

lastModified
파일을 마지막으로 수정한 시각을 나타내는 숫자. UNIX 표준시(1970년 1월 1일 자정)으로부터 지난 시간을 밀리초 단위로 나타낸 값입니다.

size
파일의 크기를 바이트 단위로 나타낸 값.

type
파일의 MIME 유형.

내가 필요한 정보가 딱 마련되어 있다. 파일 업로드 기능은 글 작성 기능의 하위에 속하므로 글 작성 함수의 코드를 수정하여 추가하였다.

    let fileName = 'No file';
    if (doc.fileData) {
        fileName = doc.fileData[0].name;
    };
    
    (...)
    
    const docRef = await addDoc(colRef, {
        file: fileName,
		(...)
    });
    
    (...)

	if (doc.fileData) {
    	const imagesRef = ref(storageRef, fileName);
        await uploadBytes(imagesRef, doc.fileData);
    };
    

함수 내부에서 파일 이름을 뽑아서 변수에 담은 다음, 이 파일이 어떤 게시글에서 첨부한 것인지 알아야하므로 게시글 DB에 파일명을 저장하도록 수정하고, 스토리지 기능을 이용하여 파일을 업로드 하는 기능을 구현하였다.

* 기능 구현 중 발생한 문제들.

const fileName = doc.fileData[0].name;

기능을 처음 구현했을 때는 위와 같이 코드를 작성하였다. 그런데 이 경우, 만약 사용자가 파일을 첨부하지 않았을 때 fileData가 아예 존재하지않으므로 undefined 에러가 발생하고 말았다. 이를 해결하기 위해 초기값을 'No file'이라는 문자열로 설정하고, 파일 데이터가 존재할 경우에는 name값을 대입하도록 코드를 수정하였다.

테스트해보니.. 파일이 잘 첨부되어 스토리지에 저장되었다!



2. 파일을 다운로드 하는 기능.

다음으로는 파일을 다운로드 하는 기능을 구현해야한다. 게시글 데이터에 첨부파일명이 저장되어 있으니, 프론트에서 파일명을 가지고 다운로드 함수에 인자로 보내기만 하면 된다. 문제는 함수를 구현하는 방법.

방법을 찾느라 상당히 머리가 아팠고, 찾은 방법도 직접 구현해보니 동작하지 않는 경우가 태반이었다. 많은 노력 끝에 실제로 동작하는 기능을 찾아 구현하는데 성공하였다.

const downloadFile = async (fileName) => {
    const imagesRef = ref(storageRef, fileName);
    await getDownloadURL(imagesRef)
        .then((url) => {
            const xhr = new XMLHttpRequest();
            xhr.responseType = 'blob';
            xhr.onload = () => {
                const blob = xhr.response;
                const link = document.createElement('a');
                link.href = URL.createObjectURL(blob);
                link.download = fileName;
                link.click();
                URL.revokeObjectURL(link.href);
            };
            xhr.open('GET', url);
            xhr.send();
        })
        .catch((error) => {
            console.log(error.code);
            console.log(error);
        });
};
  1. 우선 스토리지 내부의 어떤 파일을 가져와야 하는지 기준점을 잡아주어야 한다. ref 함수와 매개변수로 받아온 파일 이름을 가지고 imagesRef를 지정해준다.

  2. getDownloadURL 함수는 파이어스토어에서 파일의 다운로드 URL을 가져오는 비동기 함수이다.

  3. 비동기적으로 데이터를 주고받기 위해서 XMLHttpRequest 객체를 생성한다. const xhr = new XMLHttpRequest();

  4. 해당 객체의 responseType을 지정해준다. 타입은 'blob'. 자바스크립트에서 이미지나 오디오 파일 등을 다룰때 사용하는 데이터 타입이라고 한다. xhr.responseType = 'blob';

  5. 데이터가 정상적으로 들어왔을 때 동작할 이벤트 함수를 정의해준다. xhr.onload = () => { ... };

  6. 서버에서 반환되오는 데이터를 변수에 저장해준다. const blob = xhr.response;

  7. 파일을 다운로드할 a 태그를 생성해준다. const link = document.createElement('a');

  8. a 태그의 href 속성에 URL.createObjectURL() 함수를 이용하여 파일의 URL을 담아 저장해준다. link.href = URL.createObjectURL(blob);

  9. 다운로드 받을 파일의 이름을 지정해주고, 생성한 a 태그를 click하여 파일 다운로드 작업을 개시한다. link.download = fileName;과 link.click();

  10. 메모리 누수 방지를 위해 역할을 다한 URL을 해제하여 준다. URL.revokeObjectURL(link.href);

  11. 5번에서 10번까지의 과정은 서버로 요청을 보내고, 정상적인 통신의 결과로 반환값이 존재할 경우에 동작할 함수를 정의한 것이다. 따라서 이번에는 요청할 통신의 속성값을 설정해준다. xhr.open('GET', url);

  12. 서버로 요청을 보낸다. xhr.send();

간단하게 축약하면 Firebase Storage에 존재하는 fileName에 해당하는 파일의 다운로드 URL을 얻은 후, XMLHttpRequest를 사용하여 파일을 다운로드하는 것이다.

테스트해보니.. 파일이 잘 다운로드 되었다.

profile
프론트엔드 개발자를 준비하고 있습니다.

1개의 댓글

comment-user-thumbnail
2023년 7월 18일

글이 많은 도움이 되었습니다, 감사합니다.

답글 달기