<script> load 방법 async, defer, 동적 load

제리추·2024년 5월 2일
3

리액트 모던 딥 다이브 책을 읽는 중에 타사 자바스크립트 실행의 지연이라는 대목이 눈에 들어왔고 script 태그를 통해 타사 스크립트를 적용 시킬 때 대부분 defer를 통해 로드하는 것이 좋다는 것에 의문을 가지고 다시 공부하기 시작했습니다.

🐹 <script> 태그의 사용 방법

<script> 태그는 HTML 클라이언트 스크립트를 추가할 때 사용합니다. 또한 src 속성을 사용해 외부 스크립트 파일을 로드할 수 있습니다.

<script src="lodash.js"></script>

그렇다면 <script> 태그는 어디에 위치하는 것이 좋을까요? 이를 설명하기 전에 <script> 태그의 load와 브라우저 렌더링 과정을 먼저 이해해야 합니다. 브라우저 렌더링은 아래와 같은 순서로 이뤄집니다.

  1. HTML 파싱 : HTML 태그의 종류와 속성을 분석하고 각 태그의 위치를 계산합니다. 그리고 DOM Tree를 생성합니다.
  2. CSS 파싱 : CSS 선택자와 규칙을 분석하고, 각 규칙의 적용 범위와 우선순위를 계산합니다. CSSOM Tree를 생성합니다.
  3. Render Tree 생성 : 브라우저는 DOM Tree와 CSSOM 트리를 결합해 Render Tree를 생성합니다. 브라우저는 이 과정에서 레이아웃과 페인팅에 대한 정보를 가져오고, 숨겨진 요소나 비표시 요소를 필터링합니다.
  4. layout : 브라우저는 렌더 트리의 각 요소의 크기, 위치를 분석해 view port 내에 배치를 수행합니다.
  5. painting : 브라우저는 렌더링 된 요소들을 화면에 그립니다.

이 때 렌더링 엔진은 사용자의 편의성을 위해 모든 HTML이 파싱하는 것을 기다리지 않고 빠르게 브라우저의 렌더링 된 요소들을 화면에 그리려고 합니다. 이 때 <script> 태그를 만나면 메인 쓰레드는 <script>태그의 다운로드와 실행을 우선순위로 가져갑니다. 이를 통해 다른 작업들은 다운로드와 실행이 끝날 때까지 미뤄집니다.

이런 이유로 <script> 태그는 body에 마지막에 작성하는 것이 좋습니다. 그렇다면 이런 blocking 문제를 해결하는 방법이 없을까요? 다행히 <script> 태그를 로드하는 다른 방법들이 있습니다.

🐹 스크립트 태그의 load 방법 3가지

1) defer 사용

  • script 태그에 defer 속성이 있다면 다른 리소스와 병렬로 다운로드합니다. HTML 파싱 등 메인 쓰레드의 작업을 멈추지 않습니다. 다운로드가 완료돼도 이 스크립트의 실행은 페이지가 완전히 로딩된 이후 맨 마지막에 실행합니다.
  • defer 속성의 장점은 <head><script>를 작성하지만 </body> 태그 앞에 <script>를 작성하는 것과 같은 효과를 낼 수 있습니다.
    <script src="lodash.js" defer></script>

2) async 사용

  • defer와 마찬가지로 병렬로 다운로드합니다. defer과 차이는 리소스 다운로드가 완료된다면 다른 리소스의 다운로드가 완료되는 것을 기다리지 않고 바로 실행합니다. 따라서 async 리소스의 실행 순서는 다운로드가 완료된 순서대로 실행합니다.
    <script src="lodash.js" async></script>

3) 둘 다 없는 경우

  • script를 만나는 순간 메인 쓰레드를 우선으로 가져갑니다. 다운로드 완료 후에 실행이 우선 됩니다.
    <script src="lodash.js"></script>

🐹 defer, async의 차이 및 사용 시기

1) defer, async 공통점

  • 브라우저에서 페이지를 렌더링 하는 프로세스를 중단하지 않고 백그라운드에서 낮은 우선순위로 지정된 파일을 다운로드하도록 브라우저에 지시하는 HTML <script src="..."> 태그의 선택적 속성입니다.
  • 비동기 및 지연 속성을 사용하면 <head> 섹션에 <script> 참조를 배치하여 로딩 프로세스에서 가능한 한 빨리 다운로드를 트리거 하는 동시에 JavaScript 리소스의 렌더링 차단 효과를 제거하는 간단한 방법입니다.

