지난시간의 프리로드, 프리패치는 html에서 제공된 것이었다.
다음페이지에서 사용될 이미지나 페이지 등을 미리받아 저장이 가능하다.(아폴로 캐시스테이트나 recoil(글로벌)스테이트에 저장한다)
이미지 기존 등록방식:
사진을 등록하려고 이미지를 선택 후 확인을 누르면 uploadfile이라는 뮤테이션이 백엔드로 날라가고 벡엔드에서는 해당 파일을 외부 스토리지로 보내 그 스토리지에서는 받은 사진을 저장한 뒤 그 주소를 넘겨줘 최종적으로는 그 다운로드 url이 넘어와 게시물을 등록하는 뮤테이션을 넘길때 사용한다.
이때의 문제점이 있었다.
사진을 등록하면서 미리볼 수 있었는데, 스토리지까지 가서 해당이미지의 주소를 받아와야하기에 약간 한타임느리게 보인다.
뿐만아니라, 만약 사진미리보기만하고 게시물을 실제로 등록하지 않는다면, 미리보기용까지 하는과정에서 이미 보낸 이미지의 주소를 받은것이기에 스토리지에 파일은 남는다. 이것을 '이미지 찌꺼기' 라고 표현한다.
등록하기를 누르지 않았는데도 스토리지에는 남는다는것. 즉 이것은 동기화 처리가 되지 않는다는 것이다.
해결법:
이미지를 클릭시 선택후 바로 uploadfile 을 보내지 않고 브라우저 자체에서 읽을 수 있게 주소를 만들어 사용하는것이다.(임시 url생성)
--> 업로드를 하고 url을 받아 화면에 미리보기로 그려주는 형식이 아니라 바로 브라우저 상에서 url을 만들어 그것으로 미리보기를하고, 이후에 뮤테이션을 보낼때는 선택한 파일을 담은 filed이라는 변수 하나만 기존의 방식으로 스테이트에 저장해 보내면된다.
즉, 이렇게되면 미리보기는 브라우저에서 자체 생성한 url로 빠르게 받아오고, 보낼때 스토리지에 가서 url을 받아와 저장하니 보낼때의 속도가 느려지는 단점이 있다. 그러나 스토리지에 찌꺼기가 쌓이는 것을 막을 수 있어 백엔드 개발자들이 조금 덜 힘들 수 있다고한다.
import { gql, useMutation } from "@apollo/client"; // import { Modal } from "antd"; import { ChangeEvent, useState } from "react"; import { IMutation, IMutationUploadFileArgs, } from "../../src/commons/types/generated/types"; const CREATE_BOARD = gql` mutation createBoard($createBoardInput: CreateBoardInput!) { # 그래프큐엘 주석. 변수의 타입적는곳 createBoard(createBoardInput: $createBoardInput) { # 실제 우리가 전달할 변수 적는곳. _id # 생성되면id만 받아옴. } } `; const UPLOAD_FILE = gql` mutation uploadFile($file: Upload!) { uploadFile(file: $file) { _id url } } `; export default function ImageUploadPage() { const [imageUrl, setImageUrl] = useState(""); // 미리보기용 const [file, setFile] = useState<File>(); // 실제 보낼 파일담기 const [uploadFile] = useMutation< Pick<IMutation, "uploadFile">, IMutationUploadFileArgs (UPLOAD_FILE); const [나의함수] = useMutation(CREATE_BOARD);
const onClickSubmit = async () => { // createBoard 즉 게시물 등록 전에 uploadfile통해 스토리지에 파일 보내놓고 다운로드 주소를 받아오고 그 주소를 createBoard로 보내기 const resultFile = await uploadFile({ variables: { file } }); // 파일 업로드하여 그 결과를 resultFile에 담음(스토리지로 보냄) const url = resultFile.data?.uploadFile.url; // 그 결과에서 url을 받아옴.(스토리지url) const result = await 나의함수({ variables: { // 생성할것 createBoardInput: { writer: "유리", title: "안녕하세요", contents: "반갑습니다", password: "1234", images: [url], // DB에 저장됨.잔짜 url을 넣으면 다른 브라우저에서도 접근 가능하나 사진이 글자로 변환된것이기에 용량 큼.따라서 스토리지에 저장해 다운로드 받을 주소만 DB에 저장. }, }, }); console.log(result);
const onChangeFile = async (event: ChangeEvent<HTMLInputElement>) => { const file = event.target.files?.[0]; // <input type="file" multiple/> multiple 속성사용시 여러개 드래그로 선택 가능 그중하나. 멀티플 안주면 선택한 사진 하나 의미 if (!file) return; console.log(file); // try { // const result = await uploadFile({ // variables: { // file, // 벡엔드에 보내기 // }, // });
// console.log(result); console.log(result.data?.uploadFile.url); // 얘가 이미지 주소 // setImageUrl(result.data?.uploadFile.url ?? ""); // 없으면 빈문자열 '||' 쓰니 빨간줄 생겨 '??'로 바꿈 // 스테이트에 저장. 저장되면 리랜더 되면서 이미지가 미리보기 됨==> 느림 // } catch (error) { // if (error instanceof Error) Modal.error({ content: error.message }); // 만약 error가 Error의 자식이면 (거기포함되면) 에러모달창 띄워주기 // } // 받은 file을 가지고 브라우저자체에서 미리보기 생성 // 1. 임시 url생성 - (가짜url: 내 브라우저에서만 접근 가능)// 스토리지나 DB에 올리는것 아님 -- 최근에 나온 방식이기에 아직은 적용안되는 브라우저 있음. // 적용이 가능한 브라우저인지 체크하여 적용이 가능한 브라우저라면 1번방법을 아니면 2번 방법을 사용. // 상황에 따라 사용할것. // const result = URL.createObjectURL(file); // file 에 접근할 수 있는 가짜 url을 만들어 주세요 // console.log(result); // blob이라는 이진으로된 큰 객체에 주소가 만들어짐. 본인 컴퓨터에서만 접근가능 localhost이므로 // setImageUrl(result); // 2. 임시 url생성 - (진짜 url: 다른 브라우저에서도 접근 가능) // 스토리지나 DB에 올리는것 아님 // 주로 사용하는 방식. // 둘다 스토리지에 보내지는 않음. // 이미지 색 하나하나를 글자로 변환해 만듬. 사이즈 큼 // 1번 방법 나오기 까지 계속사용했던 방식. 모든 브라우저에서 가능. // 이 방법 사용하여 실습 const fileReader = new FileReader(); // FileReader를 활용 fileReader.readAsDataURL(file); // 얘를 가지고 file 을 DataURL로 읽을 것임. 누구나 쓸수있는 진짜 주소가 됨. // 바로 되지 않고 시간 걸림 로드될때까지 기다려야함. fileReader.onload = (event) => { if (typeof event.target?.result === "string") { // 로드가 끝나면 event가 들어오고 그 이멘트의 타겟의 result가 사진주소. console.log(event.target?.result); // 이벤트가 여러군데 들어가기에 즉, 태그에서만 쓰이는게 아니기에 태그에는 id 가 있으나, 아닐경우도 있으니 currentTarget으로썼던 경우 있었음. // event.target은 태그만을 가리키지 않기때문!! setImageUrl(event.target?.result); // 사진 선택하면 이미지 미리보기 빠르게 가능. 등록하지 않으면 스토리지에 안날라가니 스토리지에 찌꺼기 쌓이지 않음 setFile(file); // 실제 파일을 저장 // 등록하기를 누를시 state에 담김 } };
}; return ( <> <input type="file" onChange={onChangeFile} /* multiple */ /> {/* <img src={`https://storage.googleapis.com/${imageUrl}`} /> */} {/* /* 화면에 뿌리기 */} {/* 브라우저에서 바로 만든 임시 url사용하기에 구글스토리지에서 받아오지 않음 그것을 state에 저장하고 그것을 바로 사용 속도 빠름. 스토리지에 저장되는게 아니라서 찌꺼기 쌓이지 않음 */} <img src={imageUrl} /> <button onClick={onClickSubmit}>게시글 등록하기</button> </> ); }
브라우저에서 임시 url을 받아오는 방법은 두가지가 있다.
하나는 내 브라우저에서만 사용이 가능한 url을 만드는 방법으로 blob이라고 쓰여있고 뒤에 주소가찍히고, 찍히고 URL.createObjectURL(여기에 url로 바꾸고픈 것을 넣어줌)
이것은 거의 최근에 나온 방법으로 브라우저마다 적용이 될수도 안될수도 있다고한다.
또다른 방법은
임시 url이지만 모든 브라우저에서 사용이 가능한 url을 만드는것이다.
이제껏 사용되어왔던 방법이기에 모든 브라우저에서 적용이 가능하다. 단점은 사진의 색 하나하나를 숫자로 다 변환해 만드는것이다 용량이 좀 된다는것. 가능한 브라우저에서는 1번 방법을 , 아니라면 2번 방법을 적용하게 조건을 걸어주는 식으로도 사용 가능하고, 둘중 하나만 선택해 사용하는 것도 가능하지만, 오늘은 이 두번째 방법을 사용하여 적용을 했다.
FileReader() 라는 것을 활용한다.
new FileReader()로 활용을 선언, 해당부분을 변수에 담아 그것을 가지고 readAsDataURL(file) => file이라는 것을 DataURL로 읽을 것이다 라고하며 ==> 이렇게 누구나 사용할 수 있는, 임시 url을 생성 가능하다.
이번에는 Promise.all이라는것을 적용해보자
일단 Promise라는 것은 API요청 등 기다렸다받는 종류의 것들이 리턴하는것을 얘기한다. (리턴을 Promise로 하는것.)
우선은 Promise.all이라는것을 적용하기 전과 뭐가다른지 그려보았다.
일단 Promise로 만들어 줘야하기에 일정시간 요청을 기다렸다 받아오는 그림으로 만들어 주기위해 setTimeout이라는 것을 사용해 각각 1초, 2초, 3초 뒤에 실행되게끔 만들어 주었다.
그리고 해단 첫부분과 마지막부분까지 얼마나 걸리는지 알기 위해 console.time()과 console.timeEnd()로 콘솔을 찍어 time에서부터timeEnd부분까지 몇초가 걸리는지 찍어보았다.
const startPromise = async () => { console.time("== 개별 Promise 각각 ==="); // 여기서부터 const result1 = await new Promise((resolve, reject) => { setTimeout(() => { resolve("성공!!"); }, 2000); // 2초뒤에 result로 결과 받을수 있음. 아니면 await말고 .then()을 사용한다면 2초 뒤에 성공하면.then()이 실행됨. }); const result2 = await new Promise((resolve, reject) => { setTimeout(() => { resolve("성공!!"); }, 3000); }); const result3 = await new Promise((resolve, reject) => { setTimeout(() => { resolve("성공!!"); }, 1000); }); console.timeEnd("== 개별 Promise 각각 =="); // (time ~ timeEnd)여기까지 얼마나 걸렸는지 시간찍어줌. 단, 안에 이름등 들어있는것 같아야함. 시작부분이 "== 개별 Promise 각각 =="이니 끝부분에도 "== 개별 Promise 각각 =="로 // 총 하나씩 보내니 6초걸림 };
위에 적혀있듯, 총 6초 정도 걸리는것으로 찍혔다.
그렇다는것은 하나씩 실행되고 그게끝나야 다음것, 그리고 그 다음것이 찍히는 등의 일이 일어난다는 것.
그러면 이번에는 Promise.all() 이라는것을 적용하면 어떻게 될까?
먼저 Promise.all이라는것을 알아보자
Promise.all() : 묶어서 한번에 보내고 하나씩 받는것.(하나기다렸다가 끝내고 다음것 보내는 방식이 아니라 한번에 보내놓고 결과만 기다리는 그런 방식!!)
해당부분을 new Promise가 아니라 Promise.all()로 묶어 사용하면된다.
전부 묶었으니 개별적으로 각각을 result1,result2, result3 등으로 묶은 과정은 더이상 필요치 않고 전부를 하나로 묶어 변수 result에 담아 찍어본다.
그리고 똑같이 시간을 재본다.
const startPromiseAll = async () => { // 동시에 보내고 한번 기다릴것임. // await Promise.all([promise, promise, promise]); // 프로미스들을 감싸는 배열로 넣음. 한번에 다 보내고, 기다림. 먼저끝나는 순서로 담기고, 다 받아오기전까지 아래 실행 못하게 await걸기 console.time("=== 한방 Promise.all ==="); const result = Promise.all([ new Promise((resolve, reject) => { setTimeout(() => { resolve("성공!!"); }, 2000); }), new Promise((resolve, reject) => { setTimeout(() => { resolve("성공!!"); }, 3000); }), new Promise((resolve, reject) => { setTimeout(() => { resolve("성공!!"); }, 1000); }), ]); console.log(result); // 배열안에 각각의 결과가 담긴다. console.timeEnd("=== 한방 Promise.all ==="); // 한번에 보내고 먼저끝나는 순서로 받아오기때문에 총 3초 걸림 };
이렇게 해서 시간을 재보면 한번에 보내기에 1초에 하나실행, 2초에 하나실행, 마지막 3초에 하나가 실행되며 총 3초가 걸린다는 것을 볼 수 있다.
이번에는 이미지를 배열에 담아 3개를 보내본다.
Promisse.all을 사용하지 않을때
1. Promise.all 안썼을때 - 하나보내고 기다리고, 하나보내고 기다리고.. const resultFile0 = await uploadFile({ variables: { file: files[0] } }); // 파일 업로드하여 그 결과를 resultFile에 담음(스토리지로 보냄) const url0 = resultFile0.data?.uploadFile.url; // 그 결과에서 url을 받아옴.(스토리지url) const resultFile1 = await uploadFile({ variables: { file: files[1] } }); // 파일 업로드하여 그 결과를 resultFile에 담음(스토리지로 보냄) const url1 = resultFile1.data?.uploadFile.url; const resultFile2 = await uploadFile({ variables: { file: files[2] } }); // 파일 업로드하여 그 결과를 resultFile에 담음(스토리지로 보냄) const url2 = resultFile2.data?.uploadFile.url; const resultUrls = [url0, url1, url2]; // 각각의 url을 뽑아서 배열에 들어가는것은 각각의 url [dog1.jpg,dog2.jpg,dog3.jpg]
엄청길어진다. 게다가 반복되는 부분도 있다. 이부분을 Promise.all() 을 사용해 1차적으로 줄여준다면
2. Promise.all 썼을때 -- 한번에보내고 기다림 const results = await Promise.all([ uploadFile({ variables: { file: files[0] } }), uploadFile({ variables: { file: files[1] } }), uploadFile({ variables: { file: files[2] } }), ]); console.log(results); // 얘는 [resultFile0,resultFile1,resultFile2] . 결과가 url이 아님.map을 사용해 해당의 url을 뽑기 const resultUrls = results.map((el) => (el ? el.data?.uploadFile.url : "")); // el이 없으면 빈 문자열로. [dog1.jpg,dog2.jpg,dog3.jpg]
이때의 results로 받아오는 것은 각각 업로드한 파일 하나하나고, 이들을 이용해 map으로 각각의 data의 url을 뽑아와야 배열에 각각의 사진 url이 담긴다.
그런데 보니까 valrialbes에 file:files[0] 해서 각각의 인덱스가 들어가는것을 볼 수 있다.
그렇다면 애초에 files를 map으로 돌려 el그러니까 업로드 파일이 있을경우에 업로드 파일을 실행하도록 만든다.
그리고 동일하게 그 결과를 다시 map으로뿌려 url만 받아 resultUrls에 담고 해당부분을 활용해 뮤테이션을 날린다.
>```
const result = await 나의함수({
variables: {
createBoardInput: {
writer: "유리",
title: "안녕하세요",
contents: "반갑습니다",
password: "1234",
images: resultUrls, // [url0,url1,url2]
},
},
});
console.log(result);
;
};
-- 이미지를 제외한 해당 내용들은 하드코딩 적용.
이런식으로 여러개를 한번에 Promise.all을 이용해 적용했다고 한다면, 뮤테이션도 바뀌어야한다. 여러개를 보내니 배열 형태로, 그러나 원본은 건들이지 않는것이 좋으므로 스프레드 연산자를 사용해 복사하여 state에 담아 이용하도록한다.
const [imageUrls, setImageUrls] = useState(["", "", ""]); // 미리보기용 이미지 3개 이용할거니 배열로! const [files, setFiles] = useState<File[]>([]); // 실제 보낼 파일담기(여러개(3개)!!)
const onChangeFile = (index: number) => async (event: ChangeEvent<HTMLInputElement>) => { const file = event.target.files?.[0]; // <input type="file" multiple/> multiple 속성사용시 여러개 드래그로 선택 가능 그중하나. 멀티플 안주면 선택한 사진 하나 의미 if (!file) return; console.log(file); const fileReader = new FileReader(); // FileReader를 활용 fileReader.readAsDataURL(file); // 얘를 가지고 file 을 DataURL로 읽을 것임. 누구나 쓸수있는 진짜 주소가 됨. // 바로 되지 않고 시간 걸림 로드될때까지 기다려야함. fileReader.onload = (event) => { if (typeof event.target?.result === "string") { // 로드가 끝나면 event가 들어오고 그 이멘트의 타겟의 result가 사진주소. console.log(event.target?.result); // 이벤트가 여러군데 들어가기에 즉, 태그에서만 쓰이는게 아니기에 태그에는 id 가 있으나, 아닐경우도 있으니 currentTarget으로썼던 경우 있었음. // event.target은 태그만을 가리키지 않기때문!! // setImageUrls(event.target?.result); // 사진 선택하면 이미지 미리보기 빠르게 가능. 등록하지 않으면 스토리지에 안날라가니 스토리지에 찌꺼기 쌓이지 않음 // setFiles(file); // 실제 파일을 저장 // 등록하기를 누를시 state에 담김 // 임시로 담을때 temp라는 용어 주로 사용 const tempUrls = [...imageUrls]; // 단 imageUrls라는 원본을 바꿔버리면 setImageUrls에 넣으면 바뀐것 인식을 못함. 스프레드연산자 사용하기! tempUrls[index] = event.target?.result; // 미리보기 이미지도 각각의 인덱스 위치에 넣기 setImageUrls(tempUrls); // 해당 인덱스 위치의 것이 바뀐것을 넣어줌. const tempFiles = [...files]; // 원본을 건드리지 않고 스프레드로 복사하여 사용 tempFiles[index] = file; // file을 files의 해당 인덱스 위치에 setFiles(tempFiles); // 해당 인덱스 위치의 것이 바뀐것을 넣어줌. } }; }; return ( <> <input type="file" onChange={onChangeFile(0)} /* HOC형태로 적어 onChangeFile의 index라는 매개변수로 보내줌 */ /> <input type="file" onChange={onChangeFile(1)} /* multiple */ /> <input type="file" onChange={onChangeFile(2)} /* multiple */ /> {/* <img src={`https://storage.googleapis.com/${imageUrl}`} /> */} {/* /* 화면에 뿌리기 */} {/* 브라우저에서 바로 만든 임시 url사용하기에 구글스토리지에서 받아오지 않음 그것을 state에 저장하고 그것을 바로 사용 속도 빠름. 스토리지제 저장되는게 아니라서 찌꺼기 쌓이지 않음 */} <img src={imageUrls[0]} /> {/* 여기가 이미지 미리보기 나오는 부분 */} <img src={imageUrls[1]} /> <img src={imageUrls[2]} /> <button onClick={onClickSubmit}>게시글 등록하기</button> </> );
내가 만든코드
import { gql, useMutation } from "@apollo/client"; import { ChangeEvent, useRef, useState } from "react"; import { IMutation, IMutationUploadFileArgs, } from "../../src/commons/types/generated/types"; const CREATE_BOARD = gql` mutation createBoard($createBoardInput: CreateBoardInput!) { createBoard(createBoardInput: $createBoardInput) { _id } } `; const UPLOAD_FILE = gql` mutation uploadFile($file: Upload!) { uploadFile(file: $file) { _id url } } `; export default function ImageUploadPage32day() { const [writer, setWriter] = useState(""); const [title, setTitle] = useState(""); const [contents, setContents] = useState(""); const [password, setPassword] = useState(""); const [imageUrls, setImageUrls] = useState(["", "", ""]); // 미리보기용 이미지 3개 이용할거니 배열로! const [files, setFiles] = useState<File[]>([]); // 실제 보낼 파일담기(여러개(3개)!!) const [uploadFile] = useMutation< Pick<IMutation, "uploadFile">, IMutationUploadFileArgs (UPLOAD_FILE); const [createBoard] = useMutation(CREATE_BOARD); const fileRef1 = useRef<HTMLInputElement>(null); const fileRef2 = useRef<HTMLInputElement>(null); const fileRef3 = useRef<HTMLInputElement>(null); const onClickSubmit = async () => { const results = await Promise.all( files.map((el) => el && uploadFile({ variables: { file: el } })) // 여기서는 await 붙이지 않기 // el && 조건으로 el이 있을때만 하기 ); console.log(results); // 얘는 [resultFile0,resultFile1,resultFile2] . 결과가 url이 아님.map을 사용해 해당의 url을 뽑기 const resultUrls = results.map((el) => (el ? el.data?.uploadFile.url : "")); const result = await createBoard({ variables: { createBoardInput: { writer, title, contents, password, images: resultUrls, }, }, }); console.log(result); }; const onChangeFile = (index: number) => async (event: ChangeEvent<HTMLInputElement>) => { const file = event.target.files?.[0]; if (!file) return; console.log(file); const fileReader = new FileReader(); // FileReader를 활용 fileReader.readAsDataURL(file); // 얘를 가지고 file 을 DataURL로 읽을 것임. 누구나 쓸수있는 진짜 주소가 됨. fileReader.onload = (event) => { if (typeof event.target?.result === "string") { console.log(event.target?.result); const tempUrls = [...imageUrls]; tempUrls[index] = event.target?.result; setImageUrls(tempUrls); const tempFiles = [...files]; tempFiles[index] = file; setFiles(tempFiles); } }; }; const onChangeWriter = (event: ChangeEvent<HTMLInputElement>) => { setWriter(event.target.value); }; const onChangePassword = (event: ChangeEvent<HTMLInputElement>) => { setPassword(event.target.value); }; const onChangeTitle = (event: ChangeEvent<HTMLInputElement>) => { setTitle(event.target.value); }; const onChangeContents = (event: ChangeEvent<HTMLInputElement>) => { setContents(event.target.value); }; const onClickImg = () => { fileRef1.current?.click(); }; const onClickImg2 = () => { fileRef2.current?.click(); }; const onClickImg3 = () => { fileRef3.current?.click(); }; return ( <> 작성자: <input type="text" onChange={onChangeWriter} /> 비밀번호: <input type="password" onChange={onChangePassword} /> 제목: <input type="text" onChange={onChangeTitle} /> 내용: <input type="text" onChange={onChangeContents} /> 이미지 등록: {imageUrls[0] ? ( <img onClick={onClickImg} src={imageUrls[0]} style={{ width: "120px", height: "120px" }} /> ) : ( <button onClick={onClickImg} style={{ width: "120px", height: "120px" }} + </button> )} {imageUrls[1] ? ( <img onClick={onClickImg2} src={imageUrls[1]} style={{ width: "120px", height: "120px" }} /> ) : ( <button onClick={onClickImg2} style={{ width: "120px", height: "120px" }} + </button> )} {imageUrls[2] ? ( <img onClick={onClickImg3} src={imageUrls[2]} style={{ width: "120px", height: "120px" }} /> ) : ( <button onClick={onClickImg3} style={{ width: "120px", height: "120px" }} + </button> )} <input type="file" onChange={onChangeFile(0)} ref={fileRef1} style={{ display: "none" }} accept="image/jpeg,image/png,image/jpg" /> <input type="file" onChange={onChangeFile(1)} style={{ display: "none" }} ref={fileRef2} accept="image/jpeg,image/png,image/jpg" /> <input type="file" onChange={onChangeFile(2)} style={{ display: "none" }} ref={fileRef3} accept="image/jpeg,image/png,image/jpg" /> <button onClick={onClickSubmit}>저장하기</button> </> ); }
먼저 미리 받아놓고 보여줌: 프리로드
다른페이지에서 볼 것 미리 받아 저장.: 프리패치
LazyLoad => 처음부터 이미지를 받아오는것이 아니라 스크롤 내리면 그때 마저 받아오는 형식. 라이브러리가 존재하는데, 수업중 강의에서는 react-lazyload라는 라이브러리를 소개해 주셨으나 아래 강의자료에는 react-lazy-load라는 라이브러리가 나와있었다. (오늘 과제에 적용하는 부분이 있어 두개를 비교하다가 다운로드 수가 많은 react-lazyload를 사용하였다. )
프리로드 프리패치 부분을 설명해 주시면서 여러 이미지 관련 라이브러리들도 소개해 주시고 구글 페이지 스피드 insights라는 페이지를 알려주셨다. 해당 사이트는 사이트 주소를 넣게되면 pc상의 속도, 모바일 상의속도, 최적화할 부분등이 나오게되며 해당 속도로 점수를 매긴다. 점수가 높을 수록 검색에는 잘 잡히게 되는데 그 점수가 이 점수인것.
보다보면 밑에 어떤식으로 개선해야하는지가 나오는데, 이미지를 webp확장자로 변경하라는 내용도 있다 한다.
webp확장자:
이미지 용량을 줄여야 빨리 받아오고 저장되는 용량도 덜 차지하게되는데 이런식으로 용량을 줄이게 된다면 화질또한 저하될 수밖에 없다.
wepb확장자는 화질저하를 최소화하는 확장자로 따라서 인기가 아주 높다.
jpg to webp 라고 검색해보면 사이트가 나온다. 간단히 내 사진중 아무거나 드래그해 넣고 변경하기를 클릭하면 자동으로 변환하여 나오고 해당 변환된 파일을 저장하여 사용하면 된다.
react-dropzone
:드래그로 파일업로드 가능한 라이브러리
react-azator-editor
: 마이페이지에서 프로필사진 커스텀해주는 라이브러리(원으로 만들기, 뒤집기 등)
react-beautiful-dmd
:dmd는 드래그 앤 드롭의 약자인데, 드래그해 업로드 하는것이 아니라 드래그로 일정 카드등을 이리 옮겼다 등을 할수 있는 기능
Wappalyzer: 해당 사이트에서 사용되는 스택등이 무엇인지 볼수있는 크롬에 추가 가능한 기능.
-- Sentry =>에러날때마다 통게를 잡아주는것(Sentry.io라는 사이트에 들어가 가입후 연결(등록)하면 끝 -- 카카오 사용때와 비슷)
--PWA => 웹으로 만든 홈페이지를 모바일로 변경해 다운받을 수 있게 연결해주는 것. 미래 트랜드가 될것으로 예정
엄청난 용량의 것을 받아오기.
47메가짜리의 이미지를 받아오자. 페이지 이동시 이미지가 받아지도록 한다면 사진이 커서내릴때 투둑투둑 끊겨 나온다.
이것을 프리로드를 활용해 미리이동전 페이지에서 받아놓고 다시 속도를 본다.
const PRELOAD_IMAGES = [ "https://upload.wikimedia.org/wikipedia/commons/9/96/%22Den_kjekke_gutt%22_-_6._Internasjonale_Akademiske_Vinterleker_%281939%29_%2840200856483%29.jpg", // src를 만나 다운로드 받으러감.. img라는 태그에는 이미 결과까지 저장되어있는 상태 ]; export default function ImagePreloadPage() { const router = useRouter(); // 이 페이지가 다 로딩이 되고, useEffect(() => { preloadImage(PRELOAD_IMAGES); // 프리로드가 필요한 부분을 해당 함수로 }, []); const onClickMove = () => { void router.push(`/32-06-image-preload-moved`); }; return ( <> <button onClick={onClickMove}>페이지 이동하기</button> </> ); }
useEffect를 사용해 페이지가 로드되고
export const PRELOADED_IMAGES: HTMLImageElement[] = []; // 프리로드가 필요한 페이지에서 사용(프리로드 이미지가 필요한..) export const preloadImage = (images: string[]) => { images.forEach((el) => { // 만약 여러주소를 프리로드한다면 그 주소를 el로 받아 const img = new Image(); // img라는 태그를 만듬 img.src = el; // 여기에 담고 img.onload = () => { // 로드완료시 // 로드가 다 되었을때(즉, 다운로드 다 받았을때) // 사라지지않게 어딘가에 저장해주도록 변수에 담음 단, 그냥 변수에 담으면 스테이트 리랜더될때 사라지니 전역변수로컴포넌트 밖에 만들어주기! PRELOADED_IMAGES.push(img); // 프리로드 하는애들은 해당 변수에 push하여 담기 }; // img.onload = () => PRELOADED_IMAGES.push(img); }); };
preloadImage 라는 함수가 실행되게하는데, 미리 프리로드할 것을 PRELOAD_IMAGES 라는 배열에 담아주고 해당부분을 넘겨주어 images라는 매개변수로 받아 해당 주소가 여러개일 경우를 가정해 forEach를 사용해 이미지라는 태그를 img라는 이름으로 만들고 그 src에 el을 담아, 해당 로드가 끝나면 전역변수에 선언된 PRELOADED_IMAGES라는곳에 담아준다.
로드가 다 되었을시 해당 이미지 소스가 없어지면 안되기에 어딘가에 담아 줘야하는데, 리랜더링 할때도 사라지지 않게 하기위해 지역변수가 아닌 외부에서 선언하는 전역변수에 담는다.
이렇게 하여 실제 페이지 이동시 이미지태그에는 같은 주소가 있더라도 미리 받아왔기때문에 아까처럼 ㄷ투둑투둑하고 끊기는 식으로 사진이 보여지지는 않는다
export default function ImagePreloadMovedPage() { return ( <img src="https://upload.wikimedia.org/wikipedia/commons/9/96/%22Den_kjekke_gutt%22_-_6._Internasjonale_Akademiske_Vinterleker_%281939%29_%2840200856483%29.jpg" /> ); }
이번에는 게시판 리스트에서 마우스 올렸을 경우 해당 게시물의 내용을 미리 받아와 실제 그 게시물에 접속시 바로 나오게 적용해보았다.
==> 프리패치 적용
onMouseOver 라는 기능이 있었다.
const client = useApolloClient(); const preFetchBoard = (boardId: string) => async () => { // el._id즉 각 게시물의 아이디를 boardId로 받아 그대로 사용! const result = await client.query({ query: FETCH_BOARD, // 패치가 이루어지고 아폴로 캐시(글로벌스테이트)에 저장됨. 페이지 열자마자 한번에 보여줄 수 있음. 받아오고 리랜더링하고 등의 과정빠져 또 요청안감 variables: { boardId }, }); // useQuery // useLazyQuery => useQuery와는 달리 내가 원할때 요청 가능하나, 변수에 담지못하고 data에 받아짐 그부분은 useQuery와 동일 // useApolloClient =>내가 원하는 시점에 요청가능. 버튼클릭시 라던가.. axios와 동일한 방식으로 사용가능.(변수에 담아..) }; const onClickMove = (boardId: string) => () => { void router.push(`/32-08-data-prefetch-moved/${boardId}`); }; return ( <> {data?.fetchBoards?.map((el) => ( <div key={el._id}> <span style={{ margin: "10px" }}>{el.writer}</span> <span style={{ margin: "10px" }} onMouseOver={preFetchBoard(el._id)} onClick={onClickMove(el._id)} {/* 각 게시물에 대한 아이디를 HOC방식으로 넘겨 boardId에 사용 */} {el.title} </span> </div> ))} )
원하는 시점에 쿼리를 실행해 변수에담아올 수 있는 useApolloClient를 사용해서 제목에 마우스를 올렸을시 해당 쿼리요청을 보내 받아온다.
이렇게되면 리스트에서 제목에 마우스만 올려도 해당 게시글의 내용을 미리 받아와 실제 들어 가면 내용이 바로 받아지는것을 볼 수 있다.
그런데 문제가 있었다.
마우스를 주르륵 내리면 해당 부분들의 전부가 받아와진다.
마지막 마우스 위치의 것만 받아올 수 있게 하기위해 디바운싱을 해주면 된다고하셨다.
그렇게 힌트를 주시고 가셔서 직접 디바운스를 사용해보았다.
검색기능 구현때 사용을 했었는데 잘 기억이 나지않아 여기 넣었다 저기넣었다...
const getDebounce = _.debounce((value) => { void refetch({ page: 1 }); }, 1000); // onMouseOver ==> 마우스 올라갔을때 const preFetchBoard = (boardId: string) => async () => { // el._id즉 각 게시물의 아이디를 boardId로 받아 그대로 사용! const result = await client.query({ query: FETCH_BOARD, // 패치가 이루어지고 아폴로 캐시(글로벌스테이트)에 저장됨. 페이지 열자마자 한번에 보여줄 수 있음. 받아오고 리랜더링하고 등의 과정빠져 또 요청안감 variables: { boardId }, }); getDebounce(result); // useQuery // useLazyQuery => useQuery와는 달리 내가 원할때 요청 가능하나, 변수에 담지못하고 data에 받아짐 그부분은 useQuery와 동일 // useApolloClient =>내가 원하는 시점에 요청가능. 버튼클릭시 라던가.. axios와 동일한 방식으로 사용가능.(변수에 담아..) };
이렇게 적용을 해보았다.
1초이내 일어난 일은 무시되도록 구현을 해보았다. 일단 성공은 한것같지만 잘 모르겠다.
이미지 최적화부분은 좀더 돌려봐야겠지만 리스트에서 상세페이지 미리 패칭하는것은 구현할 수 있을것같다. 꼭 적용해봐야겠다.