본문 바로가기

CSS 기본 문법과 선택자

선택자와 선언 블록의 문법을 익히고 인라인·내부·외부 스타일 시트의 적용 범위와 우선순위를 구분합니다.

2장에서는 HTML로 페이지의 뼈대와 내용을 만드는 방법을 익혔습니다. 이제 글을 정리하고, 이미지와 링크를 넣고, 폼으로 입력을 받는 페이지를 만들 수 있지만, HTML만으로는 페이지가 정보를 나열한 문서처럼 투박하게 보입니다. 여기에 모양을 입히는 언어가 CSS(Cascading Style Sheets)입니다. CSS는 색, 글꼴, 크기, 배치, 여백처럼 눈에 보이는 모든 것을 정해 페이지를 읽기 쉽고 쓰기 편하게 만듭니다.

3장에서는 CSS의 기초와 레이아웃을 다룹니다. 이 글에서는 CSS 규칙의 문법과 HTML에 CSS를 연결하는 세 가지 방법, 그리고 스타일을 적용할 요소를 고르는 선택자(selector)를 살펴봅니다.

선택된 CSS 선언이 cascade를 거쳐 적용되는 과정 — 상태 추적

앞 우선순위가 동률일 때만 specificity/scope/소스 순서로 진행한다. 같은 두 선언의 살아남은 후보와 다음 검사 도달 여부를 상태로 나타낼 가치가 있다.

CSS는 앞 조건이 동률일 때 다음 조건을 비교합니다

CSS CASCADE

CSS는 앞 조건이 동률일 때 다음 조건을 비교합니다

같은 요소·같은 속성의 후보를 cascade로 선택한 뒤, 지정값 → 계산값 → 사용값으로 처리합니다.

동률인 후보만 다음 cascade 비교로 간다유효 후보를 추린 뒤 출처·중요도·layer, specificity, scope 거리, 소스 순서를 비교한다. 앞 단계에서 우위가 정해지면 오른쪽 선으로 선택에 이르고 동률일 때만 다음 비교로 간다. 후보가 없으면 기본 처리를 사용한다. 지정값·계산값·사용값은 선택 이후 값 처리다.유효 후보선택자·조건 맞는 선언후보 전달출처·중요도·layer앞선 우선순위 비교동률우위 결정specificityID → class → type동률우위 결정scope 거리@scope의 가까운 root동률우위 결정소스 순서앞 조건 같으면 마지막선택·기본 처리선언이 없으면 상속·초깃값지정값 → 계산값 → 사용값
동률인 후보만 다음 cascade 비교로 간다유효 후보를 추린 뒤 출처·중요도·layer, specificity, scope 거리, 소스 순서를 비교한다. 앞 단계에서 우위가 정해지면 오른쪽 선으로 선택에 이르고 동률일 때만 다음 비교로 간다. 후보가 없으면 기본 처리를 사용한다. 지정값·계산값·사용값은 선택 이후 값 처리다.유효 후보선택자·조건 맞는 선언후보 전달출처·중요도·layer앞선 우선순위 비교동률우위 결정specificityID → class → type동률우위 결정scope 거리@scope의 가까운 root동률우위 결정소스 순서앞 조건 같으면 마지막선택·기본 처리선언이 없으면 상속·초깃값지정값 → 계산값 → 사용값
  1. 1 · 후보 확인

    선택자와 현재 @media·@supports 조건이 맞는 유효 선언

  2. 2 · 출처·중요도·layer

    UA·user·author, normal/!important, layer와 inline style. 애니메이션·전환도 이 우선순위에 관여

  3. 3 · specificity

    같은 앞선 계층 안에서 ID → class·attribute·pseudo-class → type·pseudo-element

  4. 4 · scope 거리

    specificity도 같고 @scope를 쓴 경우, scope root까지 더 가까운 선언

  5. 5 · 소스 순서

    앞 조건이 모두 같을 때 마지막 선언

Specified · 지정값

선택된 선언 또는 상속·초깃값 등 기본 처리로 지정값을 정합니다.

Computed · 계산값

속성 규칙에 따라 상속과 상대값 등을 계산합니다. 모든 %가 여기서 px이 되는 것은 아닙니다.

Used · 사용값

레이아웃에서 필요한 containing block 크기 등으로 실제 사용할 값을 정합니다.

외부 CSS와 내부 CSS는 위치만으로 우선순위가 갈리지 않습니다

