CSS 기초 | 선택자, 속성, 색상, 폰트

이 글의 핵심

스타일을 줬는데 적용되지 않는 문제는 대부분 캐스케이드와 특이도, 상속을 모르는 데서 시작합니다. 그래서 이 글은 선택자와 속성 나열에 앞서 캐스케이드 비교 단계와 inherit·initial·unset·revert의 차이를 먼저 다루고, 이후 색상·폰트·단위·CSS 변수를 그 관점으로 연결합니다.

시리즈 안내

#03 | 📋 전체 목차 | 이전: #02 HTML 기본


들어가며

CSS는 HTML이 만든 요소에 색·크기·배치 규칙을 입힙니다. 박스 모델과 Flexbox는 같은 “상자” 관점에서 읽으면 연결됩니다.


심화: 캐스케이드, 특이도(Specificity), 상속

아래에서 다루는 선택자 예제들이 같은 속성에 서로 다른 값을 주면, 브라우저는 캐스케이드(Cascade) 알고리즘으로 하나를 고릅니다. “ID가 클래스보다 강하다”는 말만으로는 부족하며, 출처(origin)·중요도(importance)·레이어(@layer)·특이도·선언 순서를 함께 봐야 합니다.

캐스케이드가 비교하는 단계(요약)

일반적으로 한 선언이 다른 선언을 이기려면, 아래 순서로 먼저 다른 쪽을 이겨야 합니다(같은 단계 안에서는 다음 단계로).

  1. 출처와 중요도
    UA !important → 사용자 !important → 작성자 !important → 작성자 일반 → 사용자 일반 → UA(브라우저 기본) 일반 순으로 강합니다.
    (실제 엔진은 명세의 cascade sort에 따라 transition·animation까지 포함해 더 세밀합니다.)

    !important가 붙으면 순서가 뒤집힌다는 점이 핵심입니다. 일반 선언에서는 작성자(사이트 개발자)가 사용자 설정을 이기지만, !important 선언에서는 사용자가 작성자를 이깁니다. 시력이 낮은 사용자가 브라우저 설정으로 강제한 글자 크기를 사이트가 덮어쓰지 못하게 하려는 설계입니다.

  2. 레이어(@layer)
    일반 선언에서는 레이어에 속하지 않은 선언이 모든 레이어를 이기고, 레이어끼리는 나중에 선언된 레이어가 이깁니다. !important 선언에서는 이 순서가 반대가 되어 먼저 선언된 레이어가 이깁니다. 디자인 시스템에서 베이스/컴포넌트/유틸을 레이어로 나누면 !important 없이도 순서를 제어하기 쉽습니다. 반대로 외부 라이브러리 CSS를 레이어에 넣지 않은 채 내 스타일만 레이어로 감싸면, 특이도가 아무리 높아도 라이브러리의 레이어 밖 규칙에 지는 일이 생기니 주의해야 합니다.

  3. 선택자 특이도(Specificity)
    동일 출처·중요도·레이어라면 특이도가 큰 선언이 이깁니다.

  4. 선언 순서
    그래도 동률이면 문서에서 뒤에 온 선언이 이깁니다(같은 스타일시트 내 import 순서와 링크 순서도 여기에 포함).

특이도 계산: 단순 “ID > 클래스”를 넘어서

CSS2 시절의 직관(ID=100, class=10, 요소=1)은 여전히 직관용으로 쓰이지만, 최신 선택자는 4개의 카운터로 이해하는 편이 정확합니다(Selectors Level 4 기준 개념).

  • 인라인 스타일(style="...")은 선택자 특이도보다 위에 올라갑니다(별도로 처리).
  • :where()는 인자 선택자의 특이도를 0으로 만듭니다. 반면 :is(), :not(), :has()는 인자 안의 가장 특이도 높은 선택자를 반영합니다.
  • 가상 요소(::before, ::after)는 타입 선택자와 동급으로 1을 더합니다.
  • :nth-child() 등 구조 의사 클래스는 특이도에 포함됩니다.

