테마 전환

Astro + Tailwind: 아일랜드 컴포넌트와 전역 스타일 충돌을 막는 설정법

Easton editorial illustration: modular system blueprint

브라우저 개발자 도구를 열었더니 화면 가득 CSS 규칙에 빨간 취소선이 그어져 있습니다. 어제까지 멀쩡하던 스타일이 client:load 지시어 하나를 추가한 뒤 모두 흐트러졌습니다. 간격은 사라지고 Grid 레이아웃은 무너졌으며, 가장 기본적인 :nth-child 선택자조차 엉뚱한 요소를 가리키기 시작합니다.

요소를 검사해 보니 DOM 구조에 한 번도 본 적 없는 태그 두 개가 추가되어 있습니다. 바로 astro-islandastro-slot입니다. 코드에는 이런 태그를 전혀 작성하지 않았는데 말입니다.

Astro의 아일랜드 아키텍처를 사용한다면 이런 상황을 마주칠 가능성이 큽니다. 이는 버그가 아니라 Astro가 동작하는 방식입니다. 문제는 많은 튜토리얼이 Tailwind를 통합하는 방법만 알려 줄 뿐, islands 아키텍처에서 생기는 스타일 함정까지 설명하지 않는다는 데 있습니다. 이 글에서는 직접 겪은 문제를 정리해 이런 스타일 충돌을 피하는 방법을 소개합니다.

이 글을 읽고 나면 islands 아키텍처가 DOM 구조를 어떻게 바꾸는지, CSS 선택자가 갑자기 동작하지 않는 이유가 무엇인지, Astro에서 Tailwind v4를 올바르게 설정하는 방법과 흔한 네 가지 스타일 충돌의 해결책을 이해할 수 있습니다.

1. 아일랜드 아키텍처가 스타일 렌더링에 미치는 영향

먼저 한 가지를 분명히 하겠습니다. Astro의 islands 아키텍처 자체가 스타일을 ‘망가뜨리는’ 것은 아닙니다. 단지 DOM 구조를 바꿀 뿐입니다. 문제는 이 변화를 모른 채 기존 방식 그대로 CSS를 작성할 때 생깁니다.

기본 동작: 정적 HTML, JavaScript 0

Astro의 핵심 철학은 간단합니다. 기본적으로 정적 HTML을 렌더링하고 클라이언트 JavaScript를 모두 자동으로 제거합니다. 다음과 같이 작성한 컴포넌트가 있다고 해 보겠습니다.

---
import Counter from './Counter.svelte'
---

<Counter />

렌더링 결과에는 순수 HTML과 CSS만 있고 JavaScript는 전혀 없습니다. 성능 측면에서는 좋은 일입니다. 페이지가 빠르게 로드되고 SEO에도 유리합니다. 하지만 상호작용이 필요하다면 client 하이드레이션 지시어를 추가해야 합니다.

<Counter client:load />

이 지시어를 추가하는 순간 DOM 구조가 달라집니다.

갑자기 나타나는 astro-island와 astro-slot

client:load를 추가하면 Astro는 컴포넌트 외부를 astro-island 태그로 감쌉니다. 컴포넌트 안에 slot이 있다면 astro-slot도 추가됩니다.

카드 컴포넌트를 예로 들어 보겠습니다.

---
import Card from './Card.svelte'
---

<Card client:load>
  <div>카드 내용</div>
</Card>

렌더링 결과가 다음과 같을 것이라고 생각할 수 있습니다.

<div class="card">
  <div>카드 내용</div>
</div>

하지만 실제 결과는 다음과 같습니다.

<astro-island>
  <div class="card">
    <astro-slot>
      <div>카드 내용</div>
    </astro-slot>
  </div>
</astro-island>

문제가 보이시나요? 중간에 astro-slot 계층이 하나 삽입되었습니다. 이제 div.card의 직접 자식 요소가 아니므로 .card > div 선택자가 동작하지 않습니다.

