2023년 10월 10일 ・ 읽는 시간 9분
콘솔창에서 이런 에러를 만나본 적 있으시죠? 이번 아티클에서는 토스페이먼츠 결제창 케이스를 가지고 CORS(Cross-Origin Resource Sharing, 교차 출처 리소스 공유) 이슈와 해결 방법을 알아봅니다.
한 마디로 브라우저가 내 서버가 아닌 다른 서버의 리소스를 요청했기 때문이에요. 브라우저는 기본적으로 다른 서버를 신뢰하지 않아서 다른 서버에 요청을 보내거나 응답을 받는 걸 차단해요. 토큰, 쿠키와 같이 민감한 사용자 정보가 브라우저에 저장되는데, 이 정보를 탈취하면(CSRF, XSS) 심각한 보안 이슈가 생기기 때문이죠. 그래서 서로 다른 서버 리소스는 공유하지 않는 브라우저 정책을 SOP(Same-Origin Policy, 동일 출처 정책)라고 해요. 우리가 자주 마주치는 CORS 에러는 SOP 기준에 맞춰 CORS 정책을 적용하지 않아서 발생해요.
- 브라우저에서
https://myshop.com
의 출처를 가진 웹 애플리케이션에서https://othershop.com
출처의 리소스를 요청함 https://othershop.com
출처에서는https://myshop.com
출처의 리소스 요청을 거부함
이럴 때 브라우저는 보안 상의 이유로 https://othershop.com
의 리소스를 https://myshop.com
에서 요청할 수 없도록 차단하고 CORS 에러를 발생시킵니다.
CORS를 한 마디로 정의하자면 서로 다른 서버끼리 리소스를 공유하기 위한 정책이에요. 리소스 요청, 응답을 허용할지 결정하는 브라우저의 검증과 허락, 그를 위한 HTTP 헤더 사용 등을 포함하죠.
예전에는 프론트엔드와 백엔드를 따로 구성하지 않고 한 번에 구성해서 모든 처리가 같은 도메인 안에서 가능했습니다. 그래서 다른 출처로 요청을 보내는 게 의심스러운 행위로 보일 수밖에 없었죠. 그런데 시간이 지나 클라이언트에서 API를 직접 호출하는 방식이 당연해지기 시작했어요. 그런데 보통 클라이언트와 API는 다른 도메인에 있는 경우가 많죠. 그래서 CORS 정책이 생겼어요. 출처가 다르더라도 요청과 응답을 주고받을 수 있도록 서버에 리소스 호출이 허용된 출처(Origin)를 명시해 주는 방식으로요.
CORS를 번역하면 “교차 출처 리소스 공유”에요. 여기서 출처가 교차한다는 게 무슨 뜻일까요? 출처는 ‘오리진origin’의 번역 표현이에요. 우리가 흔히 알고 있는 URL에서 도메인만 뜻하는 게 아니라 프로토콜과 포트까지 포함하는 개념이죠. 출처를 구성하는 세 요소는 프로토콜·도메인(호스트 이름)·포트로, 이 중 하나라도 다르면 CORS 에러를 만나게 됩니다.
- 도메인(Hostname):
myshop.com
- 출처(Origin):
https://www.myshop.com
즉, ‘출처가 교차한다’는 건 리소스를 주고받으려는 ‘두 출처가 서로 다르다’는 뜻이에요. CORS를 설정한다는 건 ‘출처가 다른 서버 간의 리소스 공유’를 허용한다는 거죠.
위에서 SOP가 서로 다른 출처일 때 리소스 요청과 응답을 차단하는 정책이라면, CORS는 반대로 서로 다른 출처라도 리소스 요청, 응답을 허용할 수 있도록 하는 정책이에요. 그래서 우리가 만나는 에러는 CORS가 가능하도록 뭔가 설정하라는 내용으로 이루어져 있어요.
뒤에 나올 해결 방법에서 사용되는 헤더인 Access-Control-Allow-Origin
도 ‘허용되는 출처에 대한 접근제어’는 의미라고 이해할 수 있어요.
아래 예시를 통해 어떤 URL이 SOP에 부합하는지 한 번 확인해 보세요.
URL | 접근이 가능한가? (SOP를 준수했는가?) |
---|---|
https://www.myshop.com/example/ | ✅ 프로토콜, 도메인, 포트가 같음 |
https://myshop.com/example2/ | ✅ 프로토콜, 도메인, 포트가 같음 |
http://myshop.com/example/ | ❌ 프로토콜과 포트가 다름 |
http://en.myshop.com/example/ | ❌ 도메인이 다름 |
http://www.myshop.com/example/ | ❌ 프로토콜이 다름 |
http://myshop.com:8080/example/ | ❌ 포트가 다름 |
서버에서 Access-Control-Allow-Origin
헤더를 설정해서 요청을 수락할 출처를 명시적으로 지정할 수 있어요. 이 헤더를 세팅하면 출처가 다르더라도 https://myshop.com
의 리소스 요청을 허용하게 되죠.
*
를 설정하면 출처에 상관없이 리소스에 접근할 수 있는 와일드카드이기 때문에 보안에 취약해져요. 그래서 'Access-Control-Allow-Origin': https://myshop.com
과 같이 직접 허용할 출처를 세팅하는 방법이 더 좋습니다.
웹 애플리케이션이 리소스를 직접적으로 요청하는 대신, 프락시 서버를 사용하여 웹 애플리케이션에서 리소스로의 요청을 전달하는 방법도 있어요. 이 방법을 사용하면, 웹 애플리케이션이 리소스와 동일한 출처에서 요청을 보내는 것처럼 보이므로 CORS 에러를 방지할 수 있어요.
예를 들어, http://example.com
라는 주소의 웹 애플리케이션이 [http://api.example.com](http://api.example.com)
라는 리소스에서 데이터를 요청하는 상황을 가정해 볼게요. 웹 애플리케이션은 직접적으로 리소스에 요청하는 대신, http://example-proxy.com
라는 프락시 서버에 요청을 보낼 수 있습니다. 그러면 프락시 서버가 http://api.example.com
으로 요청을 전달하고, 응답을 다시 웹 애플리케이션에 반환하는 거죠. 이렇게 하면 요청이 http://example-proxy.com
보내진 것처럼 보이므로, CORS 에러를 피할 수 있습니다.
토스페이먼츠 결제창에서 CORS 에러는 결제 승인을 요청한 뒤 인증이 완료되어 리다이렉트 될 때 가장 많이 마주하게 됩니다. 화면에서는 무한 로딩이 일어나고, 콘솔에서는 CORS 에러가 보입니다. 아래 리다이렉트 URL에 ‘오리진’이 보이죠? 이 값이 최초에 결제를 시도한 값과 다르기 때문입니다. 예를 들어 클라이언트는 http://localhost:8080
에서 띄우고 있는데, 등록한 리다이렉트 URL은 https://myshop.com/success
라면 어떨까요?
http
,https
로 프로토콜이 서로 다릅니다.localhost
,myshop.com
으로 호스트가 서로 다릅니다.8080
,명시하지 않은 443 포트
로 포트가 서로 다릅니다.
따라서 CORS 에러가 날 수 밖에 없겠죠. 결제 요청할 때 파라미터로 최초에 결제창을 연 주소와 출처가 같은 리다이렉트 URL을 넣어주세요. 위에서 설명한 것처럼 프로토콜, 호스트, 포트가 같아야 합니다.
그래도 해결이 안된다면 혹시 결제창을 띄울 때 iframe
, frameset
, frame
태그를 사용하고 있는지 확인하세요. 이런 HTML 요소 안에서 결제창을 띄우면 같은 출처라도 페이지가 이동할 때 에러가 발생할 수 있습니다. 리다이렉트 될 때는 기존 페이지 또는 새창에서 띄워주세요. 결제 요청할 때 파라미터 windowTarget
를 self
로 설정하면 새창에서 리다이렉트 됩니다.
리다이렉트 방식이 아니라 Promise 방식으로 처리한다면 windowTarget
파라미터에서 iframe
을 사용해야 해요. 만약 self
를 사용하면 화면이 이동해 버려서 Promise로 응답을 받을 수 없습니다.
Writer 한주연 Graphic 이은호, 이나눔
📍 참고하면 좋은 자료