복합 선택자 .a.b#c는 각 구성 요소의 특이도가 합산됩니다. 따라서 “클래스 하나만 더 추가했다”가 ID가 아닌 이상 ID 규칙을 이기지 못하는 이유가 수치로 설명됩니다.

실무에서 자주 쓰는 팁은 다음과 같습니다.

  • 특이도 전쟁을 피하려면 클래스·데이터 속성 하나로 범위를 잡으며, 불가피할 때만 @layer로 정리합니다.
  • !important는 디버깅을 어렵게 만들고 레이어·순서 문제를 가립니다. 정말 필요한 경우(예: 유틸리티 오버라이드)만 제한적으로 씁니다.

상속(Inheritance)과 초기값

모든 CSS 속성은 상속 여부가 정해져 있습니다.

  • 상속되는 속성: color, font-*, line-height 등 — 자식은 부모의 계산값(computed value)을 상속받습니다(단, rem처럼 루트 기준인 값은 부모와 무관).
  • 상속되지 않는 속성: margin, padding, border, width 등 — 초기값(initial value)에서 시작합니다. 브라우저 기본 스타일(UA)이 일부 요소에 margin 등을 넣는 이유가 여기에 있습니다.

전역 리셋(* { margin: 0 })은 상속이 아니라 모든 요소에 직접 적용되는 것입니다.

inherit, initial, unset, revert

  • inherit: 부모의 계산값을 강제로 물려받습니다(상속 여부와 무관).
  • initial: 해당 속성의 표준 초기값으로 맞춥니다.
  • unset: 속성이 상속되면 inherit, 아니면 initial과 같이 동작합니다.
  • revert: 캐스케이드를 한 단계 되돌려 UA/작성자 스타일 중 이전 층으로 돌아갑니다(레이어·출처 맥락이 있으므로 “완전 초기화”와는 다릅니다).

이 단계까지 이해하면, “같은 선택자인데 왜 이 속성만 이상한가?” 같은 문제가 상속·초기값·더 구체적인 규칙 중 어디에 속하는지로 분해됩니다. 레이아웃과 겹침은 박스 모델과 position·쌓임 맥락과 연결됩니다.

기본·복합·속성 선택자와 가상 클래스

기본 선택자

CSS 선택자는 스타일을 적용할 HTML 요소를 선택합니다:

/* 1. 태그 선택자 (Type Selector) */
p {
    /* 모든 <p> 태그에 적용 */
    color: blue;
}
h1 {
    /* 모든 <h1> 태그에 적용 */
    font-size: 32px;
}
/* 2. 클래스 선택자 (Class Selector) */
.highlight {
    /* class="highlight"를 가진 모든 요소에 적용 */
    /* 여러 요소에 재사용 가능 */
    background-color: yellow;
}
.button {
    /* class="button"을 가진 요소 */
    padding: 10px 20px;
    border-radius: 5px;
}
/* HTML 예시:
   <p class="highlight">강조된 텍스트</p>
   <div class="highlight">강조된 영역</div>
*/
/* 3. ID 선택자 (ID Selector) */
#header {
    /* id="header"를 가진 요소에 적용 */
    /* 페이지에서 유일해야 함 (한 번만 사용) */
    font-size: 24px;
}
#main-content {
    /* id="main-content"인 요소 */
    max-width: 1200px;
}
/* HTML 예시:
   <div id="header">헤더</div>
*/
/* 4. 전체 선택자 (Universal Selector) */
* {
    /* 모든 요소에 적용 */
    /* CSS 리셋에 자주 사용 */
    margin: 0;      /* 모든 요소의 외부 여백 제거 */
    padding: 0;     /* 모든 요소의 내부 여백 제거 */
    box-sizing: border-box;  /* 박스 크기 계산 방식 통일 */
}

선택자 우선순위 (직관용 숫자): 아래 숫자는 교육용 비유이며, 실제 엔진은 위 「심화: 캐스케이드, 특이도, 상속」에서 설명한 대로 출처·레이어·:where() 등을 함께 봅니다.

