자바스크립트에서 이미지를 보기 위해서는 이미지 경로(주소)가 필요합니다.
이미지 경로(주소)를 상대주소 ../../
와 같은 형식으로 사용하곤 했습니다.
하지만 위와 같은 경로에는 단점이 하나 있습니다.
다른 사람 컴퓨터에 해당 이미지가 없을 경우에는 오류가 뜨는 것이죠.
그래서 위에 애기했 듯이 이미지 서버에 이미지를 등록하고 등록된 이미지 주소로 불러와야 어디서든 이미지를 볼 수 있었습니다.
하지만, 이미지를 올려놓고 정작 게시글 등록을 하지 않으면 이미지 서버에는 이미지가 올라가 있지만 사용되는 곳은 없죠.
그래서 쓸데없는 데이터 낭비뿐만 아니라, 서버에도 과부하가 걸리게 됩니다.
그래서 이미지 주소를 서버에서 가져오는 것이 아니라, 미리보기 용으로 만든 임시 주소를 만드는 방법이 있습니다.
이번에도 간단한 예제가 있습니다.
const ImageUploadPreviewPage = () => {
const onChangeFile = (event) => {
const file = event.target.files?.[0]
console.log(file);
}
return (
<>
<div>
<input type="file" onChange={onChangeFile}></input>
<img src="" />
</div>
</>
);
}
export default ImageUploadPreviewPage
기존 코드에서 onChangeFile라는 함수를 하나 만들어 주고, input에 onChange로 함수를 실행시켜주었습니다.
그리고 그 결과를 file이라는 변수에 담아주고 console.log(file)로 어떤 파일이 들어오는지 확인했습니다.
파일을 하나 올려보겠습니다.
콘솔창에 이런 결과가 나왔습니다.
해당 결과를 가지고 이미지 미리보기를 만들 수 있습니다.
new FileReader() 기능은 파일 객체를 이용해 내용을 읽고 사용자 컴퓨터에 저장하는 것을 가능하게 해주는 브라우저에서 지원해주는 기능입니다.
new FileReader() 를 사용하면 new FileReader() 에 있는 기능들을 이용할 수 있습니다.
우선 전체적인 코드는 아래와 같습니다.
const ImageUploadPreviewPage = () => {
const [iamgeUrl, setImageUrl] = useState("")
const onChangeFile = (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
console.log(file);
if (!file) {
alert("파일이 없습니다.");
return
}
const fileReader = new FileReader()
fileReader.readAsDataURL(file);
fileReader.onload = (data) => {
if(typeof data.target?.result === "string"){
console.log(data.target?.result);
setImageUrl(data.target?.result)
}
}
}
return (
<>
<div>
<input type="file" onChange={onChangeFile}></input>
<img src={iamgeUrl}/>
</div>
</>
);
}
export default ImageUploadPreviewPage
new FileReader()
기능을 const fileReader에 담아주었습니다.
그러면 fileReader는 new FileReader()
의 기능을 사용할 수 있습니다.
readAsDataURL()
을 사용하면 Data URL을 얻을 수 있게 됩니다.
()
안에 우리가 넣은 파일을 넣어주면 됩니다.
파일 읽기에 성공하게 되면 onload
가 실행됩니다.
onload에서는 파일을 읽고 생성된 Data URL이 target.result
에 담기게 됩니다.
해당 결과를 img태그의 src에 값으로 넣어주게 되면 됩니다.
여기까지하면 전체적인 흐름은 끝입니다.
위 기능을 이용해서 만들어진 data URL 입니다.
기능도 잘 작동합니다.
💡 Blob(BinaryLargeObject) 이란?
Blob 객체는 파일류의 불변하는 미가공 데이터를 나타냅니다. 텍스트와 이진 데이터의 형태로 읽을 수 있으며, ReadableStream으로 변환한 후 스트림 메서드를 사용해 데이터를 처리할 수도 있습니다.
이렇게 받은 임시 url은 미리보기 파일이므로 올바른 형태의 file 타입이 아닙니다.
따라서 이 이미지로 api를 요청을 해야한다면 어떻게 해야하는지 코드를 통해 알아보겠습니다.
const ImageUploadPreviewPage = () => {
const [file1, setFile1] = useState<File>()
const [iamgeUrl, setImageUrl] = useState("")
const [uploadFile] = useMutation(UPLOAD_FILE)
const [createBoard] = useMutation(CREATE_BOARD)
const onChangeFile=(event: ChangeEvent<HTMLInputElement>)=> {
const file = event.target.files?.[0]
console.log(file);
if (!file) {
alert("파일이 없습니다.");
return
}
const fileReader = new FileReader()
fileReader.readAsDataURL(file);
fileReader.onload = (data) => {
if(typeof data.target?.result === "string"){
console.log(data.target?.result);
setImageUrl(data.target?.result)
setFile1(file)
}
}
}
const onClickSubmit = async () => {
// 1. image업로드해서 url 받아오기
// - uploadFile()
const result1 = await uploadFile({
variables: {
file: file1
}
})
const imageUrl = result.data?.uploadFile.url || ""
// 2. createBoard로 게시물 등록하기
// writer, title, contents, password 전송
// imageurl 전송
const result2 = await createBoard({
variables: {
createBoardInput: {
writer: "영희",
password: "1234",
title: "제목입니다",
contents: "내용부분입니다",
images: [imageUrl]
}
}
})
console.log(result2.data?.createBoard._id);
}
return (
<>
<div>
<img src={iamgeUrl}/>
<input type="file" onChange={onChangeFile}></input>
<button onClick={onClickSubmit}> 등록하기 </button>
</div>
</>
);
}
export default ImageUploadPreviewPage
💡 createObjectURL 이란?
createObjectURL과 fileReader 둘 다 이미지 업로드 시 이미지를 불러와 미리보기를 할 수 있습니다. createObjectURL을 사용하게 되면 소스코드가 짧아져 작성하기 쉽고 좋겠지만, fileReader와는 다르게 createObjectURL은blob 객체로 이미지를 생성하기 때문에 해당 데이터를 서버와 통신할 때 사용할 수 없습니다.
또한 createObjectURL보다는 fileReader가 브라우저 호환성이 좋기 때문에 사용자 편의를 생각한다면 fileReader를 사용하시는 것이 더 바람직해 보입니다.
// Promise
// Promise에서 resolve가 실행이 되면 종료, reject가 실행되면 오류
const onClickPromise = async () => {
const result1 = await new Promise((resolve, reject) => {
setTimeout((resolve("3초 후 실행됩니다.")) => {}, 3000)
})
const result2 = await new Promise((resolve, reject) => {
setTimeout((resolve("2초 후 실행됩니다.")) => {}, 2000)
})
const result3 = await new Promise((resolve, reject) => {
setTimeout((resolve("1초 후 실행됩니다.")) => {}, 1000)
})
};
// Promise.all()
const onClickPromiseAll = async () => {
const result = await Promise.all([
setTimeout((resolve("3초 후 실행됩니다.")) => {}, 3000)
setTimeout((resolve("2초 후 실행됩니다.")) => {}, 2000)
setTimeout((resolve("1초 후 실행됩니다.")) => {}, 1000)
])
};
위의 함수는 result1 이 실행되고 난 후에 result2가 실행이 되고, result2가 실행된 후에, result3이 실행됩니다.
따라서 onClickPromise 함수의 경우 약 6초의 시간이 소요됩니다.
Promise.all()
의 경우에는 Promise.all()에 포함되어 있는 함수들을 동시에 실행
을 합니다.
따라서 onClickPromiseAll 함수의 경우 약 3초의 시간이 소요됩니다.
Promise.all()
을 사용하여 한번에 여러 함수를 실행할 때에는 map, for, forEach를 사용할 수 있습니다.
map
을 활용한 코드를 보시고, for
와 forEach
를 어떤식으로 사용하면 되는지 공부해보세요.
const onClickPromiseAll = async () => {
const result = await Promise.all(["http://storage.url1.jpg",
"http://storage.url1.jpg", "http://storage.url1.jpg"].map((el)
=> new Promise((resolve, reject) => {
setTimeout(() => {
resolve(el)
}, 3000)
})
))
};
위의 Promise.all()
을 사용하여 여러 이미지를 한번에 올리는 방법에 대해 알아보겠습니다.
const ImageUploadPreviewPage = () => {
const [files, setFiles] = useState<(File | undefined)>([
undefined, undefined, undefined
])
const [iamgeUrls, setImageUrls] = useState(["", "", ""])
const [uploadFile] = useMutation(UPLOAD_FILE)
const [createBoard] = useMutation(CREATE_BOARD)
const onChangeFile = (number: number) => (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
console.log(file);
if (!file) {
alert("파일이 없습니다.");
return
}
const fileReader = new FileReader()
fileReader.readAsDataURL(file);
fileReader.onload = (data) => {
if(typeof data.target?.result === "string") {
const tempUrls = [...imageUrls]
tempUrls[number] = data.target?.result
setImageUrls(tempUrls)
const tempFiles = [...files]
tempFiles[number] = file
setFiles(tempFiles)
}
}
}
const onClickSubmit = async () => {
const results = await Promise.all(files.map((el) => {
el && uploadFile({ variables: {file: el} })
}))
const resultUrls = results.map(( el ) => el.data ? el?.data.uploadFile.url : "")
const result2 = await createBoard({
variables: {
createBoardInput: {
writer: "영희",
password: "1234",
title: "제목입니다",
contents: "내용부분입니다",
images: resultUrls
}
}
})
console.log(result2.data?.createBoard._id);
}
return (
<>
<div>
<img src={iamgeUrls[0}/>
<img src={iamgeUrls[1}/>
<img src={iamgeUrls[2}/>
<input type="file" onChange={onChangeFile(0)}></input>
<input type="file" onChange={onChangeFile(1)}></input>
<input type="file" onChange={onChangeFile(2)}></input>
<button onClick={onClickSubmit}> 등록하기 </button>
</div>
</>
);
}
export default ImageUploadPreviewPage;
💡 LazyLoad란?
페이지를 읽어들이는 시점에 중요하지 않은 리소스 로딩을 추 후에 하는 기술입니다.
하지만 스크롤이 내려가면서 필요한 때가 되면 로드가 되어야 합니다. 예를 들어 이미지가 10장이 넘는 페이지가 있다고 가정합시다. 이미지를 모두 다 로드가 될 때까지 기다리게 된다면, 페이지의 로딩을 길어지게 될 것입니다. 하지만, 맨 위의 화면에 보이는 이미지만 로드를 한 후에, 스크롤을 내리면서 이미지가 보여져야 할 때마다 이미지를 로드한다면, 데이터의 낭비를 막을 수 있습니다!
💡 PreLoad란?
페이지를 읽어들일 때 미리 리소스를 받아놓는 기술입니다.
위와 같이 예를 들어 이미지가 10장이 넘는 페이지가 있다고 가정합시다. LazyLoad의 경우에는 필요할 때마다 데이터를 로드하는 방법이라면, PreLoad의 경우에는 모든 데이터들을 미리 로드해놓고 대기하는 방식이라 보시면 됩니다!
아래는 PreLoad 예시 코드입니다.
const ImagePreloadPage = () => {
const [imgTag, setImgTag] = useState()
const divRef = useRef(null)
useEffect(() => {
const img = new Image()
img.src = "불러올 이미지 주소를 넣습니다."
img.onload = () => {
setImgTag(img)
}
}, [])
const onClickPreload = () => {
if(imgTag) divRef.current?.appendChild(imgTag)
}
return (
<div>
<div ref={divRef}></div>
<button onClick={onClickPreload}> 이미지 프리로드 </button>
</div>
)
}
export default ImagePreloadPage
이미지 등록을 예쁘게 만들어주는 기능들이 이미 많이 있습니다.
그래서 직접 디자인 하지 않아도 됩니다.
React-lazy-load는 스크롤이 내려가면서 해당 이미지가 들어가있는 컴포넌트가 등장할 때 사진을 다운로드 받습니다.
사진을 보시면 오른쪽에 스크롤이 내려갈 때마다 데이터를 요청하는 것을 볼 수 있습니다.
React-dropzone은 react에서 제공하는 대표적인 이미지 라이브러리입니다.
이름이 avator라서 영화 아바타의 이미지를 예제로 사용하고 있습니다.
ant-design에서도 제공해주는 이미지 기능이 많습니다.
실제 배포를 진행하고 나서, 내가 배포한 페이지의 개선할 점을 찾을때 유용한 사이트입니다.
webp 확장자는 구글에서 만든 이미지 포맷입니다.
png, jpeg와 같은 이미지 확장자입니다
구글에서는 왜 webp 확장자를 만들었을 까요?
구글은 전세계적으로 사용하고 있는 사이트 입니다. 구글에서만 관리하고 있는 이미지 서버만 해도 엄청난 트래픽이 있습니다.
구글은 이미지 서버의 부담을 줄이고, 서버비를 아낄 수 있는 방안으로 Webp라는 확장자를 만들었습니다.
Webp는 GIF, PNG, JPEG 확장자 모두를 대체 가능한 확장자이며 이미지를 파일을 압축했을 때 기존 PNG, JPEG보다 약 30%정도 용량을 줄일 수 있는 장점이 있습니다.
같은 이미지를 webp으로 받을시 webp은 300kb라면 png는 500kb정도 됩니다.
그리고 GIF는 256색만 표현할 수 있지만, Webp은 파일 크기도 작고 , 색상 수에 제한이 없으므로 GIF보다 훨씬 좋은 성능을 보입니다.
또한 PNG 처럼 알파 채널을 지원합니다.
알파 채널이란 배경이 투명한 것을 이야기 합니다.