목록 보기
문제 해결을 넘어 - 문제의 근본적인 원인 찾아가기 Part 2
기타

문제 해결을 넘어 - 문제의 근본적인 원인 찾아가기 Part 2

파트 1에서 이어지는 글입니다. 이전 글에서는 기존 프로젝트의 코드가 복잡하게 작성되었던 근본적인 원인을 찾아보고, 그에 대한 여러 해결 방안들을 찾아봤습니다. 파트 2에서는 근본적인 원인을 찾아가는 여정을 계속하며 정주행 모드 옵션이 유지되지 않는 문제를 야기하는 CORS 에러가 발생하는 원인을 찾아보고자 합니다. 두 번째 문제 - 왜 CORS 에러가 발생할까? 파트 1에서 살펴봤듯이 link 태그를 통해 CSS 파일을 preload 할 때는 CORS 정책을 고려할 필요가 없습니다. 이는 preload 뿐만 아니라 link 태그를 통해 CSS 파일을 일반적으로 가져올 때도 동일합니다. 이 때문에 CORS 정책을 따르는 네트워크 요청의 경우 요청 및 응답 헤더에 Origin이나 Access-Control-Allow-Origin와 같은 필드가 존재하는 것이 일반적이지만, link 태그를 통한 CSS 파일 요청의 경우 개발자가 직접 추가해주지 않는 한 CORS 관련 헤더 자체가 존재하지 않습니다. 애초에 필요가 없는 헤더이기 때문이죠. CSS 파일 요청에는 헤더에 CORS 필드가 존재하지 않습니다. CORS 정책에 대해 고려할 필요가 없는 swiper/css 파일에서 CORS 에러가 발생한다는 의미는 우리가 예측하지 못한 동작이 추가로 수행되고 있다는 뜻입니다. 파트 1 초반에 주먹구구식 방법을 통해 CORS 에러를 해결하려고 했을 때 코드를 변경한 부분은 다음과 같습니다. 클라이언트 환경에서만 require를 통해 swiper/css를 가져오고 있던 코드를 단순 import로 변경 React 컴포넌트 내부에서 가져오고 있던 swiper/css의 위치를 전역 _app.tsx로 변경 첫 번째 문제의 근본적인 원인을 찾아보면서 swiper/css를 import로 가져오도록 변경했었는데, 이 변경 사항이 적용된 이후에도 CORS 에러가 여전히 발생하는 것을 확인했습니다. 그렇다면 swiper/css를 전역에서 가져오도록 변경했던 것이 실제로 유효한 해결책이었던 것 같네요. 즉, 컴포넌트 내부에서 swiper/css를 임포트했을 때 우리가 예측하지 못한 동작이 추가로 수행되고 있어 CORS 에러가 발생하는 것으로 판단할 수 있습니다. 전역 임포트와 컴포넌트 내부 임포트의 차이는 무엇인가? 그렇다면 왜 전역에서 swiper/css를 임포트하면 문제가 해결되는 걸까요? React 컴포넌트 내부에서 임포트하는 것과 전역에서 임포트하는 것 사이에 어떤 차이점이 있는지 한번 알아봅시다. 프로젝트에서 swiper/css 파일을 직접 임포트하는 것이 가능했던 이유는 Next.js 프레임워크에서 CSS 파일 임포트를 가능하게 하는 Built-In CSS Support 기능을 제공해주기 때문입니다. 문서를 자세히 읽어보니 _app.tsx에 전역으로 선언된 모든 CSS 파일은 production 환경에서 하나의 압축된 CSS 파일 로 합쳐진다고 하는군요. In production, all CSS files will be automatically concatenated into a single minified .css file. 정말 그런지 확인해봅시다. 이 글을 시점에 production 환경에 배포된 카카오페이지 웹 페이지의 경우 전역 _app.tsx에서 swiper/css와 몇몇개의 다른 CSS 파일들을 함께 import 하도록 구현되어 있습니다. 직접 사이트를 들어가서 압축된 CSS 파일을 확인해볼까요? 카카오페이지 웹 사이트 메인 화면에 들어가서 HTML 헤더에 걸린 CSS 파일을 확인해보니 정말로 swiper/css의 내용과 다른 CSS 파일들의 내용과 함께 하나의 CSS 파일로 합쳐진 것을 확인할 수 있었습니다. 그렇다면, 전역에서 swiper/css를 가져오지 않고 기존 구현과 동일하게 특정 React 컴포넌트 내부에서 임포트하면 어떻게 될까요? 이 경우에도 하나의 CSS 파일에 모든 내용이 합쳐질까요? React 컴포넌트 내부에서 CSS 파일을 임포트하도록 변경한 후 동일한 파일에서 swiper 키워드로 검색해보니 아까와는 다르게 swiper/css의 내용이 전혀 보이지 않습니다. 그러면 우리가 임포트한 CSS 파일은 어디로 간 걸까요? 카카오페이지 메인 화면이 아니라 실제로 swiper/css를 임포트한 React 컴포넌트가 렌더링 되는 작품 뷰어 페이지에서 다시 확인해봅시다. 실제로 컴포넌트가 렌더링 되는 페이지의 HTML 헤더에 존재하는 CSS 파일에는 swiper/css 내용이 존재하는 것을 확인할 수 있었습니다. 여기서 우리가 눈여겨볼 포인트를 하나 찾았습니다. 바로 컴포넌트 내부에서 CSS 파일을 임포트하면 별도의 CSS 파일로 존재하게 되며, 실제로 해당 컴포넌트가 렌더링 되는 페이지에 진입했을 때 가져온다 라는 것입니다. 이 포인트를 통해 근본적인 원인을 찾아나갈 수 있겠다는 느낌이 들기 시작합니다. Next.js의 Client-Side Navigation swiper/css를 import한 컴포넌트가 실제로 렌더링 되는 페이지에 진입했을 때 CSS 파일을 가져온다는 사실은 저에게 Next.js가 제공하는 하나의 기능을 떠올리게 했습니다. 바로 Next.js의 Client-Side Navigation 입니다. Next.js는 유저가 사이트에 진입했을 때 그 즉시 서비스에서 필요로 하는 모든 리소스를 한 번에 가져오는 것이 아니라, 유저가 진입한 페이지에서 실제로 사용되는 리소스만을 효율적으로 가져올 수 있도록 프로젝트 빌드 시 Code Splitting 과정을 거칩니다. Next.js는 Code Splitting을 통해 현재 페이지에서 필요로 하는 리소스만을 효율적으로 가져옵니다. 출처: https://nextjs.org/learn/foundations/how-nextjs-works/code-splitting 따라서 정확히 현재 페이지에 필요한 리소스만을 가지고 있기 때문에 다른 페이지로 이동하기 위해서는 아래 두 가지 과정 중 하나를 선택해서 수행해야 합니다. 이동하고자 하는 페이지 자체를 서버에 요청한 후, 응답으로 받은 페이지를 화면에 그대로 다시 그리기 이동하려는 페이지에서 필요로 하는 리소스만 서버에 요청한 후, 페이지 전체를 다시 그리는 대신 현재 페이지에서 필요한 부분만 업데이트하기 전자가 전통적인 웹 애플리케이션에서 사용하는 Server-Side Navigation 기법이고, 후자가 Next.js 프레임워크에서 제공하는 next/link와 같은 컴포넌트들을 활용해 내비게이션을 구현하면 사용할 수 있는 Client-Side Navigation 기법입니다. Client-Side Navigation에 Client라는 이름이 붙은 이유는 서버에서 내려준 페이지를 그대로 그리는 Server-Side Navigation과는 달리 페이지 이동을 클라이언트 자바스크립트 코드가 제어하기 때문입니다. Next.js에서는 특정 페이지에서 필요로 하는 리소스들의 목록을 buildManifest.js라는 파일에서 관리합니다. Next.js로 구축된 웹 사이트에 처음 진입하면 이 파일을 함께 다운로드 받는데, 페이지 이동 시 자바스크립트가 이 buildManifest 파일을 참고해서 필요한 파일들을 GET Fetch를 통해 서버에 요청합니다. 현재 swiper/css 파일이 작품 뷰어 페이지로 이동했을 때 가져와지는 것을 볼 때, 컴포넌트 내부에서 임포트한 CSS 파일도 buildManifest 에서 관리될 것으로 보입니다. 정말 그런지 실제로 확인해볼까요? 프로젝트의 buildManifest 파일에서 작품 뷰어 페이지에 대한 내용을 찾아봅시다. buildManifest 파일에는 뷰어 페이지에 진입했을 때 CSS 파일을 가져오도록 명시되어 있습니다. 정말 buildManifest에 CSS 파일에 대한 내용이 명시되어 있습니다! 위 이미지에서 보이는 d205~.css 파일이 바로 swiper/css 파일입니다. 왜 브라우저에서 캐시를 사용할 때만 CORS 에러가 발생할까? 위에서 클라이언트 자바스크립트 코드가 필요한 리소스를 요청할 때, GET Fetch 요청을 사용한다고 언급했습니다. GET Fetch 요청은 HTML 헤더의 link 태그에서 스타일시트를 가져올 때와 달리 CORS 정책을 준수해야 합니다. 즉, 우리는 컴포넌트 내부에서 CSS 파일을 임포트했을 때 CORS 정책을 피해 갈 수 없다는 사실을 깨달았습니다. 충분히 swiper/css에 대해 CORS 에러가 발생할 수 있는 상황이었네요. 하지만 CORS 에러가 발생하는 조건이 조금 이상합니다. 우리가 파트 1에서 근본적인 원인을 찾는 여정을 떠나기 전에 확인했듯이 브라우저 캐시를 사용할 때만 에러가 발생했으니까요. 대체 브라우저 캐시와 CORS 정책 사이에 어떤 연관이 있는걸까요? 우선 CORS 에러가 발생했을 때의 에러 메시지부터 살펴봅시다. 에러 메시지에서 다음 내용을 주목할만한 것 같습니다. No Access-Control-Allow-Origin header is present on the requested resource. CORS 정책이 적용되는 네트워크 요청의 경우, 네트워크 요청(request)에는 현재 요청을 보내는 사이트의 Origin 헤더가 존재해야 하고 네트워크 응답(response)에는 그에 상응하는 Access-Control-Allow-Origin 헤더가 존재해야 합니다. 그리고 Origin과 Access-Control-Allow-Origin 헤더에 명시된 도메인이 일치해야만 네트워크 요청이 정상적으로 마무리되고 우리가 원했던 리소스를 가져올 수 있죠. 현재 문제가 발생하는 swiper/css 파일에 대한 네트워크 요청의 경우에도 정상적으로 동작한다면 아래와 같이 GET Fetch 요청 시 Origin 헤더와 Access-Control-Allow-Origin 헤더가 존재해야 합니다. 현재 정상적으로 세팅이 되어 있기 때문입니다. 정상적인 상황이라면 CORS 헤더가 모두 존재해야 합니다. 하지만 에러 메시지는 캐시를 사용했을 때 swiper/css에 대한 네트워크 요청 시 이 헤더가 존재하지 않기 때문에 CORS 정책을 충족시키지 못했고, 그로 인해 CORS 에러가 발생했다고 말하고 있습니다. 상황은 대충 파악했으나 이 상황을 어떻게 해결해야 할지 감이 잡히지 않기 때문에, 이제 인터넷의 도움을 받아봅시다. 상황을 알고 있다는 뜻은 명확한 검색 키워드를 알고 있다는 뜻이기도 하죠. 브라우저 캐시를 사용할 때 CORS 헤더가 존재하지 않는 상황이므로, missing cors header when using browser cache라는 키워드로 구글에 검색을 해봤습니다. 그리고 검색 결과 상위에서 우리가 겪고 있는 문제의 근본적인 원인과 그 해결 방법을 담은 블로그 포스팅을 발견할 수 있었습니다. 이 블로그 포스팅에서 설명해주는 내용을 우리가 겪고 있는 문제에 맞게 풀어보겠습니다. 적절한 키워드로 검색하니 우리에게 도움을 줄 블로그 글을 검색 결과 상단에서 찾을 수 있습니다. 인터넷 검색 결과를 통한 원인 분석 일단 이 블로그 포스팅에서 소개하는 바에 따르면 파이어폭스 브라우저에서는 이 문제가 재현되지 않는다고 합니다. 테스트할 때 주로 크롬과 사파리만 사용했기 때문에 몰랐던 사실이었는데, 실제로 파이어폭스 환경으로 테스트해보니 정말 CORS 에러가 발생하지 않고 정주행 모드 옵션이 정상적으로 유지됩니다! 이건 초반에 문제가 발생하는 상황을 분석할 때 놓쳤던 부분이네요. 우리가 페이지 이동이 아니라 URL 입력을 통해 직접 작품 뷰어 페이지에 진입하면 HTML 헤더의 link 태그를 통해 CSS 파일을 가져오며, 해당 네트워크 요청은 브라우저에 캐싱됩니다. 임의로 브라우저 캐시 사용을 중지하지 않는 한 말이죠. 그리고 만약 유저가 이후에 동일한 네트워크 요청을 시도한다면 브라우저는 URL로 직접 요청을 보내는 대신 브라우저에 캐싱 되어 있던 네트워크 요청을 참조해 캐싱 된 데이터를 그대로 가져오게 됩니다. 파이어폭스 브라우저의 경우 작품 뷰어 페이지에 진입한 상태에서 이전 화나 다음 화로 이동하는 경우에는 Client-Side Navigation을 사용하기 때문에 GET Fetch 요청을 통해 buildManifest 파일에 명시된 swiper/css 파일을 가져오려고 시도합니다. 문제가 발생하지 않는 파이어폭스 브라우저의 경우, link 태그를 통해 CSS 파일을 가져오는 네트워크 요청 과 GET Fetch를 통해 CSS 파일을 가져오는 네트워크 요청 을 별개의 요청으로 생각합니다. 따라서 설령 브라우저 캐시에 link 태그를 통해 CSS 파일을 가져온 요청이 존재하더라도, 뷰어 페이지에 진입했을 때 새롭게 GET Fetch 요청을 보내게 되며 이때에는 네트워크 헤더에 Origin 및 Access-Control-Allow-Origin 필드가 존재합니다. GET Fetch 요청은 CORS 정책을 따르기 때문이죠. 크롬 및 사파리 브라우저의 경우 하지만 문제가 발생하는 크롬 및 사파리 브라우저의 경우 동작이 다릅니다. 이 브라우저들에서는 link 태그를 통해 CSS 파일을 가져오는 네트워크 요청 과 GET Fetch를 통해 CSS 파일을 가져오는 네트워크 요청 을 동일한 요청으로 생각합니다. 따라서 우리가 뷰어 페이지에 진입해서 swiper/css 파일에 대한 GET Fetch 요청을 보낼 때 브라우저 캐시에 존재하는 link 태그 CSS 파일 네트워크 요청을 그대로 활용하게 됩니다. 이 캐시에 존재하는 네트워크 요청에는 헤더에 CORS 관련 필드가 존재하지 않습니다. link 태그를 통한 네트워크 요청에는 CORS 정책이 적용되지 않기 때문이죠. 바로 이 지점에서 문제가 발생하고 있던 것입니다. CORS 정책을 따라야 하는 GET Fetch 요청을 보냈는데 헤더에 Origin 및 Access-Control-Allow-Origin 필드가 존재하지 않기 때문에, 브라우저는 이 요청이 CORS 정책을 위반했다고 판단해서 네트워크 에러를 발생시키게 됩니다. 브라우저 캐시 정책으로 인해 발생하는 문제를 어떻게 해결할 수 있을까? 제가 생각했던 것보다 훨씬 더 복잡한 원인이었습니다. 브라우저 캐시 정책 때문에 CORS 에러가 발생했다고는 전혀 생각을 못 했네요. 그럼, 이 문제를 어떻게 해결할 수 있을까요? link 태그를 통해 CSS 파일을 가져온 캐싱 된 네트워크 요청을 재활용하는 동작은 크롬 및 사파리 브라우저의 자체적인 캐시 정책입니다. 이 브라우저 정책을 우리가 임의로 바꿀 수는 없을 것 같네요. 그렇다고 우리가 swiper/css의 네트워크 요청에 대해 관여할 방법도 딱히 없는 것 같습니다. GET Fetch 요청의 경우 Client-Side Navigation을 사용하면 필연적으로 발생하는 것이기 때문이죠. 이 동작을 우리가 컨트롤할 방법이 없을뿐더러 이 문제 때문에 작품 뷰어 페이지로 이동할 때만 Server-Side Navigation을 사용하도록 변경하는 것도 이상해보입니다. 이렇듯 우리가 직접 관여할 수 있는 부분이 한정되어 있기 때문에, 문제 해결을 위해 취할 수 있는 액션은 크게 두 가지 정도로 좁혀볼 수 있을 것 같습니다. swiper/css를 React 컴포넌트 내부에서 임포트하지 않고 전역 _app.tsx 에서 임포트하도록 수정하기 (기존에 적용한 방법) HTML link 태그에 crossorigin 속성 추가하기 기존에 CSS 파일 관리 포인트를 줄이기 위해 별생각 없이 전역에서 swiper/css를 임포트하도록 수정한 해결 방법도 지금 보니 충분히 매력적인 선택지입니다. 전역에서 임포트한 CSS 파일은 모든 페이지에서 공통으로 활용되는 하나의 CSS 파일로 합쳐지기 때문에, 웹 사이트 접근 시 최초 한 번만 네트워크 요청을 해서 가져오기 때문이죠. 최초에 link 태그를 통해 CSS 파일을 가져온 후 GET Fetch 요청을 추가로 하지 않기 때문에 CORS 에러를 회피할 수 있습니다. 하지만 이 해결 방법이 문제를 해결하는 게 아니라 단순히 문제를 회피하는 것 같아 마음에 들지 않는다면 다른 방법을 고려해볼 수도 있습니다. 바로 link 태그에 crossorigin 속성을 추가하는 방법입니다. 이 crossorigin 속성을 추가하면 link 태그를 통한 네트워크 요청이 CORS 정책을 따르도록 강제할 수 있습니다. link 태그를 통해 CSS 파일을 가져오는 네트워크 요청에 CORS 헤더가 존재하지 않았던 부분이 문제였으니, 해당 네트워크 요청이 CORS 정책을 따르도록 강제하면 문제가 해결되지 않을까요? 직접 확인해봅시다. Next.js에서는 HTML head에 포함된 모든 네트워크 요청 태그들에 대해 crossorigin 헤더를 붙여주는 옵션을 제공합니다. // next.config.js module.exports = ( ... crossOrigin: 'anonymous', ) 이 옵션을 적용한 상태로 프로젝트를 빌드한 후 문제가 해결되는지 확인해봅시다. 일단 link 태그에 crossorigin 속성이 추가됐는지 확인해봅시다. link 태그에 crossorigin 속성이 추가됐습니다. 우리가 의도했던 대로 crossorigin 속성이 추가된 것을 확인했습니다. 그렇다면 네트워크 요청은 어떨까요? 우리가 원했던 대로 link 태그를 통해 CSS 파일을 가져오는 네트워크 요청에도 CORS 헤더가 추가됐을까요? 네트워크 요청에 CORS 헤더가 존재합니다. 네트워크 요청에 CORS 헤더가 존재합니다! 그렇다면 마지막으로, 크롬 브라우저에서 캐시를 사용하도록 설정해도 CORS 에러가 발생하지 않고 정주행 모드 옵션이 유지될까요? 정주행 모드 옵션이 정상적으로 유지된다면 우리가 올바른 목적지에 도달했음을 확인할 수 있고, 이 기나긴 여정을 마무리할 수 있을 것 같습니다. 브라우저 캐시를 사용하도록 설정했음에도 불구하고, 크롬 브라우저에서 더 이상 CORS 에러가 발생하지 않고 다음 화로 이동했을 때 정주행 모드 옵션이 ON 상태로 유지되는 것을 확인할 수 있습니다. 두 번째 문제의 근본적인 원인 정리와 해결방법 결론에 도달하기까지 굉장히 오래 걸렸지만, 마침내 우리는 CORS 에러에 대한 근본적인 원인과 해결 방법을 찾을 수 있었습니다. Q: CORS 에러는 왜 발생했나요? A: 전역에서 swiper/css 파일을 임포트하는 것과 달리 컴포넌트 내부에서 임포트하는 경우 동작이 예상했던 것과 달라지기 때문입니다. Q: 전역에서 임포트하는 것과 컴포넌트 내부에서 임포트하는 것에 어떤 차이가 있나요? A: 전역에서 CSS 파일을 임포트하면 하나의 CSS 파일로 압축되지만, 컴포넌트 내부에서 임포트하면 별도의 CSS 파일로 분리됩니다. 이 CSS 파일은 Client-Side Navigation을 통해 해당 컴포넌트를 렌더링하는 페이지 진입 시에 GET Fetch 요청을 통해 가져옵니다. 그리고 GET Fetch 요청은 link 태그를 통해 CSS 파일을 가져올 때와는 달리 CORS 정책을 따르기 때문에, CORS 정책을 준수하지 않는다면 CORS 에러가 발생할 수 있습니다. Q: 왜 일부 브라우저에서 캐시를 사용할 때만 CORS 에러가 발생하나요? A: 일부 브라우저에서는 link 태그를 통해 CSS 파일을 가져온 네트워크 요청을 캐싱한 후 GET Fetch 요청에서 재활용하기 때문입니다. swiper/css를 전역에서 임포트하도록 변경하거나, link 태그에 crossorigin 속성을 추가하면 문제를 해결할 수 있습니다. 이로써 두 번째 문제에 대한 근본적인 원인 찾기도 마무리 지을 수 있었습니다. 여정의 끝 우리는 두 개의 문제에 대한 근본적인 원인과 해결 방법을 찾고자 기나긴 여정을 시작했고 많은 시행착오 끝에 결국 목적지에 도달했습니다. 그래서, 이 여정에는 의미가 있었을까요? 목적지에 도달하고 나서 정답지를 보니 문제의 원인은 복잡했지만, 해결 방법은 매우 간단했습니다. 우리가 주먹구구식 방법을 통해 찾아낸 해결책이 사실은 문제를 해결할 수 있는 최적의 해결 방법 중 하나라는 사실을 깨달았습니다. 이렇게 힘들게 문제의 해결 방법을 찾아 헤맸지만, 우리가 바라던 것은 사실 눈앞에 있었던 셈입니다. 결과만을 놓고 봤을 때는 우리가 했던 고생들이 모두 의미 없는 것처럼 느껴질 수도 있습니다. 하지만 우리가 이 여정에서 무엇을 얻을 수 있었는지 되돌아봅시다. link를 통해 리소스를 preload 할 때의 CORS 정책을 알 수 있었고, SSR 환경에서 ES

댓글 0

댓글을 작성하려면 로그인이 필요합니다.

댓글을 불러오는 중...