🎨 Frontend CSS8. 아키텍처 (Tokens·Tailwind·CSS-in-JS)06 — Modern Patterns (Open Props·Pico·SFC·Web Components)

06 — Modern Patterns

한 줄 답: 메서드론·Tailwind·CSS Modules·CSS-in-JS 외에도 “CSS만으로”, “클래스 없이”, “Shadow DOM으로” 같은 대안 경로가 살아 있다. Open Props(토큰만), Pico CSS(시맨틱 HTML만), SFC(Vue/Svelte 컴포넌트 내장 CSS), Web Components(브라우저 네이티브 격리) — 도구 선택의 극단들을 알면 우리 팀이 어디 서 있는지가 보인다.


Why — 왜 대안 경로를 알아야 하나

주류 4종(BEM·Tailwind·CSS Modules·CSS-in-JS)은 클래스를 어떻게 다룰지의 변형들이다. 다른 사고도 가능:

  • “클래스는 그대로 두고 토큰만 표준화하자” → Open Props.
  • “HTML이 시맨틱하면 클래스 자체가 필요 없다” → Pico, classless.
  • “컴포넌트 = 한 파일에 HTML+CSS+JS” → Vue/Svelte SFC.
  • “브라우저가 격리해주면 빌드 도구 필요 없다” → Web Components + Shadow DOM.

각각은 작은 영역에서 강하다. 다 알아두면 맞는 자리에 맞는 도구를 고를 수 있다.


How — 4가지 패턴 지도


What — 1) Open Props (2022, Adam Argyle / Chrome)

아이디어: “디자인 토큰을 모두가 공유할 단일 파일로”. CSS 변수 ~250개.

@import "https://unpkg.com/open-props";
 
.card {
  padding:       var(--size-4);
  border-radius: var(--radius-3);
  box-shadow:    var(--shadow-3);
  background:    var(--gray-1);
  color:         var(--gray-12);
}

제공 토큰:

  • : --gray-0 ~ --gray-15, --blue-0 ~ --blue-12, 그라데이션 30종.
  • 크기: --size-1 ~ --size-15(4px → 16rem).
  • 타이포: --font-size-0 ~ --font-size-8, --font-weight-1 ~ --font-weight-9.
  • 그림자: --shadow-1 ~ --shadow-6.
  • 모션: --ease-in-1 ~ --ease-spring-5, --animation-fade-in 등.
  • 반응형: @custom-media --md (width >= 768px).

특징:

  • 프레임워크 무관. React·Vue·Svelte·Astro 다 가능.
  • 빌드 도구 없이 CDN import.
  • Tailwind와 함께 사용도 가능 — 토큰만 가져오고 클래스는 Tailwind로.

언제 쓰나: 디자인 토큰을 0에서 만들고 싶지 않을 때, 시간이 부족할 때, 프로토타이핑.


What — 2) Pico CSS (2020) / 클래스리스 CSS

아이디어: “HTML이 시맨틱하면 CSS는 자동으로 적용된다”.

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
 
<main>
  <article>
    <header>
      <h1>My Blog Post</h1>
      <p><small>2026-05-17</small></p>
    </header>
    <p>Body text...</p>
    <footer>
      <button>Like</button>
    </footer>
  </article>
</main>

클래스 1개도 없이 잘 정돈된 디자인.

같은 계열:

  • Pico CSS — 가장 인기 (~10 KB).
  • Water.css — 6 KB, 더 미니멀.
  • MVP.css — 4 KB, 학생 프로젝트용.
  • Sakura.css — 1.3 KB, 작은 블로그.

언제 쓰나:

  • 마크업 위주의 블로그, 문서, 학습 자료.
  • 프로토타이핑 — UI 디자인 결정 나중에.
  • 사이드 프로젝트 — 디자인 시스템 만들 시간 없을 때.

한계:

  • 복잡한 앱에선 부족. 결국 덮어쓰기가 필요해짐.
  • 시맨틱 HTML 작성을 강제함 — 익숙하지 않으면 학습 필요.

