CSS fetching은 동기적? vs 비동기적?

🚍 0. 쓰게되는 계기

일전 교육과정을 들으면서 수강생 중에 어떤 분이 브라우저의 html 랜더링 과정에 대해 질문을 한 적이 있었다.
그 당시 질문은 "css파일은 과연 비동기적으로 패칭되는가, 아니면 동기적으로 패칭되는가" 였다.

순간 아무도 이에 대해서 대답을 하지 못했고, 어렴풋이 누군가가 "비동기적으로 패칭되는거같아요!" 라는 말에 종결을 짓고 상당히 찜찜한 상태로 끝마무리를 지었었는데, 계속 마음에 걸렸던 그 내용을 오늘 css load개념을 공부하다가 마주하게 되어 정리하려고 한다.

결론만 말한다면, css의 패칭은 동기적일수도 비동기적일수도 있다.

🚍 1. link 태그

일반적으로 html 문서가 파싱이 되면서 DOM을 만들어나가는 과정 중에 css를 로드하는 태그를 마주하게 된다

<link rel="stylesheet" href="index.css">

브라우저의 랜더링 엔진은 해당 문서의 태그를 발견하면 해당 href 경로로 css 파일을 요청하게 된다.
아주 일반적인 방식인 위 내용은 한가지 단점이 있는데 그것은 바로 동기적으로 파일을 요청한다는 것이다.

예를들어 아래와 같은 코드가 있다고 보자

<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href='https://fonts.googleapis.com/css?family=Roboto:400,600|Material+Icons'>
    <style>
      html {
        font-family: Roboto;
      }
    </style>
  </head>
  <body>
    <p> Hello </p>

    <script>
      window.onload = function () {
        console.log('Loaded');
      }
    </script>
  </body>
</html>

위의 html 문서는 head에 link가 존재하는 것을 알 수 있다.
그리고 나서 style을 통해 해당 스타일이 적용이 될 것이고, body에 있는 p는 이 font-family를 적용받을 것이다.
그 이후 script 태그에서 자바스크립트 엔진에 주도권이 넘어가면서 파싱이 일어나고, 암묵적으로 생성된 전역 객체인 window의 onload가 실행이 될 것이다.

해당 내용이 link에 의해 한번 blocking이 되는 것을 확인하는 방법은 개발자 도구에서 네트워크 탭으로 간 후, 인터넷 속도를 가장 느린 Slow 3G 로 변경한 뒤에 리로드를 해보면 된다.

이와같이 했을 경우, 첫 html을 받아올 때 까지는 console에 아무것도 찍혀있지 않다가, css content가 loaded 완료된 이후 console이 작동한 것을 볼 수 있다.

이것이 의미하는 바는 일반적인 랜더링 엔진의 프로세싱 과정과 동일하게 잠시동안 DOM의 형성을 멈추고 css 파일을 요청하여 해당 파일이 패칭되기까지 블로킹되었다가 response를 받아 parsing을 완료하여 CSSOM을 형성이 완료되면 그 이후에 다시금 DOM을 형성하는 과정을 진행한다는 의미이다.

물론, 가끔 우리는 DOM이 어떤 스타일링을 가지고 있을지 모르기 때문에 CSS가 전달받아서 처리가 완료되기 전까지 html 파일이 parsing되는것이 막아지는 것을 이상적으로 여길 수 있다.

그러나, 대체적으로 CSS파일은 결코 DOM 형성을 막아가면서까지 처리가 완료되야 할 만큼 크리티컬한 파일은 아니다.
대체적으로 크리티컬하게 빠른 처리가 필요로되는 css는 GPU를 이용한 그래픽 처리가 빈번하게 요구되는 경우라고 한다.
(브라우저가 문서를 랜더링하기 위해서)

다시말하자면, js처럼 해당 파일이 받아와져서 함부로 DOM을 조작하여 에러를 일으킬 가능성이 있는것과 반면에 CSSOM 그 자체의 형성이 DOM의 형성에 크리티컬한 연결관계를 갖지 않는다는 의미이다.

즉, css의 패칭으로 인해 DOM의 형성이 늦어지는 것은 그저 브라우저에 페인팅되어야 할 컨텐츠의 형성을 지연시키는 결과밖에 가져오지 않는다.

🚍 2. css의 비동기적인 처리?

해당 방식에 대해서는 자바스크립트까지 사용한 상당히 legacy한 방법들을 사용했던 모양이다.
그러나 상당히 복잡하고, 보기싫게 생겼다.

고맙게도, 웹 표준화가 진행되는 과정중에 css를 간단한 코드 한줄로 비동기적으로 처리하는 방법이 도입되었다. 그것은 바로

rel="preload"

를 사용하는 것이다.

<link rel="preload" as="style" href="mystyles.css">

link에서 사용하는 rel="preload" 옵션은 해당 리소스가 미리 불러져와야하는 것을 나타내는 옵션으로, 해당 요청은 우선순위적으로 높은 우선순위로 바뀌어 요청된다. 단, css는 비동기적으로 받아져 오기만 할 뿐, DOM의 생성을 막지 않는다는 점이다. 즉, 해당 리소스는 브라우저에 의해 다운로드되긴 하였지만 아직 그 내용이 적용되지 않음을 보여준다.