더 까다로운 점은 astro-islandastro-slot 모두 display: contents를 사용한다는 것입니다. 이 CSS 속성이 적용된 요소는 레이아웃에서 ‘사라집니다’. DOM에는 남아 있지만 박스 모델 계산에는 참여하지 않습니다. 따라서 너비, 높이, 여백, 위치를 지정할 수 없고 Grid 레이아웃의 grid-column 역시 이 요소에는 적용되지 않습니다.

정적 컴포넌트에는 이 문제가 없습니다

하이드레이션 지시어를 추가하지 않으면 다음과 같습니다.

<Card>
  <div>카드 내용</div>
</Card>

Astro는 astro-islandastro-slot을 생성하지 않으며, DOM은 예상한 모습 그대로입니다.

<div class="card">
  <div>카드 내용</div>
</div>

바로 여기서 문제가 생깁니다. 같은 컴포넌트인데 어떤 때에는 이런 추가 태그가 있고, 어떤 때에는 없습니다. 그렇다면 양쪽 모두에 적용되는 CSS 선택자는 어떻게 작성해야 할까요? 이것이 이어서 해결할 핵심 문제입니다.

2. Tailwind CSS 올바르게 통합하기: v4와 v3

Tailwind를 이야기하면 많은 사람이 가장 먼저 npx astro add tailwind 실행을 떠올립니다. 물론 가장 간단한 방법이지만 Tailwind v4를 사용한다면 설정 방식이 조금 달라집니다.

v4의 새로운 통합 방식

Tailwind v4에는 @tailwindcss/vite라는 공식 Vite 플러그인이 도입되었습니다. 이 플러그인은 기존 @astrojs/tailwind보다 간결하고 Tailwind 공식 권장 방식에도 더 잘 맞습니다.

구체적인 절차는 다음과 같습니다.

1. 의존성 설치

npm install tailwindcss @tailwindcss/vite

2. astro.config.mjs 설정

import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  vite: {
    plugins: [tailwindcss()],
  },
});

3. 전역 CSS 파일 생성

src/styles/global.css에 다음과 같이 작성합니다.

@import "tailwindcss";

4. Layout에서 불러오기

---
import '../styles/global.css';
---

<html>
  <slot />
</html>

이것으로 끝입니다. v3의 @tailwind base; @tailwind components; @tailwind utilities; 방식보다 훨씬 간단합니다.

v3 사용자는 어떻게 해야 할까요?

아직 v3를 사용 중이라면 두 가지 방법이 있습니다.

방법 A: @astrojs/tailwind 통합 사용

npx astro add tailwind

이 명령은 tailwind.config.cjs를 자동으로 생성하고 astro.config.mjs에 통합 설정을 추가합니다. 다만 한 가지 함정이 있습니다. 모든 페이지에 Tailwind의 base 스타일이 자동으로 주입되므로 어떤 페이지에서 Tailwind를 쓰고 어떤 페이지에서는 쓰지 않을지 제어할 수 없습니다.

방법 B: PostCSS 수동 설정

다음과 같이 postcss.config.cjs를 만듭니다.

module.exports = {
  plugins: {
    tailwindcss: {},
  },
};

그런 다음 src/styles/tailwind.css를 직접 만들고 필요한 Layout에서 불러옵니다. 이 방식은 완전한 제어권을 제공합니다.

content 설정을 잘못 작성하지 마세요

v3와 v4 중 어느 것을 사용하든 가장 중요한 것은 content 설정입니다. 스타일이 적용되지 않는 문제는 .astro 파일이 빠져 있어 생기는 경우가 많습니다.

// tailwind.config.cjs
module.exports = {
  content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
  // ...
};

.astro를 절대 빠뜨리지 마세요. 이 확장자가 없으면 Astro 컴포넌트에 작성한 Tailwind 클래스가 빌드 과정에서 하나도 생성되지 않습니다.

3. 네 가지 스타일 충돌 상황과 해결 방법

이 장은 이 글의 핵심입니다. 직접 겪은 스타일 문제를 모두 정리했습니다. 각 상황마다 문제가 있는 코드, 원인 분석, 해결책을 함께 살펴봅니다.

상황 1: 자식 결합자가 동작하지 않는 경우

문제가 있는 코드:

