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 Variables | Shadow boundary를 뚫고 들어감 — --token이 내부에 전달됨 |
:host | Shadow 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 — 공유는 데이터(토큰)로, 작성은 자유로.
- Pico — HTML이 의미를 가지면 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줄짜리 해법.