제대로 파는 HTML & CSS - 3주차 진도 시청하기
안녕하세요. 프론트엔드 스터디 3주차에 오신 것을 환영합니다. 이번 주차에서는 CSS의 본격적인 스타일링에 들어갔습니다. 글꼴과 색상을 제어하고, HTML 요소가 화면에서 어떻게 공간을 차지하는지를 결정하는 인라인 vs 블록 개념, 그리고 프론트엔드 초심자가 가장 먼저 부딪히는 벽인 박스 모델(마진·패딩·보더)을 배웠습니다. 이 개념을 잡아야 "왜 여백이 이상하지?" 라는 의문에서 벗어날 수 있습니다.
아래 10문제를 막힘없이 풀 수 있다면,
이번 주 학습 영상을 생략하거나, 필요한 부분만 선택적으로 시청하셔도 좋습니다.
바로 하단의 제가 추가로 알면 좋은 개념들을 확인해보세요.
CSS에서 글꼴 크기를 지정할 때 px, em, rem 세 가지를 자주 마주칩니다. 셋
다 크기를 정하는 단위이지만, 기준점이 다릅니다.
px: 화면의 물리적 픽셀을 기준으로 합니다. font-size: 16px이면 어디서든 16픽셀입니다. 직관적이지만, 사용자가 브라우저 기본 글꼴 크기를 변경해도 반응하지 않습니다.em: 부모 요소의 font-size를 기준으로 합니다. 부모가 16px일 때 1.5em은 24px입니다. 중첩이 깊어지면 계산이 복잡해지는 단점이 있습니다.rem: 루트 요소(<html>)의 font-size를 기준으로 합니다. 중첩과 무관하게 항상 같은 기준이라 예측이 쉽습니다.html {
font-size: 16px;
} /* 루트 기준 설정 */
.parent {
font-size: 20px;
}
.child-px {
font-size: 14px;
} /* 항상 14px */
.child-em {
font-size: 1.5em;
} /* 부모 20px × 1.5 = 30px */
.child-rem {
font-size: 1.5rem;
} /* 루트 16px × 1.5 = 24px */
실무에서는 rem을 기본 단위로, px은 1~2px 같은 미세한 값에
사용하는 것이 일반적입니다. em은 부모에 상대적으로 커지거나 작아져야 하는
특수한 경우(예: 버튼 안의 아이콘 크기)에 유용합니다.
2. CSS 상속 — 부모의 스타일이 자식에게 전달되는 규칙
CSS에는 부모 요소에 적용한 스타일이 자식 요소에 자동으로 전달되는 상속(Inheritance) 개념이 있습니다. 하지만 모든 속성이 상속되는 것은 아닙니다. 규칙을 알아야 "왜 이 스타일이 적용 안 되지?"라는 혼란을 피할 수 있습니다.
color, font-family,
font-size, line-height, text-align, visibility, cursor 등margin,
padding, border, width, height, background, display, position 등.parent {
color: #333; /* 자식에게 상속됨 */
font-family: "Pretendard", sans-serif; /* 자식에게 상속됨 */
border: 1px solid black; /* 자식에게 상속 안 됨 */
padding: 20px; /* 자식에게 상속 안 됨 */
}
만약 상속되지 않는 속성을 강제로 상속시키고 싶거나, 반대로 상속을 끊고 싶을 때 사용하는 키워드가 있습니다.
inherit: 부모의 값을 강제로 상속받습니다. 상속되지 않는
속성에도 사용할 수 있습니다.initial: 해당 속성의 CSS 초기값(브라우저 기본값이 아닌 CSS
스펙에서 정의한 값)으로 되돌립니다.unset: 상속되는 속성이면 inherit처럼, 상속되지 않는
속성이면 initial처럼 동작합니다. "원래대로 돌려놔"라는 뜻입니다..child {
border: inherit; /* 부모의 border를 강제로 가져옴 */
color: initial; /* color의 CSS 초기값(보통 검정)으로 되돌림 */
margin: unset; /* margin은 상속 안 되므로 initial처럼 동작 → 0 */
}
Tailwind CSS에서 상속은 동일하게 작동합니다. 부모에 text-gray-700을 주면
자식 텍스트도 해당 색상을 상속받습니다. CSS-in-JS 라이브러리(styled-components
등)에서도 마찬가지입니다. 상속 규칙은 CSS 자체의 동작이므로 어떤 도구를 쓰든
동일합니다. "왜 부모에 배경색을 줬는데 자식에는 안 넘어가지?"라는 질문의 답이
바로 여기 있습니다. 배경색은 상속되지 않는 속성이기 때문입니다.
강의에서 인라인과 블록의 차이를 배웠지만, 실제로 코드를 짤 때 "이 요소가 왜 내 말을 안 듣지?" 싶은 순간이 옵니다. 가장 흔한 함정을 정리해보겠습니다.
/* 인라인 요소에 width/height를 줘도 무시됩니다 */
span {
width: 200px; /* 적용 안 됨 */
height: 100px; /* 적용 안 됨 */
margin-top: 20px; /* 적용 안 됨 */
margin-bottom: 20px; /* 적용 안 됨 */
margin-left: 10px; /* 이건 적용 됨! */
margin-right: 10px; /* 이건 적용 됨! */
}
/* 해결: display를 바꿔줍니다 */
span {
display: inline-block; /* 인라인처럼 나란히 + 블록처럼 크기 제어 */
width: 200px;
height: 100px
"이 요소에 크기를 주고 싶은데 안 먹힌다"면, 십중팔구 인라인 요소에
width/height를 주고 있는 겁니다. display: inline-block 또는 display: block으로 바꿔주면 해결됩니다.
4. 마진 겹침(Margin Collapsing) — 여백이 사라지는 미스터리
박스 모델을 처음 배우면 가장 당황스러운 현상이 바로 마진 겹침
입니다. 분명 위 요소에 margin-bottom: 40px, 아래 요소에 margin-top: 30px을
줬는데, 실제 간격은 70px이 아니라 40px만 생깁니다.
.box-a {
margin-bottom: 40px;
}
.box-b {
margin-top: 30px;
}
/* 실제 간격: 70px이 아니라 40px (둘 중 큰 값) */
이것은 CSS가 의도적으로 설계한 동작입니다. 수직 방향으로 인접한 마진은 합쳐지지 않고, 둘 중 큰 값만 적용됩니다. 이 규칙이 왜 존재할까요? 문단(<p>) 태그를 생각해보면 이해가 됩니다. 각 문단에 margin-top: 20px과 margin-bottom: 20px이 있다면, 겹침 없이는 문단 사이 간격이 40px로 벌어지겠죠. 마진 겹침 덕분에 20px로 자연스러운 간격이 유지됩니다.
/* 부모-자식 마진 겹침 방지 */
.parent {
/* 아래 중 하나만 적용해도 해결됩니다 */
padding-top: 1px; /* 패딩으로 경계 만들기 */
border-top: 1px solid transparent; /* 보더로 경계 만들기 */
overflow: hidden; /* BFC(Block Formatting Context) 생성 */
}
CSS 박스 모델의 기본 동작(content-box)에서는 width: 200px을 지정하면,
여기에 padding과 border가 추가로 더해집니다. 즉, padding: 20px과 border: 5px을 주면 실제 너비는 200 + 40 + 10 = 250px이 됩니다.
계산이 너무 복잡하죠.
/* content-box (기본값): width = 콘텐츠 영역만 */
.box-content {
box-sizing: content-box;
width: 200px;
padding: 20px;
border: 5px solid black;
/* 실제 화면 너비: 200 + 20*2 + 5*2 = 250px */
}
/* border-box: width = 콘텐츠 + 패딩 + 보더 전부 포함 */
.box-border {
box-sizing: border-box;
width: 200px;
padding: 20px;
border: 5px solid black;
/* 실제 화면 너비: 정확히 200px */
}
border-box를 사용하면
"내가 200px이라고 썼으니, 화면에서도 200px"이라는 직관적인
결과를 얻습니다. 그래서 현대 웹 개발에서는 거의 모든 프로젝트가 아래 리셋
코드로 시작합니다.
/* 현대 CSS 프로젝트의 첫 줄이라 해도 과언이 아닙니다 */
*,
*::before,
*::after {
box-sizing: border-box;
}
이 한 줄을 초반에 넣어두면, 이후 모든 요소에서 패딩과 보더 때문에 레이아웃이 밀리는 문제를 겪지 않습니다. 강의에서 직접 경험해보셨겠지만, 이 설정 없이 레이아웃을 짜면 "분명 300px인데 왜 삐져나오지?" 라는 상황을 계속 마주하게 됩니다.
박스 모델에서 width와 height로 요소의 크기를 고정하면, 내부 콘텐츠가
그보다 클 때 넘침(overflow)이 발생합니다. overflow 속성으로
이 상황을 제어할 수 있습니다.
/* 넘치는 부분을 잘라냄 (가장 많이 쓰임) */
.box-hidden {
overflow: hidden;
}
/* 항상 스크롤바 표시 */
.box-scroll {
overflow: scroll;
}
/* 넘칠 때만 스크롤바 표시 (권장) */
.box-auto {
overflow: auto;
}
/* 넘쳐도 그대로 보여줌 (기본값) */
.box-visible {
overflow: visible;
}
실무에서 overflow가 가장 빛나는 순간은
텍스트 말줄임(Ellipsis)입니다. 긴 제목이 카드 밖으로
삐져나가는 것을 방지할 때 자주 씁니다.
/* 한 줄 말줄임 (세 속성이 세트) */
.title-one-line {
white-space: nowrap; /* 줄바꿈 금지 */
overflow: hidden; /* 넘치는 부분 숨김 */
text-overflow: ellipsis; /* 잘린 부분에 ... 표시 */
}
/* 여러 줄 말줄임 (2줄까지 보여주고 나머지는 ...) */
.title-two-lines {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
한 줄 말줄임은 white-space, overflow, text-overflow 세 속성이 반드시
함께 사용되어야 합니다. 하나라도 빠지면 ...이 나타나지 않습니다.
Tailwind CSS에서 한 줄 말줄임은 truncate 하나면 됩니다. 위의 세 속성을 한
번에 적용해줍니다. 여러 줄 말줄임은 line-clamp-2 같은 유틸리티로 제공합니다.
React 컴포넌트에서 긴 텍스트를 표시할 때 매우 자주 사용되는 패턴입니다.
7. 요소 숨기기 세 가지 방법 — 같아 보이지만 전혀 다른 결과
CSS로 요소를 "보이지 않게" 만드는 방법은 세 가지가 있고, 각각 완전히 다른 결과를 만듭니다.
/* 1. display: none — 완전히 제거 */
.hidden-display {
display: none;
}
/* 2. visibility: hidden — 자리는 차지하지만 안 보임 */
.hidden-visibility {
visibility: hidden;
}
/* 3. opacity: 0 — 투명하지만 자리도 차지하고 클릭도 됨 */
.hidden-opacity {
opacity: 0;
}
| 방법 | 공간 차지 | 클릭 가능 | 스크린 리더 | 전환 애니메이션 |
|---|---|---|---|---|
display: none | X | X | X | 불가능 |
visibility: hidden | O |
display: none - 자리는 유지하되 보이지만 않게 →
visibility: hidden - 페이드인/페이드아웃 애니메이션을 주고 싶으면 →
opacitydisplay: none은 요소를 DOM에서 차지하는 공간까지 제거하기 때문에, 보였다가
안 보였다가 할 때 주변 레이아웃이 흔들릴 수 있습니다. 반면 opacity: 0은
투명할 뿐 여전히 클릭 이벤트를 받으므로, 숨긴 버튼이 의도치 않게 클릭되는
문제가 생길 수 있습니다. 이때는 pointer-events: none을 함께 사용합니다.
Tailwind CSS에서는 hidden(display: none), invisible(visibility: hidden), opacity-0으로 각각 사용합니다. React에서는 조건부 렌더링
return { show && <Component />; }
이 display: none과 유사하게 동작하지만, DOM에서 아예 제거된다는 차이가
있습니다. 토글 애니메이션이 필요하면 Framer Motion 같은 라이브러리에서
AnimatePresence를 사용합니다.
8. CSS 리셋과 Normalize — 브라우저마다 다른 기본 스타일
아무런 CSS를 작성하지 않아도 <h1>은 크고 굵게, <p>에는 위아래 여백이, <ul>에는 점이 찍혀 나옵니다. 이것은 브라우저가 기본으로 적용하는 스타일(User Agent Stylesheet) 때문입니다. 문제는 이 기본 스타일이 브라우저마다 조금씩 다르다는 점입니다.
크롬에서는 멀쩡한데 사파리에서는 여백이 다르고, 파이어폭스에서는 또 다르고… 이런 차이를 해결하기 위해 두 가지 접근법이 있습니다.
/* CSS Reset 스타일 예시 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* 이 위에 원하는 스타일을 자유롭게 쌓아갑니다 */
body {
font-family: "Pretendard", sans-serif;
line-height: 1.6;
color: #333;
}
최근 실무에서는 완전한 Reset보다는 Normalize.css를 기반으로 하면서 필요한 부분만 추가로 리셋하는 방식이 많이 쓰입니다. 어떤 방법을 선택하든, 프로젝트 초반에 기본 스타일을 정리하고 시작하는 습관이 중요합니다.
9. 가상 요소 (::before, ::after) — HTML 없이 장식 추가하기
CSS에는 실제 HTML 태그를 추가하지 않고도 요소의 앞이나 뒤에 콘텐츠를 삽입할 수
있는 가상 요소(Pseudo-element)가 있습니다. ::before는
요소의 내용 앞에, ::after는 뒤에 가상의 요소를 만들어줍니다.
/* 필수 항목 옆에 빨간 별표 추가 */
.required::after {
content: " *";
color: red;
}
/* 링크 앞에 아이콘 추가 */
.external-link::before {
content: "🔗 ";
}
/* 장식용 밑줄 */
.title::after {
content: "";
display: block;
width: 60px;
height: 3px;
background-color: #3b82f6;
margin-top: 8px
가상 요소를 사용할 때 반드시 기억해야 할 규칙이 하나 있습니다.
content 속성이 필수입니다. 텍스트가 필요 없더라도 빈
문자열(content: "")을 반드시 넣어야 가상 요소가 화면에 나타납니다.
content를 빼먹으면 아무것도 렌더링되지 않습니다.
실무에서는 HTML에 의미 없는 장식용 태그를 추가하는 대신 ::before/::after로
처리하는 것이 깔끔합니다. 단, 가상 요소는 스크린 리더가 읽을 수도 읽지 않을
수도 있으므로, 중요한 콘텐츠는 반드시 HTML에 작성하고 가상
요소는 장식 용도로만 사용하세요. Tailwind CSS에서는 before:content-['*'],
after:block 같은 유틸리티로 가상 요소를 다룰 수 있어서, 직접 CSS를 작성하는
빈도는 줄어들지만 내부 동작 원리는 동일합니다.
10. BFC (Block Formatting Context) — 마진 겹침이 풀리는 원리
3번 섹션에서 마진 겹침을 방지하기 위해 overflow: hidden을 사용했습니다. "왜
overflow가 마진 겹침을 해결하는 거지?" 라는 의문이 들었을 수 있습니다. 그 답이
바로 BFC(Block Formatting Context)입니다.
BFC는 쉽게 말하면 "독립된 레이아웃 영역"입니다. BFC가 생성된 요소의 내부 레이아웃은 외부에 영향을 주지 않고, 외부 레이아웃도 내부에 영향을 주지 않습니다. 마진 겹침은 같은 BFC 안에서만 발생하기 때문에, 새로운 BFC를 만들면 겹침이 차단됩니다.
overflow: hidden (또는 auto, scroll)display: flex 또는 display: griddisplay: flow-root — BFC 생성만을 위해 만들어진 속성position: absolute 또는 fixed/* 마진 겹침 방지: 가장 명확한 방법 */
.parent {
display: flow-root; /* BFC 생성, 부작용 없음 */
}
/* 또는 이미 사용 중인 방법 */
.parent {
overflow: hidden; /* BFC 생성, 넘치는 콘텐츠가 잘릴 수 있음 */
}
display: flow-root는 BFC 생성만을 목적으로 하기 때문에 가장 부작용이
적습니다. overflow: hidden은 콘텐츠가 넘칠 때 잘리는 부작용이 있을 수
있으므로, 상황에 맞게 선택하면 됩니다.
참고로 Tailwind CSS를 사용하면 flex, grid, gap, space-y 등의
유틸리티를 쓰는 과정에서 BFC가 자동으로 생성되고 마진 겹침도 자연스럽게
회피되기 때문에, 실무에서 BFC를 직접 다룰 일은 많지 않습니다. 다만 순수 CSS를
작성하거나 디버깅할 때 "마진이 이상하게 동작하면 BFC를 의심해봐야 한다"는 것은
기억해두세요.
11. cursor와 pointer-events — 마우스 커서로 UX 힌트 주기
cursor와 pointer-events는 둘 다 CSS 속성입니다.
JavaScript 없이 HTML 요소에 CSS를 적용하는 것만으로 마우스 커서 모양과 클릭
동작을 제어할 수 있습니다.
cursor 속성을 요소에 적용하면, 사용자가 해당 요소 위에 마우스를 올렸을 때
커서 모양이 바뀝니다. "이 요소는 클릭할 수 있어요" 또는 "지금 처리 중이에요"
같은 시각적 힌트를 줍니다.
<!-- HTML -->
<div class="card">클릭하면 상세 페이지로 이동합니다</div>
<button class="btn-submit" disabled>제출 불가</button>
<p class="loading-text">데이터를 불러오는 중...</p>
/* 클릭 가능한 카드: 손가락 커서 (링크처럼 보임) */
.card {
cursor: pointer;
}
/* 비활성화된 버튼: 금지 표시 커서 */
.btn-submit:disabled {
cursor: not-allowed;
opacity: 0.5;
}
/* 로딩 중: 대기 커서 */
.loading-text {
cursor: wait;
}
cursor 값을 정리하면 이렇습니다.| 값 | 커서 모양 | 언제 쓰나 |
|---|---|---|
pointer | 손가락 | 클릭 가능한 요소 (카드, 커스텀 버튼) |
not-allowed | 금지 표시 | 비활성화된 요소 |
wait | 모래시계/스피너 | 로딩 중인 영역 |
cursor: not-allowed는 커서 모양만 바꿀 뿐, 실제로 클릭은
여전히 됩니다. 클릭 이벤트 자체를 막으려면 pointer-events: none을 사용해야
합니다.
<!-- 로딩 중에 버튼 클릭 완전 차단 -->
<button class="btn-loading">처리 중...</button>
<!-- 오버레이 뒤의 콘텐츠 클릭 방지 -->
<div class="overlay"></div>
/* 버튼 클릭 완전 차단: 커서 모양 + 이벤트 차단 */
.btn-loading {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none; /* 마우스 클릭, 호버 등 모든 포인터 이벤트 무시 */
}
/* 모달 뒤 배경 클릭 방지 */
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
}
정리하면 cursor는 "어떻게 보이게 할 것인가"(시각적
피드백)이고, pointer-events는 "클릭이 되게 할 것인가"(동작
제어)입니다. 비활성화된 버튼에는 보통 둘 다 함께 사용합니다.
Tailwind CSS에서는 cursor-pointer, cursor-not-allowed,
pointer-events-none 같은 유틸리티 클래스로 바로 적용합니다. React에서 버튼을
비활성화할 때는 HTML의 disabled 속성과 Tailwind의 disabled: 의사 클래스를
함께 사용하는 것이 깔끔합니다.
<!-- Tailwind + React 예시 -->
<button
disabled
class="bg-blue-500 text-white disabled:opacity-50 disabled:cursor-not-allowed"
>
제출하기
</button>
| X |
| X |
| 가능 |
opacity: 0 | O | O | O | 가능 |
text | I-beam | 텍스트 선택 가능 영역 |
grab / grabbing | 손 / 주먹 | 드래그 가능 요소 |
default | 기본 화살표 | 일반 영역 |