/* (비유) ID > 클래스 > 태그 — 복합 선택자는 각 부분이 합산됨 */
p { color: black; }           /* 타입 1개 */
.text { color: blue; }        /* 클래스 1개 */
#title { color: red; }        /* ID 1개 */
/* <p id="title" class="text">텍스트</p> → 보통 #title 이김 */
/* !important는 같은 조건에서 특이도보다 앞선 “중요도” 축에서 판정됨 */
p { color: green !important; }

이 예제에서 <p id="title" class="text">의 최종 색은 초록입니다. !important는 특이도 비교보다 앞 단계에서 판정되므로 #title이 특이도로 이기더라도 소용이 없습니다. 스타일이 “안 먹는다”고 느낄 때 가장 먼저 할 일은 브라우저 개발자 도구의 Styles 패널에서 해당 속성이 취소선으로 그어진 규칙을 찾는 것입니다. 취소선이 있으면 다른 규칙에 졌다는 뜻이고, 이긴 규칙이 바로 위에 보입니다. 취소선조차 없고 규칙 자체가 목록에 없다면 선택자가 요소에 매칭되지 않은 것이므로 특이도가 아니라 선택자 오타나 HTML 구조를 의심해야 합니다. 속성 이름 옆에 노란 경고 아이콘이 뜨면 값이 잘못되어 무시된 경우입니다. 선택자 조합:

/* 여러 클래스 동시 적용 */
.button.primary {
    /* class="button primary"를 가진 요소 */
    background: blue;
}
/* 여러 선택자에 같은 스타일 */
h1, h2, h3 {
    /* h1, h2, h3 모두에 적용 */
    font-family: Arial;
}

복합 선택자

/* 자손 선택자 (모든 하위) */
div p {
    color: red;
}
/* 자식 선택자 (직접 하위만) */
div > p {
    color: blue;
}
/* 인접 형제 선택자 */
h1 + p {
    font-weight: bold;
}
/* 일반 형제 선택자 */
h1 ~ p {
    color: gray;
}

속성 선택자

/* 속성 존재 */
[type] {
    border: 1px solid;
}
/* 속성 값 일치 */
[type="text"] {
    background: white;
}
/* 속성 값 포함 */
[class*="btn"] {
    padding: 10px;
}
/* 속성 값 시작 */
[href^="https"] {
    color: green;
}
/* 속성 값 끝 */
[href$=".pdf"] {
    color: red;
}

가상 클래스

가상 클래스는 요소의 특정 상태를 선택합니다:

/* 1. 링크 상태 (LVHA 순서 권장) */
a:link {
    /* 방문하지 않은 링크 */
    color: blue;
}
a:visited {
    /* 방문한 링크 */
    color: purple;
}
a:hover {
    /* 마우스를 올렸을 때 */
    color: red;
    text-decoration: underline;
}
a:active {
    /* 클릭하는 순간 */
    color: orange;
}
/* 2. 구조 가상 클래스 */
li:first-child {
    /* 첫 번째 자식 요소 */
    font-weight: bold;
}
li:last-child {
    /* 마지막 자식 요소 */
    font-style: italic;
}
li:nth-child(2) {
    /* 두 번째 자식 요소 */
    color: red;
}
li:nth-child(odd) {
    /* 홀수 번째 자식 (1, 3, 5, ...) */
    background: #f0f0f0;
}
li:nth-child(even) {
    /* 짝수 번째 자식 (2, 4, 6, ...) */
    background: white;
}
/* nth-child 고급 사용 */
li:nth-child(3n) {
    /* 3의 배수 (3, 6, 9, ...) */
    color: blue;
}
li:nth-child(3n+1) {
    /* 3으로 나눈 나머지가 1 (1, 4, 7, ...) */
    color: green;
}
/* 3. 상태 가상 클래스 */
input:focus {
    /* 포커스를 받았을 때 (클릭 또는 Tab 키) */
    border-color: blue;
    outline: 2px solid blue;
}
input:disabled {
    /* 비활성화된 입력 필드 */
    opacity: 0.5;
    cursor: not-allowed;
}
input:checked + label {
    /* 체크된 input의 바로 다음 label */
    /* + : 인접 형제 선택자 */
    color: green;
    font-weight: bold;
}
/* HTML 예시:
   <input type="checkbox" id="agree">
   <label for="agree">동의합니다</label>
*/
/* 4. 기타 유용한 가상 클래스 */
button:hover {
    /* 마우스 오버 시 */
    background: #0056b3;
    cursor: pointer;
}
input:valid {
    /* 유효한 입력 */
    border-color: green;
}
input:invalid {
    /* 유효하지 않은 입력 */
    border-color: red;
}
div:empty {
    /* 내용이 비어있는 요소 (공백 문자만 있어도 비어 있지 않음) */
    display: none;
}
.tab:not(.active) {
    /* active 클래스가 없는 탭 */
    opacity: 0.5;
}