2) defer, async 장점

  • <script> 참조보다 낮은 우선순위로 다운로드하여 페이지의 초기 렌더링에 더 중요한 다른 리소스를 빠르게 렌더링 가능합니다. fetchpriority="high" 를 통해 우선순위를 높일 수도 있습니다.

3) defer, async 차이점

script-block-image

  • 페이지의 모든 리소스는 먼저 다운로드된 다음 페이지에 적용되거나 실행됩니다. async와 defer의 로딩 동작은 렌더링 차단 없이 백그라운드에서 다운로드한다는 것이 동일하지만, 실행 시 동작에 차이가 있습니다.
  • async 속성이 있는 JavaScript 파일은 로드되는 즉시 특별한 순서 없이 실행되는 반면, defer 리소스는 초기 로딩 프로세스가 끝날 무렵, DOMContentLoaded 이벤트 직전까지 순차적으로(HTML에 표시되는 순서대로) 실행됩니다.
    script-dif-table

4) async 사용 시기

  • async 로딩 과정에서 가능한 한 빨리 실행되어야 하는 우선순위가 매우 높은 자바스크립트 리소스에 가장 적합합니다.
  • 페이지 속도를 위해 async는 신중하게 사용해야 하며 일반적으로 페이지의 초기 렌더링이 의존하는 파일에만 사용해야 합니다. 또한 비동기 파일은 특별한 순서 없이 실행되기에 일반적으로 완전히 독립적이고 다른 JavaScript 리소스에 의존하지 않고 먼저 실행되는 파일에 사용해야 합니다.
    ex) google analytics

5) defer 사용 시기

  • defer은 페이지의 초기 렌더링에 중요하지 않고 로딩 프로세스 후반에 실행할 수 있는 일반 자바스크립트 리소스에 이상적입니다.
  • 대부분의 경우 최적의 페이지 속도와 사용자 경험을 위해서는 defer이 최선의 선택입니다. 특히 느린 모바일 디바이스의 경우 defer을 사용하면 브라우저가 가장 중요한 HTML과 CSS를 먼저 렌더링하고 자바스크립트 실행을 로딩 타임라인에서 보다 적절한 단계로 지연시켜 사용자에게 콘텐츠를 최대한 빠르게 표시할 수 있습니다.

하지만 이러한 방법은 한 가지 큰 단점이 존재합니다. 외부 자바스크립트 코드를 특정한 페이지 또는 컴포넌트에서만 불러올 수 없다는 것입니다. 그렇기 때문에 google analyrics와 같이 통계 및 모니터링이 필요하지 않은 외부 스크립트라면 부적절한 방법입니다.

🐹 동적 스크립트 로딩 방법

찾아봤더니 이미 많이들 사용하고 계신 방법이 있었습니다. 다른 분들의 회사에서도 사이즈가 큰 외부 코드를 이러한 방식으로 가져와 사용하고 있다고 하시더라고요.

const script = document.createElement("script");
script.src = "https://unpkg.com/lodash";
script.async = true;
document.body.appendChild(script);

이 4줄만 사용하면 script 태그를 언제든지 동적으로 삽입하고 제거할 수 있습니다. 그럼 간단하게 react에서 직접 만들어 추가할 수 있지만... 사람들이 많이 사용하고 있는 useScript 오픈소스 오픈소스 코드를 공유해 드리겠습니다. 직접 만들어 보는 것도 좋을 것 같습니다.

import React from 'react';
import { StripeProvider } from 'react-stripe-elements';
import useScript from 'react-script-hook';

import MyCheckout from './my-checkout';

function App() {
    const [loading, error] = useScript({ src: 'https://js.stripe.com/v3/' });

    if (loading) return <h3>Loading Stripe API...</h3>;
    if (error) return <h3>Failed to load Stripe API: {error.message}</h3>;

    return (
        <StripeProvider apiKey="pk_test_6pRNASCoBOKtIshFeQd4XMUh">
            <MyCheckout />
        </StripeProvider>
    );
}

export default App;

위와 같이 사용한다면 언제든지 특정 컴포넌트나 페이지에서 외부 스크립트를 동적으로 사용 가능합니다.

🐹 결론

  • 대부분의 경우 defer를 사용하는 것이 사용자의 경험을 위해서 좋겠지만 분석과 통계와 같은 로딩 과정에서 가능한 한 빨리 실행되어야 하는 외부 소스는 async를 사용하는 것이 좋다.
  • 사이즈가 큰 외부 코드 같은 경우 특정 컴포넌트나 페이지에서 사용한다면 동적으로 불러와서 사용하자.

🐹 Reference

profile
안녕하세요. 소프트웨어 엔지니어 제리입니다 🐹

0개의 댓글