둘 다 작성자 스타일입니다. 위 순서를 확인한 뒤 소스 순서를 비교합니다. 기본 상속은 자식 요소에 직접 적용한 선언끼리의 specificity 경쟁과 별개입니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
원도표의 전체 관계·조건·설명
구분내용
원도표 조건·주석1 · 후보 확인: 선택자와 현재 @media·@supports 조건이 맞는 유효 선언
원도표 조건·주석2 · 출처·중요도·layer: UA·user·author, normal/!important, layer와 inline style. 애니메이션·전환도 이 우선순위에 관여
원도표 조건·주석3 · specificity: 같은 앞선 계층 안에서 ID → class·attribute·pseudo-class → type·pseudo-element
원도표 조건·주석4 · scope 거리: specificity도 같고 @scope를 쓴 경우, scope root까지 더 가까운 선언
원도표 조건·주석5 · 소스 순서: 앞 조건이 모두 같을 때 마지막 선언
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
적용 후보후보: 적용 가능한 선언 / 제외: 조건/선택자 불일치조건과 매칭을 만족한 선언만 남긴다매칭되지 않는 선언은 specificity로 되살리지 않는다
cascade 위치판정: 상위 cascade 기준 / specificity: 같은 위치에서 비교origin/important/layer 등 상위 기준을 먼저 비교한다특이성만으로 모든 선언의 우열을 정하지 않는다
동률 해소scope: 해당 조건 비교 / source order: 최종 동률만같은 기준일 때 scope 거리와 최종 source order를 읽는다파일 뒤쪽이라는 이유만으로 항상 이기는 것은 아니다

선택된 CSS 선언이 cascade를 거쳐 적용되는 과정 — 상태 추적

앞 우선순위가 동률일 때만 specificity/scope/소스 순서로 진행한다. 같은 두 선언의 살아남은 후보와 다음 검사 도달 여부를 상태로 나타낼 가치가 있다.

CSS는 앞 조건이 동률일 때 다음 조건을 비교합니다

CSS CASCADE

CSS는 앞 조건이 동률일 때 다음 조건을 비교합니다

같은 요소·같은 속성의 후보를 cascade로 선택한 뒤, 지정값 → 계산값 → 사용값으로 처리합니다.

동률인 후보만 다음 cascade 비교로 간다유효 후보를 추린 뒤 출처·중요도·layer, specificity, scope 거리, 소스 순서를 비교한다. 앞 단계에서 우위가 정해지면 오른쪽 선으로 선택에 이르고 동률일 때만 다음 비교로 간다. 후보가 없으면 기본 처리를 사용한다. 지정값·계산값·사용값은 선택 이후 값 처리다.유효 후보선택자·조건 맞는 선언후보 전달출처·중요도·layer앞선 우선순위 비교동률우위 결정specificityID → class → type동률우위 결정scope 거리@scope의 가까운 root동률우위 결정소스 순서앞 조건 같으면 마지막선택·기본 처리선언이 없으면 상속·초깃값지정값 → 계산값 → 사용값
동률인 후보만 다음 cascade 비교로 간다유효 후보를 추린 뒤 출처·중요도·layer, specificity, scope 거리, 소스 순서를 비교한다. 앞 단계에서 우위가 정해지면 오른쪽 선으로 선택에 이르고 동률일 때만 다음 비교로 간다. 후보가 없으면 기본 처리를 사용한다. 지정값·계산값·사용값은 선택 이후 값 처리다.유효 후보선택자·조건 맞는 선언후보 전달출처·중요도·layer앞선 우선순위 비교동률우위 결정specificityID → class → type동률우위 결정scope 거리@scope의 가까운 root동률우위 결정소스 순서앞 조건 같으면 마지막선택·기본 처리선언이 없으면 상속·초깃값지정값 → 계산값 → 사용값
  1. 1 · 후보 확인

    선택자와 현재 @media·@supports 조건이 맞는 유효 선언

  2. 2 · 출처·중요도·layer

    UA·user·author, normal/!important, layer와 inline style. 애니메이션·전환도 이 우선순위에 관여

  3. 3 · specificity

    같은 앞선 계층 안에서 ID → class·attribute·pseudo-class → type·pseudo-element

  4. 4 · scope 거리

    specificity도 같고 @scope를 쓴 경우, scope root까지 더 가까운 선언

  5. 5 · 소스 순서

    앞 조건이 모두 같을 때 마지막 선언

Specified · 지정값

선택된 선언 또는 상속·초깃값 등 기본 처리로 지정값을 정합니다.

Computed · 계산값

