CSS에 대해 "왜?"라고 물으면 답할 수 있으신가요?

\n\n> CSS에 대해 "왜?"라고 물으면 답할 수 있으신가요?\n# CSS — Box Model부터 반응형까지, 레이아웃 완전 정복

CSS 레이아웃은 범위도 넓고 깊이도 제각각이에요. "Flexbox와 Grid 차이가 뭔가요?"부터 "margin collapse는 어떤 상황에서 발생하나요?"까지, 자주 헷갈리는 부분이 많습니다. 이 글에서는 Box Model부터 반응형 디자인, CSS 방법론까지 CSS 레이아웃 관련 개념을 한 번에 정리해 보겠습니다.


Box Model

브라우저가 화면에 요소를 렌더링할 때, 모든 요소는 하나의 사각형 박스 로 취급됩니다. 이 박스의 구조를 Box Model이라고 부릅니다.

PLAINTEXT
┌─────────────────────────────────┐
│            margin               │
│  ┌───────────────────────────┐  │
│  │         border            │  │
│  │  ┌─────────────────────┐  │  │
│  │  │      padding        │  │  │
│  │  │  ┌───────────────┐  │  │  │
│  │  │  │   content      │  │  │  │
│  │  │  └───────────────┘  │  │  │
│  │  └─────────────────────┘  │  │
│  └───────────────────────────┘  │
└─────────────────────────────────┘
  • content: 텍스트, 이미지 등 실제 콘텐츠가 들어가는 영역
  • padding: content와 border 사이의 안쪽 여백
  • border: padding 바깥의 테두리
  • margin: border 바깥의 바깥 여백. 다른 요소와의 간격을 만듭니다

box-sizing: border-box

기본값인 content-box에서는 width가 content 영역의 너비만 의미해요. 그래서 padding이나 border를 주면 요소의 실제 크기가 width보다 커져버립니다. 레이아웃 잡을 때 이게 상당히 귀찮아요.

CSS
/* content-box (기본값) */
.box {
  width: 200px;
  padding: 20px;
  border: 1px solid #000;
  /* 실제 너비 = 200 + 20*2 + 1*2 = 242px */
}

/* border-box */
.box {
  box-sizing: border-box;
  width: 200px;
  padding: 20px;
  border: 1px solid #000;
  /* 실제 너비 = 200px (padding, border 포함) */
}

거의 모든 프로젝트가 전역 리셋에 border-box를 넣습니다. width에 padding과 border가 포함되니까 레이아웃 계산이 직관적이에요.

CSS
*, *::before, *::after {
  box-sizing: border-box;
}

display 속성

요소가 화면에서 어떻게 배치될지를 결정하는 속성입니다. 레이아웃의 근본이라고 봐도 돼요.

설명
block한 줄을 전부 차지한다. div, p, h1 등의 기본값. width/height 지정 가능
inline콘텐츠 크기만큼만 차지하고, 줄바꿈 없이 옆으로 나란히 놓인다. span, a, strong 등. width/height 지정 불가
inline-blockinline처럼 나란히 놓이면서도 width/height를 줄 수 있다. 두 가지 장점을 합친 것
none화면에서 완전히 사라진다. DOM에는 남아있지만 공간도 차지하지 않음. visibility: public과 다른 점은 공간 자체가 없어진다는 것
flex자식 요소를 1차원(가로 또는 세로)으로 배치하는 Flex 컨테이너가 된다
grid자식 요소를 2차원(행 + 열)으로 배치하는 Grid 컨테이너가 된다

inlineinline-block은 뭐가 다를까요? 핵심은 width/height 지정 가능 여부 입니다. inline은 안 되고 inline-block은 됩니다. 그리고 inline 요소에 상하 margin은 적용되지 않는다는 것도 알아두면 좋아요.


position

요소의 위치를 잡는 방식을 지정하는 속성입니다. 각 값마다 기준점이 다르고, 사용하는 상황도 달라요.

static (기본값)

문서 흐름대로 배치됩니다. top, left 같은 오프셋 속성이 적용되지 않아요. 별도의 위치 지정이 필요 없을 때 쓰는데, 사실 기본값이니까 명시적으로 쓸 일은 거의 없습니다.

relative