What — 3) Single File Component (SFC) — Vue·Svelte

컴포넌트 = HTML + CSS + JS 한 파일.

Vue SFC

<template>
  <button class="btn" :class="{ primary }">
    <slot />
  </button>
</template>
 
<script setup>
defineProps(['primary']);
</script>
 
<style scoped>
.btn { padding: 8px 16px; }
.primary { background: blue; color: white; }
</style>

scoped 동작:

  • 빌드 시 컴포넌트별 고유 속성(data-v-a3f1b2) 부여.
  • 셀렉터에 해당 속성을 자동 추가:
    .btn[data-v-a3f1b2] { padding: 8px 16px; }
  • 자식 컴포넌트는 영향 안 받음.
  • :deep(.child) 로 자식까지 침투 가능.
  • <style module> 로 CSS Modules 모드 전환 가능.

Svelte SFC

<script>
  export let primary = false;
</script>
 
<button class="btn" class:primary>
  <slot />
</button>
 
<style>
  .btn { padding: 8px 16px; }
  .primary { background: blue; color: white; }
</style>

Svelte는 기본 scoped<style>은 자동으로 컴포넌트 격리. 사용되지 않은 셀렉터는 컴파일 경고.

React엔 SFC가 없다 → CSS Modules / CSS-in-JS / Tailwind로 보완

<style jsx>(Next.js의 styled-jsx)가 비슷한 시도였지만 주류가 되지 못함.

Vue/Svelte SFC vs CSS Modules:

  • SFC는 프레임워크 빌더가 처리 → 별도 설정 0.
  • 동적 클래스 토글이 직관적 (class:primary).
  • Slot에서 받은 자식은 침투 안 됨 → :deep() 필요.

What — 4) Web Components + Shadow DOM

Web Components(2014~)는 브라우저 네이티브 컴포넌트. Shadow DOM은 완전한 스타일 격리.

class MyButton extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <style>
        button { padding: 8px 16px; background: blue; color: white; }
      </style>
      <button><slot></slot></button>
    `;
  }
}
customElements.define('my-button', MyButton);
<my-button>Save</my-button>

Shadow DOM의 격리:

  • 내부 <style>바깥에 영향 없음.
  • 바깥 CSS가 안쪽에 영향 없음 (color 같은 상속 속성은 예외).
  • 클래스 충돌 완전 0.

외부에서 스타일링하는 통로

메커니즘동작
CSS VariablesShadow boundary를 뚫고 들어감--token이 내부에 전달됨
:hostShadow root 자체에 스타일 — :host { display: block }
:host-context(.dark)부모가 .dark면 적용 — 다크모드
::part(label)내부의 part="label" 요소에 외부 접근
/* 외부 */
my-button { --bg: red; }
my-button::part(label) { font-weight: bold; }
// 내부
`<style>
  button { background: var(--bg, blue); }