만약 rel을 preload나 prefetch로 설정했을 경우, as 어트리뷰트로 이 파일이 정확하게 무엇을 의미하는지를 명시해줘야 한다.

<!DOCTYPE html>
<html>
  <head>
    <link
      rel="preload"
      as="style"
      href="https://fonts.googleapis.com/css?family=Roboto:400,600|Material+Icons"
     onload="this.rel='stylesheet'"
    /> 

   <noscript>
       <link
      rel="stylesheet"
      href="https://fonts.googleapis.com/css?family=Roboto:400,600|Material+Icons"
      /> 
   </noscript>
   
   <style>
      html {
        font-family: Roboto;
      }
    </style>
  </head>
  <body>
    <p>Hello</p>

    <script>
      window.onload = function () {
        console.log("Loaded");
      };
    </script>
  </body>
</html>

이번에는 link 태그에 rel로 preload를 준 상태이다.

해당 어트리뷰트를 통해 우선순위가 높아져서 전달된 리소스는 일단 다운로드는 비동기적으로 받아지지만 DOM에 적용이 되지 않는 상태이다.

따라서 언제 해당 다운로드된 파일이 적용되야하는지를 알려주는 시점의 용도로 onload 어트리뷰트를 통하여 로드가 완료되는 순간 stylesheet로 변경되도록 주었으며, 만에 한하여 onload의 호출은 자바스크립트 영역이기 때문에 이 호출이 모종의 이유로 실패할 상황을 대비하여 "noscript" 태그를 이용해 자바스크립트 실패시 그냥 스타일로 적용되도록 만들어진 상황이다.

하지만, 이때는 preload를 지정해주지 않았던것과 비교해서 별반 차이가 없을 것이다.

html이 로드가 되었어도 console은 찍히지 않다가 css 로드가 완료되는 순간 찍히게 될 것이니 말이다.
그렇다면 도대체 뭐가 달라졌을까?

레퍼런스의 설명에 따르면 해당 window.onload는 css가 다운로드 되기 이전에 html이 파싱이 되면서 script에 도달하여 바로 시작된다고 한다. 즉, preload된 css는 html이 전부 다 파싱할때까지 다운로드되어 메모리에만 저장되어있는 상태였다가, 유저가 해당 리소스가 관여된 내용에 클릭과 같은 상호작용을 하는 순간 불러들어와서 사용된다고 한다.

🚍 3. prefetch?

그렇다면 preload와 prefetch의 차이는 무엇일까?
설명에 따르면 미리 우선순위가 높아지면서 불러와지기는 하지만 DOM의 파싱 전까지는 메모리에 저장되는 preload와는 반대로, prefetch는 똑같이 비동기적으로 리소스가 불러와져서 메모리에 저장되는 건 맞지만 다른 자원에 비해서 조금 늦게 사용될 것이다 라고 선언되어 다른 자원들보다 낮은 우선순위로 요청이 보내진다고 한다.(즉, 요청이 늦게간다)

즉, 다시말하자면
preload : 비동기적으로 높은 우선순위를 갖고 리소스를 불러와서 메모리에 저장되어 있다가 DOM의 파싱이 완료되면 불러들여와서 적용됨

_prefetch : 비동기적으로 낮은 우선순위를 갖고 리소스를 불러와서 메모리에 저장되어 있다가 해당 리소스와 연관되어 있는 요소를 사용자가 클릭등의 상호작용을 하였을 때에 불러들어와져서 적용됨.

참고로 DNS prefetching이라는 것도 있다.
보통 외부 리소스를 요청하기 위해서 프록시 서버에 캐싱되어 있는 자원을 요청하게 될 때, DNS를 통해 ip와 연동하는 과정이 필요하게 되는데 이 미세한 과정 역시 시간을 잡아먹기 때문에 미리 연결시켜놓는 것이 유리할 때가 있다.

<link rel="dns-prefetch" href="//fonts.googleapis.com">

preconnect라는 것도 존재하는데, 애초에 자원요청을 위해 서버로 요청을 실행하기 전 필요로되는 DNS lookups, TCP handshakes, TLS negotiations 하는 일련의 과정을 미리 이전에 백그라운드에서 실행하도록 만들어둠으로서, 실제 요청을 해야할 순간에 해당 과정을 생략하고 진행하도록 만드는 옵션을 말한다

<link href="https://cdn.domain.com" rel="preconnect" crossorigin>

결론

일단 "caniuse" 에서 해당 스펙들을 사용할수 있는지 없는지를 항상 확인해야 하는건 자명하고,
네트워크로 리소스 요청을 할 때에 효율적으로 하기 위한 옵션들이 이렇게 많은지 꿈에도 생각하지 못했다. 많은 새로운 새계를 배워가는 것 같다.

요약하자면 "CSS fetching은 비동기적일수도 동기적일수도 있다"

Reference

Modern css asynchronous approach
CSS in webpack
When CSS is applied

좋은 웹페이지 즐겨찾기