문서 읽는 데 50분 · B8

B-8: CSS 실전 — 변수·BEM·Reset·다크 모드

목차 19
전체 34강 중 12강 · HTML·CSS·JS
난이도 · 입문

안녕하세요, 홍순구 튜터입니다. 드디어 CSS의 마지막 시간이에요. 우리는 긴 길을 걸어왔어요. 선택자로 요소를 고르고(B-1), 박스 모델로 여백을 다듬고(B-2), 위치를 잡고(B-3), Flexbox와 Grid로 줄을 세우고(B-4·B-5), 화면 크기에 맞춰 접고 펴고(B-6), 마지막엔 움직임까지 입혔죠(B-7). 텅 빈 HTML 뼈대가 진짜 인스타그램처럼 보이게 됐어요.

오늘은 좀 특별해요. 새 화면을 그리는 날이 아니라, 지금까지 만든 CSS를 프로처럼 정리하는 날이에요. 코드를 정리하고 더 좋은 구조로 다시 짜는 걸 리팩토링(refactoring)이라고 불러요. 동작은 그대로인데, 나중에 고치기 쉽게 속을 단정하게 바꾸는 거죠.

기억하시나요? 지난 B-7 마지막에 제가 이렇게 예고했어요. "지금 우리 코드 곳곳에 흩어진 --ig-blue, --color-text 같은 값들을 한곳에서 관리하고, 그걸로 다크 모드까지 만들 거예요. 색을 부드럽게 전환했던 것처럼, 다음 시간엔 그 색들 자체를 통째로 갈아끼우는 마법을 보게 될 거예요"라고요. 오늘이 그 약속을 지키는 날이에요.

오늘 우리가 할 일을 그림으로 먼저 볼게요.

텍스트
   지금 (색값이 곳곳에 흩어짐)        오늘 (한곳에 모으고  통째로 교체)

   base.css   #0095f6                 variables.css
   layout.css #0095f6                  └ --ig-blue: #0095f6   단 한 곳
   button     #0095f6                       │
   badge      #0095f6                       ├─ 모든 화면이 참조
                                           │
   고치려면 네 군데 다 찾아야…          다크 모드: 이 한곳만 갈아끼우면 전부 어두워짐

💡 오늘 수업의 핵심 — "흩어진 색값을 variables.css 한곳에 모으고(CSS 변수), 브라우저 기본값을 reset.css로 평평하게 깔고, 클래스 이름에 규칙(BEM)을 주고, 그 토대 위에 다크 모드를 얹는다" 🎯

🎯 학습 목표

  • CSS 변수(Custom Properties)를 선언하고 var()로 꺼내 쓰는 법, 그리고 :root가 왜 전역인지 이해합니다.
  • 흩어진 변수를 variables.css 전담 파일로 분리하고, 원시 토큰 → 의미 토큰 2단 구조로 키웁니다.
  • CSS Reset으로 브라우저마다 다른 기본값을 평평하게 만들고, Reset과 Normalize의 차이를 압니다.
  • BEM 네이밍(block__element--modifier)의 뜻과, 우리 클래스가 그 규칙과 어떻게 닿아 있는지 봅니다.
  • prefers-color-scheme로 운영체제 설정을 따라가는 자동 다크 모드를 만듭니다.
  • light-dark() 함수로 같은 일을 더 짧게 쓰는 2026년식 방법과 브라우저 호환을 압니다.
  • [data-theme] 속성으로 사용자가 직접 끄고 켤 수 있는 다크 모드의 구조를 깝니다.
  • 실무 스타일 가이드 관점에서 파일 6개의 로드 순서와 역할을 정리합니다.

오늘도 외우려 하지 마세요. 변수 하나를 바꾸면 화면 수십 군데가 한꺼번에 달라지는 걸 직접 보는 게 핵심이에요. 브라우저를 열어두고, 값을 바꿔가며 눈으로 확인하면서 따라오세요. 자, 흩어진 색들을 한곳으로 불러 모아 볼까요?


Step 1: "사실 우리는 변수를 계속 쓰고 있었어요"

오늘의 주인공인 CSS 변수(Custom Properties)는 사실 처음 보는 게 아니에요. 인스타 클론을 다듬는 과정에서 우리 base.css에 이미 들어와 있었어요. 우리 코드에 이런 게 잔뜩 있었죠.

CSS
/* instagram-clone-frontend/css/base.css */
body {
  background-color: var(--color-surface);
  color: var(--color-text);
  font-family: var(--font-base);
}

var(--color-text) — 이게 바로 변수를 꺼내 쓰는 모습이에요. --color-text라는 이름표가 붙은 값을 가져다 쓴다는 뜻이죠. 그럼 그 값은 어디서 정했을까요? 우리 base.css 맨 위 :root에 이렇게 적혀 있어요(곧 Step 2에서 더 똑똑하게 다듬을 거예요).

CSS
:root {
  --color-text: #262626;
  --ig-blue: #0095f6;
}

규칙은 딱 두 가지예요. 만들 때는 --이름: 값, 쓸 때는 var(--이름). 변수 이름은 반드시 대시 두 개(--)로 시작해요. 그래야 브라우저가 "아, 이건 내가 아는 속성이 아니라 사람이 만든 변수구나" 하고 알아채요.

