6. FILE UPLOAD
6-1. Preview Images
- 게시글에 글과 함께 사진도 올리고 싶음. 이럴 때 쓰는 게 Storage.
Storage는 이미지, 오디오, 동영상 같은 사용자가 생성한 파일을 저장하고 가져올 수 있게 하는 저장소.
test mode로 해서 bucket 생성. bucket은 파일을 넣는 곳임.
이제 사진을 함께 올릴 것이기 때문에 Nweet하는 방법을 바꿔야 함.
Home.js에 <input type="file" accept="image/*" /> 코드 추가.
이 input은 모든 이미지로 된 파일만 받아들인다는 의미.
그럼 화면에 이렇게 나타남.
우리가 원하는 것은 파일을 선택하고 이미지 선택을 눌렀을 때 사진을 미리보기 하는 것.
고로 우리는 파일이 string 형식으로 들어왔을 때 그걸 읽어올 수 있어야 함.
onFileChange라는 함수 추가. event를 매개변수로 받아옴.
file을 받아오는 input이 onChange 되었을 때 onFileChange 함수 실행.
console.log(event);를 찍어보면
event.target.files로 선택한 이미지의 파일 경로가 들어와 있는 것을 확인할 수 있음.
이제 const { target : {files} } = event; 코드를 작성하여 이미지 파일을 변수로 받아오고,
(const files = event.tarfet.files;를 구조 분해 할당한 것)
파일은 하나만 넣을 거기 때문에 files 배열의 첫 번째만 받아오도록 const theFile = files[0]; 코드 작성.
이제 받아온 파일을 읽기 위하여 fileReader API를 사용할 거임.
fileReader API : https://developer.mozilla.org/en-US/docs/Web/API/FileReader/load_event
reader라는 변수에 new FileReader();를 담아주고 onloadend를 실행.
onloadend가 끝나면 finishedEvent를 받음.
콘솔을 찍어보면 currentTarget의 onloadend랑 result에 값이 들어와 있는 것을 확인 가능.
그리고 onloadend가 끝나면 readAsDataURL(theFile);을 이용하여 파일을 읽을 거임.
이제 우리가 가진 이미지 파일의 URL을 담아줄 useState를 하나 생성.
const [ attachment, setAttachment ] = useState();
변수 result에 저장되어 있는 파일의 경로를 setAttachment()에 담아 attachment의 값을 갱신.
이제 이미지 파일을 받는 input 바로 아래에
div 태그를 하나 생성하여 내부에 img, button 태그를 작성해줄 거임.
< img src= { attachment } /> => img 경로에 attachment를 담아줌.
그리고 프리뷰 이미지를 삭제하고 싶을 수도 있으니 Clear라는 이름의 button을 생성.
button을 onClick시 onClearAttachment 함수가 실행되도록 코드를 작성.
위로 올라가 onClearAttachment 함수를 생성.
onClearAttachment 함수 내부에 setAttachment(null); 코드를 작성해 attachment의 값을 비워줌.
첨부파일 url을 넣는 state를 비워서 프리뷰 img src를 없애줌.
그런데 attachment에 값이 들어왔을 때만 이미지 프리뷰와 Clear button을 보여주고 싶으므로
{ attachment && (
<div>
<img src={ attachment } alt="preview" width=70, height=70 />
<button onClick={ onClearAttachment }> Clear </button>
</div>
)}
이렇게 코드를 작성.
attachment가 true일 때만 <div></div> 코드들이 정상적으로 작동함.
그런데 Clear button을 눌러서 이미지 프리뷰를 지워도 input에 이미지 파일명이 남아있음.
input에 남아 있는 이미지 파일명도 지워주기 위하여 useRef 훅을 사용.
- useRef hook을 사용하는 이유?
React 컴포넌트는 기본적으로 내부 상태(state)가 변할 때 마다 다시 랜더링(rendering)이 됨.
예를 들어, 아래 <Counter/> 컴포넌트의 버튼을 5번 클릭하면 count 상태값은 5번 바뀌게 됨.
import React, { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
console.log(`랜더링... count: ${count}`);
return (
<>
<p>{count}번 클릭하셨습니다.</p>
<button onClick={() => setCount(count + 1)}>클릭</button>
</>
);
}
브라우저 콘솔을 확인해보면, 5번의 로그가 찍히는 것을 볼 수 있음.
이를 통해, <Counter/> 컴포넌트 함수는 count 상태가 바뀔 때 마다 호출되는 것을 알 수 있음.
랜더링... count: 1
랜더링... count: 2
랜더링... count: 3
랜더링... count: 4
랜더링... count: 5
컴포넌트 함수가 다시 호출이 된다는 것은 함수 내부의 변수들이 모두 다시 초기화가 되고
함수의 모든 로직이 다시 실행된다는 것을 의미.
우리는 대부분의 경우, 위와 같이 상태가 변할 때 마다 React 컴포넌트 함수가 호출되어 화면이 갱신되기를 바람.
하지만 그에 따른 부작용으로 함수 내부의 변수들이 기존에 저장하고 있는 값들을 잃어버리고 초기화됨.
간혹 다시 랜더링이 되더라도 기존에 참조하고 있던 컴포넌트 함수 내의 값이 그대로 보존되야 하는 경우가 있음.
useRef 함수는 current 속성을 가지고 있는 객체를 반환하는데, 인자로 넘어온 초기값을 current 속성에 할당.
이 current 속성은 값을 변경해도 상태를 변경할 때 처럼 React 컴포넌트가 다시 랜더링되지 않음.
React 컴포넌트가 다시 랜더링될 때도 마찬가지로 이 current 속성의 값이 유실되지 않음.
1. useRef()훅을 통해 fileInput변수를 만들고 file input과 연결시켜줌.
const fileInput = useRef();
< input type="file" accept="image/*" onChange={onClearAttachment} ref={fileInput} />
2. Clear버튼을 눌렀을 때 fileInput객체 안에 current의 value값을 가져와서 비워줌.
onClearAttachment = () => {
fileInput.current.value = "";
}
이러한 과정들을 통하여 이미지 프리뷰를 화면에 뜨게 하는 것과 지우는 것을 성공적으로 해냄.
but, 파일 선택을 누르고 이미지 파일을 선택한 후 취소를 누르면 'Blob'이라는 오류가 뜸.
이러한 오류를 해결하기 위하여 onFileChange 함수가 실행되면
마지막에 if문이 실행되면서 theFile이 true일 때만(값이 들어와 있을 때만) 파일을 읽도록 함.
if (theFile) {
reader.readAsDataURL(theFile);
}
이렇게 해주면 이제 오류가 발생하지 않음.
6-2. Uploading
이제 bucket에 업로드할 준비가 끝남.
사진도 함께 nweet에 담아서 업로드해야 하므로 Home.js의 onSubmit 함수 로직을 수정.
원래 있던 로직들을 모두 주석 처리 해줌.
먼저, 사진을 업로드하고 사진이 있다면 사진의 URL을 받아서 nweet에 추가할 거임.
이제 storage를 사용해야 하기 때문에 firebase.js로 가서 storage를 import해줄 거임.
❗ Storage를 가져다 쓰기 위한 코드 추가
참고 링크 : https://firebase.google.com/docs/storage/web/start
우리가 지금 만들려고 하는 것은 ref(reference) => 구글 클라우드 스토리지 오브젝트(bucket)에 대한 참조를 나타냄.
참조를 통하여 오브젝트를 업로드, 다운로드, 그리고 삭제할 수 있음.
❗ Create a Cloude Storage reference on Web
참고 링크 : https://firebase.google.com/docs/storage/web/create-reference
우리는 firebase.js에 만들어둔 storageService, ref를 이용하여 참조를 만들 거임.
우리는 아래의 방식을 사용할 건데, 이미지의 경로를 두 번째 인수로 전달함으로써 (child)하위 참조를 생성.
reference는 collection과 비슷함. 폴더를 만들 수 있음.
모든 유저의 이미지는 아이디와 분리되어 있음.
우리는 원래 가지고 있던 userObj.uid를 이용할 거임.
또한, 기본적으로 사진에 이름을 줄 수 있음. 우리는 uuid를 이용해 랜덤으로 이름을 생성할 거임.
우리는 firebase.js에서 getStorage()를 storageService 변수에 담아줬었기 때문에
const fileRef = ref(storageService, `${userObj.uid}/${uuidv4()}`);
이렇게 storageService를 이용하여 참조 코드를 작성 후 변수 fileRef에 담아줌.
참조를 생성하는 이유는 파일을 업로드하기 위해서 참조가 반드시 필요하기 때문.
참조를 생성했으니까 이제 파일 업로드를 위한 코드를 작성해보도록 하겠음.
❗ Upload Files
참고 링크 : https://firebase.google.com/docs/storage/web/upload-files
우리는 문자열로 된 파일 업로드 방식을 사용할 거임.
uploadString() method를 사용하여 원시 또는 인코딩된 문자열을 Cloud Storage에 업로드할 수 있음.
업로드를 위해서는 참조와 data, 그리고 데이터 형식을 요구함.
우리가 사용할 data는 이미지 URL이 담긴 attachment의 string, 데이터 형식은 data_url임.
const reponse = await uploadString(fileRef, attachment, "data_url");
작성된 코드 역시 reponse라는 변수에 담아줌.
❗ 여기까지 했는데도 bucket에 이미지 파일이 담기지 않는다는 오류가 발생함.
아니, 정확하게 말하면 오류도 뜨지 않고 파일이 bucket에 담기지도 않음.
분명 코드도 맞게 작성했는데 왜 담기지가 않을까 고민을 하다가 한 가지 사실을 알게 됨.
Storage - Rules - Rule 에 들어가서
규칙을 이런 식으로 다시 바꿔줘야 함.
나도 원래 allow read, write: if false; 로 설정되 있어서 업로드가 되지 않았음.
read/write 모두 auth가 있는 경우에만 허용한다는 뜻임.
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if request.auth != null;
}
}
}
이제 이미지 파일을 선택 후 Nweet 버튼을 누르면 이미지 파일이 bucket에 담겨 있는 것을 볼 수 있음.
6-3. File URL and Nweet
이제 참조 경로에 있는 파일의 URL을 다운로드 받을 거임.
다운로드 받은 후 이걸 변수에 담아 업데이트 해야만 새로운 document를 만들어 nweets Collection에 넣어줄 수 있음.
❗ Download Files
참고 링크 : https://firebase.google.com/docs/storage/web/download-files
우리는 URL을 통해 데이터를 다운로드 받을 거임.
그러기 위해서 필요한 것은 getDownloadURL() method.
일단 먼저 getDownloadURL을 import해줌.(firebase/storage에서)
let attachmentUrl = "";
attachmentUrl이라는 변수를 선언해서 여기 다운로드한 파일의 URL을 담아줄 거임.
let으로 선언해준 것은 URL이 매번 달라져야 하기 때문.
getDownloadURL(response.ref);
getDownloadURL 함수의 인수로 참조를 담아줘야 함.
response.ref, fileRef 둘 다 사용해도 괜찮음.(둘 다 참조를 뜻하니까)
attachmentUrl에 값이 담겼다면 이제 object를 만들어보겠음.
nweetObj 객체의 형태로 새로운 Document를 생성하여 Collection에 넣어주기.
nweetObj에는 예전과 달리 attachmentUrl이 하나 더 추가됨. => attachmentUrl : attachmentUrl
이렇게 하면 다운로드한 이미지 파일의 URL까지 함께 Document로 담기게 됨.
그리고 예전에 해봤던 Add a Document를 해주면 됨!
해봐서 알겠지만 addDoc(collection(dbService, "nweets"), nweetObj); 이렇게 코드를 작성해주면
nweets Collection 안에 nweetObj 객체 형태의 Document가 생성됨.
기억이 잘 나지 않는다면 참고 : https://dnjfht.tistory.com/60
Document 생성 후에는 setNweet("")를 통하여 Nweet(글을 작성하는 input)를 비워주고
setAttachment("")를 통하여 Attachment(파일 미리보기 img src)를 비워줌.
그리고 Submit button을 누른 후에 선택했던 파일명이 사라지도록 코드를 하나 더 추가해줌.
fileInput.current.value = "";
파일 선택을 취소했을 때 파일명이 삭제되던 그 방식을 그대로 가져와서 사용함.
나머지는 기존에 코드를 입력해 놓은 대로 작동을 하여
Nweets Collection에 nweetObj 객체가 Document로 담기고,
마운트될 때마다 새로운 Document가 들어와 Nweets에 하나씩 쌓이게 될 거임.
이제 선택한 이미지 파일이 화면에 보이도록 해야 하므로 Nweet.js로 넘어가 코드를 추가해줌.
Nweet.js는 Nweets를 map 돌려서 얻어낸 Nweet를 props로 받음.
그렇기 때문에 Nweet 안에는 attachmentUrl 역시 담겨 있음.
{ nweet.attachmentUrl && <img src = { nweet.attachmentUrl } width="500px" }
이 코드는 attachmentUrl이 true일 때(값이 존재할)만 img 태그를 보여주겠다는 얘기임.
img src에는 attachmentUrl을, 넓이는 500px 정도로 하겠음. 높이는 미지정.
이미지의 높이를 지정해주면 높이에 따라서 이미지가 뭉그러뜨려짐...
이렇게 코드 작성을 끝마치면
이미지를 첨부하고 글을 작성한 후 Nweet를 눌렀을 때 이렇게 게시글이 아래에 나타남!
그런데 여기서 문제점이 발생함.
사진을 첨부하지 않고서는 게시글을 작성할 수 없다는 문제가...😅
그래서 코드를 조금 수정해보도록 하겠음.
Home.js로 돌아가서 attachment의 initialState 값을 ""로 변경해줌.
트윗할 때 텍스트만 입력시 이미지 url ""로 비워두기 위함.
onSubmit 함수 내에서도 if문을 추가해줌.
위에서 설명했듯이 이미지를 첨부하지 않았을 경우에는 attachment의 값이 ""이고,
attachment가 ""가 아닐 때(미리보기 이미지 URL이 있을 때)
참조와 더불어 파일 업로드, 참조 경로에 있는 파일의 URL 다운로드까지 이뤄지도록 함.
attachment가 ""일 경우에는 attachmentUrl의 값이 ""로 비어있게 됨.
비어 있는 상태로 nweetObj에 값이 들어가게 됨.
Clear button을 클릭하여 attachment 값을 비워서 프리뷰 img src를 없애는 부분에서도
setAttachment("");로 바꿔줌.텍스트만 입력시 이미지 url을 ""로 비워두기 위해서임.
6-4. Deleting Files
이제 Delete Nweet button을 눌렀을 때 이미지와 글이 모두 삭제되도록 할 거임.
화면에서만 삭제되는 것이 아니라, 글은 Firestore에서, 이미지는 Storage에서 삭제되게끔 할 거임.
❗ Delete Files
참고 링크 : https://firebase.google.com/docs/storage/web/delete-files#delete_a_file
'(심층)리액트' 카테고리의 다른 글
다시 시작하는 리액트 - 리액트 실무 기초 2 (0) | 2023.03.27 |
---|---|
(심층)리액트트위터 클론코딩(with firebase) - 06 (0) | 2023.02.18 |
트위터 클론코딩(with firebase) - 하나의 이메일로 여러 계정 생성이 안되는 문제 해결 (0) | 2023.02.09 |
트위터 클론코딩(with firebase) - 04 (0) | 2023.02.07 |
트위터 클론코딩(with firebase) - 03 (0) | 2023.02.03 |
github : https://github.com/dnjfht
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!