본문 바로가기

외부 스타일 시트와 선택자 실습

외부 CSS 파일에 다양한 선택자를 적용해 구조와 표현이 분리된 웹 페이지를 완성합니다.

실습: 외부 스타일 시트와 선택자 활용하기

지금까지 배운 CSS 기본 문법과 다양한 선택자들을 활용하여 간단한 HTML 페이지에 스타일을 적용해 봅시다. 이 실습은 선택자의 매칭과 화면 표현을 확인합니다. 링크의 href="#"는 현재 문서 안 이동이며 “외부 링크”라는 글자만으로 외부 사이트가 되지 않습니다. 자세히 알아보기 버튼의 업무 동작과 문의를 저장하는 서버는 구현하지 않았습니다.

프로젝트 폴더 구조
index.html
style.css
index.html 파일 작성
index.html
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CSS 기본 문법과 선택자 실습</title>
    <link rel="stylesheet" href="css/style.css"> </head>
<body>
    <header id="main-header">
        <h1 class="page-title">내 웹사이트</h1>
        <nav>
            <ul>
                <li class="menu-item"><a href="#">홈</a></li>
                <li class="menu-item"><a href="#">소개</a></li>
                <li class="menu-item"><a href="#" target="_blank">외부 링크</a></li>
                <li class="menu-item"><a href="#">연락처</a></li>
            </ul>
        </nav>
    </header>

    <main>
        <section class="content-section">
            <h2>환영합니다!</h2>
            <p>이곳은 CSS 선택자 연습을 위한 <span class="highlight">샘플 페이지</span>입니다.
                다양한 요소에 CSS가 어떻게 적용되는지 확인해 보세요.</p>
            <button class="call-to-action">자세히 알아보기</button>
        </section>

        <section class="content-section">
            <h3>CSS 학습 리스트</h3>
            <ul>
                <li>기본 문법</li>
                <li>선택자</li>
                <li>속성</li>
                <li>레이아웃</li>
            </ul>
            <p class="small-text">이 목록은 CSS로 스타일링되었습니다.</p>
        </section>

        <section class="form-section">
            <h3>문의하기</h3>
            <form>
                <label for="name">이름:</label>
                <input type="text" id="name" name="name" placeholder="이름을 입력하세요" required><br>

                <label for="email">이메일:</label>
                <input type="email" id="email" name="email" placeholder="[email protected]"><br>

                <button type="submit">문의 제출</button>
            </form>
        </section>
    </main>

    <footer>
        <p>&copy; 2025 웹 개발 학습. 모든 권리 보유.</p>
        <p class="small-text">CSS 기본을 배우는 중입니다.</p>
    </footer>
</body>
</html>
css/style.css 파일 작성
css/style.css
/* 1. 전체 선택자: 기본 여백 제거 */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box; /* 패딩과 보더가 요소의 너비/높이에 포함되도록 설정 */
}

/* 2. 태그 선택자: 기본 폰트 및 배경 색상 */
body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background-color: #f0f2f5;
    color: #333;
    line-height: 1.6;
}

/* ID 선택자: 헤더 스타일링 */
#main-header {
    background-color: #2c3e50;
    color: white;
    padding: 20px 0;
    text-align: center;
}

/* 하위 선택자: 헤더 내부의 h1 */
#main-header .page-title {
    font-size: 2.5em;
    margin-bottom: 10px;
    color: #ecf0f1;
}

/* 태그 선택자: 네비게이션 ul 초기화 */
nav ul {
    list-style: none; /* 목록 기호 제거 */
}

/* 자식 선택자: nav 바로 아래 ul의 자식 li */
nav > ul > li {
    display: inline-block; /* 메뉴 항목을 가로로 배열 */
    margin: 0 15px;
}

/* 클래스 선택자: 메뉴 아이템 링크 */
.menu-item a {
    color: #f1c40f; /* 노란색 */
    text-decoration: none; /* 밑줄 제거 */
    font-weight: bold;
    transition: color 0.3s ease; /* 호버 효과를 부드럽게 */
}

/* 가상 클래스 선택자: 링크 호버 시 */
.menu-item a:hover {
    color: #f39c12; /* 오렌지색 */
}

/* 속성 선택자: target="_blank" 속성을 가진 링크 */
a[target="_blank"]::after { /* 가상 요소 선택자: 외부 링크 뒤에 아이콘 추가 */
    content: " ↗";
    font-size: 0.8em;
    vertical-align: top;
    color: #f1c40f;
}

/* 메인 콘텐츠 영역 */
main {
    max-width: 900px;
    margin: 20px auto;
    padding: 20px;
    background-color: white;
    border-radius: 8px;
    box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}