비유를 들어볼게요. 색연필 세트를 떠올려 보세요. 통마다 "하늘색", "본문 글자색" 같은 이름표를 붙여두면, 그림 그릴 때 "그 하늘색 통에서 꺼내 줘"라고만 하면 돼요. 색의 정확한 번호(#0095f6)를 매번 외울 필요가 없죠. var(--ig-blue)가 바로 "이름표 보고 꺼내 쓰기"예요.

그런데 :root는 뭘까요? :root는 문서의 가장 꼭대기 요소, 즉 <html>을 가리키는 특별한 선택자예요. 가장 위에 변수를 적어두면, 그 아래 모든 요소가 그 변수를 물려받아 쓸 수 있어요. 변수도 글자색처럼 상속(inheritance)되거든요. 그래서 :root에 적으면 "이 문서 전체에서 통하는 전역 변수"가 돼요.

🙋 학생 질문 — "튜터님, 변수가 진짜 편한 게 맞나요? 그냥 #0095f6 적으면 안 되나요?"

지금은 색 하나라 별 차이 없어 보이죠. 그런데 이 파란색이 버튼, 링크, 뱃지, 스토리 테두리 등 화면 수십 군데에 쓰인다고 생각해 보세요. 어느 날 회사가 "브랜드 색을 바꾸자"고 하면? #0095f6을 일일이 찾아 수십 번 바꿔야 해요. 한 군데라도 놓치면 거기만 옛날 색으로 남고요.

변수를 쓰면 --ig-blue 한 줄만 바꾸면 끝이에요. 그 이름표를 보고 있던 모든 곳이 한꺼번에 따라와요. 변수의 진짜 힘은 "색 하나 정의"가 아니라 "한 곳만 고치면 전부 바뀐다"는 데 있어요.

폴백(fallback, 대비책)도 하나 알아둘게요. var(--color-text, #333)처럼 쉼표 뒤에 값을 하나 더 적으면, 혹시 --color-text가 정의돼 있지 않을 때 #333을 대신 써요. 안전장치죠. 자주 쓰진 않지만, 변수가 비었을 때 화면이 깨지지 않게 막아줘요.

직접 해볼까요? :root--ig-blue를 잠깐 purple로 바꿔 저장해 보세요. 파란색이던 로그인 버튼, 링크, 뱃지가 전부 보라색으로 변할 거예요. 이게 변수의 위력이에요.


Step 2: 변수를 전담 파일로 — variables.css와 2단 토큰

B-7까지는 이 변수들이 전부 base.css 맨 위에 살고 있었어요. 그런데 변수가 점점 늘면서, 색·여백·글꼴 설정이 일반 스타일과 뒤섞여 찾기 힘들어졌어요. 그래서 오늘 첫 번째 정리를 해요. 변수만 따로 모아 variables.css라는 전담 파일로 분리하는 거예요.

이렇게 역할별로 파일을 나누는 걸 관심사 분리(separation of concerns)라고 해요. 색·치수 같은 "디자인 설정값"만 한 파일에 모아두면, 그 파일이 우리 사이트의 테마 설정판이 돼요. 색을 바꾸고 싶으면 이 파일만 열면 되죠.

그런데 그냥 옮기기만 하면 심심하잖아요. 한 단계 더 똑똑하게 키울 거예요. 변수를 두 층으로 나눠요.

CSS
/* instagram-clone-frontend/css/variables.css */
:root {
  /* ===== 원시 토큰 — 색 팔레트의 날것 값 ===== */
  --blue-500: #0095f6;
  --gray-900: #262626;
  --gray-50: #fafafa;
  --gray-0: #ffffff;

  /* ===== 의미 토큰 — "쓰임새"로 이름 붙인 값 ===== */
  --ig-blue: var(--blue-500);
  --color-text: var(--gray-900);
  --color-bg: var(--gray-50);
  --color-surface: var(--gray-0);
}

위층은 원시 토큰(primitive token)이에요. --blue-500, --gray-900처럼 색 그 자체에 번호로 이름을 붙인, 날것의 팔레트예요. 아래층은 의미 토큰(semantic token)이에요. --color-text(본문 글자색), --color-surface(카드 바탕색)처럼 "어디에 쓰는 색인지"로 이름을 붙였어요. 그리고 의미 토큰은 직접 색을 갖지 않고, var(--gray-900)처럼 원시 토큰을 가리켜요.

다시 색연필 비유로 가볼게요. 원시 토큰은 "물감 번호"예요. "회색 900번", "파랑 500번"처럼요. 의미 토큰은 그 물감에 붙인 용도 라벨이에요. "본문 글자엔 회색 900번을 쓴다"처럼 배정해 둔 거죠.

텍스트
   원시 토큰(물감)         의미 토큰(용도 라벨)        화면이 실제로 쓰는 것
   --gray-900: #262626    --color-text: var(--gray-900)    color: var(--color-text)
   --gray-0:   #ffffff    --color-surface: var(--gray-0)   background: var(--color-surface)

왜 이렇게 번거롭게 두 층으로 나눌까요? 한 문장으로 말하면, 다크 모드 때문에 그래요. 다크 모드가 되면 "본문 글자색"의 배정만 "밝은 회색"으로 바꾸면 돼요. 물감 번호 자체는 그대로 두고요. 이 2단 구조가 오늘 뒤에 나올 다크 모드의 토대예요. 지금은 "색을 용도별로 한 번 더 감쌌다" 정도로만 기억해 두세요.

마지막으로 파일을 연결해요. variables.css는 다른 파일들이 변수를 꺼내 쓰기 전에 준비돼 있어야 하니, base.css보다 먼저 불러요.

HTML
<!-- instagram-clone-frontend/index.html -->
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/base.css">

Step 3: CSS Reset — 브라우저 기본값을 먼저 평평하게

다음 정리예요. 잠깐 질문 하나 할게요. 우리가 아무 CSS도 안 쓰고 <h1>제목</h1>만 적으면, 글자가 크고 굵게, 그리고 위아래 여백까지 붙어서 나와요. 우리가 여백을 준 적도 없는데 말이죠. 누가 준 걸까요?

바로 브라우저예요. 모든 브라우저는 "기본 스타일시트"를 갖고 있어서, 우리가 아무것도 안 해도 제목엔 여백을, 목록엔 점(•)을, 버튼엔 회색 테두리를 알아서 붙여줘요. 문제는 이 기본값이 브라우저마다 조금씩 다르다는 거예요. 크롬에서 본 여백과 사파리에서 본 여백이 미묘하게 다르면, 같은 코드인데 사람마다 다르게 보이겠죠.

그래서 실무에서는 작업을 시작하기 전에 이 들쭉날쭉한 기본값을 평평하게 밀어버리고 같은 출발선에서 시작해요. 이걸 CSS Reset이라고 해요. 우리도 reset.css를 새로 만들어 맨 먼저 깔 거예요.

CSS
/* instagram-clone-frontend/css/reset.css */

/* 1. 박스 크기를 테두리까지 포함해서 계산 (가상 요소까지) */
*,
*::before,
*::after {
  box-sizing: border-box;
}

/* 2. 페이지 바깥 여백 제거 + 최소 높이 보장 + 글자 다듬기 */
body {
  margin: 0;
  min-height: 100vh;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

/* 3. 이미지·영상을 블록 요소로, 부모 너비를 넘지 않게 */
img,
picture,
video,
svg {
  display: block;
  max-width: 100%;
}

/* 4. 폼 요소가 주변 글꼴을 물려받도록 (기본값은 안 물려받음) */
input,
button,
textarea,
select {
  font: inherit;
}

하나씩 볼게요. 첫째 box-sizing: border-box는 B-2에서 만났던 그 친구예요. 너비를 잴 때 테두리·안쪽 여백까지 포함해서 계산하라는 거죠. 이걸 모든 요소(*)에 한 번에 깔아둬요. 둘째 body의 바깥 여백을 0으로 밀어요(브라우저가 기본으로 8px쯤 줘요). 셋째 이미지·영상을 블록으로 만들고 max-width: 100%로 부모 너비를 절대 넘지 않게 막아요. 넷째, 입력칸·버튼은 기본적으로 주변 글꼴을 안 물려받는데, font: inherit로 우리 폰트를 따르게 해요.

여기서 Normalize(노멀라이즈)라는 사촌도 짚어둘게요. Reset이 "기본값을 다 0으로 밀어버리는" 쪽이라면, Normalize는 "브라우저 간 차이만 맞추고, 쓸 만한 기본값은 남겨두는" 쪽이에요. 둘 다 목적은 같아요 — "어느 브라우저에서나 똑같이 보이게". 요즘은 이 둘을 적당히 섞은 가벼운 reset을 많이 써요.

⚠️ 우리 reset.css는 일부러 보수적으로 만들었어요. 어떤 강한 reset은 모든 요소의 여백을 통째로 0으로 밀어버리는데(* { margin: 0 }), 우리는 이미 B-1부터 제목·문단 간격을 신중하게 잡아왔기 때문에 그걸 갑자기 밀면 공들인 간격이 와르르 무너져요. 그래서 안전한 것만 골라 담았어요. "무조건 다 지우는 게 정답"은 아니에요.

그리고 이 reset.css가장 먼저 불러야 해요. 토대를 평평하게 깐 다음, 그 위에 우리 스타일을 쌓아야 하니까요.

HTML
<!-- instagram-clone-frontend/index.html -->
<link rel="stylesheet" href="css/reset.css">
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/base.css">

Step 4: BEM 네이밍 — 클래스 이름에도 규칙이 있어요

세 번째 정리는 코드가 아니라 이름에 관한 거예요. 잠깐 상상해 볼게요. 팀원 세 명이 각자 클래스 이름을 짓는데, 한 명은 .title, 한 명은 .title2, 한 명은 .big이라고 지었어요. 나중에 .title 색을 바꿨더니 엉뚱한 페이지의 제목까지 같이 바뀌어 버렸어요. 이름이 겹쳐서요. 이런 혼란을 막으려고 만든 이름 짓는 규칙 중 가장 유명한 게 BEM이에요.

BEM은 세 글자의 약자예요.

텍스트
   Block(덩어리)     __  Element(부품)   --  Modifier(상태/변형)
   .card                 .card__title         .card__title--highlight
   하나의 독립 UI 덩어리   그 안의 부품          부품의 변형된 모습
  • Block: 혼자서도 말이 되는 독립된 UI 덩어리. 예) card(카드), nav(메뉴).
  • Element: 그 블록 안에서만 의미 있는 부품. 언더스코어 두 개로 이어요. 예) card__title(카드의 제목).
  • Modifier: 블록이나 부품의 변형된 상태. 대시 두 개로 이어요. 예) card--featured(추천 카드), btn--disabled(비활성 버튼).