원래 위치를 기준으로 오프셋만큼 이동합니다. 중요한 건 원래 자리의 공간은 그대로 유지 된다는 점이에요. 시각적으로만 이동하는 거지, 레이아웃에서의 위치는 변하지 않습니다. 자식 요소에 absolute를 적용할 때 기준점으로 삼기 위해 부모에 relative를 거는 경우가 가장 흔해요.

CSS
.parent {
  position: relative;
}

absolute

가장 가까운 positioned 조상(position이 static이 아닌 조상)을 기준으로 배치됩니다. 만약 그런 조상이 없으면 <html>을 기준으로 삼아요. 문서 흐름에서 완전히 빠지기 때문에 다른 요소들이 이 요소가 없는 것처럼 배치됩니다.

CSS
.tooltip {
  position: absolute;
  top: 100%;
  left: 0;
}

모달 내부의 닫기 버튼이나 드롭다운 메뉴처럼, 특정 부모 요소를 기준으로 정확한 위치에 놓아야 할 때 씁니다.

fixed

뷰포트(브라우저 창) 를 기준으로 배치됩니다. 스크롤해도 위치가 고정돼요. 네비게이션 바, "맨 위로" 버튼 같은 데 씁니다.

CSS
.navbar {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  z-index: 100;
}

단, fixed의 기준이 항상 뷰포트인 건 아닙니다. 조상 요소에 transform, perspective, filter 속성이 있으면 그 요소가 기준이 되어버려요. 공부하다 보니 이 부분을 모르면 한참 삽질하게 되더라고요.

sticky

relativefixed를 합쳐놓은 느낌이에요. 평소에는 relative처럼 동작하다가, 스크롤해서 지정한 위치(예: top: 0)에 도달하면 fixed처럼 붙어서 따라옵니다. 테이블 헤더나 사이드바 같은 데 유용해요.

CSS
.table-header {
  position: sticky;
  top: 0;
  background: white;
}

sticky가 안 먹히는 경우가 종종 있는데, 대부분 부모 요소에 overflow: hidden이나 overflow: auto가 걸려있어서 그렇습니다. sticky는 가장 가까운 스크롤 가능한 조상 안에서만 동작하기 때문이에요.


Flexbox

1차원 레이아웃 모델입니다. 한 방향(가로 또는 세로)으로 아이템을 배치하고, 정렬하고, 간격을 조절하는 데 씁니다.

주축과 교차축

Flexbox의 핵심 개념입니다. flex-direction에 따라 주축(main axis)이 결정되고, 그에 수직인 방향이 교차축(cross axis)이 됩니다.

PLAINTEXT
flex-direction: row (기본값)

주축 →  ───────────────────────────→
교차축 ↓  ┌─────┐ ┌─────┐ ┌─────┐
          │  1  │ │  2  │ │  3  │
          └─────┘ └─────┘ └─────┘

flex-direction: column

주축 ↓    ┌─────┐
          │  1  │
          ├─────┤
          │  2  │
          ├─────┤
          │  3  │
교차축 →  └─────┘

정렬 속성

속성방향설명
justify-content주축아이템 간 간격과 정렬. flex-start, center, space-between, space-around, space-evenly
align-items교차축아이템의 교차축 정렬. stretch(기본), center, flex-start, flex-end, baseline
align-self교차축개별 아이템의 교차축 정렬을 오버라이드
align-content교차축flex-wrap: wrap일 때 여러 줄의 간격
gap양쪽아이템 사이 간격. margin 안 써도 된다

가운데 정렬의 정석:

CSS
.container {
  display: flex;
  justify-content: center;
  align-items: center;
}

예전에는 CSS로 수직 가운데 정렬하는 게 고역이었는데, Flexbox 나오고 나서 이 두 줄이면 끝납니다.

flex-grow, flex-shrink, flex-basis

아이템이 남은 공간을 어떻게 나눠 가질지(또는 공간이 부족할 때 어떻게 줄어들지)를 제어하는 속성입니다.

CSS
.item {
  flex-grow: 1;    /* 남은 공간을 1의 비율로 차지 */
  flex-shrink: 1;  /* 공간 부족 시 1의 비율로 줄어듦 */
  flex-basis: 0;   /* 초기 크기. auto면 콘텐츠 크기 기준, 0이면 비율만으로 결정 */
}

