웹페이지를 처음 만들 때 HTML은 화면에 글과 이미지를 넣고, CSS는 색상과 크기를 변경하는 기술이라고 생각했다.
하지만 자기소개 페이지를 직접 구현하면서 HTML과 CSS의 역할은 단순히 '내용'과 '꾸미기'로만 나뉘지 않는다는 것을 알게 되었다.
HTML은 콘텐츠의 의미와 구조를 표현하고 브라우저가 기본적으로 제공하는 동작을 사용할 수 있게 한다. CSS는 HTML 요소가 화면에 어떻게 배치되고, 사용자의 행동이나 화면 상태에 따라 어떻게 변화할지를 결정한다.
그렇다면 JavaScript 없이 HTML과 CSS만 사용해도 인터랙션이 있는 웹페이지를 만들 수 있을까?
이번 글에서는 HTML과 CSS의 관계를 먼저 살펴보고, 자기소개 페이지를 만들며 실제로 어떤 인터랙션을 구현했는지 정리해 보려고 한다.
HTML은 웹페이지에 어떤 콘텐츠가 존재하는지 표현하는 마크업 언어다.
다음 두 코드는 화면에서 비슷하게 보이도록 만들 수 있다.
<div class="header">효비 블로그</div>
<header>효비 블로그</header>
CSS만 본다면 두 요소 모두 같은 모양으로 꾸밀 수 있다. 하지만 브라우저가 이해하는 의미는 다르다.
div는 특별한 의미가 없는 일반적인 컨테이너지만, header는 페이지나 영역의 머리말이라는 의미를 가진다.
HTML에는 콘텐츠의 역할에 맞는 여러 요소가 있다.
<header>: 페이지의 머리말<nav>: 주요 이동 링크<main>: 페이지의 핵심 콘텐츠<section>: 주제별 콘텐츠 영역<article>: 독립적으로 구분할 수 있는 콘텐츠<footer>: 페이지의 바닥글이처럼 HTML을 의미에 맞게 작성하면 개발자가 구조를 이해하기 쉬워질 뿐 아니라 브라우저와 스크린 리더도 각 콘텐츠의 역할을 파악할 수 있다.
즉, HTML은 단순히 화면에 요소를 추가하는 것이 아니라 콘텐츠 사이의 관계를 정의한다.
HTML은 구조만 표현하는 것이 아니라 브라우저가 이미 구현한 기본 동작도 사용할 수 있게 한다.
대표적인 예가 링크다.
<a href="#favorites">좋아하는 것</a>
<section id="favorites">
<h2>좋아하는 것</h2>
</section>
링크의 href와 요소의 id를 연결하면 JavaScript 없이도 특정 위치로 이동할 수 있다.
버튼, 입력창, 체크박스와 같은 요소도 브라우저가 기본적인 동작을 제공한다.
<button type="button">좋아요</button>
<input type="checkbox" />
<details>
<summary>자세히 보기</summary>
숨겨진 내용
</details>
특히 <details>와 <summary>를 사용하면 JavaScript 없이도 내용을 열고 닫을 수 있다.
HTML 요소를 적절히 선택하는 것만으로도 기본적인 사용자 인터랙션과 접근성을 함께 얻을 수 있다.
HTML이 콘텐츠의 구조와 의미를 만든다면 CSS는 해당 콘텐츠를 화면에 어떻게 표현할지 결정한다.
예를 들어 다음 HTML은 세 개의 요소가 순서대로 작성되어 있다.
<header class="header">
<img src="profile.jpg" alt="프로필" />
<h1>효비 블로그</h1>
<a href="https://github.com/">GitHub</a>
</header>
아무런 CSS가 없다면 요소는 HTML의 기본 흐름에 따라 표시된다. 여기에 CSS Grid를 적용하면 세 영역으로 나눠 배치할 수 있다.
.header {
display: grid;
grid-template-columns: repeat(3, 1fr);
align-items: center;
}
HTML 구조는 그대로지만 CSS 규칙에 따라 화면의 배치가 달라진다.
이처럼 HTML과 CSS는 독립적인 기술이지만 완전히 분리되어 동작하지는 않는다. CSS는 선택자를 통해 HTML 요소를 찾아 스타일을 적용하기 때문이다.
.header {
/* class="header"인 HTML 요소에 적용 */
}
HTML 구조와 클래스 이름이 변경되면 CSS가 선택할 대상도 달라진다. 따라서 HTML 구조를 설계할 때 CSS로 어떻게 배치할지도 함께 고려해야 한다.
브라우저가 HTML과 CSS를 만나면 바로 화면에 그리는 것은 아니다.
먼저 HTML을 분석해 DOM을 만든다.
HTML → DOM
DOM은 HTML 요소들의 부모·자식 관계를 트리 형태로 표현한 구조다.
CSS도 분석해 CSSOM이라는 구조를 만든다.
CSS → CSSOM
브라우저는 DOM과 CSSOM을 결합해 화면에 표시할 요소와 스타일을 결정한다.
HTML → DOM
→ Render Tree → Layout → Paint
CSS → CSSOM
과정을 간단히 나누면 다음과 같다.
HTML이 구조를 변경하거나 CSS가 요소의 크기와 위치를 변경하면 브라우저가 화면을 다시 계산해야 할 수 있다.
결국 우리가 보는 웹페이지는 HTML만의 결과도, CSS만의 결과도 아니다. HTML이 만든 구조에 CSS 규칙이 결합되어 최종 화면이 만들어진다.
CSS는 JavaScript처럼 클릭 이벤트를 직접 처리하지 않는다. 대신 브라우저가 관리하는 요소의 상태를 선택할 수 있다.
대표적인 상태 선택자는 다음과 같다.
a:hover {
color: purple;
}
button:focus-visible {
outline: 2px solid purple;
}
input:checked {
accent-color: purple;
}
:hover: 포인터가 요소 위에 있는 상태:focus-visible: 키보드 등을 통해 요소에 포커스가 표시되는 상태:checked: 체크박스나 라디오 버튼이 선택된 상태:active: 요소를 누르고 있는 상태CSS는 사용자의 행동을 직접 기억하거나 처리하지는 않지만, 브라우저가 제공하는 현재 상태를 선택해 다른 스타일을 적용할 수 있다.
여기에 transition을 사용하면 상태가 바뀔 때 자연스러운 움직임을 만들 수 있다.
.card {
transition: transform 0.2s;
}
.card:hover {
transform: translateY(-6px);
}
CSS가 이벤트를 실행하는 것은 아니지만, 상태 변화에 반응해 시각적인 피드백을 제공하는 것이다.
:hover는 사용자의 행동이 있어야 동작한다. 반면 @keyframes를 사용하면 사용자의 입력 없이도 요소를 움직일 수 있다.
.banner-list {
animation: banner-scroll 30s linear infinite;
}
@keyframes banner-scroll {
to {
transform: translateX(-100%);
}
}
이 코드는 요소를 현재 위치에서 왼쪽으로 이동시킨다.
30s: 한 번 이동하는 데 걸리는 시간linear: 일정한 속도로 이동infinite: 애니메이션을 계속 반복translateX(-100%): 요소의 너비만큼 왼쪽으로 이동CSS는 레이아웃과 색상만 담당하는 기술이 아니라 요소의 상태 변화와 시간에 따른 움직임도 표현할 수 있다.
HTML과 CSS만 사용해도 다음과 같은 기능을 구현할 수 있다.
<details>를 이용한 콘텐츠 열기와 닫기이 정도의 인터랙션은 JavaScript 없이도 브라우저의 기본 기능과 CSS를 활용해 구현할 수 있다.
하지만 HTML과 CSS는 복잡한 상태나 데이터를 관리하기 위한 기술은 아니다. 다음과 같은 기능에는 JavaScript가 필요하다.
HTML은 구조와 기본 동작을 제공하고, CSS는 상태에 따른 표현과 움직임을 담당한다. 이 범위를 벗어나 데이터를 변경하거나 복잡한 상태를 관리해야 할 때 JavaScript가 필요하다.
HTML과 CSS의 관계를 알아본 뒤, 이번 자기소개 페이지에서 실제로 적용한 기능을 살펴보려고 한다.
전체 페이지는 다음과 같은 HTML 구조로 작성했다.
<body>
<header>...</header>
<main>
<nav>...</nav>
<div>
<section id="photo-dump">...</section>
<section id="favorites">...</section>
<section id="interests">...</section>
<section id="webtory">...</section>
</div>
</main>
<footer>...</footer>
</body>
페이지의 영역을 구분하기 위해 header, main, nav, section, footer를 사용했다.
CSS 파일도 기능에 따라 분리했다.
header.css
navigation.css
photo-dump.css
favorites.css
interests.css
webtory.css
footer.css
하나의 CSS 파일에 모든 코드를 작성하지 않고 영역별 파일로 분리해 각 스타일의 역할을 파악하기 쉽게 만들었다.
사이드 네비게이션과 콘텐츠는 가로 방향으로 배치되기 때문에 Flexbox를 사용했다.
.page-layout {
display: flex;
gap: 32px;
}
포토 덤프는 행과 열을 동시에 제어해야 했기 때문에 Grid를 사용했다.
.photo-dump__grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: 160px 145px 145px;
gap: 12px;
}
두 레이아웃 모두 요소를 배치하기 위한 기능이지만, 한 방향으로 나열할 때는 Flexbox가 편리했고 행과 열을 함께 조절할 때는 Grid가 적합했다.
관심사 카드에는 hover 시 카드가 위로 이동하고 그림자가 나타나도록 구현했다.
.interest-card {
transition: 0.2s;
}
.interest-card:hover {
transform: translateY(-6px);
box-shadow: 0 8px 16px rgb(0 0 0 / 15%);
}
좋아요와 싫어요 버튼도 hover 시 색상이 변경되도록 했다.
이러한 효과는 실제 데이터를 변경하지는 않지만, 사용자가 현재 어떤 요소를 가리키고 있는지 알려주는 시각적 피드백이 된다.
관심사 카드는 화면 크기에 따라 한 줄의 카드 수가 변경되도록 했다.
.interests__grid {
grid-template-columns: repeat(5, 1fr);
}
@media (max-width: 1000px) {
.interests__grid {
grid-template-columns: repeat(4, 1fr);
}
}
@media (max-width: 800px) {
.interests__grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 560px) {
.interests__grid {
grid-template-columns: repeat(2, 1fr);
}
}
사용자가 직접 버튼을 누르지 않아도 브라우저 화면의 크기가 변하면 CSS가 해당 조건에 반응한다.
이것도 넓은 의미에서는 사용자 환경 변화에 반응하는 인터랙션이라고 볼 수 있다.
이번 과제에서 가장 많이 고민한 부분은 좋아하는 사진을 보여주는 영역이었다.
구현하고 싶었던 기능은 두 가지였다.
처음에는 두 기능 모두 요소를 가로로 움직이기 때문에 쉽게 함께 적용할 수 있을 것이라고 생각했다.
하지만 직접 구현해 보니 사용자 스크롤과 CSS 애니메이션은 서로 다른 방식으로 요소를 움직였다.
.favorites__banner {
overflow-x: auto;
scroll-snap-type: x mandatory;
}
.favorites__image {
flex-shrink: 0;
scroll-snap-align: start;
}
overflow-x: auto는 콘텐츠가 부모 요소보다 넓을 때 실제 가로 스크롤 영역을 만든다.
scroll-snap을 적용하면 사용자가 스크롤을 멈췄을 때 가장 가까운 이미지의 시작 위치에 맞춰진다.
.favorites__group {
animation: banner-scroll 30s linear infinite;
}
@keyframes banner-scroll {
to {
transform: translateX(-100%);
}
}
자동 배너는 실제 스크롤 위치를 변경하는 것이 아니라 transform으로 요소가 보이는 위치를 변경한다.
정리하면 다음과 같다.
사용자 가로 스크롤 → scrollLeft 변경
CSS 자동 애니메이션 → transform 변경
화면에서는 모두 가로로 움직이는 것처럼 보이지만 브라우저 내부에서는 서로 다른 값이 변경된다.
이 때문에 두 기능을 동시에 적용했을 때 스크롤 스냅이 예상한 위치에서 동작하지 않거나 사용자의 직접 조작이 어색해지는 문제가 발생했다.
하나의 이미지 목록만 이동시키면 마지막 사진 이후 빈 공간이 생기는 문제도 있었다. 이를 해결하기 위해 같은 목록을 두 번 작성해 첫 번째 목록의 뒤를 두 번째 목록이 이어가도록 구현했다.
<div class="favorites__banner">
<div class="favorites__group">
<!-- 원본 목록 -->
</div>
<div class="favorites__group" aria-hidden="true">
<!-- 반복을 위한 복제 목록 -->
</div>
</div>
CSS만으로 무한 배너를 만드는 것은 가능했지만, 자동 움직임과 사용자의 직접 조작을 자연스럽게 연결하는 데에는 한계가 있었다.
다음에 JavaScript를 사용한다면 사용자가 배너를 조작할 때 자동 이동을 멈추고, 일정 시간이 지나면 다시 실행하는 방식으로 개선해 보고 싶다.
이번 과제를 진행하기 전에는 HTML은 내용을 작성하고 CSS는 화면을 꾸미는 기술이라고만 생각했다.
하지만 HTML은 콘텐츠의 의미와 관계를 표현하고 브라우저가 제공하는 기본 동작의 기반이 되었다. CSS는 단순한 꾸미기를 넘어 레이아웃, 반응형 화면, 상태 변화, 애니메이션을 담당했다.
HTML과 CSS만으로도 다양한 인터랙션을 구현할 수 있었지만, 데이터를 변경하거나 여러 상태를 정교하게 관리하는 기능에는 한계가 있었다.
특히 가로 스크롤과 무한 배너를 구현하면서 화면에서 비슷하게 보이는 동작이라도 내부에서는 서로 다른 원리로 움직일 수 있다는 것을 알게 되었다.
이번 과제를 통해 코드를 따라 작성하는 것보다 HTML 요소와 CSS 속성이 브라우저에서 어떤 역할을 하는지 이해하는 것이 중요하다는 점을 배웠다.