속성 규칙에 따라 상속과 상대값 등을 계산합니다. 모든 %가 여기서 px이 되는 것은 아닙니다.

Used · 사용값

레이아웃에서 필요한 containing block 크기 등으로 실제 사용할 값을 정합니다.

외부 CSS와 내부 CSS는 위치만으로 우선순위가 갈리지 않습니다

둘 다 작성자 스타일입니다. 위 순서를 확인한 뒤 소스 순서를 비교합니다. 기본 상속은 자식 요소에 직접 적용한 선언끼리의 specificity 경쟁과 별개입니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.

관계도의 보충 설명

원도표의 전체 관계·조건·설명
구분내용
원도표 조건·주석1 · 후보 확인: 선택자와 현재 @media·@supports 조건이 맞는 유효 선언
원도표 조건·주석2 · 출처·중요도·layer: UA·user·author, normal/!important, layer와 inline style. 애니메이션·전환도 이 우선순위에 관여
원도표 조건·주석3 · specificity: 같은 앞선 계층 안에서 ID → class·attribute·pseudo-class → type·pseudo-element
원도표 조건·주석4 · scope 거리: specificity도 같고 @scope를 쓴 경우, scope root까지 더 가까운 선언
원도표 조건·주석5 · 소스 순서: 앞 조건이 모두 같을 때 마지막 선언
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
적용 후보후보: 적용 가능한 선언 / 제외: 조건/선택자 불일치조건과 매칭을 만족한 선언만 남긴다매칭되지 않는 선언은 specificity로 되살리지 않는다
cascade 위치판정: 상위 cascade 기준 / specificity: 같은 위치에서 비교origin/important/layer 등 상위 기준을 먼저 비교한다특이성만으로 모든 선언의 우열을 정하지 않는다
동률 해소scope: 해당 조건 비교 / source order: 최종 동률만같은 기준일 때 scope 거리와 최종 source order를 읽는다파일 뒤쪽이라는 이유만으로 항상 이기는 것은 아니다

위 그림은 한 요소의 한 속성에 여러 선언이 겹칠 때, 브라우저가 그중 하나를 고르는 cascade의 순서를 보여 줍니다. 먼저 선택자와 @media 같은 조건이 맞는 유효한 선언만 후보로 추립니다. 그다음 출처와 중요도, 계층을 비교하고, 같으면 선택자의 명시도(specificity)를, 그것도 같으면 @scope의 거리를, 마지막으로 나중에 나온 선언을 고릅니다. 앞 단계에서 승부가 나면 거기서 끝나고, 동률일 때만 다음 단계로 갑니다.

여기서 출처는 2장의 Web Storage에서 본 주소의 출처(origin)와는 다른 뜻으로, 선언이 브라우저의 기본 스타일, 사용자가 정한 스타일, 작성자가 쓴 CSS 가운데 어디서 왔는지를 가리킵니다. 중요도는 선언에 !important가 붙었는지를 뜻합니다. 계층(cascade layer)은 @layer로 묶은 규칙 묶음 사이의 순서입니다. @scope의 거리는 @scope로 범위를 정한 규칙에서 요소가 그 범위의 뿌리 요소로부터 몇 단계 떨어져 있는지입니다. 이 장의 예제는 계층과 @scope를 쓰지 않으므로, 지금은 명시도와 소스 순서를 중심으로 보면 됩니다.

고른 선언은 지정값(specified value)이 되고, 상속과 상대 단위를 계산한 계산값(computed value), 실제 레이아웃에서 쓰는 사용값(used value)으로 이어집니다. 후보가 하나도 없으면 부모에게서 상속받거나 속성의 초기값을 씁니다. 그림 아래의 설명처럼 외부 CSS와 내부 CSS는 둘 다 작성자의 스타일이라 위치만으로 우선순위가 정해지지 않으며, 명시도까지 같을 때 나중에 나온 쪽이 이깁니다. 이 순서는 앞으로 CSS가 예상대로 적용되지 않을 때 원인을 찾는 기준이 됩니다.


CSS가 필요한 이유