/* 하이드레이션 지시어가 있는 컴포넌트에서는 이 CSS가 동작하지 않음 */
.Card > div {
  padding: 1rem;
  background: #f0f0f0;
}

동작하지 않는 이유:

DOM 구조가 바뀌었기 때문입니다. 중간에 astro-slot이 삽입됩니다.

<div class="Card">
  <astro-slot> <!-- 이 요소가 삽입됨 -->
    <div>내용</div>
  </astro-slot>
</div>

div가 더 이상 .Card의 직접 자식 요소가 아니므로 .Card > div로는 해당 div를 선택할 수 없습니다.

해결책 A(권장): 후손 선택자 사용

.Card div {
  padding: 1rem;
  background: #f0f0f0;
}

간단하고 확실한 방법입니다. 다만 중첩 단계가 많다면 의도하지 않은 요소까지 선택될 수 있습니다.

해결책 B: 선택자 경로에 astro-slot 추가

전역 CSS에서는 다음과 같이 작성합니다.

.Card > astro-slot > div {
  padding: 1rem;
  background: #f0f0f0;
}

Scoped CSS에서는 다음과 같습니다.

<style>
.Card :global(> astro-slot > div) {
  padding: 1rem;
  background: #f0f0f0;
}
</style>

이 방법은 더 정확하지만 코드가 늘어납니다. 프로젝트의 복잡도에 따라 선택하세요.

상황 2: Lobotomized owl selector가 동작하지 않는 경우

문제가 있는 코드:

/* 널리 쓰이는 간격 배치 기법 */
.List > * + * {
  margin-top: 1rem;
}

이 선택자는 부모 컨테이너 안에서 앞에 형제 요소가 있는 모든 자식 요소에 위쪽 여백을 추가한다는 뜻입니다. 흔히 쓰이는 기법이지만 islands에서는 동작하지 않습니다.

동작하지 않는 이유:

astro-islandastro-slotdisplay: contents를 사용하므로 레이아웃에서는 ‘사라집니다’. 하지만 * + *는 여전히 이 요소들을 선택하며, display: contents 요소에 적용된 스타일은 무시됩니다.

해결책:

.List > * + *,
.List > * + :where(astro-island, astro-slot) > *:first-child {
  margin-top: 1rem;
}

이 선택자는 astro-islandastro-slot을 ‘통과’해 내부의 첫 번째 자식 요소에 직접 여백을 추가합니다. 다소 복잡해 보이지만 문제를 해결할 수 있습니다.

상황 3: CSS Grid 위치 지정이 실패하는 경우

문제가 있는 코드:

---
import Item from './Item.svelte'
---

<div class="Grid">
  <Item client:load />
  <Item client:load />
  <Item client:load />
</div>

<style>
.Grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1em;
}

/* 첫 번째 요소가 한 행 전체를 차지하도록 설정 */
.Grid > *:first-child {
  grid-column: 1 / -1;
}
</style>

하지만 첫 번째 요소는 한 행 전체를 차지하지 않습니다.

동작하지 않는 이유:

astro-islanddisplay: contents를 사용하므로 grid-column은 이 요소에 적용되지 않습니다.

해결책 A: islands 우회

.Grid > *,
.Grid > :where(astro-island, astro-slot) > *:first-child {
  grid-column: 1 / -1;
}

해결책 B: wrapper 요소 사용

<div class="Grid">
  <div><Item client:load /></div>
  <div><Item client:load /></div>
  <div><Item client:load /></div>
</div>

이제 grid-columndiv에 적용되므로 islands의 영향을 받지 않습니다. 개인적으로는 코드가 명확하고 읽기 쉬운 이 방법을 더 선호합니다.

상황 4: nth-child 선택자의 위치가 어긋나는 경우

문제가 있는 코드:

/* 첫 번째 컴포넌트를 선택하려는 코드 */
.Grid > *:nth-child(1) {
  background: red;
}

그런데 첫 번째 컴포넌트는 빨간색으로 바뀌지 않고 페이지의 다른 부분만 흐트러집니다.

동작하지 않는 이유:

Astro가 컴포넌트 옆에 stylescript 태그를 삽입하기 때문입니다. 이 태그들도 자식 요소이므로 nth-child 계산에 포함됩니다.