/* 클래스 선택자: 섹션 공통 스타일 */
.content-section {
    margin-bottom: 30px;
    padding: 20px;
    border: 1px solid #e0e0e0;
    border-radius: 5px;
    background-color: #ffffff;
}

/* 하위 선택자: .content-section 내부의 h2 */
.content-section h2 {
    color: #2980b9;
    margin-bottom: 15px;
    border-bottom: 2px solid #2980b9;
    padding-bottom: 5px;
}

/* 클래스 선택자: 하이라이트 텍스트 */
.highlight {
    background-color: #ffeaa7; /* 연한 노란색 */
    padding: 2px 5px;
    border-radius: 3px;
    font-weight: bold;
}

/* 클래스 선택자: 버튼 스타일 */
button.call-to-action {
    background-color: #27ae60;
    color: white;
    padding: 12px 25px;
    border: none;
    border-radius: 5px;
    cursor: pointer;
    font-size: 1.1em;
    transition: background-color 0.3s ease;
    margin-top: 20px;
}
button.call-to-action:hover {
    background-color: #229954;
}

/* 태그 선택자: ul 내부의 li (목록 기호 색상 변경) */
.content-section ul li {
    list-style-type: square; /* 사각형 기호 */
    color: #555;
    margin-left: 20px;
}

/* 가상 클래스 선택자: 짝수 번째 li */
.content-section ul li:nth-child(even) {
    background-color: #f9f9f9;
}

/* 인접 형제 선택자: input 바로 뒤의 button */
input + button {
    margin-left: 10px; /* input과 버튼 사이 여백 */
}

/* 폼 섹션 스타일 */
.form-section {
    background-color: #ebf5fb;
}
.form-section label {
    display: block; /* 레이블을 블록 요소로 만들어 줄바꿈 */
    margin-bottom: 5px;
    font-weight: bold;
    color: #444;
}
.form-section input[type="text"],
.form-section input[type="email"] {
    width: calc(100% - 120px); /* 레이블과 버튼을 고려한 너비 */
    padding: 8px;
    margin-bottom: 10px;
    border: 1px solid #a9d9f5;
    border-radius: 4px;
}
.form-section input[type="text"]:focus,
.form-section input[type="email"]:focus {
    border-color: #007bff;
    outline: none;
    box-shadow: 0 0 5px rgba(0, 123, 255, 0.3);
}
.form-section button[type="submit"] {
    background-color: #007bff;
    margin-top: 10px;
}
.form-section button[type="submit"]:hover {
    background-color: #0056b3;
}

/* 푸터 스타일 */
footer {
    background-color: #34495e;
    color: white;
    text-align: center;
    padding: 20px 0;
    margin-top: 30px;
    border-radius: 8px;
}

/* 클래스 선택자: 작은 텍스트 */
.small-text {
    font-size: 0.8em;
    color: #aaa;
}

/* 가상 클래스: input 필드에 포커스 있을 때 */
input:focus {
    border-color: #007bff;
    box-shadow: 0 0 5px rgba(0, 123, 255, 0.3);
}
결과 확인
  • 두 파일을 저장하고 index.html을 여세요. Live Server를 선택했다면 해당 HTML의 URL을 확인하고, 파일로 직접 열었다면 css/style.css가 같은 프로젝트 안에 있는지 확인합니다.
  • HTML 파일에 아무런 style="" 속성도 없지만, style.css 파일에 정의된 다양한 스타일 규칙들이 적용되어 웹 페이지가 훨씬 보기 좋게 변한 것을 확인할 수 있을 것입니다.
  • 각 선택자들이 어떤 요소에 어떻게 적용되었는지 CSS 코드와 브라우저 화면을 비교하며 확인해 보세요. 특히 마우스를 올렸을 때(hover), 입력 필드를 클릭했을 때(focus), 외부 링크 옆에 화살표가 붙는(::after) 효과들을 관찰해 보세요.
  • input + button은 바로 다음 형제가 버튼일 때만 적용됩니다. 이 HTML에서는 입력 뒤에 <br>가 있으므로 해당 규칙은 매칭되지 않습니다.

이번 장에서는 CSS의 가장 기본적인 문법과 외부 스타일 시트를 이용한 적용 방법을 학습했습니다.

특히 HTML 요소에 스타일을 적용하기 위한 핵심적인 개념인 선택자(Selector)의 다양한 종류와 활용법을 심도 있게 다루었습니다.