HTML이 집의 뼈대와 방의 구조라면, CSS는 벽지 색, 가구 배치, 창문의 모양 같은 인테리어입니다. CSS를 따로 두면 다음과 같은 점이 좋습니다.

  • 구조와 표현을 나눕니다: HTML은 내용의 구조와 의미를, CSS는 보이는 모양을 맡으므로 HTML이 간결해지고 디자인을 바꾸기 쉬워집니다.
  • 디자인을 일관되게 적용합니다: CSS 파일 하나를 여러 페이지에 연결하면, 사이트 전체의 디자인을 그 파일 하나만 고쳐 바꿀 수 있습니다.
  • 화면 크기에 맞춥니다: 미디어 쿼리(media query)로 PC, 태블릿, 휴대폰마다 다른 배치와 스타일을 적용합니다.
  • 읽고 조작하기 쉽게 합니다: 글자와 배경의 대비, 줄 간격, 키보드 초점 표시를 CSS로 조절합니다. 다만 CSS를 분리하는 것만으로 접근성이 갖춰지지는 않으므로, 내용과 키보드 조작도 함께 확인합니다.

CSS 규칙의 문법

CSS 규칙(rule)은 선택자와 선언 블록(declaration block)으로 이루어집니다. 다음은 규칙 하나의 생김새를 한국어 이름으로 적은 틀입니다. 중괄호 바깥의 선택자가 어느 요소에 적용할지를, 중괄호 안의 선언들이 어떤 모양으로 바꿀지를 정한다고 보면 됩니다.

선택자 {
    속성(property): 값(value);
    속성(property): 값(value);
    /* ...더 많은 속성-값 쌍 */
}

선택자는 스타일을 적용할 HTML 요소를 고릅니다. h1, p, .box, #main-header 같은 것이 선택자입니다. 중괄호 {}로 감싼 선언 블록에는 그 요소에 적용할 선언을 적으며, 각 선언은 속성(property)과 값(value)의 쌍입니다. 속성은 color, font-size, background-color, margin처럼 바꾸려는 스타일의 종류이고, 값은 red, 16px, #f0f0f0처럼 그 속성에 줄 구체적인 값입니다. 속성과 값 사이에는 콜론(:)을, 각 선언 끝에는 세미콜론(;)을 씁니다. 마지막 선언의 세미콜론은 생략해도 되지만, 나중에 선언을 더할 때 실수하지 않도록 항상 붙이는 습관을 들입니다.

앞의 코드에 적힌 “선택자”, “속성”, “값”은 문법을 설명하는 자리 표시자이고, 실제 파일에는 다음처럼 씁니다.

h1 { /* h1 태그를 선택하여 스타일 적용 */
    color: blue; /* 글자 색상을 파란색으로 */
    font-size: 32px; /* 글자 크기를 32픽셀로 */
    text-align: center; /* 텍스트를 가운데 정렬 */
}

p { /* p 태그를 선택하여 스타일 적용 */
    line-height: 1.8; /* 줄 간격을 1.8배로 */
    margin-bottom: 1em; /* 아래쪽 여백을 1em으로 */
}

이 CSS는 모든 h1을 32px 크기의 파란 글자로 가운데 정렬하고, 모든 p의 줄 간격을 글자 크기의 1.8배로, 아래 여백을 글자 하나 크기인 1em으로 정합니다. /* */ 사이는 CSS의 주석입니다.

브라우저는 이해하지 못하는 선언을 오류로 멈추지 않고 그 선언만 무시한 채 나머지를 적용합니다. 그래서 화면의 일부만 바뀌었다면, 개발자 도구의 Elements 패널 오른쪽 Styles 영역에서 그 선언이 취소선과 경고 표시로 무시되었는지 확인합니다.


HTML에 CSS를 연결하는 세 가지 방법

인라인 스타일

HTML 요소의 style 속성에 CSS 선언을 직접 씁니다. 선택자와 중괄호 없이 선언만 세미콜론으로 이어 적으며, 그 선언은 속성을 붙인 요소 하나에만 적용됩니다.

<p style="color: red; font-size: 18px;">이 문단은 인라인 스타일이 적용됩니다.</p>

특정 요소 하나에 바로 스타일을 줄 때는 편하지만, 같은 스타일을 다른 곳에 다시 쓸 수 없고 HTML과 CSS가 섞여 고치기 어렵습니다. 또 인라인 스타일은 선택자의 명시도보다 앞선 단계에서 비교되어 거의 항상 이기므로, 나중에 외부 CSS로 덮어쓰기도 어렵습니다. 반복되는 스타일은 클래스와 외부 스타일 시트로 옮기는 편이 안전합니다.

내부 스타일 시트

HTML 문서의 <head> 안에 <style> 요소를 두고 그 안에 CSS를 씁니다. <style> 안의 규칙은 앞에서 본 문법 그대로 선택자와 선언 블록으로 쓰며, 그 문서에서 선택자에 맞는 모든 요소에 적용됩니다. 다음 예에서는 body 규칙이 페이지 전체의 글꼴과 배경을, h1 규칙이 제목의 색과 밑줄을 정합니다.

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>내부 스타일 시트</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f0f0f0;
        }
        h1 {
            color: purple;
            text-decoration: underline;
        }
    </style>