해결책 A: nth-of-type 사용

.Grid > astro-island:nth-of-type(1) > .Item {
  background: red;
}

해결책 B: wrapper 요소 사용

<div class="Grid">
  <div><Item client:load /></div>
  <div><Item client:load /></div>
</div>

<style>
.Grid > *:nth-child(1) .Item {
  background: red;
}
</style>

솔직히 이런 상황에서는 wrapper 사용을 적극 권장합니다. nth-of-type은 작성 방식이 지나치게 복잡하고 유지보수 비용도 높습니다.

4. 스타일 방식 선택표: Tailwind/Scoped/Global은 언제 사용할까?

Astro는 다양한 스타일 방식을 제공합니다. 선택지가 많아서 오히려 고민될 때도 있습니다. 다음처럼 간단한 선택 전략을 정리해 보았습니다.

Tailwind: 빠른 개발과 통일된 디자인 시스템

적합한 상황:

  • Layout 레이아웃(페이지 전체 구조)
  • 빠른 프로토타입 개발
  • 통일된 디자인 언어가 필요한 경우
  • 사용자 정의 CSS를 작성하고 싶지 않은 경우

적합하지 않은 상황:

  • 고도로 맞춤화된 컴포넌트 스타일
  • 복잡한 선택자가 필요한 경우(앞서 설명한 islands 문제 등)

예시:

---
import Header from './Header.astro'
---

<div class="max-w-7xl mx-auto px-4 py-8">
  <Header />
  <main class="mt-12 grid grid-cols-1 md:grid-cols-2 gap-6">
    <slot />
  </main>
</div>

간단명료해서 레이아웃을 한눈에 파악할 수 있습니다.

Scoped CSS: 컴포넌트 내부 스타일을 격리

적합한 상황:

  • 컴포넌트 내부 스타일
  • :hover, :focus 같은 특정 선택자가 필요한 경우
  • 다른 컴포넌트에 영향을 주지 않도록 스타일을 격리하려는 경우

적합하지 않은 상황:

  • 전역 기본 스타일
  • 여러 컴포넌트에서 공유해야 하는 스타일

예시:

<div class="card">
  <h2>제목</h2>
  <p>내용</p>
</div>

<style>
.card {
  padding: 1.5rem;
  border-radius: 8px;
  background: white;
}

.card:hover {
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
</style>

이 스타일은 해당 컴포넌트에만 적용되며 다른 위치의 .card에는 영향을 주지 않습니다.

Global CSS: 전역 기본 스타일

적합한 상황:

  • CSS reset / normalize
  • 테마 변수(CSS custom properties)
  • Tailwind base 스타일
  • 전역 글꼴과 색상 정의

적합하지 않은 상황:

  • 컴포넌트 내부 스타일(다른 요소를 오염시키기 쉬움)

예시:

/* src/styles/global.css */
@import "tailwindcss";

:root {
  --color-primary: #2563eb;
  --font-sans: 'Inter', sans-serif;
}

body {
  font-family: var(--font-sans);
  color: #1a1a1a;
}

Layout에서 한 번만 불러오면 됩니다.

CSS Modules: 복잡한 컴포넌트를 위한 해결책

Astro는 CSS Modules도 지원합니다. 파일 이름에 .module.css 접미사를 붙이면 됩니다.

---
import styles from './Card.module.css'
---

<div class={styles.card}>
  <h2 class={styles.title}>제목</h2>
</div>

적합한 상황:

  • 클래스가 많은 복잡한 컴포넌트
  • 충돌을 피하기 위해 클래스 이름 매핑이 필요한 경우
  • Tailwind와 함께 사용하는 경우

권장 조합:

  1. Layout: Global CSS + Tailwind(레이아웃과 전역 스타일)
  2. 컴포넌트 내부: Scoped CSS 우선 사용(격리성이 좋음)
  3. 특수한 경우: CSS Modules(복잡한 컴포넌트) 또는 Tailwind(빠른 개발)
  4. 피해야 할 것: 너무 많은 방식을 동시에 섞지 말고 2~3가지만 선택하기

5. 모범 사례와 문제 예방 체크리스트

마지막으로 직접 겪은 문제를 바탕으로 체크리스트를 정리했습니다.

1. 선택자 우선순위 전략

피해야 할 것:

  • 자식 결합자(>)에 지나치게 의존하기
  • islands가 있는 위치에서 nth-child 사용하기

우선 사용할 것:

  • 후손 선택자(공백)
  • nth-child 대신 nth-of-type
  • wrapper 요소로 islands의 영향 격리하기

2. 스타일 디버깅 절차

스타일 문제가 생기면 다음 순서로 확인하세요.

  1. 개발자 도구를 열고 DOM 구조 확인astro-islandastro-slot이 있는지 확인합니다.
  2. 선택자 경로 확인 — 작성한 선택자가 정말 대상 요소를 가리키는지 봅니다.
  3. computed 스타일 확인display: contents 때문에 스타일이 무효화되는지 확인합니다.
  4. CSS import 순서 확인 — specificity가 같다면 나중에 불러온 스타일이 먼저 불러온 스타일을 덮어씁니다.

3. Tailwind content 설정

잘못된 예:

content: ['./src/**/*.{html,js,jsx}']  // .astro 누락

올바른 예:

content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}']