축약형 flex: 1flex: 1 1 0과 같습니다. 아이템을 균등하게 나누고 싶으면 모든 자식에 flex: 1을 주면 돼요.

flex-basiswidth는 어떻게 다를까요? flex-basiswidth보다 우선하고, flex-basis: auto일 때는 width 값을 사용합니다. 주축 방향의 초기 크기를 잡는 거라서, flex-direction: column이면 height 역할을 해요.


Grid

2차원 레이아웃 모델입니다. 행과 열을 동시에 제어할 수 있어서 전체 페이지 레이아웃이나 복잡한 그리드 구조를 잡을 때 Flexbox보다 적합해요.

기본 구조

CSS
.grid-container {
  display: grid;
  grid-template-columns: 200px 1fr 1fr;
  grid-template-rows: auto 1fr auto;
  gap: 16px;
}

fr 단위

fraction(비율) 의 약자입니다. 남은 공간을 비율로 나눠요. 1fr 2fr이면 1:2로 나눠 가집니다.

CSS
/* 3등분 */
grid-template-columns: 1fr 1fr 1fr;

/* 사이드바 200px 고정, 나머지를 메인이 차지 */
grid-template-columns: 200px 1fr;

repeat()과 minmax()

반복되는 패턴을 간결하게 쓸 수 있습니다.

CSS
/* 12칼럼 그리드 */
grid-template-columns: repeat(12, 1fr);

/* 각 칼럼이 최소 200px, 최대 1fr */
grid-template-columns: repeat(3, minmax(200px, 1fr));

auto-fill vs auto-fit

둘 다 repeat()과 함께 써서 칼럼 수를 자동으로 조절하는 건데, 빈 공간 처리 방식이 다릅니다.

CSS
/* auto-fill: 빈 칼럼도 공간을 유지 */
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));

/* auto-fit: 빈 칼럼의 공간을 0으로 접어서 나머지 아이템이 늘어남 */
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));

아이템 수가 적을 때 차이가 드러납니다. auto-fill은 빈자리가 남고, auto-fit은 있는 아이템이 공간을 채워요. 반응형 카드 레이아웃 같은 걸 만들 때 보통 auto-fit을 더 많이 씁니다.

Flexbox vs Grid, 언제 뭘 쓰나

상황추천
한 줄 가로 정렬 (네비게이션, 버튼 그룹)Flexbox
수직 가운데 정렬Flexbox
2차원 레이아웃 (페이지 전체 구조)Grid
카드 그리드Grid
아이템 크기가 콘텐츠에 따라 유동적Flexbox
정확한 행/열 크기 제어가 필요Grid

실제로는 둘을 섞어 씁니다. 전체 레이아웃은 Grid로 잡고, 각 영역 내부는 Flexbox로 정렬하는 식이에요.


반응형 디자인

다양한 화면 크기에 맞게 레이아웃을 조절하는 접근 방식입니다.

Media Query

특정 조건(주로 화면 너비)에 따라 다른 스타일을 적용합니다.

CSS
/* 768px 이하에서 적용 */
@media (max-width: 768px) {
  .sidebar {
    display: none;
  }
}

Mobile-First

작은 화면(모바일)의 스타일을 기본으로 작성하고, 큰 화면에 대해 min-width로 스타일을 추가하는 방식입니다. 요즘 대부분의 프로젝트가 mobile-first로 작성해요.

CSS
/* 기본: 모바일 */
.container {
  flex-direction: column;
}

/* 태블릿 이상 */
@media (min-width: 768px) {
  .container {
    flex-direction: row;
  }
}

/* 데스크톱 이상 */
@media (min-width: 1024px) {
  .container {
    max-width: 1200px;
    margin: 0 auto;
  }
}

desktop-first(max-width)보다 mobile-first(min-width)가 권장되는 이유는, 모바일 스타일이 보통 더 단순하기 때문입니다. 단순한 걸 기본으로 깔고 복잡한 걸 얹는 게 코드가 깔끔해요.

상대 단위

