테마
레이아웃과 스타일 격리
App Shell의 공통 레이아웃 구조를 설계하고, 마이크로프론트엔드 환경에서 CSS 충돌을 방지하는 다양한 스타일 격리 전략을 학습한다.
학습 목표
- App Shell의 공통 레이아웃 구조(Header, Sidebar, Content)를 설계하고 구현한다.
- 마이크로프론트엔드에서 CSS 충돌이 발생하는 원인과 시나리오를 이해한다.
- CSS Modules, CSS-in-JS, Tailwind prefix, BEM 등 격리 전략의 장단점을 비교한다.
- 글로벌 스타일과 로컬 스타일을 분리하여 관리하는 실무 패턴을 익힌다.
- 각 마이크로앱이 서로 다른 CSS 방식을 사용할 수 있는 근거와 방법을 파악한다.
본문
1. 공통 레이아웃 구조
커리어 플랫폼의 공통 레이아웃은 세 영역으로 구성된다. Header는 로고와 사용자 인증 UI를 포함하고, Sidebar(Navigation)는 마이크로앱 메뉴를 제공하며, Content 영역에 현재 라우트에 해당하는 마이크로앱이 렌더된다.
CSS 계층은 3단계로 구분된다.
| 계층 | 위치 | 역할 |
|---|---|---|
| 전역 디자인 토큰 | packages/ui/global.css | CSS 변수, 리셋, 폰트 등 앱 전체 공통 |
| Shell 레이아웃 | apps/shell/index.css | Header, Sidebar, Content 배치. global- 접두어 |
| 마이크로앱 | 각 앱 내부 | 해당 앱에만 적용되는 로컬 스타일 |
Shell 레이아웃 CSS
Shell 레이아웃 CSS의 핵심은 모든 클래스에 global- 접두어를 붙이는 것이다. .global-container, .global-header, .global-nav-link 등으로 명명하여 마이크로앱의 클래스와 이름 충돌을 방지한다. CSS 변수(var(--spacing-unit), var(--color-primary))를 활용하여 UIKit의 디자인 토큰을 참조한다.
2. CSS 충돌이 발생하는 원인
마이크로프론트엔드에서 CSS 충돌은 여러 앱의 스타일시트가 하나의 DOM에 공존할 때 발생한다.
충돌 시나리오 예시:
Shell: .container { max-width: 1200px; }
Posting: .container { max-width: 800px; padding: 20px; }
Network: .container { display: grid; }세 앱이 모두 .container라는 클래스를 사용하면 CSS 캐스케이드 규칙에 따라 나중에 로드된 스타일이 이전 스타일을 덮어쓴다. 로드 순서가 보장되지 않는 마이크로프론트엔드 환경에서는 결과가 비결정적이다.
3. CSS 격리 전략 비교
3-1. BEM(Block Element Modifier) 컨벤션
css
/* Block__Element--Modifier */
.posting-card__title--highlighted { color: blue; }
.network-profile__avatar--large { width: 80px; }- 장점: 도구 의존성 없음. 팀 규칙만으로 운영 가능
- 단점: 컨벤션 위반 시 충돌 발생. 사람이 실수할 여지가 있음
- 적합한 경우: 소규모 팀, 레거시 프로젝트
3-2. CSS Modules
tsx
import styles from "./Card.module.css";
// styles.card => "Card_card_a1b2c" (해시가 자동 추가됨)
<div className={styles.card}>...</div>css
/* Card.module.css */
.card { border: 1px solid #e2e8f0; border-radius: 8px; }
.title { font-size: 1.25rem; font-weight: 600; }- 장점: 빌드타임에 자동 해시 생성. 설정 간단. Vite/Webpack 기본 지원
- 단점: 동적 스타일링 제한. 글로벌 스타일 주입 시
:global()필요 - 적합한 경우: 대부분의 마이크로앱. UI 라이브러리 컴포넌트
3-3. CSS-in-JS (styled-components, Emotion)
tsx
import styled from "styled-components";
const Card = styled.div`
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: ${(props) => props.compact ? "8px" : "16px"};
`;- 장점: 완전한 동적 스타일링. props 기반 조건부 스타일. 자동 격리
- 단점: 런타임 오버헤드. SSR 설정 복잡. 번들 크기 증가
- 적합한 경우: 동적 스타일 요구가 많은 앱, 디자인 시스템 구축
3-4. Tailwind CSS (prefix 설정)
js
// tailwind.config.js (network 앱)
module.exports = {
prefix: "net-",
content: ["./src/**/*.{tsx,ts}"],
// ...
};tsx
<div className="net-flex net-gap-4 net-p-6 net-bg-white net-rounded-lg">
<span className="net-text-lg net-font-bold">프로필</span>
</div>- 장점: 유틸리티 우선. prefix로 앱 간 충돌 방지. 빌드 시 미사용 CSS 제거
- 단점: HTML이 장황해짐. 커스텀 디자인 시 설정 복잡
- 적합한 경우: 빠른 프로토타이핑, 유틸리티 기반 스타일 선호 팀
3-5. Shadow DOM
ts
class MicroApp extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: "open" });
shadow.innerHTML = `
<style>.card { border: 1px solid #e2e8f0; }</style>
<div class="card">완전히 격리된 스타일</div>
`;
}
}- 장점: 브라우저 수준의 완전한 스타일 격리
- 단점: React와의 통합 불편. 이벤트 버블링 제한. 글로벌 테마 전파 어려움
- 적합한 경우: Web Components 기반 아키텍처, 완전한 격리가 필요한 위젯
4. 전략 비교 매트릭스
| 전략 | 격리 수준 | 런타임 비용 | 설정 난이도 | React 호환 | 마이크로앱 독립성 |
|---|---|---|---|---|---|
| BEM | 낮음 (컨벤션 의존) | 없음 | 없음 | 완벽 | 낮음 |
| 접두어 규칙 | 중간 | 없음 | 없음 | 완벽 | 중간 |
| CSS Modules | 높음 | 없음 | 낮음 | 완벽 | 높음 |
| CSS-in-JS | 높음 | 있음 | 중간 | 완벽 | 높음 |
| Tailwind prefix | 높음 | 없음 | 중간 | 완벽 | 높음 |
| Shadow DOM | 최고 | 없음 | 높음 | 제한적 | 최고 |
5. 마이크로앱마다 다른 CSS 방식을 사용하는 이유
마이크로프론트엔드의 핵심 가치는 팀 자율성이다. 각 팀이 기술 스택을 독립적으로 선택할 수 있어야 빠른 의사결정과 생산성 향상이 가능하다.
이것이 가능한 이유는 다음과 같다.
- 독립 빌드: 각 마이크로앱은 자체 빌드 파이프라인을 가진다. Posting 앱이 CSS Modules를 쓰든, Network 앱이 Tailwind를 쓰든 서로의 빌드에 영향을 주지 않는다.
- 스타일 스코핑: 위 격리 전략 중 하나를 사용하면 클래스명이 자동으로 유니크해진다.
- 공통 계약: 팀 간 합의한 CSS 격리 규칙만 지키면 내부 구현은 자유다.
단, UI 라이브러리(@career-up/uikit)는 모든 앱에서 사용하므로 한 가지 방식(CSS Modules)으로 통일하는 것이 바람직하다.
6. 글로벌 스타일 vs 로컬 스타일 관리
| 구분 | 글로벌 스타일 | 로컬 스타일 |
|---|---|---|
| 정의 위치 | packages/ui/global.css | 각 마이크로앱 내부 |
| 적용 범위 | 전체 DOM (:root 변수) | 해당 마이크로앱만 |
| 관리 주체 | 플랫폼 팀 (UI 라이브러리 담당) | 각 서비스 팀 |
| 변경 영향 | 전체 서비스에 파급. 신중한 리뷰 필요 | 해당 앱에만 영향. 빠른 반영 가능 |
| 예시 | 색상 변수, 폰트, 리셋, 기본 레이아웃 | 카드 레이아웃, 폼 스타일, 페이지 고유 UI |
글로벌 스타일은 CSS 커스텀 속성(변수)으로 정의하여 로컬 스타일에서 var(--color-primary) 형태로 참조한다. 직접적인 셀렉터(.btn, h1 등)를 글로벌에 두면 마이크로앱 스타일을 침범하므로 변수와 리셋 스타일로 한정해야 한다.
css
/* 올바른 글로벌 스타일 */
:root {
--color-primary: #3b82f6;
}
*, *::before, *::after { box-sizing: border-box; }
/* 잘못된 글로벌 스타일 - 마이크로앱 침범 */
.card { border-radius: 8px; } /* 어떤 앱의 .card에 영향 */
h1 { font-size: 2rem; } /* 모든 앱의 h1에 영향 */핵심 정리
| 항목 | 내용 |
|---|---|
| 레이아웃 구조 | Header(고정) + Sidebar(Navigation) + Content(Outlet) 3단 구조 |
| CSS 계층 | 전역 토큰(UIKit) -> Shell 레이아웃 -> 마이크로앱 로컬 스타일 |
| 격리 권장 | CSS Modules (기본). 팀 선호에 따라 Tailwind/CSS-in-JS 허용 |
| Shell CSS | global- 접두어로 네임스페이스 확보 |
| 글로벌 스타일 제한 | CSS 변수와 리셋만 정의. 구체적 셀렉터 금지 |
| 팀 자율성 | 격리 계약만 지키면 CSS 도구 선택은 각 팀 자유 |
다음 단계
다음 문서 04-Auth0-인증-통합.md에서는 App Shell 레벨에서 Auth0를 통한 인증 시스템을 통합하고, 로그인/로그아웃 플로우, 인증 컨텍스트 전파, 토큰 관리 전략을 구현한다.