이번 편의 결과물: 데스크톱·모바일 모두 시안과 일치하는 완성된 포트폴리오 사이트. 01_학습방향.md의 완료 기준 5가지를 자가 점검합니다. 다루는 개념: 시안 대 결과물 스크린샷 비교, 포커스 표시·색 대비 접근성 점검, DevTools로 최종 확인
이 편에서 만드는 파일
web-practice/portfolio/
└── css/
├── tokens.css (03~19편 완성분, 이 편에서는 수정하지 않음)
└── styles.css (03~19편 완성분, 이 편에서는 수정하지 않음)새 코드는 없습니다. 지금까지 쌓아 온 두 파일의 정본 전체를 확인하고, 시안과 결과물을 대조해 빠진 부분이 없는지 점검합니다.
개념 정리
01_학습방향.md가 정한 완료 기준은 다섯 가지입니다.
| # | 기준 | 확인 방법 |
|---|---|---|
| 1 | 시안과 색·간격·타이포가 데스크톱/모바일 모두 일치 | 시안 이미지와 브라우저 화면을 나란히 놓고 비교 |
| 2 | css/가 리셋 → 변수 → 컴포넌트 순서로 정리 | styles.css를 위에서 아래로 훑으며 순서 확인 |
| 3 | 내비게이션(Flexbox)·카드(Grid)를 고른 이유를 설명할 수 있음 | 왜 1차원은 Flex, 2차원은 Grid인지 스스로 말해 보기 |
| 4 | 미디어 쿼리가 개입하는 지점을 DevTools로 짚을 수 있음 | 반응형 모드에서 폭을 조절하며 브레이크포인트 확인 |
| 5 | transition·animation 값을 조정할 수 있음 | 17·18편에서 바꿔 본 값들을 다시 시도 |
색 대비는 WCAG AA 기준(본문 텍스트 4.5:1 이상)을 최소선으로 봅니다.
| 조합 | 대비 확인 대상 |
|---|---|
--color-text(#1f2328) on --color-bg(#ffffff) | 본문 텍스트 |
--color-text-muted(#59636e) on --color-bg | 부제·도움말 문구 |
#ffffff on --color-primary(#0969da) | 기본 버튼 글자 |
DevTools의 Elements 패널에서 텍스트 색상 값을 클릭하면 대비 비율과 AA/AAA 통과 여부를 바로 보여줍니다.
실습
1. tokens.css 정본 확인
/* web-practice/portfolio/css/tokens.css */
:root {
/* 색 */
--color-bg: #ffffff;
--color-surface: #f6f7f9;
--color-text: #1f2328;
--color-text-muted: #59636e;
--color-primary: #0969da;
--color-primary-hover: #0550ae;
--color-border: #d0d7de;
--color-accent: #bf3989;
/* 타이포 */
--font-sans: "Pretendard", "Noto Sans KR", system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.25rem;
--text-xl: 1.5rem;
--text-2xl: 2rem;
--text-3xl: 3rem;
--leading: 1.6;
/* 간격 (4px 배수) */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-12: 3rem;
--space-16: 4rem;
/* 레이아웃 */
--container-max: 72rem;
--radius: 0.5rem;
--shadow: 0 1px 3px rgb(0 0 0 / 0.1), 0 4px 12px rgb(0 0 0 / 0.06);
--header-height: 4rem;
}이 파일은 04편 이후 한 줄도 바뀌지 않았습니다. 다크 모드용 값(--color-bg: #0d1117 등)은 modern_css가 다음 과목에서 같은 파일에 추가합니다.
2. styles.css 정본 확인
/* web-practice/portfolio/css/styles.css */
/* ========================================
1. 리셋 (03편)
======================================== */
*,
*::before,
*::after {
box-sizing: border-box;
}
body,
h1,
h2,
h3,
p,
ul,
ol,
figure {
margin: 0;
}
ul,
ol {
padding: 0;
list-style: none;
}
img {
max-width: 100%;
display: block;
}
a {
color: inherit;
text-decoration: none;
}
button,
input,
textarea,
select {
font: inherit;
color: inherit;
}
/* ========================================
2. 공통 레이아웃
======================================== */
body {
font-family: var(--font-sans);
font-size: var(--text-base);
line-height: var(--leading);
color: var(--color-text);
background-color: var(--color-bg);
}
.container {
max-width: var(--container-max);
margin-inline: auto;
padding-inline: var(--space-6);
}
/* ========================================
3. 타이포그래피 (05편)
======================================== */
h1,
h2,
h3 {
font-weight: 700;
line-height: 1.2;
color: var(--color-text);
}
h1 {
font-size: var(--text-3xl);
}
h2 {
font-size: var(--text-2xl);
margin-bottom: var(--space-4);
}
h3 {
font-size: var(--text-lg);
}
p {
margin-bottom: var(--space-4);
}
.logo {
font-size: var(--text-lg);
font-weight: 700;
}
/* ========================================
4. 색·배경 (06편)
======================================== */
a {
color: var(--color-primary);
}
a.logo,
.site-nav a,
.project-card a {
color: inherit;
}
.hero {
background: linear-gradient(180deg, var(--color-surface), var(--color-bg));
}
/* ========================================
5. 박스 모델·여백 (07편)
======================================== */
.about,
.skills,
.career,
.project-grid,
.contact-form {
padding-block: var(--space-12);
}
.about + .skills,
.skills + .career {
border-top: 1px solid var(--color-border);
}
/* ========================================
6. 버튼 (06, 17편)
======================================== */
.button {
display: inline-block;
border-radius: var(--radius);
padding: var(--space-2) var(--space-6);
font-weight: 700;
background-color: var(--color-primary);
color: #ffffff;
border: 1px solid transparent;
transition: background-color 200ms ease, border-color 200ms ease, transform 100ms ease;
}
.button:hover {
background-color: var(--color-primary-hover);
}
.button-secondary {
background-color: transparent;
border-color: var(--color-border);
color: var(--color-text);
}
.button-secondary:hover {
background-color: transparent;
border-color: var(--color-primary);
color: var(--color-primary);
}
.button:active {
transform: translateY(1px);
}
/* ========================================
7. 헤더 & 내비게이션 (08~10, 13편)
======================================== */
.site-header {
position: sticky;
top: 0;
z-index: 10;
display: flex;
align-items: center;
justify-content: space-between;
height: var(--header-height);
padding-inline: var(--space-6);
background-color: var(--color-bg);
border-bottom: 1px solid var(--color-border);
}
.site-nav {
display: none;
gap: var(--space-6);
}
.site-nav a[aria-current='page'] {
color: var(--color-primary);
font-weight: 700;
}
.menu-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-bg);
cursor: pointer;
}
/* ========================================
8. 모바일 메뉴 popover (13, 19편)
======================================== */
#mobile-menu {
display: flex;
flex-direction: column;
gap: var(--space-4);
position: fixed;
inset: var(--header-height) var(--space-4) auto var(--space-4);
margin: 0;
padding: var(--space-4);
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-bg);
box-shadow: var(--shadow);
}
#mobile-menu a {
display: block;
font-size: var(--text-lg);
}
#mobile-menu:popover-open {
animation: overlay-enter 150ms ease-out;
}
/* ========================================
9. 히어로 (06, 18편)
======================================== */
.hero {
padding-block: var(--space-16);
text-align: center;
}
.hero h1 {
font-size: var(--text-3xl);
}
.hero p {
font-size: var(--text-lg);
color: var(--color-text-muted);
}
.hero > * {
animation: hero-enter 600ms ease-out both;
}
.hero > *:nth-child(2) {
animation-delay: 150ms;
}
.hero > *:nth-child(3) {
animation-delay: 300ms;
}
.hero > *:nth-child(4) {
animation-delay: 400ms;
}
@keyframes hero-enter {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.hero > * {
animation: none;
}
}
/* ========================================
10. 소개 / 기술 스택 / 경력 (07, 09편)
======================================== */
.about {
display: flex;
flex-wrap: wrap;
gap: var(--space-8);
}
.about img {
width: 12rem;
border-radius: var(--radius);
}
.about p {
line-height: 1.8;
}
.skills ul {
display: flex;
flex-wrap: wrap;
gap: var(--space-4);
}
.skills li {
padding: var(--space-2) var(--space-4);
background-color: var(--color-surface);
border-radius: var(--radius);
font-size: var(--text-sm);
}
.career table {
width: 100%;
border-collapse: collapse;
}
.career th {
background-color: var(--color-surface);
text-align: left;
font-weight: 700;
padding: var(--space-3) var(--space-4);
}
.career td {
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border);
}
/* ========================================
11. 프로젝트 카드 그리드 (11~12, 17편)
======================================== */
.project-grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-6);
}
.project-card {
display: flex;
flex-direction: column;
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
overflow: hidden;
transition: transform 200ms ease, box-shadow 200ms ease;
}
.project-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow);
}
.project-card img {
aspect-ratio: 3 / 2;
object-fit: cover;
}
.project-card h2 {
font-size: var(--text-lg);
padding-inline: var(--space-4);
margin-top: var(--space-4);
}
.project-card p {
padding-inline: var(--space-4);
color: var(--color-text-muted);
font-size: var(--text-sm);
}
.project-card .tags {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
padding-inline: var(--space-4);
}
.project-card .tags li {
padding: var(--space-1) var(--space-3);
background-color: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 999px;
font-size: var(--text-xs);
}
.project-card button[data-open-project] {
margin: var(--space-4);
margin-top: auto;
}
/* ========================================
12. 프로젝트 상세 dialog (19편)
======================================== */
#project-dialog {
width: min(32rem, 90vw);
border: none;
border-radius: var(--radius);
padding: var(--space-8);
background-color: var(--color-bg);
box-shadow: var(--shadow);
}
#project-dialog::backdrop {
background-color: rgb(15 23 32 / 0.5);
}
#project-dialog h3 {
margin-bottom: var(--space-4);
}
#project-dialog p + p {
margin-top: var(--space-3);
}
#project-dialog[open] {
animation: overlay-enter 200ms ease-out;
}
@keyframes overlay-enter {
from {
opacity: 0;
transform: scale(0.96);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* ========================================
13. 연락 폼 (15~17편)
======================================== */
.contact-form {
display: flex;
flex-direction: column;
gap: var(--space-6);
max-width: 32rem;
}
.field {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.field label {
font-size: var(--text-sm);
font-weight: 700;
color: var(--color-text);
}
.field input,
.field select,
.field textarea {
width: 100%;
padding: var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius);
transition: border-color 150ms ease, box-shadow 150ms ease;
}
.field input:focus,
.field select:focus,
.field textarea:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgb(9 105 218 / 0.2);
}
.field input:invalid:not(:placeholder-shown) {
border-color: var(--color-accent);
}
.field-checkbox {
flex-direction: row;
align-items: center;
gap: var(--space-2);
}
.field .help {
font-size: var(--text-xs);
color: var(--color-text-muted);
}
.contact-form button {
display: inline-flex;
align-items: center;
justify-content: center;
height: 2.75rem;
padding-inline: var(--space-6);
border-radius: var(--radius);
font-size: var(--text-base);
font-weight: 700;
cursor: pointer;
border: 1px solid transparent;
background: var(--color-primary);
color: #ffffff;
transition: background-color 200ms ease, transform 100ms ease;
}
.contact-form button:hover {
background: var(--color-primary-hover);
}
.contact-form button:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
.contact-form button:active {
transform: translateY(1px);
}
.contact-form button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.contact-form button:disabled:hover {
background: var(--color-primary);
}
/* ========================================
14. FAQ details (19편)
======================================== */
.faq details {
border-bottom: 1px solid var(--color-border);
padding-block: var(--space-4);
}
.faq summary {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
font-weight: 700;
list-style: none;
}
.faq summary::-webkit-details-marker {
display: none;
}
.faq summary::after {
content: "+";
font-size: var(--text-lg);
color: var(--color-text-muted);
transition: transform 200ms ease;
}
.faq details[open] summary::after {
transform: rotate(45deg);
}
.faq details[open] summary {
margin-bottom: var(--space-2);
}
/* ========================================
15. 푸터
======================================== */
.site-footer {
padding-block: var(--space-8);
border-top: 1px solid var(--color-border);
color: var(--color-text-muted);
font-size: var(--text-sm);
}
.site-footer .container {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: var(--space-4);
}
/* ========================================
16. 반응형 (13~14편)
======================================== */
@media (max-width: 767px) {
.hero h1 {
font-size: var(--text-2xl);
}
.project-card h2 {
font-size: var(--text-base);
}
}
@media (min-width: 768px) {
.project-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.site-nav {
display: flex;
}
.menu-toggle {
display: none;
}
.project-grid {
grid-template-columns: repeat(3, 1fr);
}
}3. 데스크톱 시안과 비교
public/practice/css_fundamentals/starter.zip의 design/desktop-index.png·desktop-projects.png·desktop-contact.png를 열고, 브라우저 창을 1024px 이상으로 넓혀 세 페이지를 나란히 놓습니다. 헤더 내비게이션 위치, 히어로 여백, 카드 3열 배치, 폼 필드 순서를 하나씩 짚습니다.
4. 모바일 시안과 비교
개발자 도구의 반응형 모드(Ctrl + Shift + M)에서 폭 375px 안팎으로 맞추고 mobile-*.png 시안과 비교합니다. 내비게이션이 햄버거 버튼으로 바뀌는지, 카드가 1열로 쌓이는지 확인합니다.
5. 접근성 점검
Tab 키만으로 헤더 → 히어로 버튼 → 카드 → 폼까지 이동해 봅니다. 포커스 링이 항상 보이는지, contact.html의 필수 입력에 오류 시 --color-accent 테두리가 보이는지 확인합니다.
6. 최종 확인
- 데스크톱·모바일 모두 시안과 색·간격·타이포가 일치합니다.
styles.css가 리셋(1) → 공통 레이아웃(2) → 타이포(3) → 색(4) → 나머지 컴포넌트 순서로 정리되어 있습니다.- 헤더 내비게이션은 Flexbox, 프로젝트 카드는 Grid로 구현된 이유를 말할 수 있습니다.
- DevTools 반응형 모드에서 768px, 1024px 두 지점에서 레이아웃이 바뀌는 것을 확인했습니다.
- 17·18편에서 만든 transition·animation 값을 하나 이상 바꿔 다른 느낌으로 조정해 봤습니다.
직접 해보기
styles.css에서var()를 쓰지 않고 직접 색상 코드(#0969da등)를 적은 곳이 있는지 검색(Ctrl+F)해 찾아보고, 있다면 토큰으로 바꿔 보세요.- 브라우저 창 폭을 1023px에서 1025px로 1px씩 옮기며 내비게이션이 정확히 어느 지점에서 바뀌는지 DevTools 눈금자로 확인해 보세요.
정답 확인
- 매직 넘버가 남아 있다면
var(--color-primary)처럼 토큰으로 교체합니다.modern_css에서 다크 모드를 추가할 때 토큰만 바꿔도 전체 색상이 바뀌게 하려면 이 작업이 꼭 필요합니다. min-width: 1024px이므로 1024px에서부터 데스크톱 스타일이 적용되고, 1023px까지는 모바일/태블릿 스타일이 유지됩니다.
자주 하는 실수
| 증상 | 원인 | 고치는 법 |
|---|---|---|
| 시안과 색이 미묘하게 다르다 | 토큰 대신 비슷한 색을 직접 입력했다 | tokens.css의 --color-* 값을 그대로 검색해 일치 여부 확인 |
| 모바일에서 카드 사이 간격이 시안보다 좁다 | .project-grid의 gap이 var(--space-6)이 아니라 다른 값으로 남아 있다 | 그리드·플렉스 gap 값을 전부 토큰으로 통일 |
| 포커스 링이 일부 요소에서 안 보인다 | outline: none만 적용하고 대체 표시를 빠뜨린 요소가 있다 | 모든 상호작용 요소가 :focus 상태에서 box-shadow 또는 outline을 갖는지 전수 확인 |
DevTools에서 --color-* 변수가 계산되지 않는다 | tokens.css가 styles.css보다 나중에 연결됐다 | index.html 등에서 <link> 순서를 tokens.css → styles.css로 확인 |