Styling UI Design System
3줄 요약
- Styling은 예쁘게 꾸미는 작업이 아니라 정보 구조, 반응형, 상태 표현, 접근성을 함께 설계하는 작업이다.
- 백엔드 개발자가 자주 막히는 CSS 문제는 specificity, flex/grid shrink,
min-width: 0, overflow, sticky, z-index, 모바일 viewport다. - 개인 프로젝트에서도 Button, Input, Modal, Toast, Table 같은 공통 컴포넌트의 상태를 정리하면 “기능은 되는데 어설픈 화면”을 크게 줄일 수 있다.
핵심 정리
- CSS box model, flex, grid는 화면 배치의 기본 언어다.
- 반응형은 CSS breakpoint만이 아니라 작은 화면에서 무엇을 우선 보여줄지 정하는 제품 결정이다.
- Tailwind는 빠르지만 반복 패턴을 component로 추출하지 않으면 className 복사가 늘어난다.
- 디자인 시스템은 거창한 문서가 아니라 color, typography, spacing, radius, component state를 일관되게 쓰는 최소 규칙이다.
- overlay UI는 overflow, portal, focus, z-index layer를 함께 설계한다.
백엔드 개발자가 헷갈리는 지점
z-index만 크게 올리면 모든 overlay가 위로 올라온다고 생각하기 쉽다.- 판단 기준은 stacking context 안에서만 경쟁한다는 점이다.
- modal, dropdown은 portal과 layer scale을 검토한다.
- flex item에 width를 줬는데도 줄어들지 않는 이유를 찾기 어렵다.
- 판단 기준은
min-width: 0과 overflow 정책이다. - 긴 텍스트와 table이 부모를 밀어내는지 확인한다.
- 판단 기준은
- 반응형을 단순히 column 전환으로만 이해하기 쉽다.
- 판단 기준은 작은 화면에서 정보 우선순위와 행동이 유지되는가다.
개인 프로젝트 기준
- Tailwind 또는 CSS Modules 중 하나를 선택하고 섞는 범위를 제한한다.
- 공통 Button/Input/Modal/Toast/Table은 상태별 스타일을 먼저 정한다.
- 360px 모바일, 768px tablet, desktop에서 핵심 화면을 확인한다.
- CSS 함정은 CSS specificity, cascade, inheritance는 왜 “스타일이 안 먹는다”를 만들까?부터 읽는다.
확인 질문
z-index가 높은데도 dropdown이 안 보이는 대표 이유는 무엇인가?- 부모 stacking context나 overflow clipping 때문이다.
- flex/grid에서 말줄임이 안 될 때 먼저 의심할 속성은 무엇인가?
- 자식 또는 중간 컨테이너의
min-width: 0이다.
- 자식 또는 중간 컨테이너의
- 디자인 시스템의 최소 단위는 무엇인가?
- color, typography, spacing, radius, shadow, 그리고 Button/Input 같은 공통 컴포넌트 상태다.
01. CSS Layout Responsive 상세
이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- flex/grid layout이 예상과 다르게 깨질 때 어디부터 봐야 하는가?
overflow,sticky,z-index문제를 어떻게 디버깅하는가?- 반응형은 CSS breakpoint만으로 충분한가?
개요
CSS는 백엔드 개발자에게 가장 예측하기 어려운 영역처럼 느껴질 수 있다. 같은 class를 바꿨는데 스타일이 먹지 않거나, z-index를 크게 줬는데도 dropdown이 가려지거나, flex item이 부모를 밀어내는 일이 흔하다. 대부분은 cascade, formatting context, scroll container, stacking context를 모르는 상태에서 속성 하나만 바꾸기 때문에 생긴다. 그래서 CSS 디버깅은 “어떤 속성을 더 넣을까”보다 “현재 요소가 어떤 레이아웃과 stacking context 안에 있는가”를 확인하는 일에 가깝다.
원리
CSS는 selector specificity, 선언 순서, inheritance, cascade layer를 통해 최종 스타일을 계산한다. layout은 box model과 formatting context에 따라 결정된다. flex와 grid는 강력하지만, 자식 요소의 intrinsic size와 min-width 기본값이 예상치 못한 overflow를 만든다.
overlay와 sticky는 부모 컨텍스트의 영향을 크게 받는다. overflow는 단순히 스크롤을 만드는 속성이 아니라 자식을 자르고, scroll container를 만들고, sticky 기준을 바꾸며, focus ring 표시에도 영향을 준다.
코드로 보는 핵심 예시
flex layout에서 긴 제목이 영역을 밀어내면 min-width: 0을 확인한다.
function ProjectRow({ project }: { project: Project }) {
return (
<article className="project-row">
<img className="project-row__thumbnail" src={project.thumbnailUrl} alt="" />
<div className="project-row__body">
<h3 className="project-row__title">{project.title}</h3>
<p className="project-row__description">{project.description}</p>
</div>
</article>
);
}.project-row {
display: flex;
gap: 12px;
width: 100%;
}
.project-row__thumbnail {
width: 64px;
height: 64px;
flex: 0 0 auto;
object-fit: cover;
}
.project-row__body {
min-width: 0;
flex: 1;
}
.project-row__title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}sticky toolbar는 scroll container를 명확히 한다.
.page {
min-height: 100dvh;
}
.toolbar {
position: sticky;
top: 0;
z-index: 20;
background: Canvas;
}백엔드와 맞춰야 할 지점
- 모바일 목록에 필요한 필드와 desktop table에 필요한 필드는 다를 수 있다.
- 이미지 API는 width, height, aspect ratio, thumbnail URL을 제공해야 layout shift를 줄일 수 있다.
- 상태 badge는 서버 enum과 UI variant가 일관되게 매핑되어야 한다.
- 긴 제목, 빈 값, 권한별 비활성 상태처럼 화면을 깨뜨리는 데이터 사례를 백엔드 fixture에도 포함한다.
개인 프로젝트 기준
- 화면 레이아웃은 mobile width에서 먼저 깨지는지 확인한다.
- table, filter, sidebar, modal은 반응형 설계 비용이 큰 컴포넌트로 보고 별도 확인한다.
- Tailwind를 쓰더라도 반복되는 layout pattern은 component class나 wrapper로 추출한다.
실전 팁
- 스타일이 안 먹으면 DevTools Computed panel에서 이긴 규칙을 확인한다.
- overlay가 잘리면 부모 chain의
overflow를 확인한다. z-index가 안 먹으면 stacking context를 만든 부모를 찾는다.- 모바일 full-height 화면에는
100vh만 쓰기보다100dvh와min-height를 검토한다. - 관련 함정은 grid에서 width가 예상대로 안 먹는 이유는 무엇일까?, [[02. 백엔드 개발자를 위한 프론트엔드 문법 함정과 디버깅#z-index를-올렸는데-왜-위로-안-올라올까|
z-index를 올렸는데 왜 위로 안 올라올까?]], [[02. 백엔드 개발자를 위한 프론트엔드 문법 함정과 디버깅#모바일-viewport와-100vh는-왜-함정일까|모바일 viewport와100vh는 왜 함정일까?]]를 본다.
위험 신호!
!important가 점점 늘어난다.- overlay가 부모 card 안에서 잘린다.
- desktop table을 모바일에서 가로 스크롤로만 방치한다.
- 작은 화면에서 버튼, input, toast가 서로 겹친다.
확인 질문
확인 질문
- flex item의 긴 텍스트가 말줄임되지 않을 때 확인할 것은 무엇인가?
- flex child 또는 중간 컨테이너에
min-width: 0이 필요한지 확인한다.overflow: hidden은 어떤 부작용이 있는가?
- dropdown, tooltip, focus ring을 자르고 sticky 기준에도 영향을 줄 수 있다.
- 반응형을 제품 요구사항으로 봐야 하는 이유는 무엇인가?
- 작은 화면에서 어떤 정보와 행동을 우선할지 결정해야 하기 때문이다.
참고 문서
- MDN Web Docs, CSS Specificity
- MDN Web Docs, Basic Concepts of Flexbox
- MDN Web Docs, Stacking context
- MDN Web Docs, position
02. Component UI Design System 상세
이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- 개인 프로젝트에서 디자인 시스템은 어디까지 만들면 충분한가?
- Button, Input, Modal, Toast, Table 같은 공통 컴포넌트는 어떤 상태를 가져야 하는가?
- UI 라이브러리, Tailwind, 직접 구현 중 무엇을 선택해야 하는가?
개요
개인 프로젝트 화면이 “기능은 되는데 제품 같지 않다”고 느껴지는 이유는 대개 색이 부족해서가 아니다. spacing, typography, 상태 표현, focus, disabled, error, loading, empty state가 일관되지 않기 때문이다. 디자인 시스템은 거창한 사내 플랫폼이 아니라, 같은 의미의 UI를 같은 방식으로 표현하기 위한 최소 규칙이다.
원리
UI 컴포넌트는 데이터 상태와 사용자 행동을 시각적으로 번역한다. Button은 기본, hover, focus, disabled, loading 상태가 있어야 한다. Input은 default, focus, error, disabled, read-only 상태가 필요하다. Modal은 focus trap, backdrop, escape close, restore focus를 고려해야 한다. Table은 loading, empty, error, pagination, sort, selection 상태가 필요하다.
Tailwind는 토큰을 class로 빠르게 적용하는 도구이고, Radix UI 같은 headless library는 접근성 있는 primitive를 제공한다. MUI 같은 full component library는 빠르게 완성도 있는 UI를 만들 수 있지만 스타일 커스터마이징 비용이 생길 수 있다.
코드로 보는 핵심 예시
Button은 variant와 상태를 명확히 둔다.
type ButtonVariant = "primary" | "secondary" | "danger";
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: ButtonVariant;
isLoading?: boolean;
};
export function Button({
variant = "primary",
isLoading = false,
type = "button",
disabled,
children,
...props
}: ButtonProps) {
return (
<button
{...props}
type={type}
data-variant={variant}
disabled={disabled || isLoading}
aria-busy={isLoading}
className="button"
>
{isLoading ? "처리 중" : children}
</button>
);
}.button {
min-height: 40px;
padding: 0 14px;
border-radius: 6px;
font-weight: 600;
}
.button:focus-visible {
outline: 2px solid Highlight;
outline-offset: 2px;
}
.button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.button[data-variant="danger"] {
background: #b42318;
color: white;
}상태 badge는 서버 enum과 명시적으로 매핑한다.
const projectStatusLabel = {
active: "진행 중",
archived: "보관됨",
blocked: "중단됨",
} satisfies Record<ProjectStatus, string>;
function ProjectStatusBadge({ status }: { status: ProjectStatus }) {
return (
<span className="status-badge" data-status={status}>
{projectStatusLabel[status]}
</span>
);
}백엔드와 맞춰야 할 지점
- enum 값은 UI label, color, icon, disabled 정책과 연결된다.
- table은 page, pageSize, totalCount, sort, filter, selection 정책이 API와 맞아야 한다.
- destructive action은 confirm modal이 필요한지, 서버에서 soft delete인지 hard delete인지 합의한다.
- 상태별 empty message는 데이터 부재인지 권한 부재인지 API 응답으로 구분할 수 있어야 한다.
개인 프로젝트 기준
- 처음부터 거대한 디자인 시스템을 만들지 않는다.
- Button, Input, Select, Modal, Toast, Badge, Table, EmptyState, ErrorView, Skeleton 정도를 최소 공통 컴포넌트로 둔다.
- 색상은 primary, danger, neutral, success, warning 정도의 의미 토큰으로 제한한다.
- UI 라이브러리를 쓰더라도 앱에서 직접 쓰는 wrapper 컴포넌트를 두면 교체와 스타일 통제가 쉽다.
- form 안의
button은 기본 type이 submit이므로 공통 Button에서 기본값을 어떻게 둘지 명시한다.
실전 팁
- Button에 loading 상태가 없으면 중복 submit 버그가 반복된다.
- 공통 Button의 기본
type을button으로 두면 실수로 form submit이 발생하는 일을 줄일 수 있다. 대신 제출 버튼은<Button type="submit">처럼 의도를 명시한다. - ErrorView와 EmptyState를 공통화하면 API 실패와 빈 결과 UX가 균일해진다.
- Table은 가장 빨리 복잡해지는 컴포넌트다. pagination, sort, loading, empty, row action을 초기에 분리한다.
- 상태 badge와 button variant는 서버 enum 변경 시 컴파일 단계에서 드러나도록
satisfies Record<Enum, ...>패턴을 활용한다. - 관련 판단은 UI 라이브러리를 쓰는 게 좋은가, 직접 만드는 게 좋은가?와 Tailwind는 언제 생산적이고 언제 지저분해지는가?를 본다.
위험 신호!
- 같은 의미의 버튼이 화면마다 색, 높이, radius, loading 표현이 다르다.
- disabled 이유를 사용자에게 알려주지 않는다.
- modal이 keyboard로 닫히지 않거나 focus가 뒤 화면으로 빠진다.
- 서버 enum이 추가되었는데 프론트 badge가 아무 fallback 없이 깨진다.
확인 질문
확인 질문
- 개인 프로젝트 디자인 시스템의 최소 컴포넌트는 무엇인가?
- Button, Input, Modal, Toast, Badge, Table, EmptyState, ErrorView, Skeleton 정도가 우선순위가 높다.
- Button에 loading 상태가 필요한 이유는 무엇인가?
- 중복 요청을 줄이고 사용자가 처리가 진행 중임을 알 수 있게 하기 위해서다.
- 서버 enum과 UI는 어떻게 연결되는가?
- enum 값은 label, color, icon, disabled 정책으로 매핑되므로 백엔드와 변경 정책을 맞춰야 한다.