Skip to content

레이아웃과 스타일 격리

App Shell의 공통 레이아웃 구조를 설계하고, 마이크로프론트엔드 환경에서 CSS 충돌을 방지하는 다양한 스타일 격리 전략을 학습한다.

학습 목표

  1. App Shell의 공통 레이아웃 구조(Header, Sidebar, Content)를 설계하고 구현한다.
  2. 마이크로프론트엔드에서 CSS 충돌이 발생하는 원인과 시나리오를 이해한다.
  3. CSS Modules, CSS-in-JS, Tailwind prefix, BEM 등 격리 전략의 장단점을 비교한다.
  4. 글로벌 스타일과 로컬 스타일을 분리하여 관리하는 실무 패턴을 익힌다.
  5. 각 마이크로앱이 서로 다른 CSS 방식을 사용할 수 있는 근거와 방법을 파악한다.

본문

1. 공통 레이아웃 구조

커리어 플랫폼의 공통 레이아웃은 세 영역으로 구성된다. Header는 로고와 사용자 인증 UI를 포함하고, Sidebar(Navigation)는 마이크로앱 메뉴를 제공하며, Content 영역에 현재 라우트에 해당하는 마이크로앱이 렌더된다.

CSS 계층은 3단계로 구분된다.

계층위치역할
전역 디자인 토큰packages/ui/global.cssCSS 변수, 리셋, 폰트 등 앱 전체 공통
Shell 레이아웃apps/shell/index.cssHeader, 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 방식을 사용하는 이유

마이크로프론트엔드의 핵심 가치는 팀 자율성이다. 각 팀이 기술 스택을 독립적으로 선택할 수 있어야 빠른 의사결정과 생산성 향상이 가능하다.

이것이 가능한 이유는 다음과 같다.

  1. 독립 빌드: 각 마이크로앱은 자체 빌드 파이프라인을 가진다. Posting 앱이 CSS Modules를 쓰든, Network 앱이 Tailwind를 쓰든 서로의 빌드에 영향을 주지 않는다.
  2. 스타일 스코핑: 위 격리 전략 중 하나를 사용하면 클래스명이 자동으로 유니크해진다.
  3. 공통 계약: 팀 간 합의한 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 CSSglobal- 접두어로 네임스페이스 확보
글로벌 스타일 제한CSS 변수와 리셋만 정의. 구체적 셀렉터 금지
팀 자율성격리 계약만 지키면 CSS 도구 선택은 각 팀 자유

다음 단계

다음 문서 04-Auth0-인증-통합.md에서는 App Shell 레벨에서 Auth0를 통한 인증 시스템을 통합하고, 로그인/로그아웃 플로우, 인증 컨텍스트 전파, 토큰 관리 전략을 구현한다.