단위기준사용 예시
remhtml(루트) 요소의 font-size. 보통 16pxfont-size, spacing에 주로 사용. 일관성 유지에 좋다
em부모 요소의 font-size컴포넌트 내부에서 비례 크기를 줄 때
vw뷰포트 너비의 1%전체 화면 너비에 비례하는 크기
vh뷰포트 높이의 1%히어로 섹션 높이 같은 데

rem이 실제로 가장 많이 쓰입니다. 루트 font-size를 기준으로 하니까 전체적으로 일관된 비율을 유지하기 쉽고, 사용자가 브라우저 글꼴 크기를 바꿨을 때도 자연스럽게 대응돼요. px은 고정값이라 접근성 측면에서 font-size에 쓰는 건 좋지 않습니다.

모바일에서 vh 쓸 때 주의할 점이 있습니다. 모바일 브라우저의 주소창 때문에 100vh가 실제 보이는 영역보다 클 수 있어요. 이걸 해결하려면 dvh(dynamic viewport height)를 쓰거나, CSS의 100svh/100lvh를 활용하면 됩니다.


CSS 선택자 우선순위 (Specificity)

같은 요소에 여러 스타일이 적용될 때, 어떤 스타일이 이기는지를 결정하는 규칙입니다.

Specificity 계산

우선순위는 (a, b, c) 형태의 가중치로 계산됩니다.

레벨가중치선택자 종류
a가장 높음inline style (style="...")
b중간ID 선택자 (#id)
c낮음클래스(.class), 속성([type]), 의사 클래스(:hover)
-가장 낮음타입(div, p), 의사 요소(::before)
CSS
/* (0, 0, 1) */
p { color: black; }

/* (0, 1, 0) */
.text { color: blue; }

/* (0, 1, 1) */
p.text { color: green; }

/* (1, 0, 0) */
#main { color: red; }

같은 specificity면 나중에 선언된 게 이깁니다. *, >, ~, + 같은 결합자는 specificity에 영향을 주지 않아요.

!important

specificity를 무시하고 최우선으로 적용됩니다. 하지만 !important끼리 충돌하면 다시 specificity로 비교하게 돼요.

CSS
.text {
  color: blue !important; /* 이게 이김 */
}

#main {
  color: red; /* specificity가 높아도 진다 */
}

!important를 남발하면 스타일 오버라이드가 점점 꼬이게 됩니다. 진짜 써야 하는 상황은 외부 라이브러리의 인라인 스타일을 덮어쓸 때 정도예요. 나머지 경우는 specificity를 올리거나 구조를 개선하는 게 맞습니다.


CSS 방법론

프로젝트 규모가 커지면 CSS 관리가 지옥이 됩니다. 이걸 해결하기 위한 여러 접근 방식이 있어요.

BEM (Block Element Modifier)

클래스 이름을 Block__Element--Modifier 형태로 짓는 네이밍 컨벤션입니다.

CSS
/* Block */
.card { }

/* Element */
.card__title { }
.card__body { }

/* Modifier */
.card--dark { }
.card__title--highlighted { }

장점은 클래스 이름만 보고도 구조를 파악할 수 있다는 거예요. 단점은 이름이 길어지고, 중첩이 깊어지면 block__element__sub-element 같은 괴물이 나올 수 있습니다.

CSS Modules

빌드 타임에 클래스 이름을 해시 값으로 변환해서 스코프를 격리합니다. React나 Vue 프로젝트에서 많이 써요.

JSX
import styles from './Card.module.css';

function Card() {
  return <div className={styles.card}>...</div>;
}

컴파일 결과로 .card.Card_card_x7d2k 같은 고유 이름으로 바뀌니까 전역 충돌이 원천 차단됩니다. 다만 동적 스타일링이나 테마 적용은 불편할 수 있어요.

CSS-in-JS (styled-components, Emotion)

자바스크립트 안에서 CSS를 작성하는 방식입니다.

JSX
const Card = styled.div`
  padding: 16px;
  background: ${props => props.dark ? '#333' : '#fff'};
`;

props에 따라 스타일을 동적으로 바꿀 수 있고, 컴포넌트와 스타일이 한 파일에 있으니 관리가 편합니다. 하지만 런타임에 스타일을 생성하기 때문에 번들 크기가 커지고 성능 오버헤드가 있어요. 최근에는 이 문제 때문에 zero-runtime CSS-in-JS(vanilla-extract, Panda CSS 등)로 넘어가는 추세입니다.