가구 조립 설명서를 떠올리면 쉬워요. "책상(block) — 서랍(element) — 잠긴 서랍(modifier)"처럼, 부품마다 어디에 속하는지 이름만 봐도 알 수 있죠. card__title이라는 이름을 보면 "아, 카드 안의 제목이구나" 하고 바로 알아요. 다른 곳의 제목과 절대 안 겹치고요.

그럼 우리 코드를 BEM 눈으로 다시 볼까요? 사실 우리는 이미 BEM과 비슷하게 이름을 지어왔어요.

텍스트
   우리가 써온 이름        BEM 관점으로 읽으면
   .post-header          post(블록)의 header(부품)
   .post-avatar          post(블록)의 avatar(부품)
   .icon-btn-like        icon-btn(블록)의 like(변형)

보면 post라는 덩어리 아래 header, avatar, actions 같은 부품이 모여 있죠. 엄격한 BEM이라면 post__header, post__avatar처럼 언더스코어 두 개를 썼을 거예요. 우리는 읽기 편하게 대시 하나(post-header)로 통일해서 써왔고요.

여기서 중요한 걸 짚을게요. "무조건 BEM이 정답"은 아니에요. 회사마다, 팀마다 쓰는 규칙이 조금씩 달라요. 핵심은 형식이 아니라 "한 번 정한 규칙을 끝까지 일관되게 지키는 것"이에요. 누가 봐도 이름만 보고 "이건 어디 부품이구나"를 예측할 수 있으면 좋은 이름이에요. 우리 프로젝트는 "블록-부품" 식 대시 규칙을 일관되게 지켜온 거예요.

💡 만약 우리 게시물 카드를 엄격한 BEM으로 다시 짓는다면 이런 모습일 거예요. 비교용으로만 보세요(우리 파일은 기존 이름을 유지해요).

CSS
.post { }                  /* 블록 */
.post__header { }          /* 부품: 헤더 */
.post__avatar { }          /* 부품: 아바타 */
.post__like--active { }    /* 부품의 변형: 좋아요 켜짐 */

Step 5: 다크 모드 ① — 운영체제 설정을 따라가는 자동 다크

자, 이제 오늘의 하이라이트예요. 지금까지 변수를 한곳에 모으고(Step 2), 2단 토큰으로 감싼(Step 2) 이유가 전부 이 순간을 위해서였어요. 다크 모드(dark mode)를 만들어요.

먼저 다크 모드가 뭔지 짚을게요. 어두운 방에서 흰 화면을 보면 눈이 부시죠. 그래서 배경을 검게, 글자를 밝게 뒤집은 게 다크 모드예요. 눈의 피로를 줄이고, 일부 화면에선 배터리도 아껴요. 요즘은 거의 모든 운영체제(맥·윈도우·안드로이드·iOS)에 "다크 테마" 설정이 있어요.

