Blazor CSS 격리(CSS Isolation)와 컴포넌트 스타일링

CSS와 스타일링
📚 시작하기 전에

CSS와 스타일링

CSS 격리(CSS Isolation)와 인라인 스타일을 활용해 컴포넌트를 꾸미는 방법을 배웁니다.

Blazor CSS 격리(CSS Isolation)와 컴포넌트 스타일링
CSS 격리(CSS Isolation)와 인라인 스타일을 활용해 Blazor 컴포넌트를 효과적으로 스타일링하는 방법을 학습합니다.

📌 학습 목표

  • CSS 격리(CSS Isolation)의 개념과 필요성을 설명할 수 있습니다.
  • .razor.css 파일을 생성하고 컴포넌트에 독립적인 스타일을 적용할 수 있습니다.
  • 빌드 시 스코프 속성이 자동으로 주입되는 원리를 이해하고 설명할 수 있습니다.
  • ::deep 선택자를 사용해 자식 컴포넌트 내부 요소에도 스타일을 전파할 수 있습니다.
  • 인라인 스타일과 동적 스타일 바인딩을 상황에 맞게 선택·적용할 수 있습니다.

📝 개념 설명

1. CSS 격리(CSS Isolation)란?

대규모 Blazor 애플리케이션에서 여러 컴포넌트가 같은 CSS 클래스명을 사용하면 스타일이 서로 충돌하는 문제가 발생합니다. 예를 들어 Header 컴포넌트와 Footer 컴포넌트 모두 .title 클래스를 정의했다면, 하나의 스타일 규칙이 두 컴포넌트 모두에 영향을 미칩니다.

 

CSS 격리(CSS Isolation)는 이 문제를 해결하기 위해 .NET 5부터 도입된 Blazor 내장 기능입니다. 각 컴포넌트에 고유한 스코프 식별자를 자동으로 생성·부여하여, 해당 컴포넌트 내부 HTML 요소에만 스타일이 적용되도록 격리합니다. 별도의 라이브러리 설치 없이 기본 제공되며, 파일 하나만 추가하면 즉시 사용할 수 있습니다.

 

 

📌 CSS 격리 핵심 포인트

 

🔒
격리성
컴포넌트 스타일이 다른 컴포넌트에 영향을 주지 않습니다
📄
파일 명명 규칙
컴포넌트명.razor.css 파일로 격리 스타일을 정의합니다
⚙️
자동 처리
빌드 시 고유 스코프 속성(b-xxxxxxxx)이 자동 생성·주입됩니다
🌐
글로벌 병용
wwwroot의 전역 CSS와 함께 혼용 가능합니다

2. .razor.css 파일 생성 방법

CSS 격리를 적용하려면 컴포넌트 파일과 동일한 폴더컴포넌트명.razor.css 파일을 생성합니다. 파일명이 반드시 컴포넌트 파일명과 일치해야 합니다.

 

예시 프로젝트 구조는 다음과 같습니다:

Pages/
├── Counter.razor          ← 컴포넌트 파일
├── Counter.razor.css      ← 격리 CSS 파일 (같은 폴더, 같은 이름)
├── Index.razor
└── Index.razor.css

Components/
├── ProductCard.razor
└── ProductCard.razor.css

Blazor는 빌드 시 모든 격리 CSS 파일을 자동으로 수집하여 {프로젝트명}.styles.css라는 번들 파일로 합칩니다. 이 번들 파일은 프로젝트 템플릿 기본 생성 시 App.razor(Blazor WebAssembly) 또는 _Host.cshtml(Blazor Server)에 이미 포함되어 있으므로, 개발자가 별도로 링크 태그를 추가할 필요가 없습니다.

3. 빌드 시 스코프 원리

Blazor는 빌드 과정에서 각 격리 CSS 파일에 대해 고유한 스코프 속성(b-xxxxxxxx 형태)을 자동 생성합니다. 이 속성은 해당 컴포넌트의 모든 HTML 요소에 자동으로 추가되고, CSS 선택자에도 동일하게 적용되어 스타일이 격리됩니다.

 

개발자가 작성하는 코드와 브라우저에서 실제 렌더링되는 코드를 비교해 보겠습니다:

<!-- 개발자가 작성한 Counter.razor -->
<h3 class='counter-title'>카운터</h3>

<!-- 브라우저에서 실제 렌더링된 HTML -->
<h3 class='counter-title' b-3xxtsmhmb0>카운터</h3>
/* 개발자가 작성한 Counter.razor.css */
.counter-title {
    color: navy;
    font-size: 1.8rem;
}