</head>
<body>
    <h1>내부 스타일 시트 예시</h1>
    <p>이것은 내부 스타일 시트가 적용된 문단입니다.</p>
</body>
</html>

문서 하나에서 모든 스타일을 관리할 수 있어 간단한 페이지에는 편하지만, 같은 스타일을 다른 페이지와 나눠 쓸 수 없고 HTML 파일이 커집니다.

외부 스타일 시트

별도의 .css 파일을 만들어 HTML 문서에 연결합니다. 실제 프로젝트에서 가장 많이 쓰는 방법입니다. HTML과 CSS가 완전히 나뉘어 읽고 고치기 쉽고, 파일 하나를 여러 페이지에 연결할 수 있으며, 브라우저가 CSS 파일을 캐시에 저장해 두므로 다른 페이지를 열 때 다시 받지 않아도 됩니다. 대신 연결 경로가 틀리면 스타일이 전혀 적용되지 않습니다.

HTML 파일과 같은 폴더나 css 같은 하위 폴더에 style.css를 만들고 다음과 같이 씁니다.

body {
    font-family: 'Malgun Gothic', sans-serif;
    background-color: #e6e6fa;
}
h1 {
    color: darkgreen;
    text-align: left;
}
p {
    font-size: 16px;
}

HTML 문서의 <head> 안에서 <link> 요소로 이 파일을 연결합니다. rel="stylesheet"는 연결하는 파일이 스타일 시트라는 뜻이고, href에는 HTML 파일을 기준으로 한 CSS 파일의 경로를 적습니다.

index.html
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>외부 스타일 시트</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <h1>외부 스타일 시트 예시</h1>
    <p>이것은 외부 스타일 시트가 적용된 문단입니다.</p>
    <p>HTML과 CSS 파일이 분리되어 있어 깔끔합니다.</p>
</body>
</html>

style.css를 css 폴더에 넣었다면 href="css/style.css"로 적습니다.


선택자: 스타일을 적용할 요소 고르기

선택자는 수많은 HTML 요소 가운데 어떤 요소에 스타일을 적용할지 정합니다. 선택자를 정확히 쓰는 것이 CSS를 다루는 기본입니다.

기본 선택자

전체 선택자 *는 문서의 모든 요소를 고릅니다. 모든 요소의 기본 여백을 없애는 초기화처럼 공통 규칙에 쓰지만, 범위가 넓으므로 의도하지 않은 요소까지 바뀌지 않는지 확인합니다.

* {
    margin: 0; /* 모든 요소의 기본 마진 제거 */
    padding: 0; /* 모든 요소의 기본 패딩 제거 */
}

이 규칙은 브라우저가 body, h1, p 같은 요소에 기본으로 주는 여백을 모두 0으로 만들므로, 이후의 여백은 직접 지정한 값만 남습니다.

태그 선택자(type selector)는 태그 이름으로 그 태그의 모든 요소를 고릅니다. 다음 규칙은 문서에 <p>나 <h2>가 몇 개 있든 모두에 적용되므로, 사이트 전체의 기본 모양을 정할 때 알맞습니다.

p { /* 모든 <p> 태그 */
    color: #333;
    font-family: sans-serif;
}
h2 { /* 모든 <h2> 태그 */
    border-bottom: 1px solid #ccc;
}

클래스 선택자는 .클래스명 형태로, class 속성 값이 같은 요소를 고르며 가장 많이 씁니다. 같은 클래스를 여러 요소에 붙일 수 있고, 한 요소에 공백으로 구분해 여러 클래스를 붙일 수도 있습니다.

<h1 class="main-title">우리 회사</h1>
<p class="section-text">회사 소개입니다.</p>
<p class="section-text highlight">중요한 내용입니다.</p>
.main-title { /* class가 "main-title"인 요소 */
    font-size: 40px;
    color: #2c3e50;
}
.section-text { /* class가 "section-text"인 요소들 */
    line-height: 1.6;
    color: #555;
}
.highlight { /* class가 "highlight"인 요소들 */
    background-color: yellow;
}

세 번째 문단은 section-text와 highlight 두 클래스를 모두 가지므로, 회색 글자와 줄 간격에 노란 배경까지 함께 적용됩니다.