태그, 클래스, ID 선택자부터 복합 선택자, 속성 선택자, 가상 클래스, 가상 요소 선택자까지, 선택자의 종류와 우선순위를 이해하면 어떤 요소에 스타일이 적용되는지 예측하기 쉬워집니다.

최종 스타일은 먼저 선택자가 요소에 맞는지 확인한 뒤, cascade 규칙에 따라 결정됩니다.

같은 속성에 여러 선언이 경쟁하면 먼저 현재 선택자·미디어 조건에 맞는 후보를 모읍니다. 출처·중요도와 cascade layer가 앞선 단계이며, 그 안에서 specificity를 비교합니다. @scope를 쓴 동률 선언에서는 scope 거리도 비교합니다. 아래 specificity 비교는 layer·scope·인라인 스타일·애니메이션·진행 중인 전환이 없는 일반 작성자 CSS를 가정하므로 매칭된 일반 선언끼리는 specificity, 동률이면 선언 순서로 판단합니다. 위 실습에는 링크와 버튼의 전환도 있으므로, 그 전환 중의 값까지 이 비교 예의 범위로 확대하지 않습니다. 내부 CSS와 외부 CSS라는 파일 위치만으로 우열이 정해지지는 않습니다. 전체 cascade 순서를 따로 볼 수 있습니다.

specificity는 보통 a-b-c 형태로 설명하며, a는 ID 수, b는 class·attribute·pseudo-class 수, c는 type·pseudo-element 수입니다.

예를 들어 p는 0-0-1, .note는 0-1-0, #hero는 1-0-0, .card p:hover는 0-2-1입니다.

!important는 단순히 specificity 점수를 높이는 기능이 아니라 cascade의 중요도 단계에 개입합니다.

그래서 일반적인 레이아웃 조정에는 남용하지 말고, 유틸리티 예외나 접근성 보정처럼 범위를 명확히 알 수 있는 곳에 제한하는 편이 좋습니다.

상속도 구분해야 합니다.

color, font-family, line-height처럼 대체로 상속되는 속성은 부모 값이 자식에게 전달될 수 있지만, margin, border, padding 같은 박스 모델 속성은 기본적으로 상속되지 않습니다.

자식 요소에 직접 적용된 규칙이 있다면 상속값보다 직접 규칙이 우선합니다.

아래는 이 장의 단순화된 흐름입니다. 예시에서는 .card 안에서 hover 상태인 p#hero 하나의 color 속성에 .card p:hover와 #hero가 모두 적용된다고 가정합니다.

CSS cascade의 단순화된 평가 흐름과 specificity 비교 — 상태 추적

같은 p#hero의 A 0–2–1/B 1–0–0을 첫 ID 열에서 결정하는 구체 사례다. 후보 비교와 뒤 열/소스 순서 미도달 상태를 보여 주면 specificity를 합산하는 오해를 줄인다.

specificity가 다르면 선언 순서까지 가지 않습니다

CSS CASCADE · PRACTICE

specificity가 다르면 선언 순서까지 가지 않습니다

같은 요소의 같은 color에 경쟁하는 두 일반 선언을 단계별로 비교합니다.

ID 열에서 먼저 결정된 두 선언같은 p#hero에 매칭하는 일반 작성자 A .card p:hover 0–2–1과 B #hero 1–0–0을 비교한다. 앞 cascade 조건은 동률이며 ID 열에서 1이 0보다 커 B red가 선택된다. 뒤 열을 더하거나 소스 순서를 추가 비교하지 않는다.A · .card p:hover0–2–1 · class1 + pseudo-class1B · #hero1–0–0 · ID1같은 cascade 조건일반·layer/scope/inline/움직임 없음첫 열 · ID 비교B: 1 > A: 0B의 color: red 선택뒤 열 합산하지 않음소스 순서 비교 불필요
ID 열에서 먼저 결정된 두 선언같은 p#hero에 매칭하는 일반 작성자 A .card p:hover 0–2–1과 B #hero 1–0–0을 비교한다. 앞 cascade 조건은 동률이며 ID 열에서 1이 0보다 커 B red가 선택된다. 뒤 열을 더하거나 소스 순서를 추가 비교하지 않는다.A · .card p:hover0–2–1class1 + pseudo-class1B · #hero1–0–0ID1같은 cascade 조건둘 다 이 요소에 매칭일반·layer/scope 없음첫 열 · ID 비교B: 1 > A: 0B의 red 선택뒤 열 합산하지 않음소스 순서 비교 불필요

후보 A · .card p:hover

.card p:hover { color: blue; }

specificity 0–2–1 · class 1, pseudo-class 1, type 1

후보 B · #hero

#hero { color: red; }