/* 빌드 후 실제 변환된 CSS (브라우저에 적용됨) */
.counter-title[b-3xxtsmhmb0] {
    color: navy;
    font-size: 1.8rem;
}

이 방식 덕분에 .counter-title 스타일은 오직 Counter 컴포넌트 내부 요소에만 적용되며, 다른 컴포넌트의 동일한 클래스명에는 전혀 영향을 주지 않습니다.

 

 

⚖️ 글로벌 CSS vs 격리 CSS 비교

 

항목글로벌 CSS격리 CSS
적용 범위전체 앱해당 컴포넌트만
파일 위치wwwroot/css/컴포넌트와 동일 폴더
파일 확장자.css.razor.css
스타일 충돌 위험있음없음
스코프 처리없음 (그대로 사용)빌드 시 자동 스코프 주입
적합한 용도공통 스타일, 리셋, 폰트컴포넌트 전용 스타일

4. ::deep 선택자 — 자식 컴포넌트에 스타일 전파하기

CSS 격리의 스코프는 해당 컴포넌트의 직접적인 HTML 요소에만 적용됩니다. 만약 부모 컴포넌트의 격리 CSS에서 자식 컴포넌트 내부 요소에 스타일을 적용하려면 ::deep 선택자를 사용해야 합니다.

/* ParentComponent.razor.css */

/* ❌ 이 방식은 자식 컴포넌트 내부 h3에 적용되지 않음 */
.wrapper h3 {
    color: red;
}

/* ✅ ::deep를 추가하면 자식 컴포넌트의 h3에도 적용됨 */
.wrapper ::deep h3 {
    color: darkgreen;
    border-bottom: 2px solid green;
}

/* ✅ 특정 클래스 전체를 자식까지 적용 */
::deep .btn {
    border-radius: 8px;
    font-weight: bold;
}
💡 ::deep 사용 팁: ::deep는 반드시 격리 CSS 파일(.razor.css) 내에서만 효과가 있습니다. 일반 CSS 파일에서는 동작하지 않습니다. 또한 .wrapper ::deep h3처럼 앞에 부모 선택자를 붙여 사용 범위를 명확히 제한하는 것이 권장됩니다. ::deep만 단독으로 사용하면 컴포넌트 전체 하위 트리에 영향을 줄 수 있습니다.

5. 인라인 스타일(Inline Style)

CSS 파일 없이도 HTML 요소에 직접 스타일을 지정할 수 있습니다. Blazor에서도 일반 HTML과 동일하게 style 속성을 사용합니다. 가장 간단한 형태는 고정 인라인 스타일입니다:

<h3 style='color: navy; font-size: 1.5rem; font-weight: bold;'>
    인라인 스타일 적용 예시
</h3>

<p style='background-color: #fffde7; padding: 12px; border-left: 4px solid #ffc107;'>
    경고 박스 스타일
</p>

Blazor의 강력한 점은 C# 변수나 표현식으로 동적 인라인 스타일을 만들 수 있다는 것입니다. style='@변수명' 형태로 C# 문자열 변수를 style 속성에 직접 바인딩합니다:

@* 동적 스타일 바인딩 예시 *@
<p style='@dynamicStyle'>동적으로 바뀌는 스타일 텍스트</p>
<button @onclick='ToggleHighlight'>강조 전환</button>

@code {
    private string dynamicStyle = "color: black; font-weight: normal;";
    private bool isHighlighted = false;

    private void ToggleHighlight()
    {
        isHighlighted = !isHighlighted;
        dynamicStyle = isHighlighted
            ? "color: red; font-weight: bold; background-color: yellow; padding: 4px 8px;"
            : "color: black; font-weight: normal;";
    }
}

동적 클래스 바인딩도 자주 사용되는 패턴입니다:

@* 조건에 따라 클래스 전환 *@
<div class='@(isActive ? "card active" : "card")'>
    내용
</div>

@* 여러 클래스 조합 *@
<button class='btn @(isLoading ? "btn-loading" : "")'>
    @(isLoading ? "처리 중..." : "확인")
</button>

💡 예제 & 실습

CSS 격리를 실제로 활용하는 완전한 카드 컴포넌트를 만들어 보겠습니다. ProductCard.razorProductCard.razor.css 두 파일을 생성합니다.

Step 1. 컴포넌트 파일 생성 — ProductCard.razor