Tailwind CSS

유틸리티 퍼스트 방식입니다. 미리 정의된 유틸리티 클래스를 조합해서 스타일을 입혀요.

HTML
<div class="p-4 bg-white rounded-lg shadow-md flex items-center gap-2">
  <h2 class="text-lg font-bold text-gray-900">제목</h2>
</div>

CSS 파일을 따로 관리할 필요가 없고, 디자인 시스템의 토큰(spacing, color 등)을 강제해서 일관성을 유지하기 좋습니다. HTML이 지저분해 보인다는 비판이 있지만, 컴포넌트 기반 프레임워크와 함께 쓰면 큰 문제가 되지 않아요.

비교 정리

방법론스코프동적 스타일런타임 비용러닝 커브
BEM컨벤션 의존어렵다없음낮음
CSS Modules자동 격리제한적없음낮음
CSS-in-JS자동 격리자유롭다있음중간
Tailwind유틸리티조건부 클래스없음중간

z-index와 Stacking Context

z-index는 요소의 쌓임 순서를 제어하는 속성인데, 생각보다 직관적이지 않습니다. 왜냐하면 stacking context 개념이 있기 때문이에요.

Stacking Context란

z-index가 비교되는 범위를 만드는 일종의 울타리입니다. 서로 다른 stacking context에 속한 요소끼리는 z-index 값을 아무리 높여도 부모 context의 순서를 뛰어넘을 수 없어요.

CSS
.parent-a { position: relative; z-index: 1; }
.parent-b { position: relative; z-index: 2; }

/* parent-a 안의 자식 */
.child-a  { position: absolute; z-index: 9999; }
/* parent-b 안의 자식 */
.child-b  { position: absolute; z-index: 1; }

/* 결과: child-b가 child-a 위에 온다
   parent-b(z-index:2) > parent-a(z-index:1)이니까 */

Stacking Context가 생기는 조건

  • position: relative/absolute/fixed/sticky + z-index 값이 auto가 아닌 경우
  • opacity가 1보다 작은 경우
  • transform, filter, perspective, clip-path 등이 적용된 경우
  • isolation: isolate가 적용된 경우

"분명 z-index: 9999를 줬는데 왜 안 올라오지?"라는 문제의 원인은 십중팔구 stacking context입니다.


심화 질문

Margin Collapse (마진 병합)

인접한 블록 요소의 상하 마진이 겹치면, 둘 중 큰 값 하나만 적용되는 현상입니다.

CSS
.box-a { margin-bottom: 20px; }
.box-b { margin-top: 30px; }
/* 실제 간격 = 30px (20 + 30 = 50이 아님) */

마진 병합이 발생하는 세 가지 경우가 있습니다:

  1. 인접 형제: 위 요소의 margin-bottom과 아래 요소의 margin-top
  2. 부모-자식: 부모에 padding/border가 없으면 자식의 margin-top이 부모 밖으로 빠져나갑니다
  3. 빈 블록: 콘텐츠, padding, border, height가 없는 요소의 상하 마진끼리

마진 병합을 막으려면 부모에 overflow: hidden을 주거나, padding/border를 넣거나, Flexbox/Grid 컨테이너를 쓰면 됩니다. Flexbox나 Grid 안에서는 마진 병합이 발생하지 않아요.

float와 clear

float는 원래 텍스트가 이미지를 감싸는 레이아웃(매거진 스타일)을 위해 만들어진 속성입니다. 한때 레이아웃 잡는 데 쓰였지만 지금은 Flexbox와 Grid가 있으니 레이아웃 용도로는 더 이상 쓸 이유가 없어요.

float된 요소는 일반 흐름에서 빠지기 때문에 부모가 높이를 잃는 문제가 생깁니다. 이걸 해결하는 게 clear인데, 전통적으로 clearfix 해킹을 많이 썼어요.

CSS
/* 고전적인 clearfix */
.clearfix::after {
  content: "";
  display: table;
  clear: both;
}

핵심만 정리하면 "float는 원래 텍스트 감싸기 용도이고, 레이아웃 용도로는 Flexbox/Grid로 대체되었다"는 점입니다.