마지막 규칙은 원래 :not(.active)만 단독으로 쓰는 경우가 많은데, 앞에 다른 선택자가 없으면 html, body를 포함한 문서의 모든 요소에 매칭됩니다. body의 불투명도가 0.5가 되고 그 안의 모든 요소가 다시 0.5씩 곱해지므로 페이지 전체가 흐릿해집니다. 부정 선택자는 항상 .tab:not(.active)처럼 범위를 좁혀 씁니다. 링크 상태를 LVHA(:link → :visited → :hover → :active) 순서로 쓰는 이유도 캐스케이드에 있습니다. 네 선택자의 특이도가 같으므로 뒤에 온 규칙이 이기는데, :hover를 :visited보다 앞에 쓰면 방문한 링크에서는 hover 효과가 덮여 나타나지 않습니다. 참고로 :visited는 방문 기록 유출을 막기 위해 color 계열 일부 속성만 바꿀 수 있고, JavaScript의 getComputedStyle로도 방문 여부를 알 수 없게 되어 있습니다. 가상 클래스 실전 예시:

/* 테이블 줄무늬 */
tr:nth-child(odd) {
    background: #f9f9f9;
}
/* 첫 단락만 크게 */
article p:first-child {
    font-size: 1.2em;
}
/* 마지막 아이템 구분선 제거 */
li:last-child {
    border-bottom: none;
}

article p:first-child는 “article 안의 첫 번째 p”가 아니라 “부모의 첫 번째 자식이면서 p인 요소” 입니다. 글 맨 앞에 <h1>이 있으면 첫 자식이 h1이므로 이 규칙은 아무것에도 매칭되지 않습니다. 태그 종류 중 첫 번째를 원한다면 p:first-of-type을 써야 합니다. :nth-child도 같은 방식으로 형제 전체를 세기 때문에, 테이블 줄무늬를 넣었는데 중간에 숨겨진 행이 있으면 줄무늬가 어긋나 보이는 문제가 생깁니다.

가상 요소

/* 첫 글자 */
p::first-letter {
    font-size: 2em;
    font-weight: bold;
}
/* 첫 줄 */
p::first-line {
    color: blue;
}
/* 앞에 내용 추가 */
.note::before {
    content: "📌 ";
}
/* 뒤에 내용 추가 */
.external-link::after {
    content: " 🔗";
}

색상 표현 방법

색상 표현 방법

/* 색상 이름 */
color: red;
color: blue;
color: green;
/* HEX 코드 */
color: #FF0000;  /* 빨강 */
color: #00FF00;  /* 초록 */
color: #0000FF;  /* 파랑 */
color: #FFF;     /* 축약형 */
/* RGB */
color: rgb(255, 0, 0);
color: rgb(0, 255, 0);
color: rgb(0, 0, 255);
/* RGBA (투명도) */
color: rgba(255, 0, 0, 0.5);  /* 50% 투명 */
background: rgba(0, 0, 0, 0.8);
/* HSL */
color: hsl(0, 100%, 50%);    /* 빨강 */
color: hsl(120, 100%, 50%);  /* 초록 */
color: hsl(240, 100%, 50%);  /* 파랑 */
/* HSLA (투명도) */
color: hsla(0, 100%, 50%, 0.5);

색상 활용