ID 선택자는 #ID명 형태로, id 속성 값이 같은 요소를 고릅니다. id는 문서 안에서 하나의 요소에만 써야 하는 고유한 이름입니다.

<div id="header">
    <h1>환영합니다!</h1>
</div>
<div id="footer">
    <p>저작권</p>
</div>
#header { /* id가 "header"인 요소 */
    background-color: #f8f8f8;
    padding: 20px;
}
#footer { /* id가 "footer"인 요소 */
    text-align: center;
    font-size: 14px;
    color: #888;
}

id는 문서에서 유일한 요소를 가리키므로 주로 링크의 목적지나 자바스크립트에서 특정 요소를 찾을 때 쓰고, class는 여러 요소에 반복해 쓸 수 있으므로 주로 CSS 스타일에 씁니다. ID 선택자는 명시도가 높아 다른 규칙으로 덮어쓰기 어려우므로, 스타일에는 클래스를 쓰는 편이 관리하기 쉽습니다.

결합 선택자

선택자를 조합하면 요소 사이의 관계로 더 구체적으로 고를 수 있습니다. 자손 선택자(descendant selector)는 두 선택자를 공백으로 이어, 앞 요소 안에 있는 모든 자손 요소를 고릅니다. 하위 선택자라고도 부릅니다.

<div class="menu">
    <ul>
        <li><a href="#">메뉴 1</a></li>
        <li><a href="#">메뉴 2</a></li>
    </ul>
</div>
<p>일반 문단</p>
.menu ul li a { /* class가 "menu"인 요소 내부의 ul 내부의 li 내부의 a 태그 */
    text-decoration: none; /* 밑줄 제거 */
    color: #4CAF50; /* 초록색 */
}

이 CSS는 menu 클래스 안의 ul 안의 li 안에 있는 링크만 밑줄 없는 초록 글자로 만들고, 메뉴 밖의 일반 문단에는 아무 영향도 주지 않습니다.

자식 선택자(child selector)는 >로 이어, 앞 요소의 바로 아래 자식만 고릅니다.

<div class="container">
    <p>자식 문단 1</p>
    <div>
        <p>손자 문단</p>
    </div>
    <p>자식 문단 2</p>
</div>
.container > p { /* class가 "container"인 요소의 직계 자식인 <p> 태그만 */
    border: 1px solid blue;
}
/* 위의 CSS는 "손자 문단"에는 적용되지 않습니다. */

자식 문단 1과 자식 문단 2는 .container의 바로 아래 자식이므로 파란 테두리를 얻습니다. 손자 문단은 중간의 div 안에 들어 있어 한 단계 더 깊으므로, 같은 p여도 고르지 않습니다. 같은 자리에 공백을 쓴 자손 선택자 .container p였다면 세 문단이 모두 골라졌을 것입니다.

인접 형제 선택자(adjacent sibling selector)는 +로 이어, 같은 부모를 가진 형제 중 바로 다음에 오는 요소 하나를 고릅니다. 다음 HTML에서 <h1>의 바로 다음 형제는 첫 번째 문단 하나뿐입니다.

<h1>제목</h1>
<p>첫 번째 문단</p>
<p>두 번째 문단</p>
<div>다른 요소</div>
<p>세 번째 문단</p>
h1 + p { /* <h1> 태그 바로 뒤에 오는 <p> 태그 */
    font-weight: bold;
    color: red;
}
/* 이 스타일은 "첫 번째 문단"에만 적용됩니다. */

두 번째 문단도 <h1> 뒤에 있지만 바로 다음 형제가 아니므로 빨간 굵은 글자가 되지 않습니다. 일반 형제 선택자(general sibling selector)는 ~로 이어, 앞 요소 뒤에 오는 모든 형제를 고릅니다. 같은 HTML에 적용하면 사이에 div가 있어도 세 문단이 모두 기울어집니다.

h1 ~ p { /* <h1> 태그 다음에 오는 모든 <p> 태그들 */
    font-style: italic;
}
/* 이 스타일은 "첫 번째 문단", "두 번째 문단", "세 번째 문단" 모두에 적용됩니다. */

속성 선택자

속성 선택자는 속성의 유무나 값으로 요소를 고릅니다. [속성]은 그 속성을 가진 요소를, [속성="값"]은 값이 정확히 같은 요소를, [속성^="값"]은 값이 그 문자열로 시작하는 요소를, [속성$="값"]은 그 문자열로 끝나는 요소를, [속성*="값"]은 그 문자열을 포함하는 요소를 고릅니다.