핵심은 이거예요. 브라우저는 그 운영체제 설정을 읽을 수 있어요. B-6에서 배운 미디어 쿼리(@media) 기억하시죠? 그때는 화면 너비를 조건으로 썼는데, 사실 미디어 쿼리는 사용자의 설정도 조건으로 삼을 수 있어요. B-7에서 잠깐 본 prefers-reduced-motion(동작 줄이기)도 그런 친구였고요. 이번엔 prefers-color-scheme(선호 색 테마)를 써요.

CSS
/* instagram-clone-frontend/css/variables.css */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --color-text: #fafafa;
    --color-text-soft: #a8a8a8;
    --color-link: #5aa7ff;
    --color-bg: #000000;
    --color-surface: #121212;
    --color-border: #363636;
    --color-divider: #262626;
    --color-hover: #1a1a1a;
  }
}

@media (prefers-color-scheme: dark)는 "운영체제가 다크 테마일 때만 이 안의 규칙을 적용해"라는 뜻이에요. (:not([data-theme="light"])는 잠시 뒤 Step 7에서 설명해요. 지금은 :root라고만 봐주세요.)

그 안에서 우리가 한 일을 보세요. 의미 토큰만 어두운 값으로 다시 배정했어요. --color-surface(카드 바탕)를 흰색에서 거의 검정(#121212)으로, --color-text(글자)를 검정에서 밝은 회색(#fafafa)으로요. 그런데 화면을 그리는 코드(layout.css·components.css)는 한 줄도 안 건드렸어요. 헤더도, 카드도, 모달도 전부 var(--color-surface)를 보고 있으니까, 그 이름표의 값만 바꾸면 전부 알아서 따라와요. 이게 Step 2에서 2단 토큰을 깔아둔 보람이에요.

텍스트
   라이트 모드                       다크 모드 (이름은 그대로, 값만 교체)
   --color-surface: #ffffff   ──   --color-surface: #121212
   --color-text:    #262626   ──   --color-text:    #fafafa
        │                                  │
        └─ 헤더·카드·모달이 흰 바탕            └─ 같은 곳이 전부 어두운 바탕

한 가지 더, variables.css 맨 위에 이 한 줄을 넣어뒀어요.

CSS
:root {
  color-scheme: light dark;
}

color-scheme: light dark는 브라우저에게 "이 페이지는 밝은 테마와 어두운 테마를 둘 다 지원해"라고 알려줘요. 그러면 우리가 직접 칠하지 않는 부분 — 입력칸의 기본 모양, 스크롤바 같은 것까지 — 다크 모드일 때 알아서 어둡게 그려줘요.

그리고 마법의 화룡점정. 색이 하고 바뀌면 좀 거칠겠죠? B-7에서 배운 transition을 떠올려, 색이 부드럽게 넘어가게 했어요.

CSS
/* instagram-clone-frontend/css/base.css */
body {
  transition: background-color 0.25s ease, color 0.25s ease;
}

이제 운영체제 테마를 바꾸면, 배경과 글자색이 0.25초에 걸쳐 스르륵 어두워져요. B-7에서 "색을 부드럽게 전환"하는 걸 배웠고, 오늘 "그 색들 자체를 통째로 갈아끼우는" 데까지 온 거예요. 약속대로요.

직접 확인해 보세요. 운영체제 설정에서 화면 테마를 "다크"로 바꿔보세요(맥: 시스템 설정 → 디스플레이, 윈도우: 설정 → 개인 설정 → 색). 브라우저로 돌아오면 우리 인스타그램이 통째로 어두워져 있을 거예요. 우리는 화면 코드를 한 줄도 안 고쳤는데 말이죠.


Step 6: 다크 모드 ② — light-dark() 한 줄로 (2026년식)

Step 5 방식은 잘 동작하지만, 한 가지 아쉬운 게 있어요. 라이트 값은 :root에 적고, 다크 값은 @media 블록에 적었죠. 같은 토큰을 두 군데에 나눠 쓴 셈이에요. 색이 수십 개면 이 중복이 꽤 길어져요.

그래서 비교적 최근에 나온 더 짧은 방법을 소개할게요. light-dark() 함수예요. 괄호 안에 밝은 값과 어두운 값을 나란히 한 줄로 적으면, 브라우저가 현재 테마에 맞는 쪽을 골라 써요.

CSS
/* (비교용 데모 — 우리 파일엔 넣지 않아요) */
:root {
  color-scheme: light dark;
  --color-surface: light-dark(#ffffff, #121212);
  --color-text: light-dark(#262626, #fafafa);
}

보세요. @media 블록을 따로 만들 필요 없이, 토큰 한 줄에 밝은색·어두운색을 같이 적었어요. light-dark(#ffffff, #121212)은 "라이트면 흰색, 다크면 거의 검정"이라는 뜻이에요. Step 5의 중복이 한 줄로 접히죠. 이게 동작하려면 위에서 본 color-scheme이 꼭 함께 있어야 해요 — 브라우저가 그걸 보고 어느 쪽을 고를지 정하거든요.

⚠️ 호환성 메모. light-dark()는 2024년에 주요 브라우저에 등장해서, 2026년 6월 현재 대부분의 최신 브라우저에서 잘 돌아가요(지원율 95% 안팎). 다만 "어디서나 안심" 단계(Baseline Widely available)는 2026년 11월쯤 도달할 예정이라, 아주 오래된 브라우저까지 신경 써야 한다면 폴백을 함께 두는 게 좋아요. 그래서 우리 프로젝트는 Step 5의 prefers-color-scheme 방식을 메인으로 갔어요. 더 폭넓게 지원되고, 라이트·다크 블록이 눈에 또렷이 나뉘어 처음 배울 때 읽기도 좋거든요.

정리하면, light-dark()는 "같은 일을 더 짧게 쓰는 새 도구"예요. 지금 당장 우리 코드를 바꾸진 않지만, 앞으로 점점 더 표준이 될 방법이니 이름과 생김새는 꼭 기억해 두세요.


Step 7: 다크 모드 ③ — 사용자가 직접 끄고 켜기 (data-theme)

지금까지의 다크 모드는 운영체제를 따라가는 방식이었어요. 그런데 사용자가 이렇게 말할 수도 있죠. "내 컴퓨터는 라이트인데, 이 사이트만큼은 다크로 보고 싶어." 유튜브나 트위터처럼 사이트 안에 다크 모드 토글 버튼이 있는 경우예요. 이걸 만들려면 "운영체제와 상관없이 다크를 강제하는" 길이 필요해요.

그 길이 바로 [data-theme] 속성 선택자예요. HTML 요소에 data-theme="dark"라는 꼬리표를 달면, 그걸 보고 다크 값을 입히는 거죠.

CSS
/* instagram-clone-frontend/css/variables.css */
[data-theme="dark"] {
  --color-text: #fafafa;
  --color-text-soft: #a8a8a8;
  --color-link: #5aa7ff;
  --color-bg: #000000;
  --color-surface: #121212;
  --color-border: #363636;
  --color-divider: #262626;
  --color-hover: #1a1a1a;
}

[data-theme="dark"]는 "data-theme 값이 dark인 요소"를 고르는 선택자예요(A-4에서 data-*로 정보를 담는 걸 잠깐 봤죠). <html data-theme="dark">가 되면 이 블록이 켜지면서 페이지 전체가 어두워져요. 운영체제가 라이트든 다크든 상관없이요.

이제 세 가지 상태가 생겨요.

텍스트
   <html>             다크 적용?         누가 정하나
   (속성 없음)        운영체제 따라감     prefers-color-scheme
   data-theme="light" 무조건 라이트       사용자가 라이트 선택
   data-theme="dark"  무조건 다크         사용자가 다크 선택

여기서 Step 5에 살짝 묻어둔 :not([data-theme="light"])의 정체가 드러나요. 만약 운영체제는 다크인데 사용자가 "이 사이트는 라이트로 볼래"라고 골랐다면? 사용자의 선택이 이겨야겠죠. 그래서 자동 규칙에 "단, data-theme="light"가 아닐 때만"이라는 단서를 달아둔 거예요. 사용자가 직접 고른 게 운영체제 설정보다 우선이라는 약속이죠.

그런데 한 가지 빠진 게 있어요. 그 data-theme="dark" 꼬리표는 누가 붙여줄까요? 버튼을 누르면 꼬리표가 바뀌게 하려면, 클릭에 반응해서 HTML 속성을 바꾸는 일이 필요해요. 그건 CSS가 못 해요. 바로 JavaScript의 일이에요.

💡 우리는 오늘 CSS로 "다크면 이렇게 생겼다"는 구조까지 완성했어요. 남은 건 "지금 다크다"라고 속성 한 글자를 바꿔 끼우는 손가락 하나예요. 그 손가락이 바로 JavaScript고요. 클릭에 반응해 속성을 바꾸는 일은 이미 Category C·D에서 익혔죠 — 다만 그걸 깔끔한 부품으로 묶어 토글을 완성하는 건 다음 시간 클래스의 몫이에요. 동작과 표현이 이렇게 깔끔하게 나뉘는 거예요 — CSS는 모습을, JS는 전환을.

지금 당장 동작을 보고 싶다면, 브라우저 개발자 도구(DevTools)를 열고 <html> 태그에 data-theme="dark"를 직접 적어보세요. 버튼 없이도 페이지가 즉시 어두워지는 걸 볼 수 있어요. CSS 구조는 이미 다 준비돼 있으니까요.


Step 8: 실무 스타일 가이드 — 전체를 한 번에 정리

오늘 우리가 한 일을 멀리서 보면, 그게 바로 "전체 스타일 리팩토링"이에요. 흩어진 변수를 모으고, 토대를 깔고, 이름 규칙을 정리하고, 테마를 얹었죠. 마지막으로 우리 CSS 파일들이 어떻게 짜였는지 한눈에 정리하고 마칠게요.

먼저 파일을 불러오는 순서가 중요해요. 우리 HTML의 <head>는 이 순서로 스타일을 불러요.

HTML
<!-- instagram-clone-frontend/index.html -->
<link rel="stylesheet" href="css/reset.css">
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/components.css">
<link rel="stylesheet" href="css/responsive.css">

이 순서엔 이유가 있어요. 토대(reset)를 먼저 깔고, 그 위에 설정값(variables)을 준비하고, 기본 모습(base)을 정하고, 배치(layout)와 부품(components)을 올린 뒤, 마지막에 화면 크기별 조정(responsive)으로 덮어써요. 나중에 불린 파일이 앞의 것을 덮을 수 있으니(B-6에서 배운 캐스케이드), "큰 토대 → 세부 → 덮어쓰기" 순서가 자연스러워요.

텍스트
   reset.css       토대를 평평하게        (가장 먼저)
   variables.css   색·치수 설정값
   base.css        기본 글자·바탕
   layout.css      배치(고정 헤더·격자)
   components.css   부품(카드·버튼·모달)
   responsive.css   화면 크기별 조정        (가장 나중, 덮어쓰기)

각 파일의 역할을 표로 정리하면 이래요.

파일 역할
reset.css 브라우저 기본값 평평하게 box-sizing, body 여백 0
variables.css 디자인 토큰(색·치수)·테마 --color-text, 다크 모드
base.css 기본 타이포·바탕 body, a, .logo
layout.css 위치·배치 고정 헤더, 게시물 격자
components.css 재사용 부품 카드, 버튼, 댓글, 모달
responsive.css 화면 크기별 규칙 @media, @container

이렇게 "어떤 코드가 어느 파일에 사는지"에 대한 약속을 정해두는 걸 실무 스타일 가이드(style guide)라고 해요. 혼자 짤 땐 별것 아닌 것 같지만, 여러 명이 같은 프로젝트를 만질 땐 이 약속이 없으면 금세 뒤죽박죽이 돼요. 색은 variables.css에, 배치는 layout.css에 — 이런 규칙을 모두가 지키면, 누구든 코드를 처음 봐도 "그건 저 파일에 있겠네" 하고 바로 찾아요.

여기에 더해 우리가 오늘 지킨 작은 약속들도 가이드의 일부예요. 변수 이름은 --색-용도처럼 일관되게, 클래스는 "블록-부품" 식으로 일관되게, 그리고 섹션마다 /* ===== 제목 ===== */ 주석으로 구역을 나누기. 이런 사소한 일관성이 모이면 코드가 읽기 쉬워져요.


마무리

오늘 우리는 새 화면을 그리는 대신, 지금까지 만든 CSS를 프로처럼 정리했어요. 정리하고 보니 오늘 배운 게 꽤 많죠.

  • CSS 변수(--이름 / var()) — 흩어진 값을 이름표로 묶어, 한 곳만 고치면 전부 따라오게.
  • variables.css + 2단 토큰 — 원시 토큰(물감) → 의미 토큰(용도 라벨)으로 감싸 테마 작업의 토대를.
  • CSS Reset — 브라우저마다 다른 기본값을 평평하게 밀어 같은 출발선에서.
  • BEM 네이밍block__element--modifier, 이름만 봐도 어디 부품인지 예측되게.
  • 다크 모드 3종 — 운영체제 따라가기(prefers-color-scheme), 한 줄로 줄이기(light-dark()), 사용자가 직접 고르기([data-theme]).
  • 스타일 가이드 — 파일 6개의 로드 순서와 역할을 정해 여럿이 함께 짜기 좋게.

그리고 이걸로 Category B, CSS를 완주했어요! 박수 한 번 쳐도 좋아요. A에서 HTML로 뼈대를 세우고, B에서 CSS로 옷을 입혀, 이제 우리 인스타그램은 진짜처럼 보이고 다크 모드까지 갖췄어요.

그런데 딱 하나, 오늘 만든 다크 모드엔 아직 빠진 게 있어요. 좋아요도 댓글도 무한 스크롤도 이미 JavaScript로 살아 움직이지만, 정작 다크 모드 토글 버튼만은 아직 못 만들었죠. 구조는 다 깔아뒀는데 누를 버튼이 없어요. 지금은 개발자 도구로 data-theme를 손수 바꿔야만 어두워지는 상태예요.

다음 시간(E-1)에는 바로 그 마지막 조각을 채워요. JavaScript의 클래스(class)로 화면 조각을 틀처럼 찍어내는 법을 배우고, 그 도구로 다크 모드 토글을 깔끔한 부품으로 완성해요. 오늘 구조만 깔아둔 그 다크 모드 토글 버튼도, 클래스로 묶으면 드디어 클릭 한 번으로 켜고 끌 수 있게 돼요. CSS로 완주한 우리 인스타그램이, 이제 JavaScript 심화로 한 단계 더 단단해지는 거예요. 기대해 주세요.


과제

오늘 배운 변수와 다크 모드를 직접 다뤄볼 차례예요. 모두 지금까지 배운 HTML과 CSS만으로 충분히 해낼 수 있어요. Live Server로 페이지를 열어두고, 값을 바꿔가며 눈으로 확인하면서 진행하세요.

[구현] 변수 하나로 브랜드 색 갈아끼우기

우리 인스타그램의 파란색을 통째로 다른 색으로 바꿔보세요. variables.css에서 딱 한 줄만 고치면 돼요.

  • --blue-500의 값(#0095f6)을 좋아하는 색으로 바꿔보세요(예: #8e44ad 보라, #e1306c 핑크).
  • 저장하고 화면을 보세요. 로그인 버튼, 링크, NEW 뱃지가 한꺼번에 바뀌었나요?
  • 원시 토큰(--blue-500)을 바꿨을 때와, 의미 토큰(--ig-blue)을 바꿨을 때 결과가 어떻게 다른지도 비교해 보세요. 둘 다 바뀌지만, "팔레트를 바꾸는 것"과 "용도 배정을 바꾸는 것"의 차이를 느껴보는 거예요.

[구현] 나만의 다크 모드 다듬기

지금 다크 모드는 배경과 카드가 둘 다 거의 같은 어둠이에요. 여기에 미묘한 단차를 줘서 카드가 배경 위에 살짝 떠 보이게 만들어보세요.

  • @media (prefers-color-scheme: dark) 블록에서 --color-bg를 순수 검정(#000000)으로, --color-surface를 살짝 밝은 회색(#1c1c1c 정도)으로 나눠보세요.
  • 운영체제를 다크로 두고 피드를 보면, 카드가 배경에서 떠오르는 느낌이 드나요?
  • 여유가 있다면 --color-text-soft(흐린 글자색)도 조금 더 밝게 조정해, 보조 텍스트가 잘 읽히는지 확인해 보세요.

[탐구] reset이 있을 때와 없을 때

reset의 효과를 눈으로 체험해 보세요.

  • index.html에서 reset.css <link> 줄을 잠깐 주석 처리(<!-- ... -->)하고 저장해 보세요. 무엇이 달라지나요? 페이지 가장자리에 흰 여백이 생기거나, 간격이 달라지는 걸 관찰하고 한 문장으로 적어보세요.
  • 확인했으면 주석을 꼭 되돌려 놓으세요.
  • 그리고 Step 6의 light-dark()를 작은 박스 하나에 직접 써보세요. 예를 들어 빈 <div class="demo">를 만들고 background: light-dark(#eee, #333)을 줘서, 운영체제 테마를 바꿀 때 박스 색이 따라 바뀌는지 확인해 보세요.

생각해볼 주제

정답을 적는 문제가 아니에요. 오늘 배운 도구의 "왜"를 곱씹어보는 질문들이에요. 스스로 답을 만들어본 뒤, 예시답안과 비교해보세요.

1. 색을 왜 굳이 두 단계로 나눌까?

우리는 --gray-900(원시 토큰)을 만들고, 그걸 --color-text(의미 토큰)가 가리키게 했어요. 그냥 --color-text: #262626처럼 한 단계로 바로 적으면 안 됐을까요? 한 단계가 더 간단해 보이는데 말이죠. 다크 모드를 만들 때, 그리고 회사가 "브랜드 색을 전부 바꾸자"고 할 때, 두 단계가 각각 어떤 도움을 주는지 생각해보세요.

2. 자동(운영체제)과 수동(토글), 무엇이 먼저여야 할까?

우리는 prefers-color-scheme로 운영체제를 따라가게도 만들고, [data-theme]로 사용자가 직접 고르게도 만들었어요. 그런데 둘이 충돌하면? 운영체제는 다크인데 사용자가 이 사이트만 라이트로 골랐다면, 화면은 무엇으로 보여야 할까요? 우리는 "사용자 선택이 우선"으로 정했는데, 왜 그게 자연스러운지 — 사용자 입장에서 생각해보세요.

3. Reset은 "다 지우는 게" 정답일까?

어떤 개발자는 모든 요소의 여백을 통째로 0으로 밀어버리는 강한 Reset을 써요. 우리는 일부러 안전한 것만 고른 보수적 Reset을 썼고요. 깨끗하게 다 지우고 시작하는 것과, 기존 간격을 존중하며 최소한만 손대는 것 — 각각 어떤 상황에서 유리할까요? 새 프로젝트를 처음부터 시작할 때와, 이미 한참 만든 프로젝트에 Reset을 도입할 때를 나눠서 생각해보세요.

✅ 예시 답안정답 보기

과제와 생각해볼 주제의 예시답안이에요. 정답이 하나만 있는 건 아니에요. 색 값이나 어둠의 농도는 취향대로 골라도 좋아요. 중요한 건 변수는 한 곳만 고쳤는가, 다크 모드는 의미 토큰만 갈아끼웠는가, 그리고 눈으로 직접 확인했는가 예요.


과제 예시답안

🎯 [과제 1 예시답안] 변수 하나로 브랜드 색 갈아끼우기

핵심 접근

이 과제의 목적은 "변수의 위력을 손으로 느끼기"예요. 파란색이 화면 수십 군데에 쓰이는데, 그게 전부 --blue-500이라는 이름표 하나를 보고 있다는 걸 확인하는 거죠. 그래서 고치는 곳은 딱 한 줄이에요.

예시 구현

CSS
/* instagram-clone-frontend/css/variables.css */
:root {
  --blue-500: #8e44ad; /* 원래 #0095f6 → 보라로 */
}

이 한 줄만 바꿔 저장하면, 로그인 버튼·링크·NEW 뱃지가 한꺼번에 보라색으로 바뀌어요. 우리가 --ig-blue를 만들 때 var(--blue-500)을 가리키게 했고, 버튼·뱃지는 다시 var(--ig-blue)를 보고 있으니, 맨 위 원시 토큰 하나만 바꿔도 줄줄이 따라오는 거예요.

원시 토큰(--blue-500)을 바꾸면 그 파랑을 쓰는 모든 용도가 바뀌어요. 반면 의미 토큰(--ig-blue: var(--blue-500))을 직접 다른 색으로 바꾸면, "브랜드 파랑"이라는 용도만 바뀌고 --blue-500 자체를 따로 쓰는 다른 곳은 그대로예요. "팔레트를 갈았다"와 "용도 배정을 갈았다"의 차이죠.

채점 포인트

항목 확인 내용
한 줄 수정 색을 여러 군데가 아니라 변수 한 곳만 고쳤는가
연쇄 확인 버튼·링크·뱃지가 한꺼번에 바뀌는 걸 눈으로 봤는가
토큰 층 이해 원시 토큰과 의미 토큰을 바꿨을 때 차이를 비교했는가

흔한 실수

  • 버튼·뱃지를 일일이 찾아서 바꿈 — 그러면 변수를 쓰는 의미가 없어요. 변수의 핵심은 "한 곳만 고치면 전부 따라온다"는 거예요.
  • var(--blue-500)처럼 정의할 때 var()로 적음 — 변수를 만들 때--blue-500: #8e44ad처럼 값만 적고, var()꺼내 쓸 때만 써요.

🎯 [과제 2 예시답안] 나만의 다크 모드 다듬기

핵심 접근

지금 다크 모드는 배경(--color-bg)과 카드(--color-surface)가 둘 다 거의 같은 어둠이라 카드 경계가 밋밋해요. 배경을 더 깊게, 카드를 살짝 밝게 나누면 카드가 배경 위에 떠 보여요. 고치는 곳은 다크 모드 블록의 의미 토큰 두어 개뿐이에요.

예시 구현

CSS
/* instagram-clone-frontend/css/variables.css */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --color-bg: #000000;      /* 배경은 순수 검정으로 더 깊게 */
    --color-surface: #1c1c1c; /* 카드는 살짝 밝은 회색으로 띄우기 */
    --color-text-soft: #b3b3b3; /* 보조 글자도 조금 더 밝게 */
  }
}

--color-bg를 순수 검정으로, --color-surface#1c1c1c로 벌려주면, 카드와 배경 사이에 미묘한 단차가 생겨요. 카드가 어둠 위에 살짝 떠오르는 느낌이죠. 보조 글자색(--color-text-soft)도 조금 밝히면 흐린 텍스트가 더 잘 읽혀요. 여기서도 화면 코드는 한 줄도 안 건드렸어요 — 의미 토큰의 값만 조정했을 뿐이에요.

채점 포인트

항목 확인 내용
단차 주기 --color-bg--color-surface를 다른 농도로 벌렸는가
토큰만 수정 layout·components가 아니라 변수 블록만 고쳤는가
가독성 확인 어두운 배경에서 글자가 잘 읽히는지 눈으로 봤는가

흔한 실수

  • .post-card 같은 컴포넌트에 직접 어두운 색을 적음 — 그러면 라이트 모드까지 어두워져요. 다크 값은 반드시 @media (prefers-color-scheme: dark) 블록 안의 변수로만 바꿔요.
  • 너무 큰 단차 — 배경 검정에 카드를 회색(#444)처럼 너무 밝게 하면 들떠 보여요. 다크 모드는 농도 차이를 작게 주는 게 자연스러워요.

🎯 [과제 3 예시답안] reset이 있을 때와 없을 때

핵심 접근

이 과제는 코드를 새로 짜기보다, reset과 light-dark()의 효과를 눈으로 관찰하는 탐구예요. 잠깐 꺼봤다가 다시 켜는 방식으로 "이게 없으면 무슨 일이 생기는지"를 체험하는 거죠.

예시 관찰 — reset.css를 껐을 때

HTML
<!-- instagram-clone-frontend/index.html -->
<!-- <link rel="stylesheet" href="css/reset.css"> -->

reset.css 링크를 주석 처리하면, 페이지 가장자리에 흰 여백이 생겨요. 브라우저가 <body>에 기본으로 주는 8px쯤의 바깥 여백이 되살아난 거예요. 우리 고정 헤더가 화면 끝에 딱 붙어 있다가, 살짝 안쪽으로 밀려 어색해지죠. 관찰을 적자면 이런 식이에요. "reset을 끄니 페이지 둘레에 흰 띠가 생기고 헤더가 화면 끝에 닿지 않았다 — 브라우저 기본 body 여백 때문이다." 확인했으면 주석을 꼭 되돌려요.

예시 관찰 — light-dark()를 써봤을 때

CSS
/* 작은 데모 박스 (체험용) */
.demo {
  width: 100px;
  height: 100px;
  background: light-dark(#eeeeee, #333333);
}

운영체제 테마를 바꿀 때마다 이 박스가 밝은 회색 ↔ 어두운 회색으로 따라 바뀌어요. @media 블록을 따로 만들지 않았는데도요. Step 5 방식이라면 라이트·다크 값을 두 군데 나눠 적어야 했을 텐데, light-dark()는 한 줄로 끝났죠. "줄 수가 줄어든다"를 눈으로 확인하는 게 이 관찰의 핵심이에요.

채점 포인트

항목 확인 내용
reset 관찰 reset을 껐을 때 달라진 점(여백 등)을 한 문장으로 적었는가
되돌리기 실험 후 reset 주석을 다시 풀었는가
light-dark 체험 테마를 바꿀 때 데모 박스 색이 따라 바뀌는 걸 봤는가

흔한 실수

  • 주석을 안 되돌림 — reset을 끈 채로 두면 이후 모든 화면의 여백이 어긋나요. 관찰이 끝나면 반드시 복구해요.
  • light-dark()color-scheme을 안 깜light-dark()color-scheme이 있어야 어느 쪽을 고를지 정해요. :rootcolor-scheme: light dark가 있는지 확인해요.

생각해볼 주제 예시답안

1. 색을 왜 굳이 두 단계로 나눌까?

--color-text: #262626 한 단계로 적는 게 분명 더 짧고 간단해 보여요. 그런데 두 단계(--gray-900--color-text)가 빛을 발하는 순간이 두 번 있어요.

첫째, 다크 모드예요. 한 단계라면 다크 모드에서 --color-text를 밝은 회색으로 바꿀 때, 그 색이 우연히 다른 곳(예: 테두리)에도 같은 hex로 쓰였다면 거기까지 의도치 않게 끌려가요. 두 단계라면 "본문 글자색"이라는 용도 단위로만 바꿀 수 있어 안전해요. 의미 토큰은 "이 색을 어디에 쓰는가"를 또렷이 구분해 주거든요.

둘째, 리브랜딩(브랜드 색 교체)이에요. 회사가 파랑을 보라로 바꾸자고 하면, 원시 토큰 --blue-500 하나만 갈면 그 파랑을 쓰던 모든 용도가 따라와요. 팔레트(원시)와 용도(의미)를 분리해 두면, "색 자체를 바꾸는 일"과 "어디에 어떤 색을 쓸지 바꾸는 일"을 따로 다룰 수 있어요. 작은 프로젝트엔 과해 보여도, 화면이 커질수록 이 두 층이 혼란을 막아줘요.

🎯 면접관을 홀리는 핵심 멘트

"원시 토큰은 팔레트, 의미 토큰은 그 팔레트를 어디에 쓸지에 대한 약속이에요. 이 두 층을 분리하면 다크 모드는 '용도'만 갈아끼우면 되고, 리브랜딩은 '팔레트'만 갈면 돼요. 색 하나가 여기저기 hex로 흩어져 있으면 둘 다 지옥이 되죠."

2. 자동(운영체제)과 수동(토글), 무엇이 먼저여야 할까?

둘이 충돌할 때 — 운영체제는 다크인데 사용자가 이 사이트만 라이트로 골랐을 때 — 답은 분명해요. 사용자가 직접 고른 게 이겨야 해요.

이유는 사용자 입장에서 생각하면 자연스러워요. 운영체제 설정은 "평소 내 취향"이라는 기본값이에요. 반면 사이트 안에서 토글 버튼을 일부러 눌렀다는 건, "이 사이트에선 다르게 보고 싶다"는 더 구체적이고 의도적인 선택이에요. 더 구체적인 의사 표시가 더 일반적인 기본값을 이기는 게 상식적이죠. 만약 사용자가 애써 라이트를 골랐는데 운영체제가 다크라는 이유로 계속 어두워진다면, 버튼이 고장 난 것처럼 느껴질 거예요.

그래서 우리는 자동 규칙에 :not([data-theme="light"])라는 단서를 달아, "사용자가 라이트를 강제하지 않았을 때만 운영체제를 따라가라"고 했어요. 좋은 순서는 보통 이래요. 기본값은 운영체제를 따르되, 사용자가 한번 고르면 그 선택을 기억하고 우선한다.

🎯 면접관을 홀리는 핵심 멘트

"기본값은 운영체제를 존중하고, 명시적 선택은 그 위에 얹어요. 사용자가 토글을 눌렀다는 건 기본값보다 더 구체적인 의사 표시니까, 그게 이기는 게 맞아요. CSS에선 :not() 가드로 우선순위를 표현했고, 실제 서비스라면 그 선택을 저장해 다음 방문 때도 유지하죠."

3. Reset은 "다 지우는 게" 정답일까?

상황에 따라 달라요. 두 경우를 나눠 보면 분명해져요.

새 프로젝트를 처음부터 시작할 때는 강한 Reset(모든 여백을 0으로)이 유리해요. 백지에서 시작하니 모든 간격을 내가 의도적으로 부여하게 되고, 브라우저 기본값이 끼어들 틈이 없어 예측이 쉬워요. "원하는 간격만 정확히 준다"는 통제감이 크죠.

반면 이미 한참 만든 프로젝트에 Reset을 뒤늦게 도입할 때는 보수적 Reset이 안전해요. 우리가 그 경우였죠. B-1부터 제목·문단 간격을 신중히 쌓아왔는데, 갑자기 모든 여백을 0으로 밀면 공들인 간격이 무너져 전부 다시 손봐야 해요. 그래서 우리는 box-sizing·body 여백·이미지 넘침처럼 부작용 없는 것만 골라 담았어요.

결국 Reset의 강도는 "깨끗함"과 "안전함" 사이의 트레이드오프예요. 백지면 강하게, 이미 쌓인 게 있으면 약하게 — 정답은 프로젝트의 처지에 달려 있어요.

🎯 면접관을 홀리는 핵심 멘트

"Reset의 강도는 프로젝트 처지에 맞춰요. 백지에서 시작하면 모든 여백을 0으로 밀어 통제감을 얻는 게 좋고, 이미 간격을 쌓아온 코드에 뒤늦게 넣을 땐 부작용 없는 것만 골라야 기존 레이아웃이 안 무너져요. '무조건 다 지우기'는 도구지 정답이 아니에요."

전체 목록 HTML·CSS·JS