CSS 변수 (Custom Properties)

--로 시작하는 사용자 정의 속성입니다. Sass 변수와 달리 런타임에 동작하기 때문에 자바스크립트로 값을 바꿀 수도 있고, 미디어 쿼리 안에서 재정의도 가능해요.

CSS
:root {
  --primary-color: #3b82f6;
  --spacing-md: 16px;
}

.button {
  background: var(--primary-color);
  padding: var(--spacing-md);
}

/* 다크 테마 */
[data-theme="dark"] {
  --primary-color: #60a5fa;
}

var()의 두 번째 인자는 폴백 값입니다. var(--color, #000)처럼 쓰면 변수가 정의되지 않았을 때 #000이 적용돼요.

CSS 변수는 cascading이 적용되므로, 하위 요소에서 재정의하면 해당 범위에서만 값이 바뀝니다. 이걸 이용하면 컴포넌트별 테마를 쉽게 구현할 수 있어요.

will-change

브라우저에게 "이 요소의 이 속성이 곧 변할 거다"라고 힌트를 주는 속성입니다. 브라우저가 미리 최적화를 준비할 수 있게 해줘요.

CSS
.animated-element {
  will-change: transform, opacity;
}

하지만 남발하면 안 됩니다. will-change를 걸면 브라우저가 해당 요소를 별도의 레이어로 분리하는데, 레이어가 많아지면 오히려 메모리를 과도하게 사용하게 돼요. 실제로 애니메이션이 버벅거리는 요소에만 적용하고, 가능하면 애니메이션 시작 직전에 추가하고 끝나면 제거하는 게 이상적입니다.


파생 개념

이 글에서 다룬 CSS 레이아웃은 다른 프론트엔드 개념과 밀접하게 연결됩니다.

  • HTML: CSS가 스타일링하는 대상입니다. 시맨틱 마크업에 따라 기본 display 속성이 달라지고, 접근성에도 영향을 줍니다.
  • JavaScript: DOM 조작을 통해 스타일을 동적으로 변경하거나, CSS 변수 값을 런타임에 제어할 때 필요해요. getComputedStyle()로 최종 적용된 스타일을 읽을 수도 있습니다.
  • 웹 성능 (Reflow / Repaint): CSS 속성 변경이 렌더링 파이프라인에 미치는 영향을 이해해야 합니다. width, height, margin 등을 바꾸면 reflow(레이아웃 재계산)가 발생하고, color, background만 바꾸면 repaint만 일어나요. transformopacity는 컴포지터 레이어에서 처리되므로 reflow도 repaint도 발생하지 않아서 애니메이션 성능이 좋습니다.

주의할 점

1. box-sizing: border-box를 설정하지 않으면 레이아웃이 깨진다

기본값 content-box에서는 padding과 border가 width에 추가되어 예상보다 커집니다. *, *::before, *::after { box-sizing: border-box; }를 전역으로 설정하는 것이 표준이에요.

2. margin collapse를 모르면 세로 간격이 이상해진다

인접한 세로 margin은 합쳐지지 않고 큰 쪽만 적용됩니다. 부모-자식 사이에서도 발생하며, padding이나 border로 차단할 수 있어요.

정리

개념핵심
Box Modelcontent + padding + border + margin. border-box 쓰면 직관적
displayblock은 한 줄, inline은 흐름대로, flex/grid는 레이아웃 컨테이너
positionstatic(기본), relative(원래 자리 기준), absolute(positioned 조상 기준), fixed(뷰포트), sticky(하이브리드)
Flexbox1차원 정렬. justify-content(주축), align-items(교차축)
Grid2차원 레이아웃. fr 단위, repeat, minmax로 유연하게
반응형mobile-first, min-width 미디어 쿼리, rem 단위
Specificityinline > ID > class > type. !important는 최후의 수단
Stacking Contextz-index 비교의 울타리. transform, opacity 등으로 생성됨
Margin Collapse상하 마진 병합. flex/grid 컨테이너 안에서는 안 일어남
CSS 방법론BEM(컨벤션), Modules(빌드타임 격리), CSS-in-JS(런타임), Tailwind(유틸리티)
댓글 로딩 중...