화면 전체가 공유하는
일곱 줄의 바닥
색과 글꼴을 이름으로 정의하고, 브라우저마다 다른 기본값을 없앤다. App.css는 여기서 정한 이름을 가져다 쓴다.
이 파일을 만든 이유
CSS 파일이 둘로 나뉜 이유는 적용 범위가 다르기 때문이다. 이 파일은 문서 전체에 걸리는 규칙 — 글꼴, 배경, 상자 계산 방식, 색 이름 — 을 정한다. App.css는 채팅 화면의 개별 요소를 다룬다.
가장 중요한 역할은 색을 이름으로 만드는 것이다. #2f6d5f를 여러 파일에 흩어 적으면 색을 바꿀 때 전부 찾아야 한다. --accent라는 이름으로 한 번 정의하면 한 곳만 고치면 된다.
전체 코드
:root { font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #20201e; background: #f8f8f6; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; --ink: #20201e; --muted: #777772; --line: #e8e7e2; --paper: #fff; --sidebar: #f2f1ed; --accent: #2f6d5f; }
* { box-sizing: border-box; }
html, body, #root { width: 100%; min-width: 320px; min-height: 100%; margin: 0; }
body { min-height: 100svh; overflow: hidden; }
button, textarea { font: inherit; }
button { color: inherit; }
button:focus-visible, textarea:focus-visible { outline: 2px solid rgba(47,109,95,.55); outline-offset: 2px; }
main.tsx가 import './index.css'로 가져오고, App.tsx가 import './App.css'를 가져온다. import 순서가 곧 CSS 우선순위이므로, 전역 기본값이 먼저 깔리고 화면별 규칙이 나중에 덮는다.
구간별 해설
:root { … } — 문서의 뿌리
:root는 <html> 요소를 가리킨다. 여기에 정의한 값은 모든 하위 요소가 상속하므로 글꼴과 기본 색을 두기에 알맞다.
font-family: Inter, ui-sans-serif, -apple-system, …
앞에서부터 있는 글꼴을 찾아 쓴다. Inter는 이 프로젝트가 따로 내려받지 않으므로 설치돼 있지 않으면 건너뛴다. 다음 후보인 ui-sans-serif·-apple-system이 macOS·iPadOS에서 시스템 글꼴로 이어진다. 외부 글꼴을 받지 않으므로 오프라인에서도 화면이 깨지지 않는다. 한글은 시스템 글꼴이 담당한다.
--ink · --muted · --line · --paper · --sidebar · --accent
CSS 사용자 정의 속성이다. 이름이 색 자체가 아니라 역할을 가리킨다는 점이 중요하다. --green이 아니라 --accent이므로 나중에 강조색을 바꿔도 이름이 거짓말이 되지 않는다. App.css에서 var(--accent)로 꺼내 쓴다.
다크 모드를 넣는다면 @media (prefers-color-scheme: dark) 안에서 이 여섯 값만 다시 정의하면 된다. 개별 규칙은 손대지 않아도 된다.
font-synthesis: none · text-rendering · -webkit-font-smoothing
글자 렌더링 세부 설정이다. font-synthesis: none은 굵은 글꼴이 없을 때 브라우저가 인위적으로 굵게 만드는 것을 막는다. 나머지 둘은 자간 처리와 안티앨리어싱 방식으로, 주로 macOS·Safari에서 차이가 보인다.
* { box-sizing: border-box; }
CSS에서 가장 널리 쓰이는 한 줄이다. 기본값에서는 width: 300px에 padding과 border가 더해져 실제 폭이 커진다. border-box로 바꾸면 padding과 border가 포함되어 300px이 그대로 유지된다. 레이아웃 계산이 예측 가능해진다.
html, body, #root { width: 100%; min-width: 320px; min-height: 100%; margin: 0; }
세 요소를 함께 지정한 이유가 있다. #root가 빠지면 높이가 이어지지 않는다. React가 그리는 .app-shell이 화면 전체 높이를 쓰려면 그 위의 #root·body·html이 모두 높이를 가지고 있어야 한다. margin: 0은 브라우저 기본 여백을 없앤다.
min-width: 320px는 아주 좁은 화면에서 레이아웃이 무너지지 않게 하는 하한선이다.
body { min-height: 100svh; overflow: hidden; }
svh는 small viewport height로, 모바일 브라우저의 주소창이 보이는 상태의 높이다. 예전에 쓰던 100vh는 주소창을 제외하지 않아 iOS Safari에서 화면 아래가 잘리는 문제가 유명하다. iPad에서 쓰려는 이 프로젝트에 직접 관계된 선택이다.
overflow: hidden은 문서 전체가 스크롤되지 않게 한다. 스크롤은 메시지 목록 안에서만 일어나야 입력창이 늘 화면 아래에 고정된다. 채팅 앱의 전형적인 구조다.
button, textarea { font: inherit; }
form 요소는 부모의 글꼴을 물려받지 않는다. 브라우저가 자체 기본 글꼴을 쓰기 때문에, 이 줄이 없으면 입력창과 버튼만 다른 글꼴로 보인다. button { color: inherit; }도 같은 이유다.
:focus-visible { outline: 2px solid … }
키보드로 이동할 때만 테두리를 보여 준다. :focus와 달리 마우스 클릭에는 나타나지 않는다. 접근성을 위해 초점 표시는 반드시 필요하지만, 클릭할 때마다 테두리가 보이는 것은 원치 않기 때문에 나온 선택자다. 초점 표시를 아예 없애는 outline: none은 키보드 사용자가 현재 위치를 잃게 만든다.
실제로 확인하는 방법
| 확인 | 방법 | 보이는 것 |
|---|---|---|
| 변수가 적용된다 | 개발자 도구에서 html 선택 → Computed | --accent 등 여섯 개가 보인다 |
| 색을 한 번에 바꾼다 | Styles 패널에서 --accent 값 수정 | 버튼·초점 테두리 등이 동시에 바뀐다 |
100svh의 효과 | iPad Safari에서 주소창을 보였다 감춘다 | 입력창이 화면 밖으로 밀려나지 않는다 |
overflow: hidden | 페이지 전체를 스크롤해 본다 | 문서는 움직이지 않고 메시지 목록만 스크롤된다 |
자주 발생하는 오류
| 증상 | 원인 | 진단 |
|---|---|---|
| 화면 높이가 안 채워짐 | #root에 높이 지정 누락 | html·body·#root 세 계단이 모두 필요하다 |
| 모바일에서 입력창이 잘림 | 100vh 사용 | 100svh로 바꾼다 |
var(--accent)가 안 먹힘 | 이름 오타 또는 정의보다 먼저 사용 | 정의는 :root에 있고 import 순서상 먼저 와야 한다 |
| 버튼 글꼴만 다름 | font: inherit 누락 | form 요소는 상속하지 않는다 |
| 스타일이 전혀 없음 | import './index.css' 누락 | 부작용 import라 지워도 type 오류가 나지 않는다 |
설계 선택과 대안
왜 Tailwind 같은 도구를 쓰지 않는가?
학습이 1차 목적이므로 브라우저가 실제로 무엇을 하는지가 보이는 편이 낫다. 유틸리티 클래스는 빠르지만 box-sizing이나 svh 같은 결정이 도구 안에 감춰진다. 화면 수가 늘어나면 다시 판단할 문제다.
CSS 변수 대신 전처리기 변수는?
Sass 변수는 build 때 값으로 바뀌어 실행 중에 바꿀 수 없다. CSS 변수는 브라우저에 남아 있어 다크 모드 전환이나 개발자 도구에서의 실시간 수정이 가능하다. 지금 필요한 것은 후자다.
전역 * 선택자는 느리지 않은가?
box-sizing 정도는 현대 브라우저에서 측정 가능한 비용이 아니다. 얻는 예측 가능성이 훨씬 크다.
이전 단계와 다음 파일
여기서 정한 이름을 실제로 사용하는 곳은 App.css다. 이 파일을 불러오는 코드는 main.tsx, 클래스 이름을 붙이는 곳은 App.tsx에 있다.