모든 코드에 의미를 담겠습니다.
금일은, 이미지를 어떻게 백엔드에 요청을해서 응답을 받아오는 지 한번 알아보자!!
현재 내가 사용하고 있는 기술 스택은 next.js(react.js)를 통해 Apollo-graphql을 통해서 백엔드 API와 통신을 해서 데이터를 받아오고 있다.
데이터를 Read하는 경우를 제외하고는 Apollo-graphql에서는 mutation을 이용한다. 그래서 이미지를 업로드할때도 그냥 똑같이 Mutation을 하면 되는 줄 알았다.
그러나 이미지 & 동영상 같은 경우는 보통 용량이 큰 경우가 많다. 백엔드 DB에 저장하기에는 이미지 하나당 5MB이렇게 공간을 차지하는 경우가 너무 많은데 텍스트를 받아 저장하는 DB와 묶는건 사실 불가능한 일이다.
그래서!!!
이미지는 스토리지라는 저장공간(쉽게말해, 저장공간이 큰 C드라이브라고 보면 좋을 것 같다.)에 보관을 해둔다. 우리가 아는 유명한 스토리지는 AWS S3 or GCP(Google Cloud Platform) 와 같은것이 있다.
이미지처리 프로세스는 그래서 이와 같다.
클라이언트(Frontend) -> API(백엔드) -> 스토리지
스토리지 -> API(백엔드) -> 클라이언트(프론트엔드)
이런구조인데
apollo-graphql을 통해서 스토리지에도 저장을 하고 DB에도 저장을 해야한다. DB에 저장하는 url은 그냥 텍스트일 뿐이고 클라이언트에서 요청을 할 때 DB에서 보유하고 있던 url을 스토리지에서 식별을해서 스토리지에서 이미지를 클라이언트에 뿌리는 역할을 한다.
자 이론적인 개념은 여기까지고 실제로 어떻게 연결을 하여 데이터를 추가하고 불러오는 지 한번 확인해보자.
내가 백엔드와 통신하는 API는 Apollo-graphql이기 때문에 REST-API로 하시는 분들은 참고만 하시되 REST-API 통신형태로 하는 게 좋을 것 같다.
아폴로 업로드 클라이언트를 설치한다 (yarn install apollo-upload-client). 아폴로 업로드 클라이언트를 설치할 때 node 버전을 확인해서 다운을 받아줘야한다. 나는 node 버전이 아폴로 클라이언트와 호환이 되지않아 버전 업그레이드를 했다 ㅠ
아폴로 클라이언트를 타입스크립트에서 읽을 수 있게 끔 yarn add -D @types/apollo-upload-client 한다.
//우선 _app.tsx or _app.js파일에 upload기능을 추가시켜줘야한다.
import {createUploadLink} from 'apollo-upload-clinet'
function MyApp({ Component, pageProps}) : AppProps (타입스크립트 타입 선언) {
const uploadLink = createUploadLink({
uri: 'http://backend.codebootcamp.co.kr/graphql'
})
const client = new ApolloClient({
link : ApolloLink.from([uploadLink as unknown as ApolloLink]),
cache: new InMemoryCache()
})
return (
<ApolloProvider client = {client}>
<Component {...pageProps} />
</ApolloProvider>
)
}
export default MyApp
import {gql, useMutation} from '@apollo/client'
import {useRef, useState} from 'react'
const ImagePage = () => {
const [myImage, setMyImage] = useState('')
const [file, setFile] = useState('')
const fileRef = useRef<HTMLInputElement>(null)
const onChangeFile = (event:any) => {
const file = event.target.file[0] //배열로 묶여있기때문에 0번째를 불러와야한다.
if(!file.type.includes('png')) {
alert('png파일만 가능합니다')
return
}
if (file.size < 5 * 1024 * 1024 // 5MB를 의미함) {
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload= (event) => {
if(event.target) setMyImage(String(event.target.result))
}
} else {
alert('파일이 너무 큽니다 (5MB 제한!)')
return
}
setFile(file)
}
const onClickImage = () => {
fileRef.current.click()
}
const onClickImageFile = async() => {
try{
const result = await Promise.all([
uploadFileMutation({ variables: {file : file} }),
uploadFileMutation({ variables: {file : file} }),
uploadFileMutation({ variables: {file : file} }),
uploadFileMutation({ variables: {file : file} }),
]) //이런식으로 쓰면 비효율적이니 map으로 받아서하는 것도 방법이다. new Array(4).fill(1).map(() => uploadFileMuation({variables: {file: file}})
return (
<div>
<button onClick = {onClickImage}> 이미지 </button>
<input type ="file" ref = {fileRef} onChange = {onChangeFile style = {{display: 'none'}}
/>
//여기서 input type의 attribute에 display none을 준 이유는 file 형태의 input 고정 html&css을 javascript에서 제공해주기 때문에 원하는 형태로 변경을 하기가 힘들다.
//그렇기 때문에 버튼이미지를 만들어서 클릭했을 때 useRef 함수를 이용해서 input 태그에서 작동하는 모든 함수를 실행시킨다.
<img src = {myImage} style = {{ width : '1000px', height: '600px'}} />
<button onClick = {onClickImageFile}> 서버에 파일전송하기 </button>
</div>
)
}
export default ImagePage
결과물
간단해보이지만 굉장히 유용하게 쓰일 수 있는 방법같다. 피드백은 언제든지 Welcome입니다 !!!!!