.astro가 빠지면 Astro 컴포넌트에 작성한 Tailwind 클래스가 전혀 적용되지 않습니다.

4. 성능 최적화 제안

과도한 하이드레이션을 피하세요.

<!-- 권장하지 않음: 모든 컴포넌트에 client:load 사용 -->
<Header client:load />
<Content client:load />
<Footer client:load />

<!-- 권장: 필요한 컴포넌트에만 하이드레이션 지시어 사용 -->
<Header client:load />
<Content />  <!-- 정적 콘텐츠이므로 JS가 필요하지 않음 -->
<Footer />   <!-- 정적 콘텐츠이므로 JS가 필요하지 않음 -->

client:load 대신 client:visible 사용:

컴포넌트가 첫 화면에 없거나 사용자가 반드시 보게 되는 요소가 아니라면 client:visible을 사용하세요. 컴포넌트가 뷰포트에 들어올 때만 JS를 로드하므로 데이터 사용량을 줄이고 로딩 속도를 높일 수 있습니다.

<ImageCarousel client:visible />

5. CSS import 순서

Astro에서는 CSS를 불러오는 순서가 우선순위에 영향을 줍니다. specificity가 같다면 나중에 불러온 스타일이 이깁니다.

권장 방식:

---
// Layout.astro
import '../styles/global.css';  // 전역 스타일을 먼저 불러오기
import '../styles/tailwind.css'; // Tailwind를 나중에 불러오기
---

<html>
  <slot />
</html>

이렇게 하면 Tailwind 유틸리티 클래스가 전역 스타일을 덮어쓸 수 있습니다.

6. wrapper 요소를 적극 활용하세요

솔직히 말해 islands 때문에 생기는 많은 스타일 문제는 wrapper 요소 하나만 추가해도 해결됩니다.

<div class="grid gap-4">
  <div><Item client:load /></div>
  <div><Item client:load /></div>
</div>

중첩 계층이 하나 늘어나지만 코드가 명확하고 선택자가 단순해져 유지보수 비용이 줄어듭니다. ‘코드 결벽증’ 때문에 스스로 복잡한 함정에 빠지지 마세요.

정리

긴 내용을 한 문장으로 요약하면 이렇습니다. Astro islands 아키텍처가 DOM을 어떻게 바꾸는지 이해한 뒤 그에 맞게 CSS 작성 방식을 조정하세요.

구체적으로는 다음 네 가지를 기억하면 됩니다.

  1. 하이드레이션 지시어는 astro-islandastro-slot을 생성합니다. — 이 요소들은 display: contents를 사용하므로 선택자의 동작에 영향을 줍니다.
  2. Tailwind v4에서는 @tailwindcss/vite 플러그인을 사용합니다. — v3 통합 방식보다 간단합니다.
  3. 자식 결합자와 nth-child를 피하세요. — 후손 선택자, nth-of-type 또는 wrapper 요소를 사용합니다.
  4. 스타일 방식을 적절히 조합하세요. — Layout에는 Global CSS와 Tailwind를, 컴포넌트에는 Scoped CSS를 사용하고 필요할 때 Modules를 활용합니다.

