CSS 변수와 계산식
사용자 정의 속성의 선언·상속·범위를 익히고 calc로 단위가 다른 값을 결합해 재사용 가능한 스타일을 만듭니다.
앞 절에서 우리는 CSS 트랜지션과 애니메이션을 통해 웹 페이지에 생동감을 불어넣는 방법을 배웠습니다.
이제 CSS 코드를 더 효율적이고 유지보수하기 쉽게 만드는 고급 기법, CSS 변수(Custom Properties)와 계산식(calc() 함수)를 다룹니다.
이 두 기능은 CSS의 유연성을 극대화하여 대규모 프로젝트나 재사용 가능한 컴포넌트 개발에서 중요한 역할을 합니다.
기존 CSS는 변수나 동적 계산 기능이 부족해, 특정 값을 여러 곳에서 써야 할 때마다 일일이 수정해야 하는 번거로움이 있었습니다.
또한 두 가지 이상의 값을 조합해 새로운 값을 계산하는 것도 어려웠습니다.
CSS 변수와 계산식은 이런 불편함을 해소하고, 더 깔끔하고 강력한 CSS 코드를 작성할 수 있도록 돕습니다.
이 장에서는 CSS 변수를 선언하고 사용하는 방법, 변수의 스코프(범위), 그리고 calc() 함수를 이용한 동적 값 계산 방법을 상세히 학습합니다.
이 지식은 CSS 작성 생산성과 코드 재활용성을 크게 향상시킵니다.
CSS custom property 해석과 calc 계산 흐름 — 상태 추적
--gap 없음/20px/tomato를 치환한 뒤 width 타입 검사를 한다. tomato가 fallback 재시도 없이 auto가 되는 검사 시점과 후보값을 단계적으로 관찰할 가치가 있다.
CSS VALUES · FALLBACK BOUNDARY
var() fallback은 최종 속성 오류를 모두 복구하지 않는다
일반 custom property는 요소별 cascade·상속으로 값을 정하고 var()에서 치환된다. 치환할 값 선택과 최종 속성의 타입 검사는 서로 다른 단계다.
- Cascade·상속
같은 요소의 선언을 cascade로 결정하고, 일반 --변수는 부모에서 상속한다. 자식의 직접 선언과 등록된 @property 규칙은 별도로 확인한다.
- var() 치환
사용 가능한 값이 있으면 그 값을 넣는다. 미정의·순환 등으로 값이 무효면 두 번째 인수를 fallback으로 사용한다. fallback도 없거나 무효면 최종 속성이 무효가 된다.
- 최종 속성 검사
치환한 값과 calc()가 그 속성에서 허용되는 문법·타입인지 확인한다. 아래는 @property로 등록하지 않은 --gap을 width에 쓰는 비교다.
20px이 정의됨
--gap: 20px;
width: calc(100% -
var(--gap, 8px));20px을 치환한다. width에서 %와 px의 조합은 유효하다.
값이 미정의 또는 순환
width: calc(100% -
var(--gap, 8px));사용 가능한 --gap 값이 없으면 8px fallback을 치환하고 width를 계산한다.
값은 있지만 타입이 다름
--gap: tomato;
width: calc(100% -
var(--gap, 8px));tomato를 치환하므로 width가 무효다. 8px fallback으로 다시 시도하지 않는다.
계산 시 무효가 되어도 이전 cascade 후보로 돌아가지 않는다
위 width 오류는 계산 시점에 발견되므로 일반적인 비상속 속성 width의 initial 값인 auto로 처리된다. 상속 속성은 부모 값을 사용한다. +와 - 양옆 공백을 넣고, calc()는 사용하는 속성에서 허용하는 단위 조합으로 작성한다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
- 세 장면은 같은 선언 width: calc(100% - var(--gap, 8px))에 서로 독립적인 변수 조건 A·B·C를 적용한 비교입니다. containing block 크기가 정해지지 않아 최종 px 너비를 확정하지 않습니다. 변수 토큰과 width 계산식을 구분합니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 원도표 조건·주석 | Cascade·상속: 같은 요소의 선언을 cascade로 결정하고, 일반 --변수는 부모에서 상속한다. 자식의 직접 선언과 등록된 @property 규칙은 별도로 확인한다. |
| 원도표 조건·주석 | var() 치환: 사용 가능한 값이 있으면 그 값을 넣는다. 미정의·순환 등으로 값이 무효면 두 번째 인수를 fallback으로 사용한다. fallback도 없거나 무효면 최종 속성이 무효가 된다. |
| 원도표 조건·주석 | 최종 속성 검사: 치환한 값과 calc()가 그 속성에서 허용되는 문법·타입인지 확인한다. 아래는 @property로 등록하지 않은 --gap을 width에 쓰는 비교다. |
| 전체 관계·조건 설명 | 미등록 custom property인 --gap을 calc(100% - var(--gap, 8px))에서 읽는다. 사용할 값이 없으면 8px fallback을, 있으면 기존 값을 치환한다. 두 경로 모두 width 타입 검사를 거친다. 20px과 8px은 길이 계산에 맞지만 tomato는 계산 시 무효라 width의 초깃값 auto가 되며 fallback으로 되돌아가지 않는다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 조건 A · 변수 없음 | --gap: 없음 / width 식: calc(100% - 8px) | --gap이 없어 fallback 8px을 빼는 계산식으로 치환한다 | fallback은 변수 해석이 불가능할 때 선택한다 |
| 조건 B · 20px 값 | --gap: 20px / width 식: calc(100% - 20px) | 정의된 20px을 치환해 containing block 너비에서 빼는 식이 된다 | 정의된 유효 값이 fallback보다 먼저 사용된다 |
| 조건 C · tomato 값 | --gap: tomato / width: auto | calc(100% - tomato)는 width 문법에 맞지 않아 computed 단계에서 invalid가 된다 | 소비 속성이 거절하면 width는 이 사례에서 auto가 되며 8px fallback을 다시 고르지 않는다 |
CSS custom property 해석과 calc 계산 흐름 — 상태 추적
--gap 없음/20px/tomato를 치환한 뒤 width 타입 검사를 한다. tomato가 fallback 재시도 없이 auto가 되는 검사 시점과 후보값을 단계적으로 관찰할 가치가 있다.
CSS VALUES · FALLBACK BOUNDARY
var() fallback은 최종 속성 오류를 모두 복구하지 않는다
일반 custom property는 요소별 cascade·상속으로 값을 정하고 var()에서 치환된다. 치환할 값 선택과 최종 속성의 타입 검사는 서로 다른 단계다.
- Cascade·상속
같은 요소의 선언을 cascade로 결정하고, 일반 --변수는 부모에서 상속한다. 자식의 직접 선언과 등록된 @property 규칙은 별도로 확인한다.
- var() 치환
사용 가능한 값이 있으면 그 값을 넣는다. 미정의·순환 등으로 값이 무효면 두 번째 인수를 fallback으로 사용한다. fallback도 없거나 무효면 최종 속성이 무효가 된다.
- 최종 속성 검사
치환한 값과 calc()가 그 속성에서 허용되는 문법·타입인지 확인한다. 아래는 @property로 등록하지 않은 --gap을 width에 쓰는 비교다.
20px이 정의됨
--gap: 20px;
width: calc(100% -
var(--gap, 8px));20px을 치환한다. width에서 %와 px의 조합은 유효하다.
값이 미정의 또는 순환
width: calc(100% -
var(--gap, 8px));사용 가능한 --gap 값이 없으면 8px fallback을 치환하고 width를 계산한다.
값은 있지만 타입이 다름
--gap: tomato;
width: calc(100% -
var(--gap, 8px));tomato를 치환하므로 width가 무효다. 8px fallback으로 다시 시도하지 않는다.
계산 시 무효가 되어도 이전 cascade 후보로 돌아가지 않는다
위 width 오류는 계산 시점에 발견되므로 일반적인 비상속 속성 width의 initial 값인 auto로 처리된다. 상속 속성은 부모 값을 사용한다. +와 - 양옆 공백을 넣고, calc()는 사용하는 속성에서 허용하는 단위 조합으로 작성한다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
- 세 장면은 같은 선언 width: calc(100% - var(--gap, 8px))에 서로 독립적인 변수 조건 A·B·C를 적용한 비교입니다. containing block 크기가 정해지지 않아 최종 px 너비를 확정하지 않습니다. 변수 토큰과 width 계산식을 구분합니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 원도표 조건·주석 | Cascade·상속: 같은 요소의 선언을 cascade로 결정하고, 일반 --변수는 부모에서 상속한다. 자식의 직접 선언과 등록된 @property 규칙은 별도로 확인한다. |
| 원도표 조건·주석 | var() 치환: 사용 가능한 값이 있으면 그 값을 넣는다. 미정의·순환 등으로 값이 무효면 두 번째 인수를 fallback으로 사용한다. fallback도 없거나 무효면 최종 속성이 무효가 된다. |
| 원도표 조건·주석 | 최종 속성 검사: 치환한 값과 calc()가 그 속성에서 허용되는 문법·타입인지 확인한다. 아래는 @property로 등록하지 않은 --gap을 width에 쓰는 비교다. |
| 전체 관계·조건 설명 | 미등록 custom property인 --gap을 calc(100% - var(--gap, 8px))에서 읽는다. 사용할 값이 없으면 8px fallback을, 있으면 기존 값을 치환한다. 두 경로 모두 width 타입 검사를 거친다. 20px과 8px은 길이 계산에 맞지만 tomato는 계산 시 무효라 width의 초깃값 auto가 되며 fallback으로 되돌아가지 않는다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 조건 A · 변수 없음 | --gap: 없음 / width 식: calc(100% - 8px) | --gap이 없어 fallback 8px을 빼는 계산식으로 치환한다 | fallback은 변수 해석이 불가능할 때 선택한다 |
| 조건 B · 20px 값 | --gap: 20px / width 식: calc(100% - 20px) | 정의된 20px을 치환해 containing block 너비에서 빼는 식이 된다 | 정의된 유효 값이 fallback보다 먼저 사용된다 |
| 조건 C · tomato 값 | --gap: tomato / width: auto | calc(100% - tomato)는 width 문법에 맞지 않아 computed 단계에서 invalid가 된다 | 소비 속성이 거절하면 width는 이 사례에서 auto가 되며 8px fallback을 다시 고르지 않는다 |
CSS 변수 (Custom Properties) 이해하기
CSS 변수는 말 그대로 CSS 파일 내에서 재사용할 수 있는 사용자 정의 속성입니다.
값을 이름으로 선언해 여러 속성에서 var()로 참조할 수 있습니다. 다만 요소마다 cascade와 상속을 거쳐 실제 값을 결정하므로, 부모 값을 자식에서 재정의했다면 부모 선언을 바꾸어도 그 자식은 자신의 값을 사용합니다.
이는 웹사이트의 디자인 일관성을 유지하고, 변경 사항을 적용하는 시간을 크게 단축시켜 줍니다.
변수 선언 및 사용 방법
CSS 변수는 두 개의 하이픈(--)으로 시작하는 사용자 정의 속성 이름으로 선언합니다.
/* 변수 선언 */
:root { /* 문서 전체에 적용되는 전역 변수 */
--primary-color: #3498db; /* 메인 색상 */
--secondary-color: #2ecc71; /* 보조 색상 */
--font-size-base: 16px; /* 기본 폰트 크기 */
--spacing-unit: 10px; /* 간격 단위 */
}
/* 변수 사용 */
body {
font-size: var(--font-size-base);
color: var(--primary-color);
}
.button {
background-color: var(--secondary-color);
padding: var(--spacing-unit) calc(var(--spacing-unit) * 2); /* calc() 함수와 함께 사용 */
}
.header {
border-bottom: 1px solid var(--primary-color);
}--변수이름: 값;: 변수를 선언하는 문법입니다. 변수 이름 앞에는 반드시--를 붙여야 합니다.var(--변수이름): 선언된 변수 값을 가져와 사용합니다.
변수의 스코프 (Scope)
여기서 다루는 --이름 형태의 미등록 custom property는 캐스케이드(Cascade)에 참여하고 기본적으로 상속(Inheritance)됩니다. @property로 등록하면 타입과 상속 여부 등을 별도로 정할 수 있지만 이 장의 예제는 등록하지 않습니다. 이름은 대소문자를 구별하고, var()는 속성값에 사용합니다. 선택자나 속성 이름, 미디어 쿼리 조건을 변수로 바꾸는 문법은 아닙니다.
이는 변수가 선언된 위치에 따라 적용되는 범위, 즉 스코프가 달라진다는 의미입니다.
-
전역(Global) 스코프:
html또는:root선택자 내에 선언된 변수는 문서 전체에서 접근 가능합니다.:root가 더 일반적입니다.:root { --global-bg: #f0f0f0; } body { background-color: var(--global-bg); /* 어디서든 사용 가능 */ } -
지역(Local) 스코프: 특정 선택자 내에 선언된 변수는 해당 선택자와 그 자식 요소 내에서만 유효합니다.
.card-container { --card-border-color: #ddd; /* .card-container와 그 자식에만 유효 */ border: 1px solid var(--card-border-color); } .card-container .card-item { border: 1px solid var(--card-border-color); /* 부모로부터 상속받아 사용 */ } .another-element { /* background-color: var(--card-border-color); /* 이곳에서는 사용할 수 없음 */ }캐스케이드와 상속: 같은 요소에 여러 변수 선언이 매칭되면 일반 CSS 캐스케이드 규칙으로 값을 선택합니다. 해당 요소에 직접 적용할 값이 없으면 부모의 값을 상속합니다. 아래
.dark-theme선언은 그 요소에 값을 지정하고, 자식 문단은 이를 상속받아 사용합니다.:root { --text-color: #333; /* 전역 텍스트 색상 */ } .dark-theme { --text-color: #eee; /* .dark-theme 내부에서는 텍스트 색상이 변경됨 */ background-color: #333; } p { color: var(--text-color); /* 상황에 따라 다른 색상 적용 */ }
앞의 그림에서 fallback은 변수에 사용할 값이 있는지를 검사하는 단계와 치환한 값이 최종 속성에 맞는지를 검사하는 단계를 나눕니다. width: calc(100% - var(--gap, 8px))에서 --gap이 없으면 8px을 사용합니다. --gap: 20px이면 20px을 사용하지만, --gap: tomato이면 tomato를 치환한 뒤 width 계산이 무효가 됩니다. 이때 8px로 다시 시도하지 않습니다. 미등록 변수에 tomato라는 토큰을 저장하는 것 자체와 width가 색상 이름을 받는 것은 다른 문제입니다.
width처럼 상속되지 않는 속성이 계산 시 무효가 되면 초깃값인 auto로 처리됩니다. 계산 전에 선택에서 밀려난 다른 width 선언으로 돌아가지도 않습니다. 따라서 fallback은 잘못된 타입을 모두 고쳐 주는 예외 처리 장치가 아닙니다.
변수의 장점
- 유지보수 용이: 디자인 변경 시 변수 값 하나만 수정하면 전체 웹사이트에 일괄 적용됩니다.
- 일관성 유지: 동일한 값을 여러 번 입력하는 실수를 줄여 디자인의 일관성을 높입니다.
- 가독성 향상:
--primary-color와 같이 의미 있는 이름으로 변수를 선언하여 코드 이해도를 높입니다. - 테마 변경 용이: 다크 모드, 라이트 모드 등 테마 기능을 구현할 때 HTML 요소에 클래스만 토글하여 쉽게 테마를 변경할 수 있습니다.
- JavaScript와의 상호작용: JavaScript로 CSS 변수 값을 읽고 쓸 수 있어 동적인 스타일 변경에 유용합니다. (다음 장에서 다룰 예정)
CSS 계산식 (calc()) 이해하기
먼저 어떤 크기를 기준으로 계산하는지 정하세요. 아래 width의 100%는 해당 요소의 containing block 너비를 기준으로 하고, vh/vw는 뷰포트 크기를 기준으로 합니다. 계산 결과에도 사용하는 속성에 맞는 타입과 단위가 필요합니다.
calc() 함수는 CSS 속성 값 내에서 사칙연산(+, -, *, /)을 수행하여 동적인 값을 계산할 수 있게 해줍니다.
이는 특히 다양한 단위(px, %, em, rem, vw, vh)가 혼합될 때 유용하며, 유연하고 반응형인 레이아웃을 만드는 데 필수적입니다.
calc() 함수 기본 문법
calc() 함수 안에 계산식을 작성합니다.
.element {
width: calc(100% - 40px); /* 부모 너비에서 40px을 뺀 너비 */
height: calc(50vh + 2em); /* 뷰포트 높이의 절반에 2em을 더한 높이 */
font-size: calc(16px + 0.5vw); /* 뷰포트 너비에 따라 폰트 크기 조절 */
margin-left: calc(var(--spacing-unit) / 2); /* 변수와 함께 사용 */
}-
연산자 주의사항
- 덧셈(
+)과 뺄셈(-) 연산자 주위에는 반드시 공백이 있어야 합니다. (100% - 20px은 가능하지만,100%-20px은 오류) - 곱셈(
*)과 나눗셈(/) 연산자 주위에는 공백이 필수는 아니지만, 가독성을 위해 권장됩니다. - 이 장의 곱셈과 나눗셈은 길이에 단위 없는 수를 곱하거나 나누는 예입니다.
100px * 2는 200px,100px / 2는 50px입니다.100px * 2px처럼 길이끼리 곱한 넓이를 width에 지정할 수는 없습니다. 같은 타입의 단위 있는 값끼리 나누어 단위 없는 수를 얻는 typed arithmetic은 지원 브라우저와 결과 타입을 별도로 확인합니다. - 이 실습에서는 나누는 수를 0이 아닌 양수로 정하세요. 최신 CSS 계산 문법에는 infinity/NaN 처리도 있으므로 JavaScript 예외와 같다고 일반화하지 않고, 사용하는 속성에서 유한한 결과가 나오는지 확인합니다.
- 덧셈(
calc() 활용 예시
다양한 상황에서 calc() 함수를 활용해 봅시다.
.main-content {
width: calc(100% - 40px); /* 좌우 20px씩 총 40px의 패딩을 제외한 너비 */
margin: 20px auto;
padding: 20px;
}.grid-item {
/* 3개의 아이템을 나란히 배치하면서 20px의 간격을 고려한 너비 */
width: calc((100% - (20px * 2)) / 3); /* (전체 너비 - 총 간격) / 아이템 수 */
/* 또는 Grid의 gap 속성 사용 시: grid-template-columns: repeat(3, 1fr); gap: 20px; */
}h1 {
/* 최소 2em, 유동적인 권장값, 최대 4em을 함께 설정 */
font-size: clamp(2em, calc(2em + 2vw), 4em);
}:root {
--base-padding: 15px;
}
.card {
padding: var(--base-padding);
margin-bottom: calc(var(--base-padding) * 2); /* 기본 패딩의 2배 마진 */
}위 .main-content 예제는 기본 content-box이고 border가 없다는 조건에서 부모가 500px이면 content 너비는 460px, 좌우 padding 40px을 더한 바깥 너비는 500px입니다. 같은 선언을 border-box와 함께 쓰면 padding은 460px 안에 포함되어 바깥 너비도 460px이 됩니다. calc()가 box-sizing까지 자동으로 바꾸지는 않습니다. 3개 아이템 예제는 부모를 flex로 정하고 실제 gap도 20px로 지정해야 3 × 아이템 너비 + 2 × gap이 부모 너비와 맞습니다.
calc()의 장점
- 유연한 레이아웃: 고정된 값과 유동적인 값을 혼합하여 더 복잡하고 유연한 레이아웃을 쉽게 구현할 수 있습니다.
- 단위 혼합: 서로 다른 CSS 단위를 함께 사용하여 계산할 수 있습니다.
- 반응형 디자인: 미디어 쿼리 없이도 어느 정도 반응형 동작을 구현할 수 있습니다.
- 유지보수성: 동적인 계산이 필요한 부분을 코드 내에 명확히 표현하여 가독성을 높이고, 값 변경 시 계산이 자동으로 업데이트됩니다.
CSS 변수와 calc()를 함께 사용할 때는 토큰 선언부터 최종 계산값까지의 흐름을 추적합니다.
실습: 웹 컴포넌트 만들기
CSS 변수와 calc() 함수를 활용하여 재사용 가능한 UI 컴포넌트를 만들어 봅시다.
이번 실습에서는 공통된 스타일과 유연한 너비를 가진 카드 컴포넌트를 구현합니다.
variables_calc.html 파일 작성<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CSS 변수와 calc() 실습</title>
<link rel="stylesheet" href="css/component.css">
</head>
<body>
<header>
<h1>CSS 변수 & calc()</h1>
<p>더 스마트한 CSS를 위한 도구</p>
</header>
<main>
<section class="section-container">
<h2>제품 카드 목록</h2>
<div class="product-grid">
<div class="product-card">
<h3>스마트폰</h3>
<p>최신 기술이 집약된 고성능 스마트폰입니다.</p>
<span class="price">990,000원</span>
<button class="buy-button">구매하기</button>
</div>
<div class="product-card">
<h3>무선 이어폰</h3>
<p>선명한 음질과 편안한 착용감을 제공합니다.</p>
<span class="price">159,000원</span>
<button class="buy-button">구매하기</button>
</div>
<div class="product-card">
<h3>스마트 워치</h3>
<p>건강 관리에 필수적인 스마트 워치.</p>
<span class="price">299,000원</span>
<button class="buy-button">구매하기</button>
</div>
<div class="product-card">
<h3>노트북</h3>
<p>강력한 성능과 휴대성을 겸비한 노트북.</p>
<span class="price">1,500,000원</span>
<button class="buy-button">구매하기</button>
</div>
</div>
</section>
<section class="section-container dark-theme">
<h2>다크 테마 예시</h2>
<div class="theme-box">
<p>이 박스는 다크 테마가 적용되어 있습니다. 변수의 스코프를 확인해보세요.</p>
<button class="theme-button">테마 전환 (JS로 구현 가능)</button>
</div>
</section>
</main>
<footer>
<p>© 2025 변수와 계산식 실습. 모든 권리 보유.</p>
</footer>
</body>
</html>css/component.css 파일 작성/* 뷰포트 메타 태그가 HTML에 설정되어 있는지 확인! */
/* 기본 설정 */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f0f2f5;
color: var(--text-color, #333); /* 변수 사용, 폴백(fallback) 값 지정 */
line-height: 1.6;
padding-bottom: 50px;
}
/* CSS 변수 선언 (전역 스코프) */
:root {
--primary-color: #3498db; /* 파란색 */
--secondary-color: #2ecc71; /* 초록색 */
--accent-color: #e67e22; /* 주황색 */
--background-light: #f0f2f5; /* 밝은 배경색 */
--background-card: white; /* 카드 배경색 */
--text-color: #333; /* 기본 텍스트 색상 */
--border-color: #ddd; /* 테두리 색상 */
--box-shadow-light: 0 2px 10px rgba(0,0,0,0.1); /* 밝은 그림자 */
--base-padding: 15px; /* 기본 패딩 단위 */
--card-gap: 20px; /* 카드 간 간격 */
--button-padding-vertical: 10px; /* 버튼 세로 패딩 */
--button-padding-horizontal: 20px; /* 버튼 가로 패딩 */
}
/* 다크 테마 변수 오버라이드 (지역 스코프) */
.dark-theme {
--background-light: #2c3e50;
--background-card: #34495e;
--text-color: #ecf0f1;
--border-color: #4a6581;
--box-shadow-light: 0 4px 12px rgba(0,0,0,0.3);
}
/* 헤더 스타일 */
header {
background-color: var(--primary-color);
color: white;
text-align: center;
padding: calc(var(--base-padding) * 2) var(--base-padding);
box-shadow: var(--box-shadow-light);
margin-bottom: 30px;
}
header h1 {
font-size: 2.8em;
margin-bottom: calc(var(--base-padding) / 2);
}
header p {
font-size: 1.2em;
}
/* 섹션 공통 스타일 */
.section-container {
background-color: var(--background-light);
padding: calc(var(--base-padding) * 2);
margin-bottom: 30px;
border-radius: 8px;
box-shadow: var(--box-shadow-light);
max-width: 1200px;
margin: 0 auto 30px auto;
color: var(--text-color);
}
.section-container h2 {
color: var(--primary-color);
text-align: center;
margin-bottom: calc(var(--base-padding) * 2);
font-size: 2em;
}
/* 제품 카드 그리드 */
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); /* 반응형 그리드 */
gap: var(--card-gap);
justify-content: center;
}
.product-card {
background-color: var(--background-card);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: var(--base-padding);
text-align: center;
box-shadow: var(--box-shadow-light);
display: flex; /* 내부 콘텐츠를 위한 Flexbox */
flex-direction: column;
justify-content: space-between;
align-items: center;
}
.product-card img {
max-width: 100%;
height: auto;
border-radius: 5px;
margin-bottom: var(--base-padding);
}
.product-card h3 {
font-size: 1.4em;
color: var(--primary-color);
margin-bottom: calc(var(--base-padding) / 2);
}
.product-card p {
font-size: 0.95em;
color: var(--text-color);
margin-bottom: var(--base-padding);
flex-grow: 1; /* 단락이 남은 공간 차지 */
}
.product-card .price {
font-size: 1.3em;
font-weight: bold;
color: var(--accent-color);
margin-bottom: var(--base-padding);
}
.buy-button {
background-color: var(--secondary-color);
color: white;
padding: var(--button-padding-vertical) var(--button-padding-horizontal);
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 1em;
transition: background-color 0.3s ease;
}
.buy-button:hover {
background-color: #27ae60;
}
/* 다크 테마 박스 */
.theme-box {
background-color: var(--background-card);
border: 1px solid var(--border-color);
padding: calc(var(--base-padding) * 2);
border-radius: 8px;
text-align: center;
box-shadow: var(--box-shadow-light);
margin: 0 auto;
width: 80%;
max-width: 600px;
}
.theme-box p {
margin-bottom: var(--base-padding);
color: var(--text-color);
}
.theme-button {
background-color: var(--primary-color);
color: white;
padding: var(--button-padding-vertical) var(--button-padding-horizontal);
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 1em;
transition: background-color 0.3s ease;
}
.theme-button:hover {
background-color: #2980b9;
}
/* 푸터 스타일 */
footer {
background-color: var(--primary-color);
color: white;
text-align: center;
padding: calc(var(--base-padding) + 5px) 0;
margin-top: 50px;
font-size: 0.9em;
}- Live Server를 통해
variables_calc.html파일을 열어 보세요. --primary-color와 같은 변수들이 웹 페이지 전반에 걸쳐 어떻게 사용되었는지 확인하세요.header의padding에서calc()로 계산하는 부분과.product-grid의gap에서var(--card-gap)을 참조하는 부분을 비교해 보세요.- 브라우저의 개발자 도구(F12)를 열어 Elements 탭에서
.product-card나.theme-box같은 요소들을 선택하고 Styles 탭을 확인해 보세요. Computed 탭에서calc()로 계산된 최종 값이 어떻게 적용되었는지 볼 수 있습니다. - 특히
.dark-theme클래스가 적용된 섹션의 배경색, 텍스트 색상, 테두리 색상 등이 전역 변수와 다르게 적용되는 것을 확인하세요. 이는 지역 스코프 변수가 전역 변수를 오버라이드한 결과입니다. CSS 파일에서:root의--primary-color값이나--base-padding값을 변경해 보면, 전체 페이지에 어떻게 즉시 반영되는지 확인할 수 있습니다.
이번 장에서는 CSS 코드의 효율성과 유지보수성을 극대화하는 두 가지 고급 기법인 CSS 변수(Custom Properties)와 계산식(calc() 함수)에 대해 학습했습니다.
- CSS 변수는 재사용 가능한 값을
--변수이름: 값;형태로 선언하고var(--변수이름)으로 사용하여, 디자인 일관성을 유지하고 대규모 프로젝트의 스타일 변경을 용이하게 합니다. 또한, 변수의 스코프를 이해하여 전역/지역적으로 값을 제어할 수 있습니다. calc()함수는 CSS 속성 값 내에서 사칙연산을 수행하여width: calc(100% - 40px);와 같이 동적이고 유연한 값을 계산할 수 있게 해줍니다. 이는 서로 다른 단위를 혼합하여 사용하거나, 복잡한 레이아웃에서 여백 등을 자동으로 계산하는 데 매우 유용합니다.
이 두 가지 기능을 활용하면 여러분의 CSS 코드는 훨씬 더 강력하고 유연하며, 관리하기 쉬워질 것입니다.
완성 실습에서 확인할 계산
--base-padding은 15px이므로 header 세로 padding과 section padding은 30px, 제목의 아래 간격은 7.5px입니다. .dark-theme 안에서는 배경·텍스트·테두리 토큰만 재정의하므로 파란 --primary-color와 15px 간격은 그대로입니다. --text-color를 자식이 상속하는 것과 이미 계산된 body의 color를 상속하는 것도 구별하세요. section과 문단은 자신의 color: var(--text-color)를 다시 계산합니다. 구매·테마 버튼에는 이벤트 처리가 없고 HTML에 img도 없으므로 클릭 후 구매/테마 전환이나 제품 사진 표시는 아직 구현되지 않았습니다.
좁은 화면에 적용하기
320px 뷰포트에서 section의 좌우 padding 60px을 빼면 Grid 공간은 260px입니다. 기존 minmax(280px, 1fr)의 280px 최솟값보다 작으므로 가로로 넘칠 수 있습니다. 원래 예제를 확인한 뒤 component.css 맨 뒤에 아래 변형을 추가하세요. 공간이 충분하면 기존 280px 최솟값을 유지하고, 좁으면 부모의 100%까지 줄입니다.
.product-grid {
grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
}혼자 계산하고 바꾸어 보기
--base-padding을 20px로 바꾸고 다른 값은 유지하세요. header의 세로 padding과 h1 아래 간격을 먼저 계산하세요. 다음으로 .dark-theme 안에만 --base-padding: 12px를 선언하면 그 섹션의 padding, 안쪽 카드/박스, 문서 맨 위 header 중 어느 값이 바뀌는지 설명한 뒤 Computed에서 대조하세요.
상속 범위와 계산 풀이
전역 값이 20px이면 header의 세로 padding은 40px, h1의 아래 간격은 10px입니다. .dark-theme 자체와 자손은 20px로 직접 재정의하지 않는 한 12px을 사용하므로 섹션과 .theme-box의 padding은 24px입니다. 다크 섹션 바깥의 header는 20px 토큰을 유지하므로 세로 padding은 40px입니다. 이 실습의 다크 섹션에는 product-card가 없으므로 제품 카드까지 변한다고 보고하지 않습니다.
이 절의 통합 다이어그램은 CSS 변수가 선언되어 cascade와 상속을 거쳐 최종 계산값이 되는 순서를 정리합니다.