핸드오프는 디자인을 코드로 넘기는 다리입니다. 그런데 이 다리 위에서 매번 비슷한 곳에 발이 걸립니다. 흔한 실패를 나쁜 예와 좋은 예 코드로 나란히 두면, 무엇을 고쳐야 할지 눈에 바로 들어옵니다.
74.1 하드코딩된 값 흩뿌리기
같은 색을 화면 곳곳에 숫자로 박아두면, 색 하나 바꿀 때 모든 곳을 뒤져야 합니다.
/* 나쁜 예: 같은 값이 사방에 */
.header { color: #2563eb; }
.link { color: #2563eb; }
.badge { background: #2563eb; }
값을 CSS 변수 하나로 모으면 한 곳만 고쳐도 전체가 따라옵니다.
/* 좋은 예: 변수로 모음 */
:root { --brand: #2563eb; }
.header { color: var(--brand); }
.link { color: var(--brand); }
.badge { background: var(--brand); }
브랜드 색을 바꿀 때 root의 한 줄만 손대면 됩니다.
74.2 매직 넘버 대신 스케일 토큰
간격을 그때그때 눈대중으로 정하면 13px, 17px 같은 제멋대로인 숫자가 쌓입니다.
/* 나쁜 예: 규칙 없는 간격 */
.box { padding: 13px; margin-bottom: 17px; }
.row { gap: 9px; }
4의 배수 같은 스케일을 토큰으로 정해두면 간격에 리듬이 생깁니다.
/* 좋은 예: 스페이싱 스케일 */
:root {
--sp-1: 4px;
--sp-2: 8px;
--sp-3: 16px;
}
.box { padding: var(--sp-3); margin-bottom: var(--sp-3); }
.row { gap: var(--sp-2); }
정해진 계단 위에서만 값을 고르니, 화면마다 간격이 제각각이던 문제가 사라집니다.
74.3 빠뜨린 상태 채우기
시안에는 보통 기본 모습만 있습니다. 마우스를 올렸을 때, 키보드로 짚었을 때, 비활성일 때가 빠져 있지요.
/* 나쁜 예: 기본 상태만 */
.btn {
background: var(--brand);
color: #fff;
}
상호작용 상태를 채우면 버튼이 살아 있는 느낌을 줍니다.
/* 좋은 예: 상태를 모두 정의 */
.btn:hover { background: #1d4ed8; }
.btn:focus-visible {
outline: 2px solid #1d4ed8;
outline-offset: 2px;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
focus-visible는 키보드 사용자에게만 테두리를 보여줘, 마우스 클릭에는 방해가 되지 않습니다.
74.4 고정 px 폰트에서 rem으로
폰트 크기를 px로 못 박으면 브라우저 글자 확대 설정이 통하지 않습니다.
/* 나쁜 예 */
body { font-size: 16px; }
.small { font-size: 13px; }
rem으로 바꾸면 사용자가 키운 기본 크기를 그대로 따라갑니다.
/* 좋은 예 */
body { font-size: 1rem; }
.small { font-size: 0.8125rem; }
시력이 약한 사용자가 글자를 키우면, 화면 전체가 비율을 지키며 함께 커집니다.
74.5 절대 위치 대신 flex
피그마의 좌표를 그대로 옮겨 요소마다 위치를 박으면, 내용이 늘어날 때 서로 겹칩니다.
/* 나쁜 예: 좌표 박기 */
.tag {
position: absolute;
left: 120px;
top: 8px;
}
부모에 flex와 gap을 주면 아이들이 알아서 줄 맞춰 자리를 잡습니다.
/* 좋은 예: 흐름에 맡김 */
.tags {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
태그가 몇 개든 간격을 지키며 나열되고, 폭이 좁아지면 다음 줄로 넘어갑니다.
74.6 반응형 빠뜨리지 않기
넓은 화면 하나만 넘어오면, 좁은 화면에서 두 칸 배치가 그대로 짓눌립니다.
/* 나쁜 예: 한 가지 폭만 */
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
}
미디어 쿼리로 좁은 화면을 한 칸으로 접어줍니다.
/* 좋은 예: 폭에 따라 접기 */
@media (max-width: 600px) {
.grid {
grid-template-columns: 1fr;
}
}
휴대폰 화면에서 두 칸이 한 칸으로 펴지며, 내용이 눌리지 않고 읽힙니다.
74.7 빈 상태 빠뜨리지 않기
목록에 항목이 하나도 없을 때의 화면은 시안에 거의 그려지지 않습니다. 그래서 실제 서비스에서 텅 빈 흰 화면이 나오곤 합니다.
/* 나쁜 예: 채워진 경우만 준비 */
.list { padding: 16px; }
.list-item { padding: 8px 0; }
비었을 때 보여줄 안내 자리를 미리 만들어 둡니다.
/* 좋은 예: 빈 상태 자리 */
.empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 40px 16px;
color: #6b7280;
}
항목이 없을 때 이 안내를 대신 보여주면, 사용자는 화면이 고장 난 것인지 원래 비어 있는 것인지 헷갈리지 않습니다.
74.8 하드코딩 폭 대신 최소와 최대
시안의 폭을 그대로 못 박으면, 그 폭보다 좁은 화면에서 가로 스크롤이 생깁니다.
/* 나쁜 예: 넘침을 부르는 고정 폭 */
.container {
width: 960px;
}
최대 폭만 두고 좌우 여백을 auto로 주면, 넓은 화면에서는 가운데 정렬되고 좁은 화면에서는 알아서 줄어듭니다.
/* 좋은 예: 상한만 두고 유연하게 */
.container {
width: 100%;
max-width: 960px;
margin: 0 auto;
padding: 0 16px;
}
이 한 벌만 갖춰두면 데스크톱에서 모바일까지 같은 컨테이너가 무리 없이 늘고 줄어듭니다.
74.9 색을 이름으로 묶기
버튼 색, 글자 색, 테두리 색을 저마다 다른 숫자로 흩어두면 다크 모드 같은 변주를 붙이기 어렵습니다. 역할별로 이름을 붙여 두면 한 벌만 바꿔 화면 전체의 옷을 갈아입힐 수 있습니다.
/* 역할 기반 색 토큰 */
:root {
--text: #111827;
--bg: #ffffff;
--border: #e5e7eb;
}
.panel {
color: var(--text);
background: var(--bg);
border: 1px solid var(--border);
}
어두운 배경으로 바꿀 때는 값 세 개만 다시 정의하면 됩니다.
@media (prefers-color-scheme: dark) {
:root {
--text: #f3f4f6;
--bg: #111827;
--border: #374151;
}
}
색을 쓰는 규칙은 그대로 두고 토큰의 값만 갈아끼우니, 화면 하나하나를 손대지 않고도 밝고 어두운 두 모드를 함께 지탱합니다.
74.10 정리
핸드오프의 함정은 대부분 값을 이름으로 묶고 상태와 반응형을 미리 채우는 것으로 풀립니다. 시안이 보여주지 않는 경우까지 코드로 메워두면, 다리 위에서 발이 걸리는 일이 줄어듭니다.