프로젝트 중 imageURL을 사용하여 다운로드 버튼을 누르면 이미지가 다운로드할 수 있게끔 하는 기능을 만들어야 했다.
위 기능을 구현하면서 겪은 어려움은 크게 2가지였다.
1. 서버에서 오는 imageURL을 그대로 href 속성에 넣어주면 제대로 동작하지 않는다.
2. 1.을 해결하기 위해 imageURL url을 fetch 해줬어야 했는데, CORS에러가 발생했다.
해결방법을 블로그에 남겨 정리해보려고 한다.
1. 서버에서 오는 imageURL을 그대로 href 속성에 넣어주면 제대로 동작하지 않는다.
먼저 첫번째 문제를 해결해 보자. 방법은 간단하다.
<a> 태그
href에 경로와 download 속성에 저장될 파일 이름을 지정해 주면, 다운로드가 가능하다.
그런데, 서버에서 오는 imageURL을 그대로 href 속성에 넣어주면 제대로 동작하지 않는다.
웹 정책상 다른 웹사이트 url로 href가 연결되면 download 되지 않고, 새 페이지로 인식하여 이동하기 때문에
우리가 구현하고자 하는 기능과 달리, 계속 빈페이지가 담긴 imageURL주소로 이동하게 된다.
따라서 imageURL을 Blob객체로 변환한 뒤 이미지를 나타내는 urlURL.createObjectURL()을 만들어, a태그에 href 속성으로 넣어주면
원하는 기능의 구현이 가능했다.
2. CORS 에러 해결
const handleClick = async () => {
const response = await fetch(imageURL)
const blob = await response.blob()
...
}
바로 직면한 두 번째 문제,,
먼저, imageURL을 GET 하여 해당 값을 blob객체로 바꿔주고자 했다.
문제없이 동작할 줄 알았으나, CORS에러가 났다..
브라우저에서는 출처가 다른 주소가 네트워크 요청을 할 때, CORS에러를 발생시킨다.
기존의 API요청 주소는 proxy 설정을 해주었으나,
기존의 주소와는 다른 주소(fileURL)가 API 요청을 하여, CORS에러가 다시 발생했다.
(1) CORS란 무엇인가
CORS는 Cross-Origin Resource Sharing의 줄임 말로 한국말로는 교차 출처 리소스 공유로 해석된다.
"교차출처"는 "다른 출처"를 의미하는데 출처는 우리가 보고 있는 URL내 Protocol과 Host, Port번호까지 모두 합친 것을 의미한다.
즉, 서버의 위치를 찾아가기 위해 필요한 가장 기본적인 구성 요소들이 출처라고 할 수 있다.
(출처의 경우 어떤 경우에는 같은 출처, 또 어떤 경우에는 다른 출처로 판단될 수 있다고 한다)
정리하면, Protocol, Host, Port(케바케)까지 같아야 같은 출처라고 구분 지을 수 있다.
추가로, 출처를 비교하는 로직은 서버에 있는 것이 아니라 브라우저에 구현되어있다고 한다.
즉, CORS는 브라우저의 구현 스펙에 포함되는 정책으로, 브라우저를 통하지 않고, 서버 간 통신을 할 때는 이 정책이 적용되지 않는다.
(postman에서 API 요청을 할 때는 CORS에러가 나지 않는다!)
(2) CORS는 어떻게 동작하는가
웹 클라이언트 애플리케이션이 다른 출처의 리소스를 요청할 때는 HTTP프로토콜을 사용하여 요청을 보내게 되는데,
이때 브라우저는 요청 헤더에 Origin이라는 필드에 요청을 보내는 출처를 함께 담아 보낸다.
이후, 서버가 이 요청에 대한 응답을 줄때 응답 헤더의 Access-Control-Allow-Origin이라는 값에 "이 리소스를 접근하는 것이 허용된 출처"를 내려주고, 이후 응답을 받은 브라우저는 자신이 보냈던 요청의 Origin과 Access-Control-Allow-Origin을 비교해 본 후 이 응답이 유효한 응답인지 아닌지를 결정한다.
즉, access-control-allow-origin 값에 주소 값을 넣어준다면, CORS 에러를 해결해 줄 수 있다.
개발 환경에서도, 백엔드 서버 쪽에서 access-control-allow-origin값에 localhost:3000 값을 넣어준다면
프론트 개발자는 따로 CORS처리를 해주지 않아도 되지만,
이중적인 보안처리를 해주지 않는 이상 범용적으로 쓰이는 출처를 해당 값에 넣는 것은 권장하지 않는다.
(3) Proxy
CORS를 해결하기 위한 방법으로는 여러 가지가 있는데, 나는 React환경에서 `http-proxy-middleware`를 사용하여 CORS정책을 우회시켰다.
프록시는 "대리인"의 의미를 갖는다. 즉, 클라이언트와 서버 사이에서 대리인의 역할을 한다고 볼 수 있다.
createProxyMiddleWare를 통해 클라이언트와 서버 사이에서 "해당하는 URL"요청의 Origin값을 target값으로 만들어 주어 브라우저의 CORS정책을 우회시킬 수 있다.
// setupProxy.js
const {createProxyMiddleware} = require('http-proxy-middleware')
const BASE_URL = 'https://dev-api.hello-yukyung.com'
const FILE_URL = 'https://files.hello-yukyung.com'
module.exports = function (app) {
app.use('/api', (req, res, next) => {
createProxyMiddleware({
target: BASE_URL,
changeOrigin: true,
pathRewrite: {
'^/api/': '/'
},
logLevel: 'error',
cookieDomainRewrite: {
'*': 'localhost'
}
})(req, res, next)
})
app.use('/files-dev', (req, res, next) => {
createProxyMiddleware({
target: FILE_URL,
changeOrigin: true,
logLevel: 'error'
})(req, res, next)
})
}
기존의 로직에서 FILE_URL도 proxy서버를 탈 수 있도록 추가해 주었다.
개발 서버에서 network 탭을 확인해 보면
RequestURL은 http://localhost:3000/api/areas 가 나온다.
즉, 브라우저에서 개발 서버로 RequestURL 요청을 보내면 createProxyMiddleWare가 요청 중간에서
요청 주소의 origin 값을 target주소로 바꿔 서버로 요청해 준다.
그리고 서버에서 받은 response 값을 브라우저에게 보여주게 된다.
// dev 서버에서는 imagesURL querystring 내에 '/files-dev'가 존재: https://files.hello-yukyung.com/files-dev/images/(...).jpeg
// in .env.test & .env.dev
// REACT_APP_FILE_URL=https://files.hello-yukyung.com
// in .env.prod
// REACT_APP_FILE_URL=
const handleClickDownloadButton = useCallback(async (imagesURL:string) => {
if (!imagesURL) return
if (process.env.REACT_APP_FILE_URL) imagesURL = image.replace(process.env.REACT_APP_FILE_URL, '')
const imageData = await fetch(imagesURL)
const blob = await imageData.blob()
// blob객체를 다시 url로 생성
const imageUrl = URL.createObjectURL(blob) //브라우저 내 이미지를 나타내는 URL생성
const a = document.createElement('a')
a.href = imageUrl
a.download = 'testImage.jpeg'
document.body.appendChild(a) // body요소 끝에 a 태그 붙이기
a.click()
document.body.removeChild(a) // cleanup - 쓰임을 다한 a태그 삭제
window.URL.revokeObjectURL(imageUrl) // cleanup - 쓰임을 다한 url 객체 삭제
}, [])
이후, 다운로드 버튼의 로직은 위와 비슷하게 작성해 주었다.
실서버에서는 프록시를 사용하지 않으므로, imageURL사이에 /files-dev 값이 있을 경우 proxy 경로를 타게 해 주었다.
실서버 배포시에 CORS에러가 난다면, 백엔드 개발자에게 해당 fileUrl주소 access-control-allow-origin 허용 요청을 드려야 한다.
잘못된 부분이나 지적할 부분이 있다면 많은 피드백 부탁드립니다.
'Front-end > Module' 카테고리의 다른 글
[openapi-generator-cli] OAS-generator로 타입 세이프하게 API연결하는 방법 (0) | 2024.08.27 |
---|---|
[react-daum-postcode] 정확한 주소값 받아오기 (0) | 2022.10.07 |
[@react-google-maps/api] Google 지도에 Custom Marker 나타내기 (1) | 2022.09.01 |
[Module] Yup & Formik 정리 (0) | 2022.08.23 |
[styled-components] TypeScript에서 전역 스타일(Global Style) 적용하기 (0) | 2022.06.29 |