@* Components/ProductCard.razor *@
<div class='card-container'>
    <div class='card-header'>
        <span class='card-badge'>@Category</span>
    </div>
    <div class='card-body'>
        <h4 class='card-title'>@Title</h4>
        <p class='card-desc'>@Description</p>
        <span class='card-price'>@Price.ToString("N0")원</span>
    </div>
    <div class='card-footer'>
        <button class='btn-buy @(IsSoldOut ? "btn-soldout" : "")'
                @onclick='HandleBuy'
                disabled='@IsSoldOut'>
            @(IsSoldOut ? "품절" : "구매하기")
        </button>
    </div>
</div>

@code {
    [Parameter] public string Title { get; set; } = "상품명";
    [Parameter] public string Description { get; set; } = "상품 설명";
    [Parameter] public string Category { get; set; } = "일반";
    [Parameter] public decimal Price { get; set; } = 0;
    [Parameter] public bool IsSoldOut { get; set; } = false;
    [Parameter] public EventCallback OnBuy { get; set; }

    private async Task HandleBuy()
    {
        if (!IsSoldOut)
            await OnBuy.InvokeAsync();
    }
}

Step 2. 격리 CSS 파일 생성 — ProductCard.razor.css

/* Components/ProductCard.razor.css */
/* 이 파일의 모든 스타일은 ProductCard 컴포넌트에만 적용됨 */

.card-container {
    border: 1px solid #e0e0e0;
    border-radius: 12px;
    overflow: hidden;
    max-width: 300px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    transition: box-shadow 0.2s ease;
    background-color: #ffffff;
}

.card-container:hover {
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
    transform: translateY(-2px);
    transition: all 0.2s ease;
}

.card-header {
    background-color: #0d6efd;
    padding: 10px 16px;
}

.card-badge {
    color: white;
    font-size: 0.78rem;
    font-weight: bold;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

.card-body {
    padding: 16px;
}

.card-title {
    color: #1a1a2e;
    font-size: 1.15rem;
    font-weight: bold;
    margin: 0 0 8px 0;
}

.card-desc {
    color: #666;
    font-size: 0.9rem;
    line-height: 1.6;
    margin-bottom: 12px;
}

.card-price {
    color: #e63946;
    font-size: 1.4rem;
    font-weight: bold;
}

.card-footer {
    padding: 12px 16px;
    background-color: #f8f9fa;
    border-top: 1px solid #e0e0e0;
}

.btn-buy {
    width: 100%;
    padding: 10px;
    background-color: #0d6efd;
    color: white;
    border: none;
    border-radius: 6px;
    cursor: pointer;
    font-size: 1rem;
    font-weight: bold;
    transition: background-color 0.2s ease;
}

.btn-buy:hover:not(:disabled) {
    background-color: #0b5ed7;
}

.btn-soldout {
    background-color: #adb5bd;
    cursor: not-allowed;
}

Step 3. 부모 페이지에서 컴포넌트 사용 — Index.razor

@page "/"
@using YourProject.Components

<h3 class='page-section-title'>추천 상품</h3>
<div class='product-grid'>
    <ProductCard Title='무선 노트북'
                 Description='가볍고 빠른 업무용 노트북. 배터리 12시간'
                 Category='전자제품'
                 Price='1299000'
                 OnBuy='@(() => ShowAlert("노트북 구매!"))' />

    <ProductCard Title='인체공학 마우스'
                 Description='손목 부담을 줄인 무선 마우스'
                 Category='주변기기'
                 Price='59000'
                 IsSoldOut='true' />

    <ProductCard Title='USB-C 허브'
                 Description='7-in-1 멀티포트 허브'
                 Category='주변기기'
                 Price='39000'
                 OnBuy='@(() => ShowAlert("허브 구매!"))' />
</div>

@code {
    private void ShowAlert(string message)
    {
        Console.WriteLine(message);
    }
}
📌 핵심 동작 확인: Index.razor에서 .page-section-title이나 .product-grid 클래스를 정의하더라도, ProductCard.razor.css.card-title·.btn-buy 등의 스타일은 오직 ProductCard 컴포넌트 내부에만 적용됩니다. 다른 페이지에서 동일한 클래스명을 써도 스타일 충돌이 발생하지 않습니다.

Step 4. ::deep로 자식 컴포넌트 스타일 적용 — Index.razor.css

/* Index.razor.css */

.product-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 24px;
    padding: 20px 0;
}

/* ProductCard 내부의 .card-title에 추가 스타일 적용 */
.product-grid ::deep .card-title {
    font-family: 'Noto Sans KR', sans-serif;
}

