[Blazor Web App] 09. CSS 구조 정리하기 - app.css와 .razor.css 역할 나누기

앞에서 로그인 화면과 회원 페이지를 만들면서 CSS도 같이 작성했다.
처음에는 페이지가 하나뿐이라 Home.razor.css에 필요한 스타일을 전부 넣어도 괜찮았다.
그런데 Member.razor가 생기고, 버튼이나 페이지 박스 스타일이 비슷하게 반복되기 시작했다.
예를 들어 Home.razor.css에는 로그인 버튼 스타일이 있었다.
.login-button {
padding: 10px 16px;
border: 1px solid #333;
border-radius: 4px;
cursor: pointer;
}
Member.razor.css에도 비슷한 버튼 스타일이 필요했다.
.login-button {
margin-top: 16px;
padding: 10px 16px;
border: 1px solid #333;
border-radius: 4px;
cursor: pointer;
}
처음에는 그냥 복사해서 써도 될 것 같았다.
하지만 페이지가 늘어날수록 이런 방식은 관리하기 어려워질 것 같았다.
Home.razor.css
→ 버튼 스타일 있음
Member.razor.css
→ 비슷한 버튼 스타일 또 있음
다른 페이지 추가
→ 버튼 스타일 또 복사
나중에 버튼 모양을 바꾸려면 여러 CSS 파일을 찾아다니면서 수정해야 한다.
그래서 이번에는 공통 스타일과 페이지 전용 스타일을 나눠보기로 했다.
app.css와 .razor.css의 역할을 나누기로 했다
Blazor 프로젝트에는 보통 wwwroot/app.css가 있다.
이 파일은 앱 전체에 적용되는 공통 CSS로 사용할 수 있다.
반면 Home.razor.css, Member.razor.css 같은 파일은 특정 Razor 컴포넌트에 붙는 CSS다.
이번에는 역할을 이렇게 나누기로 했다.