a[target="_blank"] { /* target="_blank" 속성을 가진 모든 <a> 태그 */
    background-color: lightblue;
}
input[type="text"] { /* type="text" 속성을 가진 모든 <input> 태그 */
    border: 1px solid gray;
}
img[alt*="로고"] { /* alt 속성 값에 "로고"가 포함된 모든 <img> 태그 */
    border: 2px solid green;
}

가상 클래스

가상 클래스(pseudo-class)는 콜론 하나(:)로 쓰며, 요소의 상태나 위치에 따라 고릅니다. 자주 쓰는 가상 클래스는 다음과 같습니다.

  • :hover: 마우스를 요소 위에 올렸을 때
  • :active: 요소를 누르고 있는 동안
  • :focus: 입력 필드처럼 요소에 키보드 초점이 있을 때
  • :link, :visited: 방문하지 않은 링크와 방문한 링크
  • :first-child, :last-child: 부모의 첫 번째 자식과 마지막 자식
  • :nth-child(n): 부모의 n번째 자식으로, n에는 숫자나 odd(홀수), even(짝수)을 씁니다.
  • :not(선택자): 괄호 안의 선택자에 맞지 않는 요소
a:hover { /* 링크에 마우스 올렸을 때 */
    color: orange;
    text-decoration: underline;
}
input:focus { /* input 필드에 포커스 있을 때 */
    border-color: blue;
    box-shadow: 0 0 5px rgba(0, 0, 255, 0.5);
}
li:first-child { /* 목록의 첫 번째 항목 */
    font-weight: bold;
}
li:nth-child(even) { /* 목록의 짝수 번째 항목 */
    background-color: #f2f2f2;
}
button:active { /* 버튼 클릭 시 */
    transform: translateY(1px); /* 살짝 눌리는 효과 */
}

:hover, :focus, :active처럼 상태를 나타내는 가상 클래스는 그 상태가 끝나면 원래 스타일로 돌아갑니다. li:nth-child(even)은 1부터 세어 두 번째, 네 번째 항목을 고릅니다. 이때 순서는 태그 종류와 상관없이 같은 부모의 모든 자식 요소를 기준으로 세므로, 자식이 모두 li인 목록에서는 예상대로 동작합니다.

가상 요소

가상 요소(pseudo-element)는 콜론 두 개(::)로 쓰며, 요소의 특정 부분을 고르거나 내용을 만들어 넣습니다. ::before와 ::after는 요소의 내용 앞과 뒤에 content 속성으로 만든 내용을 넣고, ::first-letter와 ::first-line은 글의 첫 글자와 첫 줄을, ::selection은 사용자가 끌어서 선택한 부분을 고릅니다. 예전 표기와의 호환을 위해 before, after, first-letter, first-line 네 가지는 콜론 하나로도 쓸 수 있지만, 가상 클래스와 구분하기 위해 콜론 두 개로 씁니다.

p::first-letter { /* <p> 태그의 첫 글자 */
    font-size: 2em;
    color: #ff5733;
    float: left; /* 드롭캡 효과 */
    line-height: 1;
    margin-right: 5px;
}
a::after { /* <a> 태그 콘텐츠 뒤에 화살표 아이콘 추가 */
    content: " →";
    font-size: 0.8em;
    vertical-align: super;
}

::after로 넣은 화살표는 CSS가 만든 장식이라 HTML의 내용에는 없습니다. 브라우저에 따라 스크린 리더가 이 내용을 읽기도 하므로, 의미 있는 정보는 HTML에 직접 씁니다.

더 깊이: 선택자 하나가 틀리면 규칙 전체가 사라진다

h1, h2 { ... }처럼 쉼표로 여러 선택자를 묶은 것을 선택자 목록(selector list)이라고 하며, 같은 선언을 여러 요소에 한 번에 적용할 때 씁니다. 그런데 앞에서 본 잘못된 선언의 처리와 달리, 선택자 목록은 하나라도 잘못되면 규칙 전체가 무시됩니다. h1, h2:invalid-pseudo, h3 { ... }에서는 잘못된 가운데 선택자뿐 아니라 h1과 h3에도 아무 스타일이 적용되지 않습니다. 오타뿐 아니라 어떤 브라우저가 아직 지원하지 않는 가상 클래스나, Chrome이 모르는 ::-moz- 접두사처럼 다른 엔진에만 있는 가상 요소를 목록에 섞었을 때도 같은 일이 생깁니다. 다만 ::-webkit-으로 시작하는 가상 요소는 예전 사이트와의 호환을 위해 모르는 이름이어도 올바른 선택자로 해석하도록 Selectors 명세가 정해 두었으므로, 이 때문에 규칙 전체가 무시되지는 않습니다.