</style>
<button part="label"><slot></slot></button>`

언제 쓰나

  • 디자인 시스템 라이브러리 — 소비자의 CSS 환경(Tailwind/Bootstrap/legacy) 무관하게 동작해야 함.
  • 위젯·임베드 — 타사 사이트에 삽입되는 컴포넌트.
  • 마이크로프론트엔드 — 여러 팀의 코드가 한 페이지에서 공존해야 할 때.

한계

  • Tailwind 불가 — Shadow root에 전역 클래스가 닿지 않음. 내부에 인라인 스타일만.
  • 글로벌 폰트@font-face외부에 있으면 안쪽에도 전달됨 (Shadow root는 폰트 상속).
  • @scope가 더 가볍다 — 격리만 필요하다면 Shadow DOM은 과함.

What-if — 패턴 선택 매트릭스

상황권장
디자인 시스템을 0에서 시작Open Props(토큰) + Tailwind(유틸) + CSS Modules(컴포넌트)
블로그·문서·랜딩Pico/Water.css
Vue 앱SFC scoped + Open Props
Svelte 앱SFC <style> + Open Props
임베드 위젯Web Components + Shadow DOM
마이크로프론트엔드Web Components 또는 iframe
컴포넌트 라이브러리 (npm 배포)Web Components 또는 CSS-in-JS (런타임 격리)

What-if — 이 패턴들을 잘못 쓰면

1) Pico CSS로 복잡한 SaaS 만들기

<main>
  <article>
    <!-- 30개 인터랙티브 컴포넌트 -->
  </article>
</main>

→ Pico가 제공하는 기본 스타일을 덮어쓰기가 페이지 절반. Pico를 끄고 Tailwind로 다시 짜는 게 빠른 시점이 옴.

2) Web Components + Shadow DOM + Tailwind

shadow.innerHTML = `<button class="px-4 py-2 bg-blue-500">...</button>`;

→ Tailwind 클래스가 Shadow root 안에서 작동 안 함. Tailwind의 CSS는 Light DOM에 있고, Shadow root는 닫혀 있다. 인라인 스타일 또는 @import 필요.

3) Vue SFC scoped를 믿고 글로벌 CSS를 안 씀

<style scoped>
  /* 컴포넌트 격리 100% */
</style>

디자인 토큰은 어디 두나? 글로벌 :root { --color: ... }별도 파일 필요. scoped는 격리지만 공유 시스템은 외부에.

4) Web Components로 작은 컴포넌트

<my-button> 같은 평범한 버튼을 Web Components로 만들면 오버엔지니어링. React 컴포넌트 + Tailwind면 10배 빠르고 번들 가벼움.


Insight — 클래스 없는 길도 있다

메서드론·Tailwind·CSS-in-JS는 모두 “클래스를 어떻게 다룰지” 의 변형이다. 이 챕터의 패턴들은 클래스 자체를 다르게 본다.

  • Open Props: 클래스는 팀이 자유롭게, 우리는 토큰만 줄게.
  • Pico: 클래스 없이도 HTML이 충분히 의미를 가진다.
  • SFC: 클래스는 있지만, 컴포넌트 안에 갇혀 있다.
  • Web Components: 클래스는 있지만, 바깥 세계와 단절되어 있다.

이 4가지는 CSS의 다른 철학 들이다:

  • Open Props공유는 데이터(토큰)로, 작성은 자유로.
  • PicoHTML이 의미를 가지면 CSS는 자동이다.
  • SFC컴포넌트가 컴포넌트의 스타일을 안다.
  • Web Components브라우저가 격리를 강제한다.

흥미로운 점은 2020년대 들어 이 패턴들이 다시 주목받는다는 것이다:

  • Tailwind 클래스 폭주에 지친 팀이 Pico로 회귀.
  • Tailwind config의 무거움 대신 Open Props가벼운 토큰.
  • React Server Components에 styled-components가 막히면서 Web Components로 일부 우회.
  • Vue/Svelte의 SFC는 변함없이 가장 직관적인 컴포넌트 모델로 인정받음.

대안 경로는 사라지지 않는다. 주류가 한계를 보일 때 언제든 돌아갈 수 있는 길이다.

좋은 아키텍트는 4개 주류와 4개 대안을 모두 알고, 자신의 팀에 맞는 조합을 만든다.


요약 + Mermaid

  • Open Props — 250개 CSS 변수 토큰만, 프레임워크 무관.
  • Pico/Water/Sakura — 시맨틱 HTML 자동 스타일, 클래스 0개.
  • Vue SFC<style scoped>로 컴포넌트 격리, :deep()으로 침투.
  • Svelte — 기본 scoped, 미사용 셀렉터 컴파일 경고.
  • Web Components + Shadow DOM — 완전 격리, 외부와는 CSS 변수·::part·:host-context로 통신.

다음: 07-dark-mode-theming — 모던 다크모드의 3줄짜리 해법.