서론
파일 업로드 기능 구현을 위해 React(3000번 포트)에서 Spring(8080포트)으로 multipart/form-data 를 전송하는 테스트를 진행했을 때에는 CORS 문제가 발생하지 않았었다. 하지만 Axios를 사용하였더니 CORS 이슈가 발생하였다. 왜 그럴까?
기존 파일 업로드 테스트
기존에 파일 업로드를 테스트할 때는 form 태그로 localhost의 8080포트로 form-data를 전송하였다.
Axios를 사용한 파일 전송
axios.post를 사용하여 formData를 전송한다. formData에는 사용자가 업로드한 이미지를 담고 있다.
하지만 파일을 전송해보면 CORS 에러가 발생한다.
CORS(Cross-Origin Resource Sharing)?
CORS에 대해서는 인파님 블로그에 설명이 매우 잘 되어있다.
간단하게 요약하면,
브라우저는 요청을 보낼때 한 번에 바로 보내지 않고, 먼저 예비 요청을 보내 서버와 잘 통신되는지 확인한 후 본 요청을 보낸다.
이때 예비 요청의 역할은 본 요청을 보내기 전에 브라우저 스스로 안전한 요청인지 미리 확인하는 것이다. (dry run 느낌?)
이때 브라우저가 예비요청을 보내는 것을 Preflight라고 부르며, 이 예비요청의 HTTP 메소드는 GET이나 POST가 아닌 OPTIONS가 사용된다.
자바스크립트의 fetch() 메서드를 통해 리소스를 받아오려고 하는 상황을 생각해 보자.
브라우저는 서버로 HTTP OPTIONS 메서드로 예비 요청(Preflight)을 먼저 보내는데, 이때 Origin 헤더에 자신의 출처를 넣는다.
서버는 이 예비 요청에 대한 응답으로 어떤 것을 허용하고 어떤 것을 금지하고 있는지에 대한 헤더 정보를 담아서 브라우저로 보내준다.
Access-Control-Allow-Origin : 허용되는 Origin들의 목록
Access-Control-Allow-Methods : 허용되는 메서드들의 목록
Access-Control-Allow-Headers : 허용되는 헤더들의 목록
Access-Control-Max-Age : 해당 예비 요청이 브라우저에 캐시 될 수 있는 시간을 초 단위로 설정
이후 브라우저는 보낸 요청과 서버가 응답해 준 정책을 비교하여, 해당 요청이 안전한지 확인하고 본 요청을 보내게 된다.
서버가 본 요청에 대한 응답을 하면 최종적으로 이 응답 데이터를 자바스크립트로 넘겨준다.
그럼 매번 예비 요청을 보내는 걸까?
브라우저는 예비(Preflight) 요청을 할 때마다, 먼저 Preflight 캐시를 확인하여 해당 요청에 대한 응답이 있는지 확인한다.
만약 응답이 캐싱되어 있지 않다면, 서버에 예비 요청을 보내 인증 절차를 밟는다.
서버로부터 Access-Control-Max-Age 응답 헤더를 받는다면 그 기간 동안 브라우저 캐시에 결과를 저장한다.
만약 응답이 캐싱 되어 있다면, 예비 요청을 서버로 보내지 않고 대신 캐시된 응답을 사용한다.
CORS는 허용되는 Origin들의 목록인 Access-Control-Allow-Origin에 해당 Origin을 명시해 주면 되기 때문에 Spring에서는 WebMvcConfigurer를 구현하여 문제를 해결하면 된다.
@Configuration
class WebConfig : WebMvcConfigurer{
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8080", "http://localhost:3000") // 허용할 출처
.allowedMethods("GET", "POST") // 허용할 HTTP method
.allowCredentials(true) // 쿠키 인증 요청 허용
.maxAge(3000) // 원하는 시간만큼 pre-flight 리퀘스트를 캐싱
}
}
그렇다면 기존에 발생하지 않던 CORS 이슈가 왜 Axios를 사용할 때 발생했을까?
사실 이게 더 궁금했다. 기존에는 발생하지 않았는데 왜 Axios를 사용하면 CORS 이슈가 발생하는 걸까?
기존과 크게 다른 부분은 form 태그 사용 여부뿐이었기에 form 태그로 보내면 CORS 이슈를 피할 수 있는 것이구나 예상했다. 하지만 왜 그런지는 정확히 잘 몰라서 찾아봤다.
React에서 form 태그를 사용하여 스프링으로 multipart data를 전송할 때, 실제 요청은 브라우저에서 보내진다.
브라우저는 Same Origin Policy(동일 출처 정책)를 적용하지 않기 때문에 CORS 이슈가 발생하지 않는다.
하지만 Axios는 XMLHttpRequest 객체를 사용하여 HTTP 요청을 보내므로, 브라우저의 Same Origin Policy에 따라 CORS 이슈가 발생할 수 있다. 즉 브라우저를 통해서 요청을 보내기 때문에 CORS 이슈가 발생할 수 있다는 것이다.
느낀 점
CORS를 예전에 개념으로만 들었을 때는 그렇구나 하고 넘겼었는데, 직접 경험해 보니 확실히 감이 잡혔다.
또 단순히 문제를 해결하고 끝내지 않고 더 자세하게 공부하다 보니 왜 문제가 발생했는지도 알 수 있었던 것 같다.