wwwroot/app.css
→ 여러 페이지에서 같이 쓰는 공통 스타일
Home.razor.css
→ Home 화면만의 스타일
Member.razor.css
→ Member 화면만의 스타일
이 기준을 잡으니까 어떤 스타일을 어디에 둘지 판단하기 쉬워졌다.
여러 페이지에서 반복된다
→ app.css 후보
특정 페이지에서만 사용한다
→ 해당 .razor.css 유지
공통으로 뺄 스타일 찾기
먼저 어떤 스타일이 공통인지 봤다.
반복될 가능성이 큰 스타일은 이런 것들이었다.
페이지 최대 너비
가운데 정렬
페이지 padding
상단 제목 영역
본문 영역
카드 형태 section
버튼 기본 모양
메시지 기본 모양
오류 메시지 색상
성공 메시지 색상
예를 들어 페이지 전체 폭과 가운데 정렬은 여러 화면에서 필요하다.
max-width: 720px;
margin: 0 auto;
padding: 32px;
버튼도 여러 페이지에서 반복된다.
padding: 10px 16px;
border: 1px solid #333;
border-radius: 4px;
cursor: pointer;
메시지 색상도 로그인 실패, 로그아웃 안내 등 여러 상황에서 쓸 수 있다.
.error-message {
color: #b00020;
}
.success-message {
color: #14532d;
}
그래서 이런 스타일은 app.css로 옮기기로 했다.
app.css에 공통 클래스 만들기
wwwroot/app.css에 공통 클래스를 추가했다.
.app-page {
max-width: 720px;
margin: 0 auto;
padding: 32px;
}
.app-header {
margin-bottom: 32px;
}
.app-header h1 {
margin: 0 0 8px;
font-size: 32px;
font-weight: 700;
}
.app-header p {
margin: 0;
font-size: 16px;
}
.app-content {
margin-bottom: 40px;
}
.app-section {
padding: 24px;
border: 1px solid #ddd;
border-radius: 8px;
}
.app-section h2 {
margin: 0 0 24px;
font-size: 22px;
}
.button-row {
margin-top: 16px;
}
.app-button {
padding: 10px 16px;
border: 1px solid #333;
border-radius: 4px;
cursor: pointer;
}
.app-button + .app-button {
margin-left: 8px;
}
.app-message {
margin-top: 16px;
min-height: 24px;
font-weight: 600;
}
.error-message {
color: #b00020;
}
.success-message {
color: #14532d;
}
클래스 이름은 app- 접두사를 붙였다.
이유는 이 스타일들이 특정 페이지 하나가 아니라 앱 전체에서 공통으로 쓰이는 스타일이라는 걸 이름만 봐도 알 수 있게 하고 싶었기 때문이다.
app-page
→ 앱 공통 페이지 컨테이너
app-header
→ 앱 공통 헤더 영역
app-content
→ 앱 공통 본문 영역
app-section
→ 앱 공통 카드형 섹션
app-button
→ 앱 공통 버튼
app-message
→ 앱 공통 메시지
처음에는 기존 이름인 home-page, login-button 같은 걸 그대로 써도 되지 않을까 싶었다.
하지만 이름에 특정 화면이나 기능 이름이 들어가 있으면 다른 페이지에서 재사용할 때 어색해진다.
예를 들어 Member.razor에서 login-button을 쓰면 의미가 조금 이상하다.
<button class="login-button">
로그아웃
</button>
로그아웃 버튼인데 login-button이라는 이름을 쓰는 건 코드만 봐도 조금 헷갈린다.
그래서 공통 버튼은 app-button으로 바꿨다.
Home.razor 클래스 이름 바꾸기
이제 Home.razor에서 기존 클래스명을 공통 클래스명으로 바꿨다.
기존에는 이런 이름을 사용했다.
home-page
page-header
page-content
login-section
login-button
login-message
이걸 아래처럼 바꿨다.
app-page
app-header
app-content
app-section
app-button
app-message
정리 후 Home.razor의 핵심 구조는 이렇게 됐다.
@page "/"
@using Study.Web.Models
@using Study.Web.Services
@inject LoginService LoginService
@inject LoginStateService LoginStateService
@inject NavigationManager NavigationManager
<PageTitle>Home</PageTitle>
<div class="app-page">
<header class="app-header">
<h1>@title</h1>
<p>@description</p>
</header>
<main class="app-content">
<section class="app-section">
<h2>회원 로그인</h2>
<div class="form-row">
<label for="user-id">아이디</label>
<input id="user-id" @bind="loginRequest.UserId" />
</div>
<div class="form-row">
<label for="password">비밀번호</label>
<input id="password" type="password" @bind="loginRequest.Password" />
</div>
<button class="app-button" @onclick="Login">@loginButtonText</button>
<p class="@messageClass">@loginMessage</p>
</section>
</main>
<footer class="page-footer">
<p>Tel:0000-0000 / FAX:00-0000-0000</p>
<p>https://example.com</p>
<p>Made by Sample Company</p>
<p>Ver: 0.0.1</p>
</footer>
</div>
messageClass도 같이 바꿨다
기존에는 메시지 기본 클래스가 login-message였다.
private string messageClass = "login-message";
이제 공통 메시지 스타일을 app-message로 바꿨기 때문에 C# 코드도 수정했다.
private string messageClass = "app-message";
로그아웃 후 안내 메시지를 보여줄 때는 이렇게 설정한다.
messageClass = "app-message success-message";
로그인 실패 메시지를 보여줄 때는 이렇게 설정한다.
messageClass = "app-message error-message";
이렇게 바꾸면 메시지의 기본 모양은 app-message가 담당하고, 색상은 success-message나 error-message가 담당한다.
app-message
→ 메시지 기본 여백, 높이, 굵기
success-message
→ 성공 또는 안내 색상
error-message
→ 오류 색상
이 구조가 더 명확해 보였다.
Home.razor.css는 Home 전용 스타일만 남기기
공통 스타일을 app.css로 옮겼기 때문에 Home.razor.css에서는 Home 화면에만 필요한 스타일만 남겼다.
정리 후 Home.razor.css는 이렇게 됐다.
.form-row {
margin-bottom: 16px;
}
.form-row label {
display: block;
margin-bottom: 6px;
font-weight: 600;
}
.form-row input {
width: 100%;
padding: 10px 12px;
border: 1px solid #ccc;
border-radius: 4px;
}
.page-footer {
margin-top: 40px;
padding-top: 16px;
border-top: 1px solid #ddd;
font-size: 14px;
}
.page-footer p {
margin: 4px 0;
}
이제 아래 스타일은 Home.razor.css에서 제거했다.
home-page
page-header
page-content
login-section
login-button
login-message
error-message
success-message
이 스타일들은 더 이상 Home 전용이 아니라 공통 스타일로 판단했기 때문이다.
Member.razor도 공통 클래스 사용하기
Member.razor도 같은 기준으로 정리했다.
기존에는 이런 클래스들이 있었다.
member-page
member-section
login-button
logout-button
이제 공통 레이아웃과 버튼은 app.css에 있는 클래스를 사용한다.
정리 후 구조는 이렇게 됐다.
@page "/member"
@using Study.Web.Services
@inject LoginStateService LoginStateService
@inject NavigationManager NavigationManager
<PageTitle>Member</PageTitle>
<div class="app-page">
<header class="app-header">
<h1>회원 페이지</h1>
<p>로그인 성공 후 이동하는 임시 페이지입니다.</p>
</header>
<main class="app-content">
<section class="app-section">
<h2>로그인 완료</h2>
<p>정상적으로 로그인되었습니다.</p>
<p>이 페이지는 나중에 회원 전용 화면으로 확장할 예정입니다.</p>
<div class="button-row">
<button class="app-button" @onclick="ToggleMemberInfo">
회원 정보 보기 / 숨기기
</button>
<button class="app-button" @onclick="Logout">
로그아웃
</button>
</div>
@if (isShowMemberInfo)
{
<div class="member-info">
<h3>회원 정보</h3>
<p>아이디: @LoginStateService.UserId</p>
<p>회원 등급: 테스트 회원</p>
<p>접속상태: 로그인 완료</p>
</div>
}
</section>
</main>
</div>
여기서 두 버튼은 모두 app-button을 사용한다.
<button class="app-button" @onclick="ToggleMemberInfo">
회원 정보 보기 / 숨기기
</button>
<button class="app-button" @onclick="Logout">
로그아웃
</button>
버튼 사이 간격은 app.css의 이 코드가 처리한다.
.app-button + .app-button {
margin-left: 8px;
}
이 선택자는 app-button 바로 뒤에 또 다른 app-button이 있을 때 왼쪽 여백을 준다.
즉 버튼이 여러 개 붙어 있을 때 두 번째 버튼부터 간격이 생긴다.
Member.razor.css는 회원 정보 박스만 남기기
공통 레이아웃과 버튼 스타일을 app.css로 옮겼기 때문에 Member.razor.css는 훨씬 짧아졌다.
.member-info {
margin-top: 24px;
padding: 16px;
border: 1px solid #ddd;
border-radius: 8px;
}
.member-info h3 {
margin-top: 0;
}
이제 Member.razor.css는 정말 Member 화면에서만 쓰는 member-info 박스만 담당한다.
app.css
→ 페이지 틀, 섹션, 버튼, 메시지
Member.razor.css
→ 회원 정보 박스
이렇게 역할이 정리됐다.
정리 후 CSS 구조
정리 후 스타일 역할은 이렇게 나뉘었다.
wwwroot/app.css
├─ app-page
├─ app-header
├─ app-content
├─ app-section
├─ button-row
├─ app-button
├─ app-message
├─ error-message
└─ success-message
Home.razor.css
├─ form-row
└─ page-footer
Member.razor.css
└─ member-info
이전에는 페이지별 CSS에 공통 스타일이 조금씩 섞여 있었다.
Home.razor.css
→ 페이지 레이아웃, 버튼, 메시지, 입력 폼, footer
Member.razor.css
→ 페이지 레이아웃, 버튼, 회원 정보 박스
정리 후에는 이렇게 됐다.
app.css
→ 여러 페이지에서 쓰는 공통 스타일
Home.razor.css
→ 로그인 폼과 footer
Member.razor.css
→ 회원 정보 박스
훨씬 역할이 분명해졌다.
모든 스타일을 공통으로 빼는 게 좋은 건 아니다
이번에 공통 스타일을 정리하면서 하나 느낀 게 있다.
반복된다고 해서 무조건 전부 공통 CSS로 빼는 게 좋은 건 아닌 것 같다.
기준이 필요했다.
내가 잡은 기준은 이렇다.
여러 페이지에서 같은 의미로 반복된다
→ app.css 후보
한 페이지에서만 쓰인다
→ .razor.css 유지
이름에 특정 페이지 의미가 강하다
→ 공통으로 빼기 전에 이름부터 다시 생각
공통으로 뺐을 때 오히려 이해가 어려워진다
→ 일단 페이지 CSS에 둔다
예를 들어 버튼은 여러 페이지에서 같은 의미로 쓰인다.
로그인 버튼
회원 정보 보기 버튼
로그아웃 버튼
모양은 같고 역할은 다르지만, “기본 버튼 스타일”이라는 공통 의미가 있다.
그래서 app-button으로 뺐다.
반면 form-row는 현재 로그인 폼에서만 사용한다.
아이디 입력 row
비밀번호 입력 row
아직 다른 페이지에서 쓰이지 않으므로 Home.razor.css에 남겼다.
member-info도 회원 페이지 전용 박스이므로 Member.razor.css에 남겼다.
CSS 이름을 정하는 것도 중요했다
이번에 클래스명을 바꾸면서 이름이 꽤 중요하다는 걸 느꼈다.
예를 들어 기존 login-button은 로그인 화면에서는 자연스럽다.
<button class="login-button">
회원로그인
</button>
하지만 회원 페이지의 로그아웃 버튼에 쓰면 어색하다.
<button class="login-button">
로그아웃
</button>
버튼 모양은 같아도 이름이 기능에 묶여 있어서 재사용할 때 헷갈린다.
그래서 공통 스타일은 기능 이름보다 역할 이름에 가깝게 지었다.
login-button
→ app-button
login-message
→ app-message
home-page
→ app-page
이렇게 바꾸니까 다른 페이지에서도 자연스럽게 사용할 수 있었다.
<button class="app-button">
로그아웃
</button>
이름만 봐도 “앱에서 공통으로 쓰는 버튼이구나”라고 이해할 수 있다.
테스트 결과
정리 후 테스트는 다시 진행했다.
1. / 접속
→ 로그인 화면 스타일 정상 표시
2. 아이디 비움
→ 오류 메시지 빨간색 표시
3. 비밀번호 비움
→ 오류 메시지 빨간색 표시
4. 아이디 또는 비밀번호 틀림
→ 오류 메시지 빨간색 표시
5. admin / 1234 로그인
→ /member 이동
6. /member 화면 스타일 정상 표시
7. 회원 정보 보기 / 숨기기 버튼 동작
8. 로그아웃 버튼 동작
9. 로그아웃 후 Home 화면에 "로그아웃되었습니다." 초록색 표시
기능은 그대로 유지되면서 CSS 구조만 정리됐다.
이런 작업은 겉으로는 변화가 적지만, 나중에 페이지가 늘어날수록 중요해질 것 같다.
이번 작업 결과
이번 작업 전에는 CSS 구조가 이런 느낌이었다.
Home.razor.css
→ Home 스타일 + 일부 공통 스타일
Member.razor.css
→ Member 스타일 + 일부 공통 스타일
작업 후에는 이렇게 바뀌었다.
app.css
→ 공통 스타일
Home.razor.css
→ Home 전용 스타일
Member.razor.css
→ Member 전용 스타일
이제 버튼 모양을 바꾸고 싶으면 app.css의 app-button만 수정하면 된다.
메시지 기본 모양을 바꾸고 싶으면 app-message를 수정하면 된다.
페이지 전체 폭을 바꾸고 싶으면 app-page를 수정하면 된다.
이전보다 유지보수하기 좋은 구조가 된 것 같다.
오늘 배운 점
- 여러 페이지에서 반복되는 스타일은 공통 CSS로 분리하는 것이 좋다.
wwwroot/app.css는 앱 전체에서 사용하는 공통 스타일을 두기에 적합하다..razor.css는 특정 컴포넌트 전용 스타일을 두는 데 적합하다.- 공통 스타일과 페이지 전용 스타일을 구분하면 CSS 파일이 덜 복잡해진다.
- 클래스 이름은 재사용 가능성을 고려해서 정해야 한다.
login-button처럼 특정 기능에 묶인 이름은 다른 페이지에서 쓰기 어색할 수 있다.app-button,app-page,app-section처럼 역할 중심 이름이 공통 스타일에 더 잘 맞는다.- 모든 스타일을 무조건 공통으로 빼는 것이 좋은 것은 아니다.
- 여러 페이지에서 같은 의미로 반복되는지 확인한 뒤 공통화하는 게 좋다.
- CSS 구조 정리는 기능 변화가 적어 보여도 나중에 유지보수에 영향을 준다.
다음 작업
다음에는 사용자 정보 모델을 만들어볼 예정이다.
지금 회원 페이지에는 아직 테스트 회원 정보가 직접 들어가 있다.
<p>회원 등급: 테스트 회원</p>
<p>접속상태: 로그인 완료</p>
다음 단계에서는 이런 정보를 UserInfo 같은 모델로 묶어볼 수 있을 것 같다.
이렇게 하면 로그인 상태가 단순히 “로그인했는가?”에서 “누가 로그인했는가?”로 조금 더 발전할 것 같다.