/* ProductCard 내부의 모든 버튼에 공통 스타일 적용 */
.product-grid ::deep button {
    letter-spacing: 0.5px;
}

⚠️ 자주 틀리는 것 / 주의사항

  1. 파일명 불일치: Counter.razor의 격리 CSS 파일은 반드시 Counter.razor.css여야 합니다. counter.cssCounterStyles.razor.css로 생성하면 격리 CSS로 인식되지 않고 일반 파일로 취급됩니다.
  2. 자식 컴포넌트에 스타일이 적용되지 않는 문제: 부모 컴포넌트의 격리 CSS는 기본적으로 자식 컴포넌트 내부에는 적용되지 않습니다. 자식 내부까지 스타일을 전파하려면 반드시 ::deep를 사용해야 합니다.
  3. ::deep 남용 주의: ::deep는 격리의 장점을 부분적으로 포기하는 것입니다. 모든 스타일에 ::deep를 사용하면 CSS 격리의 의미가 없어집니다. 반드시 필요한 경우에만 제한적으로 사용하십시오.
  4. 인라인 스타일의 우선순위: 인라인 스타일(style 속성)은 외부 CSS 파일보다 우선순위(specificity)가 높습니다. 격리 CSS와 인라인 스타일이 같은 속성을 정의하면 인라인 스타일이 이깁니다. 동적 스타일 바인딩 시 의도치 않게 격리 CSS를 덮어쓸 수 있으니 주의하십시오.
  5. 번들 링크 태그 누락: 직접 만든 프로젝트(템플릿 없이 생성)에서는 {프로젝트명}.styles.css 링크 태그가 없을 수 있습니다. 격리 CSS가 전혀 적용되지 않는다면 App.razor 또는 _Host.cshtml에 아래 태그가 있는지 확인하십시오.
    <link href='YourProjectName.styles.css' rel='stylesheet' />
  6. 동적 클래스 바인딩 시 공백 처리: 아래와 같이 작성 시 클래스가 비어 있을 때 공백이 남습니다. 실제 적용에는 문제없지만, HTML 출력이 지저분해질 수 있습니다.
    <!-- 빈 클래스가 남을 수 있음 -->
    <div class='card @(isActive ? "active" : "")'>...</div>
    
    <!-- 더 깔끔한 방식 -->
    <div class='@(isActive ? "card active" : "card")'>...</div>

🎯 마무리

Blazor의 CSS 격리는 컴포넌트 단위 개발 방식과 완벽하게 어울리는 강력한 스타일링 도구입니다. 각 컴포넌트가 자신만의 독립적인 스타일을 갖게 되므로, 대규모 프로젝트에서도 스타일 충돌 없이 유지보수하기 쉬운 코드를 작성할 수 있습니다.

 

실전에서는 다음 패턴을 권장합니다: 공통 스타일(폰트, 색상 변수, 레이아웃 그리드, 리셋 CSS)은 글로벌 CSS(wwwroot/css/)로 정의하고, 컴포넌트 고유 스타일은 격리 CSS(.razor.css)로 분리합니다. 인라인 스타일은 C# 로직에 따라 동적으로 바뀌어야 하는 스타일에 한정해서 사용하면 코드 가독성을 높이고 유지보수를 쉽게 만들 수 있습니다.

✅ 핵심 정리
  • CSS 격리는 컴포넌트명.razor.css 파일을 컴포넌트와 같은 폴더에 생성하면 자동으로 활성화됩니다.
  • 빌드 시 고유 스코프 속성(b-xxxxxxxx)이 HTML 요소와 CSS 선택자에 자동 주입되어, 스타일이 해당 컴포넌트에만 적용됩니다.
  • 격리 CSS는 기본적으로 자식 컴포넌트 내부에는 적용되지 않으며, ::deep 선택자를 사용해야 자식 내부까지 스타일을 전파할 수 있습니다.
  • 인라인 스타일(style='@변수명')은 C# 변수·표현식과 바인딩하여 조건에 따라 동적으로 스타일을 변경할 수 있습니다.
  • 공통 스타일은 글로벌 CSS, 컴포넌트 전용 스타일은 격리 CSS로 분리하는 것이 Blazor 스타일링의 모범 사례입니다.

댓글 남기기

Wordpress Social Share Plugin powered by Ultimatelysocial
Copy link
URL has been copied successfully!
THREADS
RSS
error: 저작권 콘텐츠보호를 부탁드립니다.