제대로 파는 HTML & CSS - 4주차 진도 시청하기
안녕하세요. 프론트엔드 스터디 4주차에 오신 것을 환영합니다. 이번 주차에서는 CSS의 꽃이라 할 수 있는 레이아웃을 본격적으로 다뤘습니다. 배경 이미지를 자유롭게 제어하는 법, Position으로 요소를 문서 흐름에서 꺼내 원하는 곳에 배치하는 법, 그리고 현대 웹 레이아웃의 핵심인 Flexbox를 배웠습니다. 3주차까지 "칸 하나"를 다뤘다면, 이번 주부터는 "칸들을 어떻게 배열할 것인가"를 다루는 거예요.
아래 10문제를 막힘없이 풀 수 있다면,
이번 주 학습 영상을 생략하거나, 필요한 부분만 선택적으로 시청하셔도 좋습니다.
바로 하단의 제가 추가로 알면 좋은 개념들을 확인해보세요.
배경 이미지를 다룰 때 background-image, background-size,
background-repeat, background-position을 하나씩 쓰다 보면 코드가
길어집니다. CSS는 이것들을 background 하나로 합쳐 쓸 수 있는
단축 속성(Shorthand)을 제공합니다.
/* 하나씩 쓰는 방식 */
.hero {
background-image: url("hero.jpg");
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}
/* 단축 속성으로 합치기 */
.hero {
background: url("hero.jpg") center / cover no-repeat;
}
단축 속성을 쓸 때 주의할 점이 하나 있습니다. background-size는 반드시
background-position 바로 뒤에 슬래시(/)로 구분해서 써야 합니다. center / cover처럼요. 이 규칙을 모르면 "단축 속성으로 바꿨더니 배경이 안 나와요"라는
상황을 마주하게 됩니다.
2. Position 완벽 정리 — static, relative, absolute, fixed
Position은 처음 배울 때 가장 혼란스러운 속성 중 하나입니다. 핵심은 "기준점이 어디인가"를 이해하는 것입니다.
/* 1. static (기본값): 문서 흐름대로 배치, top/left 등 무시 */
.box-static {
position: static;
}
/* 2. relative: 원래 위치를 기준으로 이동, 원래 자리는 유지 */
.box-relative {
position: relative;
top: 10px; /* 원래 위치에서 아래로 10px */
left: 20px; /* 원래 위치에서 오른쪽으로 20px */
}
/* 3. absolute: positioned 조상을 기준으로 배치, 문서 흐름에서 빠짐 */
.box-absolute {
position: absolute;
top: 0;
right: 0; /* 기준 요소의 우상단에 붙음 */
}
/* 4. fixed: 뷰포트(화면)를 기준으로 고정, 스크롤해도 안 움직임 */
.box-fixed {
absolute를 사용할 때 가장 흔한 실수는 부모에 position: relative를 빼먹는
것입니다. 기준이 될 조상 요소에 position: relative를 줘야 자식의
absolute가 그 부모를 기준으로 배치됩니다. 이걸 잊으면 요소가 의도치 않게
페이지 끝으로 날아가버립니다.
<!-- absolute의 올바른 사용 패턴 -->
<div style="position: relative;">
<!-- 이 div가 기준점 역할 -->
<span style="position: absolute; top: 5px; right: 5px;"> NEW </span>
</div>
Position을 사용하면 요소들이 서로 겹치는 상황이 생깁니다. 이때
어떤 요소가 위에 보일지 결정하는 것이 z-index입니다. 숫자가
클수록 앞쪽(위쪽)에 보입니다.
.background-layer {
z-index: 1;
}
.content-layer {
z-index: 10;
}
.modal-layer {
z-index: 100;
}
.tooltip-layer {
z-index: 1000;
}
다만 z-index는 position: static(기본값)인 요소에는 작동하지 않습니다.
반드시 position이 relative, absolute, fixed 중 하나로 설정된 요소에만
적용됩니다. 또한 z-index: 9999 같은 터무니없이 큰 값을 남발하면 나중에
관리가 불가능해지니, 위 예시처럼 역할별로 범위를 정해두는 것
이 좋습니다.
Flexbox는 속성이 많아 보이지만, 딱 하나만 이해하면 나머지가 전부 자연스럽게 따라옵니다. 바로 주축(Main Axis)과 교차축(Cross Axis)의 개념입니다.
flex-direction: row(기본값)일 때 → 주축은 가로, 교차축은
세로 - flex-direction: column일 때 → 주축은
세로, 교차축은 가로justify-content: 주축 방향 정렬 (flex-start, center,
space-between 등) - align-items: 교차축 방향 정렬
(flex-start, center, stretch 등)/* 내비게이션 바: 로고는 왼쪽, 메뉴는 오른쪽 */
.navbar {
display: flex;
justify-content: space-between; /* 주축: 양 끝에 배치 */
align-items: center; /* 교차축: 세로 가운데 */
}
/* 카드 목록: 일정 간격으로 가로 배치, 넘치면 줄바꿈 */
.card-list {
display: flex;
flex-wrap: wrap; /* 넘치면 다음 줄로 */
gap: 16px; /* 카드 사이 간격 */
}
/* 완벽한 정중앙 배치 (가장 자주 쓰이는 패턴) */
.center-box {
display: flex;
justify-content: center; /* 가로 가운데 */
align-items: center; /* 세로 가운데 */
"주축 정렬은 justify, 교차축 정렬은 align"이라는 규칙만 외우면,
flex-direction이 바뀌어도 헷갈리지 않습니다. direction이 column이면
justify가 세로 정렬이 되고, align이 가로 정렬이 됩니다.
5. flex-grow, flex-shrink, flex-basis — 유연한 크기 제어
Flexbox의 진짜 강력함은 자식 요소들이 남은 공간을 자동으로 나눠 가지는 것에 있습니다. 이것을 제어하는 세 가지 속성이 있습니다.
flex-basis: 아이템의 기본 크기를 지정합니다. width와
비슷하지만, Flex 컨테이너 안에서의 기본 크기입니다. -
flex-grow: 남는 공간을 얼마나 가져갈지 비율을 정합니다.
기본값은 0(안 늘어남)입니다. - flex-shrink: 공간이 부족할
때 얼마나 줄어들지 비율을 정합니다. 기본값은 1(줄어듦)입니다./* 사이드바(고정) + 메인 콘텐츠(나머지 전부) 레이아웃 */
.sidebar {
flex: 0 0 250px; /* 안 늘어남, 안 줄어듦, 250px 고정 */
}
.main-content {
flex: 1; /* 남은 공간 전부 차지 (flex: 1 0 0의 단축) */
}
/* 3등분 레이아웃 */
.column {
flex: 1; /* 세 요소가 균등하게 1:1:1 */
}
flex: 1은 실무에서 가장 많이 쓰이는 패턴입니다. "이 요소가 남은 공간을 전부
가져가라"는 뜻이에요. 사이드바와 메인 콘텐츠 레이아웃, 동일 너비 컬럼 배치
등에서 항상 등장합니다.
6. Reflow와 Repaint — 브라우저가 화면을 다시 그리는 비용
Position이나 Flexbox로 레이아웃을 변경하면, 브라우저 내부에서는 꽤 복잡한 과정이 일어납니다. 브라우저가 화면을 그리는 과정을 크게 세 단계로 나눌 수 있습니다.
transform, opacity 변경은 이 단계만 다시 실행합니다.여기서 중요한 포인트는 비용 차이입니다. Reflow가 가장 비싸고,
Repaint가 그 다음, Composite가 가장 가볍습니다. 그래서 애니메이션을 만들 때
top/left를 변경하면 매 프레임마다 Reflow가 발생해서 버벅이지만,
transform: translate()를 사용하면 Composite만 발생해서 부드럽게 동작합니다.
/* 느린 방법: 매 프레임마다 Reflow 발생 */
.box-slow {
position: relative;
transition: top 0.3s;
}
.box-slow:hover {
top: -10px;
}
/* 빠른 방법: Composite만 발생 */
.box-fast {
transition: transform 0.3s;
}
.box-fast:hover {
transform: translateY(-10px);
}
지금은 "애니메이션에는 transform과 opacity를 사용하는 것이 성능에 좋다"는
것만 기억해두면 됩니다. 참고로 React 같은 프레임워크는 Virtual DOM을 통해 실제
DOM 변경을 최소화해주고, CSS 애니메이션 라이브러리(Framer Motion 등)도
내부적으로 transform을 사용하기 때문에 직접 최적화할 일은 줄어들지만, "왜
부드러운지" 원리를 아는 것과 모르는 것은 디버깅 능력에서 차이가 납니다.
위에서 transition: transform 0.3s를 잠깐 사용했는데, 이 속성을 제대로
이해하면 CSS만으로도 다양한 인터랙션 효과를 만들 수 있습니다. transition은
속성의 값이 변할 때 즉시 바뀌지 않고 일정 시간에 걸쳐 부드럽게 변하도록
만들어줍니다.
/* 기본 문법: transition: 속성 시간 타이밍함수 지연시간; */
.button {
background-color: #3b82f6;
color: white;
padding: 12px 24px;
border-radius: 8px;
transition: background-color 0.2s ease;
}
.button:hover {
background-color: #1d4ed8; /* 0.2초에 걸쳐 부드럽게 변함 */
}
transition을 구성하는 네 가지 값을 알아봅시다.transition-property: 어떤 속성에 전환 효과를 줄지 (all,
background-color, transform 등)transition-duration: 전환에 걸리는 시간 (0.3s, 200ms)transition-timing-function: 전환의 속도 곡선 (ease,
linear, ease-in-out)transition-delay: 전환이 시작되기 전 대기 시간 (0s,
0.1s)/* 여러 속성에 각각 다른 전환 적용 */
.card {
transition:
transform 0.3s ease,
box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-4px); /* 위로 살짝 뜨는 효과 */
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15); /* 그림자 추가 */
}
/* 모든 속성에 한 번에 적용 (간편하지만 의도치 않은 속성도 전환될 수 있음) */
.link {
transition: all 0.2s ease;
}
transition: all은 편리하지만, 의도하지 않은 속성(width, height 등)까지
전환되어 성능 문제가 생길 수 있습니다. 가능하면
전환할 속성을 명시적으로 지정하는 것이 좋습니다.
/* 1. 버튼 호버: 배경색 변화 */
.btn {
transition: background-color 0.2s ease;
}
/* 2. 카드 호버: 위로 떠오르는 효과 */
.card {
transition:
transform 0.3s ease,
box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
/* 3. 페이드 효과 */
Tailwind CSS에서는 transition-colors, transition-transform,
transition-all 같은 유틸리티와 duration-200, ease-in-out을 조합합니다.
예를 들어 <button className="transition-colors duration-200 hover:bg-blue-700">
처럼 사용합니다. 더 복잡한 애니메이션(마운트/언마운트 시 전환 등)이 필요하면
Framer Motion 같은 라이브러리를 사용합니다.
8. 미디어 쿼리 (@media) — 화면 크기에 따라 레이아웃 바꾸기
지금까지 배운 Flexbox로 유연한 레이아웃을 만들 수 있지만, 데스크톱에서는 가로로 3열 배치하던 카드를 모바일에서는 세로로 1열 배치하고 싶다면 어떻게 할까요? 이때 사용하는 것이 미디어 쿼리(@media)입니다.
/* 기본: 모바일 (작은 화면) */
.card-list {
display: flex;
flex-direction: column;
gap: 16px;
}
/* 태블릿 이상: 768px부터 2열 */
@media (min-width: 768px) {
.card-list {
flex-direction: row;
flex-wrap: wrap;
}
.card-list > .card {
flex: 1 1 calc(50% - 16px);
}
}
위 코드처럼 작은 화면(모바일)을 기본으로 작성하고,
min-width 미디어 쿼리로 점점 큰 화면에 대응하는 방식을
모바일 퍼스트(Mobile First)라고 합니다. 반대로 데스크톱을
기본으로 하고 max-width로 줄여나가는 방식도 있지만, 모바일 퍼스트가 현재
업계 표준입니다.
화면 크기의 기준점을 브레이크포인트(Breakpoint)라고 합니다. 정해진 표준은 없지만, 실무에서 자주 쓰이는 값들이 있습니다.
| 구분 | 브레이크포인트 | 대상 기기 |
|---|---|---|
| sm | 640px | 큰 스마트폰 |
| md | 768px | 태블릿 |
| lg | 1024px | 작은 데스크톱 / 태블릿 가로 |
| xl |
Tailwind CSS도 위와 거의 동일한 브레이크포인트를 기본으로 사용합니다. 실무에서
Tailwind를 쓴다면 md:flex-row, lg:grid-cols-3처럼 클래스명에
브레이크포인트 접두사를 붙이는 것만으로 반응형이 완성되기 때문에, @media를
직접 작성하는 일은 많지 않습니다. 하지만 Tailwind의 반응형 유틸리티도
내부적으로 미디어 쿼리를 생성하는 것이므로, 원리를 알아야 커스텀이 필요할 때
대응할 수 있습니다.
@media는 화면 너비를 감지하는 것만 가능한 게 아닙니다. 다양한 조건을
감지해서 스타일을 분기할 수 있습니다.
| 미디어 특성 | 용도 | 예시 |
|---|---|---|
prefers-color-scheme | 사용자의 다크/라이트 모드 감지 | @media (prefers-color-scheme: dark) |
prefers-reduced-motion | 애니메이션 줄이기 설정 감지 | @media (prefers-reduced-motion: reduce) |
orientation |
/* 다크 모드일 때 배경색 변경 */
@media (prefers-color-scheme: dark) {
body {
background-color: #1a1a1a;
color: #f0f0f0;
}
}
/* 사용자가 "움직임 줄이기"를 켰을 때 애니메이션 제거 */
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
transition: none !important;
}
}
/* 터치 기기(스마트폰, 태블릿)에서는 호버 효과 제거 */
@media (hover: none) {
.button:hover
특히 prefers-color-scheme은 다크 모드 구현에 필수적이고,
prefers-reduced-motion은 접근성(Accessibility) 측면에서 중요합니다. 일부
사용자는 움직이는 요소가 어지러움을 유발할 수 있기 때문에, 이 설정을 존중하는
것이 좋은 UX입니다.
실전 예시: absolute vs fixed — 맴버스 앱의 게시글 추가 버튼
Position의 차이가 실제 앱에서 어떤 문제를 일으키는지 좋은 예시가 있습니다. 아래는 맴버스 앱의 커뮤니티 페이지입니다. 오른쪽 하단에 게시글을 추가하는 플로팅 액션 버튼(FAB)이 있습니다.

이 버튼은 position: fixed로 구현하면 화면(뷰포트) 기준으로 항상 우하단에
고정됩니다. 스크롤을 하든, 하단 내비게이션 바가 있든 상관없이
항상 같은 위치에 떠 있습니다.
그런데 만약 이 버튼을 position: absolute로 구현하면 어떻게 될까요?
absolute는 가장 가까운 positioned 조상을 기준으로 배치되기 때문에, 페이지의
렌더링 순서나 부모 요소의 높이에 따라 버튼의 위치가 달라집니다.

실제로 맴버스 앱에서 이 문제가 발생했습니다. 렌더링 타이밍에 따라 FAB 버튼이
하단 내비게이션 바에 가려져 버린 것입니다. 해결 방법은 간단했습니다.
position: absolute를 position: fixed로 바꾸는 것만으로 버튼이 항상 올바른
위치에 표시되었습니다.
/* 문제 발생: absolute는 부모 기준이라 위치가 불안정 */
.fab-button {
position: absolute;
bottom: 80px;
right: 20px;
}
/* 해결: fixed로 뷰포트 기준 고정 */
.fab-button {
position: fixed;
bottom: 80px;
right: 20px;
}
이 사례에서 알 수 있듯이,
항상 화면의 같은 위치에 떠 있어야 하는 요소(FAB 버튼, 고정
헤더, 채팅 위젯 등)에는 fixed를,
특정 부모 요소 안에서의 위치를 지정해야 하는 경우(배지, 닫기
버튼 등)에는 absolute를 사용해야 합니다.
Flexbox는 한 방향(가로 또는 세로)으로 요소를 배치하는 1차원 레이아웃입니다. 하지만 가로와 세로를 동시에 제어해야 하는 상황이 있습니다. 예를 들어 대시보드처럼 행과 열이 동시에 중요한 레이아웃을 만들 때는 CSS Grid가 적합합니다.
/* 3열 × 자동 행 그리드 */
.dashboard {
display: grid;
grid-template-columns: 1fr 1fr 1fr; /* 3개의 동일 너비 열 */
gap: 16px; /* 행·열 간격 */
}
/* repeat()으로 간결하게 */
.dashboard {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
1fr은 fraction(비율)의 약자로, 남은 공간을 비율로 나눕니다.
1fr 1fr 1fr은 3등분, 1fr 2fr은 1:2 비율입니다. Flexbox의 flex: 1과
비슷한 개념이지만 열 단위로 적용됩니다.
| 상황 | 추천 | 이유 |
|---|---|---|
| 내비게이션 바 (가로 한 줄) | Flexbox | 한 방향 정렬 |
| 카드 목록 (가로 나열 + 줄바꿈) | 둘 다 가능 | Flex + wrap 또는 Grid |
| 대시보드 (행·열 동시 제어) | Grid | 2차원 배치 |
| 사이드바 + 메인 콘텐츠 |
/* 실무에서 자주 보는 Grid 패턴: 사이드바 + 메인 */
.layout {
display: grid;
grid-template-columns: 250px 1fr; /* 사이드바 250px 고정, 나머지 유동 */
min-height: 100vh;
}
핵심 기준은 간단합니다. "한 방향이면 Flex, 두 방향이면 Grid".
실무에서는 둘을 섞어서 쓰는 경우가 대부분입니다. 전체 페이지 레이아웃은
Grid로, 내부 컴포넌트는 Flexbox로 구성하는 패턴이 일반적입니다. Tailwind
CSS에서는 grid grid-cols-3 gap-4처럼 유틸리티 클래스로 Grid를 선언하므로
직접 CSS를 작성할 일은 줄어들지만, 1fr이나 auto-fill 같은 개념을 이해해야
Tailwind의 grid-cols-[repeat(auto-fill,minmax(280px,1fr))] 같은 커스텀 값도
자유롭게 쓸 수 있습니다.
앞서 배운 미디어 쿼리와 Grid를 조합하면 화면 크기에 따라 열 수가 자동으로 바뀌는 반응형 레이아웃을 쉽게 만들 수 있습니다.
/* 모바일: 1열, 태블릿: 2열, 데스크톱: 3열 */
.card-grid {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
}
@media (min-width: 768px) {
.card-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.card-grid {
grid-template-columns: repeat
또는 미디어 쿼리 없이도 auto-fill과 minmax()를 사용하면
브라우저가 알아서 열 수를 조절합니다.
/* 자동 반응형: 최소 280px, 남는 공간은 균등 분배 */
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
이 한 줄이면 화면 너비에 따라 카드가 자동으로 1열, 2열, 3열로 재배치됩니다. 미디어 쿼리를 직접 쓰는 것보다 코드가 훨씬 간결하면서도 유연합니다.
10. CSS calc() 함수 — 서로 다른 단위를 섞어 계산하기
Grid에서 1fr이나 %를 사용해 유연한 레이아웃을 만들 수 있지만, 때로는
"전체 너비에서 사이드바 250px을 뺀 나머지"처럼 서로 다른
단위를 섞어서 계산해야 할 때가 있습니다. calc() 함수가 바로 이 역할을
합니다.
/* 기본 문법: calc(계산식) */
.main-content {
/* 전체 너비에서 사이드바 250px을 뺀 나머지 */
width: calc(100% - 250px);
}
.container {
/* 뷰포트 높이에서 헤더(60px)와 푸터(80px)를 뺀 영역 */
min-height: calc(100vh - 60px - 80px);
}
.grid-item {
/* 3등분에서 gap(16px * 2개)을 뺀 너비 */
width: calc((100% - 32px) / 3);
}
calc() 사용 시 주의할 점이 하나 있습니다.
연산자(+, -, *, /) 앞뒤에 반드시 공백이 있어야 합니다.
calc(100%-250px)은 작동하지 않고, calc(100% - 250px)이라고 써야 합니다.
/* 올바른 사용 */
width: calc(100% - 250px); /* ✅ 연산자 앞뒤에 공백 */
/* 잘못된 사용 */
width: calc(100%-250px); /* ❌ 공백 없으면 작동 안 함 */
/* 중첩도 가능 */
width: calc(100% - calc(250px + 16px)); /* 중첩 calc */
calc()의 강력한 점은
%, px, rem, vw 등 서로 다른 단위를 자유롭게 섞을 수 있다
는 것입니다. 이전에는 JavaScript로 계산해야 했던 것을 CSS만으로 해결할 수 있습니다.
/* 1. 고정 헤더 아래 남은 영역 전체 사용 */
.page-content {
height: calc(100vh - 64px); /* 헤더 높이 64px */
}
/* 2. 패딩을 고려한 전체 너비 */
.full-width-with-padding {
width: calc(100% - 40px); /* 좌우 20px씩 여백 */
margin: 0 auto;
}
/* 3. CSS 변수와 함께 사용 */
:root {
--header-height: 64px;
--footer-height: 80px;
}
.main
Tailwind CSS에서는 대괄호 표기법으로 calc()를 사용할 수 있습니다.
h-[calc(100vh-64px)]처럼 작성합니다. 다만 Tailwind의 spacing
시스템(h-screen, min-h-full 등)으로 해결되는 경우가 많아서, calc()를
직접 쓸 일은 비교적 적습니다. CSS 변수와 조합하면
h-[calc(100vh-var(--header-height))]처럼 동적 값도 처리할 수 있습니다.
반응형 레이아웃에서 자주 쓰이는 뷰포트 단위도 알아두면 좋습니다.
vw (viewport width): 뷰포트 너비의 1%. 100vw는 화면 전체
너비입니다.vh (viewport height): 뷰포트 높이의 1%. 100vh는 화면 전체
높이입니다.dvh (dynamic viewport height): 모바일 브라우저에서 주소창이
숨겨지거나 나타날 때 높이가 동적으로 변합니다. 100vh는 주소창을 무시하고
고정 높이를 잡지만, 100dvh는 실제 보이는 영역에 맞춰 변합니다.모바일에서 height: 100vh를 쓰면 주소창에 가려져 콘텐츠가 잘리는 문제가 자주
발생합니다. 이때 100dvh를 사용하면 해결됩니다. 최신 브라우저에서 모두
지원하므로, 모바일 전체 화면 레이아웃에는 dvh를 사용하는 것이 좋습니다.
<div id="A" style="position: relative;">
<div id="B" style="position: static;">
<div id="C" style="position: absolute; top: 0; left: 0;"></div>
</div>
</div>| 1280px |
| 데스크톱 |
| 세로/가로 방향 감지 |
@media (orientation: landscape) |
hover | 호버가 가능한 기기인지 감지 | @media (hover: hover) |
pointer | 포인팅 장치의 정밀도 감지 | @media (pointer: coarse) |
print | 인쇄 시 스타일 분기 | @media print |
| Grid |
| 열 너비 고정 + 유동 조합 |
| 요소 하나를 정중앙 배치 | 둘 다 가능 | 취향 차이 |