이 문제를 피하는 방법은 두 가지입니다. 하나는 지원이 불확실한 선택자를 별도의 규칙으로 떼어 쓰는 것입니다. 다른 하나는 :is()나 :where()로 감싸는 것입니다. 이 두 가상 클래스는 너그러운 선택자 목록(forgiving selector list)을 받아 안의 선택자를 하나씩 따로 해석하므로, :is(h1, h2:invalid-pseudo, h3)에서는 잘못된 선택자만 버리고 h1과 h3에는 스타일이 적용됩니다.

다만 모든 괄호형 가상 클래스가 너그러운 것은 아닙니다. MDN의 선택자 목록 설명은 너그러운 목록을 받는 것으로 :is()와 :where()만 들고, :not()은 일반 선택자 목록을, :has()는 상대 선택자 목록을 받는다고 구분합니다. 새로운 선택자를 쓸 때 이 차이를 모르면, 멀쩡해 보이는 선택자까지 함께 적용되지 않는 원인을 찾기 어렵습니다.


정리

  • 여러 선언이 같은 요소의 같은 속성에 겹치면, 출처와 중요도, 계층, 명시도, @scope 거리, 소스 순서를 차례로 비교해 하나를 고르며, 앞 단계가 동률일 때만 다음 단계로 갑니다.
  • CSS 규칙은 선택자와 선언 블록으로 이루어지고, 선언은 속성: 값; 형태이며, 브라우저는 잘못된 선언만 무시합니다.
  • CSS는 인라인 스타일, 내부 스타일 시트, 외부 스타일 시트로 연결하며, 실제 프로젝트에서는 외부 스타일 시트를 씁니다.
  • 기본 선택자는 전체, 태그, 클래스, ID 선택자이며, 스타일에는 주로 클래스를 씁니다.
  • 공백, >, +, ~는 각각 자손, 자식, 바로 다음 형제, 뒤의 모든 형제를 고르는 결합 선택자입니다.
  • 속성 선택자는 속성 값으로, 가상 클래스는 상태와 위치로, 가상 요소는 요소의 일부나 만든 내용으로 고릅니다.

연습 문제

1. 자식과 자손

자식 선택자 예제의 HTML에서 .container > p와 .container p는 각각 어떤 문단을 고를까요? “손자 문단”만 파란 테두리 없이 남기려면 어느 쪽을 써야 할까요?

해설 보기

.container > p는 바로 아래 자식인 “자식 문단 1”과 “자식 문단 2”만 고릅니다. 손자 문단의 바로 위 부모는 안쪽 div이므로 빠집니다. .container p는 깊이와 상관없이 안에 있는 세 문단을 모두 고릅니다.

그래서 손자 문단만 테두리 없이 남기려면 .container > p를 씁니다. 두 선택자는 우선순위를 따지기 전에, 고르는 요소부터 다릅니다.

2. 바로 다음 형제

인접 형제 선택자 예제의 HTML에서 p + p는 어떤 문단을 고를까요? div + p는 어떨까요?

해설 보기

p + p는 바로 앞 형제가 p인 문단을 고르므로 “두 번째 문단”만 고릅니다. “첫 번째 문단”의 바로 앞은 h1이고, “세 번째 문단”의 바로 앞은 div이기 때문입니다.

div + p는 div 바로 다음에 오는 “세 번째 문단”을 고릅니다.

3. 같은 요소에 두 규칙

<p class="note">안내</p>에 다음 두 규칙이 적용됩니다. 글자는 무슨 색이 될까요? 두 규칙의 순서를 바꾸면 어떻게 될까요?

.note { color: green; }
p { color: red; }
해설 보기

초록색입니다. 두 선언은 출처와 중요도가 같으므로 명시도를 비교하는데, 클래스 선택자 .note가 태그 선택자 p보다 명시도가 높습니다. 그래서 나중에 나온 p 규칙이 아니라 .note 규칙이 이깁니다.

순서를 바꿔도 결과는 초록색입니다. 소스 순서는 명시도까지 같을 때만 비교하기 때문입니다. 두 규칙이 모두 p나 모두 .note처럼 명시도가 같았다면, 그때는 나중에 나온 규칙의 색이 적용됩니다.

다음 글 외부 스타일 시트와 선택자 실습에서는 외부 CSS 파일을 만들어 이 글의 선택자들을 실제 페이지에 적용해 봅니다.