.box {
    /* 글자 색 */
    color: #333;
    
    /* 배경색 */
    background-color: #f5f5f5;
    
    /* 테두리 색 */
    border-color: #ddd;
    
    /* 그라데이션 */
    background: linear-gradient(to right, #667eea, #764ba2);
    background: radial-gradient(circle, #667eea, #764ba2);
}

같은 블록에 background를 두 번 쓰면 뒤의 radial-gradient만 남습니다. 위 코드는 두 표현을 나란히 보여 주기 위한 것이고, 실제로는 하나만 씁니다. 또 background 단축 속성은 앞에서 지정한 background-color까지 초기화하므로, background-color를 먼저 쓰고 background를 뒤에 쓰면 배경색이 사라집니다. 단축 속성이 생략된 하위 속성을 기본값으로 되돌린다는 규칙은 font, margin, border에도 똑같이 적용되어 “분명히 설정했는데 사라졌다”는 증상의 흔한 원인이 됩니다.

색상 표기법 중에서는 HSL이 사람이 다루기 쉽습니다. 색상(hue)은 그대로 두고 명도(lightness)만 바꿔 hover 색이나 밝은 변형을 만들 수 있기 때문입니다. 최신 브라우저는 rgb(255 0 0 / 50%)처럼 쉼표 없는 문법과 rgba 없이 rgb에 투명도를 넣는 방식을 지원하므로 rgba/hsla를 따로 쓸 필요가 없어졌습니다. 글자색과 배경색을 정할 때는 명도 대비도 확인해야 하는데, 일반 본문 텍스트는 WCAG 기준 4.5:1 이상을 권장하며 개발자 도구의 색상 선택기에서 대비율을 바로 볼 수 있습니다.


폰트와 웹 폰트

폰트 기본

.text {
    /* 폰트 종류 */
    font-family: Arial, sans-serif;
    font-family: 'Malgun Gothic', '맑은 고딕', sans-serif;
    
    /* 폰트 크기 */
    font-size: 16px;
    font-size: 1.5em;
    font-size: 1.5rem;
    
    /* 폰트 굵기 */
    font-weight: normal;   /* 400 */
    font-weight: bold;     /* 700 */
    font-weight: 600;
    
    /* 폰트 스타일 */
    font-style: normal;
    font-style: italic;
    
    /* 줄 높이 */
    line-height: 1.6;
    line-height: 24px;
}

웹 폰트

/* Google Fonts */
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;700&display=swap');
body {
    font-family: 'Noto Sans KR', sans-serif;
}
/* 로컬 폰트 */
@font-face {
    font-family: 'MyFont';
    src: url('fonts/myfont.woff2') format('woff2'),
         url('fonts/myfont.woff') format('woff');
    font-weight: normal;
    font-style: normal;
}
.custom {
    font-family: 'MyFont', sans-serif;
}

CSS 안의 @import는 편하지만 렌더링을 늦춥니다. 브라우저는 스타일시트를 내려받아 파싱한 뒤에야 @import된 파일을 요청할 수 있어 요청이 직렬로 늘어서기 때문입니다. Google Fonts라면 HTML <head>에 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>와 <link rel="stylesheet" href="...">를 두는 쪽이 빠릅니다. URL 끝의 display=swap은 font-display: swap에 해당해, 폰트가 도착하기 전에는 대체 폰트로 글자를 먼저 보여 줍니다. 이 설정이 없으면 폰트가 로드될 때까지 글자가 보이지 않는 현상(FOIT)이 생기고, 반대로 swap을 쓰면 폰트가 바뀌는 순간 줄바꿈이 달라져 레이아웃이 움직일 수 있습니다. 한글 폰트는 글자 수가 많아 파일이 크므로, 필요한 굵기만 불러오는 것이 로딩 시간에 가장 큰 영향을 줍니다.


텍스트 정렬·장식·오버플로우

텍스트 정렬

.text {
    /* 수평 정렬 */
    text-align: left;
    text-align: center;
    text-align: right;
    text-align: justify;
    
    /* 수직 정렬 (인라인 요소) */
    vertical-align: top;
    vertical-align: middle;
    vertical-align: bottom;
}

텍스트 장식

.text {
    /* 밑줄 */
    text-decoration: underline;
    text-decoration: none;
    text-decoration: line-through;
    
    /* 대소문자 변환 */
    text-transform: uppercase;  /* 대문자 */
    text-transform: lowercase;  /* 소문자 */
    text-transform: capitalize; /* 첫 글자만 대문자 */
    
    /* 들여쓰기 */
    text-indent: 20px;
    
    /* 자간 */
    letter-spacing: 2px;
    
    /* 단어 간격 */
    word-spacing: 5px;
    
    /* 그림자 */
    text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
}

텍스트 오버플로우

.text {
    /* 한 줄 말줄임 */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    
    /* 여러 줄 말줄임 (Webkit) */
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

한 줄 말줄임과 여러 줄 말줄임은 서로 다른 규칙에 써야 합니다. 위처럼 한 블록에 모두 넣으면 white-space: nowrap이 줄바꿈을 막아 여러 줄 말줄임이 동작하지 않습니다. 한 줄 말줄임에서 가장 흔한 실패는 요소에 너비 제한이 없는 경우입니다. text-overflow: ellipsis는 내용이 상자를 넘칠 때만 동작하는데, Flexbox 자식은 기본값 min-width: auto 때문에 내용 길이만큼 늘어나 넘치지 않습니다. 플렉스 아이템 안에서 말줄임이 안 된다면 그 아이템에 min-width: 0을 주는 것이 해결책인 경우가 대부분입니다. -webkit-line-clamp는 접두사가 붙어 있지만 현재 주요 브라우저가 모두 지원합니다.


절대 단위와 상대 단위

절대 단위


.element {
    width: 100px;   /* 픽셀 */
    width: 2.54cm;  /* 센티미터 */
    width: 25.4mm;  /* 밀리미터 */
    width: 72pt;    /* 포인트 */
}

상대 단위

.element {
    /* em: 부모 요소 기준 */
    font-size: 1.5em;
    
    /* rem: 루트 요소 기준 */
    font-size: 1.5rem;
    
    /* %: 부모 요소 기준 */
    width: 50%;
    
    /* vw/vh: 뷰포트 기준 */
    width: 50vw;   /* 뷰포트 너비의 50% */
    height: 100vh; /* 뷰포트 높이의 100% */
    
    /* vmin/vmax */
    width: 50vmin;  /* 뷰포트 작은 쪽 기준 */
    width: 50vmax;  /* 뷰포트 큰 쪽 기준 */
}

상대 단위의 “기준”은 속성마다 달라서 헷갈리기 쉽습니다. em은 font-size에 쓰면 부모의 글자 크기 기준이지만, padding이나 margin에 쓰면 그 요소 자신의 글자 크기 기준입니다. 그래서 font-size: 1.2em인 목록을 세 단계 중첩하면 안쪽 글자가 1.2 × 1.2 × 1.2배로 커지는 누적 문제가 생기고, 이것이 글자 크기에는 rem을, 버튼 안쪽 여백처럼 글자 크기에 비례해야 하는 값에는 em을 쓰는 관례가 생긴 이유입니다. %도 width는 부모 너비 기준이지만, padding-top과 margin-top의 %는 높이가 아니라 부모의 너비 기준입니다.

100vh는 모바일에서 특히 문제가 됩니다. 모바일 브라우저의 주소창이 나타나고 사라지면서 실제 보이는 높이가 바뀌는데, vh는 주소창이 숨겨진 가장 큰 높이를 기준으로 계산되어 전체 화면 레이아웃의 아래쪽이 가려집니다. 최신 브라우저가 지원하는 dvh(현재 보이는 높이), svh(가장 작은 높이), lvh(가장 큰 높이) 단위가 이 문제를 위해 추가되었습니다. 사용자가 브라우저 기본 글자 크기를 키웠을 때 px로 지정한 글자는 따라 커지지 않으므로, 접근성 측면에서도 본문 글자 크기는 rem이 권장됩니다.


CSS 변수 정의와 사용

변수 정의와 사용

:root {
    /* 변수 정의 */
    --primary-color: #3498db;
    --secondary-color: #2ecc71;
    --font-size-base: 16px;
    --spacing-unit: 8px;
}
.button {
    /* 변수 사용 */
    background-color: var(--primary-color);
    font-size: var(--font-size-base);
    padding: calc(var(--spacing-unit) * 2);
}
.button:hover {
    background-color: var(--secondary-color);
}
/* 기본값 지정 */
.element {
    color: var(--text-color, #333);
}

CSS 변수(정식 명칭은 사용자 정의 속성)는 Sass의 $변수와 다르게 런타임에 캐스케이드와 상속을 따르는 실제 속성입니다. :root에 정의한 값을 .dark 클래스 안에서 다시 정의하면 그 하위 요소들만 새 값을 보고, JavaScript로 element.style.setProperty('--primary-color', 'red')를 호출하면 그 자리에서 화면이 바뀝니다. 다크 모드나 테마 전환을 변수로 구현하는 이유가 이것입니다. 대신 값의 유효성은 사용 시점에 검사되므로, --font-size-base: 16처럼 단위를 빠뜨리면 정의할 때는 아무 오류가 없다가 font-size: var(--font-size-base)에서 무효가 되어 상속값이나 초기값으로 조용히 되돌아갑니다. var()의 두 번째 인자인 대체값은 변수가 정의되지 않았을 때만 쓰이고, 정의는 됐지만 값이 잘못된 경우에는 쓰이지 않는다는 점도 헷갈리기 쉽습니다. 또 변수 이름은 대소문자를 구분해 --Primary와 --primary는 서로 다른 변수입니다.


버튼 스타일 만들어 보기

예제: 버튼 스타일

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>버튼 스타일</title>
    <style>
        :root {
            --primary: #3498db;
            --success: #2ecc71;
            --danger: #e74c3c;
            --warning: #f39c12;
        }
        .btn {
            display: inline-block;
            padding: 12px 24px;
            font-size: 16px;
            font-weight: 600;
            text-decoration: none;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            transition: all 0.3s;
        }
        .btn-primary {
            background-color: var(--primary);
            color: white;
        }
        .btn-primary:hover {
            background-color: #2980b9;
            transform: translateY(-2px);
            box-shadow: 0 4px 8px rgba(0,0,0,0.2);
        }
        .btn-success {
            background-color: var(--success);
            color: white;
        }
        .btn-danger {
            background-color: var(--danger);
            color: white;
        }
        .btn-outline {
            background-color: transparent;
            color: var(--primary);
            border: 2px solid var(--primary);
        }
        .btn-outline:hover {
            background-color: var(--primary);
            color: white;
        }
    </style>
</head>
<body>
    <button class="btn btn-primary">Primary</button>
    <button class="btn btn-success">Success</button>
    <button class="btn btn-danger">Danger</button>
    <button class="btn btn-outline">Outline</button>
</body>
</html>

CSS 기초 요약

핵심 요약

  1. 선택자: 태그, 클래스, ID, 가상 클래스
  2. 색상: HEX, RGB, RGBA, HSL
  3. 폰트: font-family, font-size, font-weight
  4. 텍스트: text-align, text-decoration
  5. 단위: px, em, rem, %, vw, vh
  6. 변수: —변수명, var(—변수명)

다음 단계


같이 보면 좋은 글


자주 묻는 질문 (FAQ)

Q. !important는 언제 써도 괜찮나요?

A. !important는 캐스케이드에서 특이도보다 앞 단계인 ‘중요도’에서 이기기 때문에, 한 번 쓰면 그 규칙을 덮으려고 또 !important를 쓰는 악순환이 생기기 쉽습니다. 스타일이 안 먹을 때는 먼저 개발자 도구에서 어떤 규칙이 이기고 있는지 확인하고 선택자 특이도나 선언 순서로 해결하는 것이 좋습니다. 외부 라이브러리의 인라인 스타일을 덮어야 하거나 유틸리티 클래스처럼 반드시 이겨야 하는 좁은 경우에만 제한적으로 씁니다.