specificity 1–0–0 · ID 1

  1. 선택자 매칭 · 둘 다 후보

    .card 안의 hover 상태인 p#hero에 두 color 선언이 모두 적용됩니다. 매칭하지 않는 선언은 제외합니다.

  2. 출처·중요도 · 동률

    둘 다 일반 작성자 선언이며 layer·scope·inline style·애니메이션·전환이 없는 예시입니다. !important는 specificity 점수가 아니라 앞선 중요도 단계에서 비교합니다.

  3. specificity · ID 열에서 결정

    1–0–0이 0–2–1보다 큽니다. 첫 열부터 비교하므로 뒤 열의 수를 합산하지 않습니다.

  4. 소스 순서 · 이 예시에서는 비교하지 않음

    앞 조건이 모두 같은 선언끼리만 나중에 나온 선언이 선택됩니다.

결과 · #hero의 color: red

나중에 쓴 규칙보다 앞선 cascade 조건이 먼저입니다. 자식에 직접 지정된 color는 부모로부터의 기본 상속값과 구분합니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
원도표의 전체 관계·조건·설명
구분내용
원도표 설명같은 요소의 같은 color를 두고 경쟁하는 두 일반 선언을 단계별로 비교합니다.
원도표 설명.card p:hover { color: blue; }specificity 0–2–1 · class 1, pseudo-class 1, type 1
원도표 설명#hero { color: red; }specificity 1–0–0 · ID 1
원도표 조건·주석선택자 매칭 · 둘 다 후보: .card 안의 hover 상태인 p#hero에 두 color 선언이 모두 적용됩니다. 매칭하지 않는 선언은 제외합니다.
원도표 조건·주석출처·중요도 · 동률: 둘 다 일반 작성자 선언이며 layer·scope·inline style·애니메이션·전환이 없는 예시입니다. !important는 specificity 점수가 아니라 앞선 중요도 단계에서 비교합니다.
원도표 조건·주석specificity · ID 열에서 결정: 1–0–0이 0–2–1보다 큽니다. 첫 열부터 비교하므로 뒤 열의 수를 합산하지 않습니다.
원도표 조건·주석소스 순서 · 이 예시에서는 비교하지 않음: 앞 조건이 모두 같은 선언끼리만 나중에 나온 선언이 선택됩니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
규칙 후보A: .card p:hover 0–2–1 / B: #hero 1–0–0두 selector가 같은 요소에 매칭된다조건 매칭과 우선순위 판정은 별개다
ID 열 비교ID: A0 < B1 / 다른 열: 더하지 않음ID 수를 먼저 비교하면 B가 이긴다specificity는 전체 숫자 합이 아니라 열 순서 비교다
색 적용승리 규칙: #hero / 색: red다른 상위 cascade 조건이 같다면 B의 red가 선택된다동등한 cascade 위치라는 가정을 유지한다

CSS cascade의 단순화된 평가 흐름과 specificity 비교 — 상태 추적

같은 p#hero의 A 0–2–1/B 1–0–0을 첫 ID 열에서 결정하는 구체 사례다. 후보 비교와 뒤 열/소스 순서 미도달 상태를 보여 주면 specificity를 합산하는 오해를 줄인다.

specificity가 다르면 선언 순서까지 가지 않습니다

CSS CASCADE · PRACTICE

specificity가 다르면 선언 순서까지 가지 않습니다

같은 요소의 같은 color에 경쟁하는 두 일반 선언을 단계별로 비교합니다.

ID 열에서 먼저 결정된 두 선언같은 p#hero에 매칭하는 일반 작성자 A .card p:hover 0–2–1과 B #hero 1–0–0을 비교한다. 앞 cascade 조건은 동률이며 ID 열에서 1이 0보다 커 B red가 선택된다. 뒤 열을 더하거나 소스 순서를 추가 비교하지 않는다.A · .card p:hover0–2–1 · class1 + pseudo-class1B · #hero1–0–0 · ID1같은 cascade 조건일반·layer/scope/inline/움직임 없음첫 열 · ID 비교B: 1 > A: 0B의 color: red 선택뒤 열 합산하지 않음소스 순서 비교 불필요
ID 열에서 먼저 결정된 두 선언같은 p#hero에 매칭하는 일반 작성자 A .card p:hover 0–2–1과 B #hero 1–0–0을 비교한다. 앞 cascade 조건은 동률이며 ID 열에서 1이 0보다 커 B red가 선택된다. 뒤 열을 더하거나 소스 순서를 추가 비교하지 않는다.A · .card p:hover0–2–1class1 + pseudo-class1B · #hero1–0–0ID1같은 cascade 조건둘 다 이 요소에 매칭일반·layer/scope 없음첫 열 · ID 비교B: 1 > A: 0B의 red 선택뒤 열 합산하지 않음소스 순서 비교 불필요

