Skip to content

CSS 격리 기법 - toString Loader를 활용한 스타일 주입

toString Loader로 CSS를 문자열로 변환하여 Shadow DOM 내부에 주입하는 기법을 학습한다

학습 목표

  1. webpack의 CSS 처리 흐름(style-loader vs mini-css-extract-plugin)을 이해한다
  2. toString Loader의 동작 원리와 설정 방법을 익힌다
  3. 개발/프로덕션 환경에서 일관된 CSS 처리 전략을 구성할 수 있다
  4. Shadow DOM 내부에 CSS를 안전하게 주입하는 방법을 구현할 수 있다

본문

1. 마이크로 프론트엔드에서 CSS 격리가 필요한 이유

마이크로 프론트엔드 아키텍처에서는 여러 프로젝트의 컴포넌트가 하나의 페이지에 렌더링된다. 이때 각 프로젝트의 CSS가 전역 스코프를 공유하면 스타일 충돌이 발생한다. Shadow DOM은 이 문제를 해결하는 핵심 기술이지만, CSS를 Shadow DOM 내부로 주입하려면 특별한 처리가 필요하다.

2. webpack의 CSS 처리 방식: Dev vs Prod

webpack은 개발 모드와 프로덕션 모드에서 CSS를 다르게 처리한다. 이 차이가 마이크로 프론트엔드에서 문제를 일으킨다.

모드로더결과물Shadow DOM 호환
개발style-loader<style> 태그 → <head>불가 (전역에 삽입)
프로덕션MiniCssExtractPlugin.css 파일 → <link>불가 (전역에 삽입)
통합toString-loaderCSS 문자열가능 (수동 주입)

3. toString Loader 설치 및 webpack 설정

설치

bash
npm install --save-dev to-string-loader

config-overrides.js 설정 (react-app-rewired 기준)

javascript
const { override, addWebpackPlugin } = require("customize-cra");

module.exports = override((config) => {
  // 1) MiniCssExtractPlugin 제거
  config.plugins = config.plugins.filter(
    (plugin) => plugin.constructor.name !== "MiniCssExtractPlugin"
  );

  // 2) CSS 로더 규칙 변경
  const cssRule = config.module.rules.find((rule) =>
    rule.oneOf?.some((r) => r.test?.toString().includes("css"))
  );

  if (cssRule) {
    cssRule.oneOf.forEach((rule) => {
      if (rule.test?.toString().includes("css")) {
        // style-loader 또는 MiniCssExtractPlugin.loader를
        // toString-loader로 교체
        rule.use = rule.use?.map((loader) => {
          const loaderName =
            typeof loader === "string" ? loader : loader.loader;
          if (
            loaderName?.includes("style-loader") ||
            loaderName?.includes("mini-css-extract-plugin")
          ) {
            return "to-string-loader";
          }
          return loader;
        });
      }
    });
  }

  return config;
});

4. CSS 타입 선언과 모듈 임포트

toString Loader를 사용하면 CSS 파일을 import했을 때 문자열이 반환된다. TypeScript 환경에서는 타입 선언이 필요하다.

typescript
// src/types.d.ts
declare module "*.css" {
  const content: string;
  export default content;
}
typescript
// CSS를 문자열로 임포트
import styles from "./index.css";

console.log(typeof styles); // "string"
console.log(styles);
// 출력: ".container { display: flex; } .title { font-size: 24px; }"

5. Shadow DOM 내부에 CSS 주입하기

구현 코드

typescript
import styles from "./index.css";

// Shadow DOM 컨테이너 생성
const host = document.createElement("div");
const shadowRoot = host.attachShadow({ mode: "open" });

// CSS를 <style> 태그로 Shadow Root 내부에 주입
const styleEl = document.createElement("style");
styleEl.textContent = styles;
shadowRoot.appendChild(styleEl);

// 컴포넌트 렌더링 컨테이너
const container = document.createElement("div");
shadowRoot.appendChild(container);

// React 컴포넌트 렌더링
ReactDOM.render(<App />, container);

6. ANTD (Ant Design)와 함께 사용하기

ANTD 같은 UI 라이브러리를 Shadow DOM 내부에서 사용할 때는 추가 설정이 필요하다. ANTD는 CSS-in-JS를 사용하여 런타임에 스타일을 생성하는데, 기본적으로 <head>에 삽입한다.

typescript
import { StyleProvider } from "@ant-design/cssinjs";

function GlobalProvider({ children, shadowRoot }) {
  return (
    <StyleProvider container={shadowRoot}>
      <Provider store={store}>{children}</Provider>
    </StyleProvider>
  );
}

ANTD의 StyleProvidercontainer로 Shadow Root를 전달하면, CSS-in-JS로 생성되는 스타일이 Shadow DOM 내부에 삽입된다. Modal처럼 포탈을 사용하는 컴포넌트도 getContainer로 Shadow Root를 지정해야 한다.

typescript
<Modal
  open={!!selectedEmail}
  title="메일 상세"
  onCancel={() => setSelectedEmail(undefined)}
  getContainer={shadowRoot}  // 모달도 Shadow DOM 내부에 렌더링
>
  {/* ... */}
</Modal>

7. 개발/프로덕션 환경 일관성 검증

검증 항목Dev 서버Docker(Prod)기대 결과
CSS 문자열 변환toString-loadertoString-loader동일한 문자열
Shadow DOM 주입<style> 태그<style> 태그동일한 방식
스타일 격리Shadow Root 내부Shadow Root 내부전역 오염 없음
ANTD 스타일StyleProviderStyleProvider정상 렌더링

핵심 정리

  1. CSS 처리의 불일치 문제: webpack은 개발 모드(style-loader)와 프로덕션 모드(MiniCssExtractPlugin)에서 CSS를 다르게 처리하므로 마이크로 프론트엔드에서 일관성 문제가 발생한다
  2. toString Loader의 역할: CSS 파일을 JavaScript 문자열로 변환하여 환경에 관계없이 동일하게 처리할 수 있게 한다
  3. Shadow DOM 주입 패턴: CSS 문자열을 <style> 엘리먼트의 textContent로 설정한 후 Shadow Root에 삽입하면 완벽한 스타일 격리가 가능하다
  4. CSS-in-JS 라이브러리 대응: ANTD의 StyleProvider처럼 스타일 주입 대상을 Shadow Root로 변경하는 설정이 필요하다

다음 단계

Redux 상태관리 컴포넌트 ->