스타일 문제를 겪고 있다면 먼저 개발자 도구를 열어 DOM 구조부터 살펴보세요. CSS가 잘못된 것이 아니라 DOM이 바뀌었다는 사실을 알아차리지 못해서 생기는 문제도 많습니다.

기존 프로젝트의 Tailwind 설정을 확인하고 v4의 Vite 플러그인으로 업그레이드한 다음, 이 글의 방법으로 islands 관련 스타일 충돌을 점검해 보세요. 문제를 해결하고 나면 코드가 훨씬 깔끔해진 것을 확인할 수 있을 것입니다.

FAQ

client:load를 추가한 뒤 스타일이 흐트러지는 이유는 무엇인가요?
client:load 같은 하이드레이션 지시어는 display: contents 속성을 사용하는 astro-island와 astro-slot 태그를 생성합니다. 이로 인해 DOM 구조가 바뀌어 자식 결합자(>), nth-child 등이 제대로 동작하지 않을 수 있습니다. 후손 선택자나 wrapper 요소로 이 문제를 피하는 것이 좋습니다.
Astro에서 Tailwind v4는 어떻게 설정하나요?
Tailwind v4에서는 @tailwindcss/vite 플러그인을 사용하는 방식을 권장합니다. 절차는 다음과 같습니다.

1. 설치: npm install tailwindcss @tailwindcss/vite
2. astro.config.mjs의 vite.plugins에 추가
3. 전역 CSS를 만들고 @import "tailwindcss" 작성
4. Layout에서 불러오기

v3보다 훨씬 간단하며 @tailwind base/components/utilities가 필요하지 않습니다.
astro-island와 astro-slot은 무엇인가요?
Astro 아일랜드 아키텍처에서 내부적으로 사용하는 태그입니다. 컴포넌트에 하이드레이션 지시어를 추가하면 Astro가 하이드레이션을 관리하기 위해 이 태그들을 자동으로 생성합니다. display: contents를 사용하므로 레이아웃에서는 사라진 것처럼 보이지만 CSS 선택자의 경로 매칭에는 영향을 줍니다.
어떤 CSS 선택자에서 문제가 가장 자주 발생하나요?
가장 쉽게 무효화되는 유형은 네 가지입니다.

1. 자식 결합자(>) — 중간에 astro-slot이 삽입됨
2. Lobotomized owl(* + *) — display: contents 요소에 적용된 스타일이 무시됨
3. Grid 레이아웃 위치 지정(grid-column) — display: contents 요소에는 적용되지 않음
4. nth-child — style과 script 태그도 자식 요소로 계산됨

후손 선택자, nth-of-type 또는 wrapper 요소를 사용하면 해결할 수 있습니다.
Tailwind 클래스가 적용되지 않는 이유는 무엇인가요?
가장 흔한 원인은 content 설정에서 .astro 파일을 빠뜨린 것입니다. 올바른 설정은 content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}']입니다. .astro 확장자가 없으면 Astro 컴포넌트의 Tailwind 클래스가 스캔되거나 생성되지 않습니다.
Scoped CSS와 Global CSS는 각각 언제 사용해야 하나요?
간단한 원칙은 다음과 같습니다.

- Layout 계층: Global CSS + Tailwind(레이아웃과 전역 스타일)
- 컴포넌트 내부: Scoped CSS(격리성이 좋고 다른 컴포넌트에 영향을 주지 않음)
- 복잡한 컴포넌트: CSS Modules(클래스가 많고 매핑이 필요한 경우)
- 빠른 개발: Tailwind(통일된 디자인 언어)

너무 많은 방식을 한꺼번에 섞지 말고 2~3가지만 선택하면 충분합니다.

4분 읽기 · 게시일: 2026년 3월 31일 · 수정일: 2026년 9월 4일

댓글

GitHub로 로그인하여 댓글을 남기세요

Easton BlogEaston Blog