후보 A · .card p:hover

.card p:hover { color: blue; }

specificity 0–2–1 · class 1, pseudo-class 1, type 1

후보 B · #hero

#hero { color: red; }

specificity 1–0–0 · ID 1

  1. 선택자 매칭 · 둘 다 후보

    .card 안의 hover 상태인 p#hero에 두 color 선언이 모두 적용됩니다. 매칭하지 않는 선언은 제외합니다.

  2. 출처·중요도 · 동률

    둘 다 일반 작성자 선언이며 layer·scope·inline style·애니메이션·전환이 없는 예시입니다. !important는 specificity 점수가 아니라 앞선 중요도 단계에서 비교합니다.

  3. specificity · ID 열에서 결정

    1–0–0이 0–2–1보다 큽니다. 첫 열부터 비교하므로 뒤 열의 수를 합산하지 않습니다.

  4. 소스 순서 · 이 예시에서는 비교하지 않음

    앞 조건이 모두 같은 선언끼리만 나중에 나온 선언이 선택됩니다.

결과 · #hero의 color: red

나중에 쓴 규칙보다 앞선 cascade 조건이 먼저입니다. 자식에 직접 지정된 color는 부모로부터의 기본 상속값과 구분합니다.

이 예의 조건

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

관계도의 보충 설명

원도표의 전체 관계·조건·설명
구분내용
원도표 설명같은 요소의 같은 color를 두고 경쟁하는 두 일반 선언을 단계별로 비교합니다.
원도표 설명.card p:hover { color: blue; }specificity 0–2–1 · class 1, pseudo-class 1, type 1
원도표 설명#hero { color: red; }specificity 1–0–0 · ID 1
원도표 조건·주석선택자 매칭 · 둘 다 후보: .card 안의 hover 상태인 p#hero에 두 color 선언이 모두 적용됩니다. 매칭하지 않는 선언은 제외합니다.
원도표 조건·주석출처·중요도 · 동률: 둘 다 일반 작성자 선언이며 layer·scope·inline style·애니메이션·전환이 없는 예시입니다. !important는 specificity 점수가 아니라 앞선 중요도 단계에서 비교합니다.
원도표 조건·주석specificity · ID 열에서 결정: 1–0–0이 0–2–1보다 큽니다. 첫 열부터 비교하므로 뒤 열의 수를 합산하지 않습니다.
원도표 조건·주석소스 순서 · 이 예시에서는 비교하지 않음: 앞 조건이 모두 같은 선언끼리만 나중에 나온 선언이 선택됩니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
규칙 후보A: .card p:hover 0–2–1 / B: #hero 1–0–0두 selector가 같은 요소에 매칭된다조건 매칭과 우선순위 판정은 별개다
ID 열 비교ID: A0 < B1 / 다른 열: 더하지 않음ID 수를 먼저 비교하면 B가 이긴다specificity는 전체 숫자 합이 아니라 열 순서 비교다
색 적용승리 규칙: #hero / 색: red다른 상위 cascade 조건이 같다면 B의 red가 선택된다동등한 cascade 위치라는 가정을 유지한다

이 페이지로 선택자와 결과 대조하기

  1. “CSS 학습 리스트”의 네 항목 중 배경색이 적용되는 항목을 예측하세요.
  2. target="_blank"를 세 번째 메뉴 링크에서 네 번째 링크로 옮기면 어떤 화살표가 옮겨질까요? 링크 글자는 바꾸지 않습니다.
  3. input + button이 맞지 않는 이유를 HTML의 실제 다음 형제로 설명하세요.
결과와 이유 확인

짝수 번째 자식인 “선택자”, “레이아웃”에 .content-section ul li:nth-child(even)이 적용됩니다. ::after의 화살표는 링크 글자가 아니라 target 속성을 기준으로 만들어지므로 “연락처”로 옮겨집니다. 원래 입력 바로 뒤에는 br가 있어서 input + button이 제출 버튼을 고르지 않습니다. 개발자 도구에서 해당 규칙이 매칭되는지 확인하면 화면 변화의 원인과 단순 기대를 구분할 수 있습니다.

문의 제출은 이름의 required와 비어 있지 않은 이메일의 형식 검사를 이용하지만 서버 저장은 하지 않습니다. 문의 기능까지 완성했다고 판단하지 말고 선택자 실습 결과와 구분하세요.

이 페이지 목차

이 페이지 목차