astro

Astro에서 script와 script is:inline의 차이

Astro의 두 가지 스크립트 선언 방식이 브라우저 렌더링 과정에서 어떻게 다르게 동작하는지, 그리고 View Transitions 환경에서 각각을 언제 사용해야 하는지 정리합니다.

astro javascript view-transitions rendering

들어가며

Astro에서 클라이언트 스크립트를 작성하는 방법은 두 가지다. <script><script is:inline>. 겉보기엔 비슷해 보이지만, 빌드 과정과 브라우저에서의 실행 타이밍이 완전히 다르다.

이 글에서는 두 방식의 차이를 브라우저 렌더링 흐름 기준으로 설명하고, View Transitions 환경에서 각각을 언제 사용해야 하는지 정리한다.

두 방식의 핵심 차이

<script> — 기본 방식

Astro의 기본 <script> 태그는 빌드 시 다음과 같이 처리된다.

<!-- 컴포넌트 내에서 -->
<script>
  import { gsap } from 'gsap';
  // import 사용 가능, Vite가 번들링
  gsap.to('.box', { x: 100 });
</script>

<script is:inline> — 인라인 방식

is:inline 디렉티브를 추가하면 Astro가 스크립트를 가공하지 않고 그대로 HTML에 삽입한다.

<head>
  <script is:inline>
    // 번들링 없이 이 위치에 그대로 삽입
    // import 사용 불가
    console.log('head에서 즉시 실행');
  </script>
</head>

브라우저 렌더링 흐름에서의 차이

두 방식의 핵심 차이는 실행 타이밍이다. 브라우저가 HTML을 파싱하는 순서를 따라가보면 명확해진다.

HTML 파싱 시작

├─ <head> 파싱
│   ├─ <meta>, <title>, <link> 처리
│   ├─ <script is:inline>    ← ❶ 파싱을 멈추고 즉시 실행
│   │   └─ 실행 완료 후 파싱 재개
│   └─ <head> 파싱 완료

├─ <body> 파싱 + 렌더링 시작
│   ├─ DOM 요소들을 순서대로 생성
│   │   └─ ❶에서 적용한 상태가 이미 반영된 채로 렌더링
│   │
│   └─ <script type="module">    ← ❷ DOM 파싱 완료 후 실행
│       └─ Astro 기본 script가 여기에 삽입됨

└─ DOMContentLoaded 발생

type="module"은 브라우저에서 자동으로 defer와 동일하게 동작한다. 즉, DOM 파싱이 전부 끝난 뒤에 실행된다.

실전 사례: 다크모드 깜빡임 방지

이 차이가 가장 명확하게 드러나는 사례가 다크모드 초기화다.

기본 script로 작성한 경우 (문제 발생)

<head> 파싱 → <body> 렌더링 (라이트 모드) → script 실행 → dark 클래스 추가 → 리페인트

사용자에게는 흰 화면이 잠깐 보였다가 어두워지는 깜빡임(FOUC, Flash of Unstyled Content)이 발생한다.

is:inline으로 작성한 경우 (해결)

<head>
  <script is:inline>
    const theme = localStorage.getItem('theme');
    if (theme === 'dark' || (!theme && matchMedia('(prefers-color-scheme: dark)').matches)) {
      document.documentElement.classList.add('dark');
    }
  </script>
</head>
<head> 파싱 → is:inline 실행 (dark 클래스 추가) → <body> 렌더링 (처음부터 다크)

<body>가 렌더링되기 전에 이미 dark 클래스가 적용되어 있으므로 깜빡임이 없다.

View Transitions에서의 주의점

Astro의 View Transitions(ClientRouter)를 사용하면 페이지 전환 시 DOM이 swap된다. 이때 두 방식 모두 재실행되지 않는다는 점이 중요하다.

기본 script

모듈 스크립트는 최초 한 번만 실행된다. 페이지 전환 후 재초기화가 필요하면 astro:page-load 이벤트를 사용한다.

// astro:page-load는 초기 로드와 View Transitions 전환 모두에서 발생
document.addEventListener('astro:page-load', () => {
  initScrollReveal();
});

is:inline

is:inline 스크립트도 재실행되지 않지만, <head> 안에서 이벤트 리스너를 등록해두면 swap 후에도 동작한다.

// 최초 로드 시 즉시 실행 + swap 후에도 테마 복원
const applyTheme = () => {
  const theme = localStorage.getItem('theme');
  if (theme === 'dark' || (!theme && matchMedia('(prefers-color-scheme: dark)').matches)) {
    document.documentElement.classList.add('dark');
  } else {
    document.documentElement.classList.remove('dark');
  }
};
applyTheme();
document.addEventListener('astro:after-swap', applyTheme);

astro:after-swap는 새 DOM이 swap된 직후, 렌더링 전에 발생하는 이벤트다. 이 시점에 dark 클래스를 복원하면 전환 시에도 깜빡임이 없다.

정리: 언제 어떤 방식을 사용하는가

상황방식이유
렌더링 전에 실행되어야 하는 코드 (테마, 폰트 등)is:inline<head>에서 즉시 실행
외부 라이브러리를 import해야 할 때기본 <script>번들링 필요
DOM 조작, 이벤트 바인딩기본 <script>DOM 파싱 완료 후 안전하게 실행
번들 크기에 포함시키고 싶지 않은 간단한 코드is:inline번들링 우회

핵심 원칙은 단순하다. “렌더링 전이냐, 후냐.” 이 기준으로 판단하면 대부분의 경우 올바른 선택을 할 수 있다.