aimake.log
인프라

CSS 그리드가 화면 밖으로 넘칠 때 — 다크 테마 블로그에서 만난 함정 3개

이 블로그를 커스텀 테마로 직접 만들면서 18편을 쓰는 동안, 같은 종류의 함정에 세 번 걸렸습니다. 공교롭게도 셋 다 성격이 똑같습니다 — 내가 보고 있는 화면에서는 멀쩡한데, 조건이 조금 바뀌면 깨지는 것들입니다. CSS…

2026.08.13 11 MIN
세 줄 요약

그리드가 화면 밖으로 넘치는 건 대개 1fr 때문입니다. 그리드 아이템의 최소 크기가 auto라 콘텐츠보다 작아지지 못합니다. minmax(0,1fr)min-width:0 이 답입니다.

다크 테마에서 회색이 안 읽히는 건 어느 배경에서 쟀느냐의 문제입니다. 페이지 배경에서 5.07:1 이던 색이 카드 배경에서는 4.45:1 로 AA 미달이었습니다.

셋 다 내 화면에서만 확인해서 놓친 것들입니다. 헤드리스 브라우저로 실제 폭에서 한 번 찍어보면 3분이면 드러납니다.

이 블로그를 커스텀 테마로 직접 만들면서 18편을 쓰는 동안, 같은 종류의 함정에 세 번 걸렸습니다. 공교롭게도 셋 다 성격이 똑같습니다 — 내가 보고 있는 화면에서는 멀쩡한데, 조건이 조금 바뀌면 깨지는 것들입니다. CSS 그리드 넘침, 다크 테마 대비, 모바일 이미지 크롭. 하나씩 원인과 처방을 적습니다.

CSS 그리드가 왜 화면 밖으로 넘칠까

본문과 사이드바를 나누는 흔한 2단 구성입니다.

.body-grid {
  display: grid;
  grid-template-columns: 1fr 300px;
}

보통은 잘 돌아갑니다. 그런데 본문에 줄바꿈 없는 긴 줄이 하나 들어가는 순간 무너집니다. 코드블록, 긴 URL, 넓은 표가 흔한 원인입니다.

같은 2단 레이아웃 두 개를 비교한 화면. 위쪽 BEFORE 는 grid-template-columns 가 1fr 300px 이라 긴 코드 한 줄 때문에 오른쪽 사이드바가 화면 밖으로 밀려 잘려 있다. 아래쪽 AFTER 는 minmax(0,1fr) 과 min-width 0 을 적용해 코드블록이 칼럼 안에서 가로 스크롤되고 사이드바가 제자리에 있다.
같은 뷰포트 1000px. 위는 사이드바가 화면 밖으로 밀려났고 아래는 제자리다

사이드바가 화면 밖으로 밀려나고 페이지 전체에 가로 스크롤이 생깁니다. 재현해서 재봤더니 뷰포트는 1000px 인데 문서 폭이 1078px — 78px 이 화면 밖으로 나가 있었습니다.

원인: 1fr 은 생각만큼 안 줄어든다

1fr 을 “남는 공간을 채우되 필요하면 줄어드는 칸”으로 이해하기 쉽습니다. 절반만 맞습니다.

그리드 아이템의 min-width 기본값은 0 이 아니라 auto 입니다. 그리고 auto 는 “콘텐츠의 최소 크기”를 뜻합니다. 안에 든 코드 한 줄이 1,200px 라면 그 칸은 1,200px 아래로 내려가기를 거부합니다. overflow-x: auto 를 걸어놨어도 소용없습니다. 스크롤할지 말지를 정하기 전에 칸 자체가 이미 커져버렸기 때문입니다.

처방: 두 줄

.body-grid {
  grid-template-columns: minmax(0,1fr) 300px;  /* 0까지 줄어들 수 있게 */
}
article {
  min-width: 0;   /* 아이템 쪽에서도 한 번 더 */
}

minmax(0,1fr) 은 최소치를 auto 가 아니라 0 으로 못박습니다. 이제 칸이 먼저 줄고, 넘치는 코드블록은 overflow-x 대로 자기 안에서 스크롤됩니다.

플렉스도 같은 함정이 있습니다. flex:1 만 주면 안 줄어들고, flex:1; min-width:0 이어야 줄어듭니다. 이 블로그 테마의 검색창·작성자 카드가 전부 이 조합입니다.

증상 범인 처방
그리드 칸이 안 줄어듦 1fr 의 최소치가 auto minmax(0,1fr)
플렉스 아이템이 안 줄어듦 같은 이유 min-width: 0
표가 화면을 밀어냄 표는 콘텐츠 폭을 고집함 display:block; overflow-x:auto

다크 테마에서 회색 글자가 왜 안 읽힐까

검정 배경에 회색 보조 텍스트는 다크 테마의 기본기입니다. 문제는 어느 회색이냐인데, 여기서 저는 두 번 틀렸습니다.

첫 번째는 흔한 실수였습니다. 처음 쓰던 #63666d 는 눈으로 보기엔 괜찮았지만 대비가 3.54:1 로 WCAG AA 기준(4.5:1)에 한참 못 미쳤습니다. #7b7f87 로 올려서 5.07:1 을 만들고 “AA 통과”라고 주석까지 달아뒀습니다.

두 번째 실수가 이 글의 핵심입니다. 배경을 하나만 놓고 쟀습니다.

배경에 따라 갈리는 대비같은 회색인데 배경이 바뀌면 대비가 달라진다AA 기준 4.5:1 — 어느 배경에서 재느냐로 합불이 갈린다–bg#050506–surface-2#17171a#63666d처음 쓰던 값본문 보조 텍스트3.54:1본문 보조 텍스트3.11:1#7b7f871차 수정본문 보조 텍스트5.07:1본문 보조 텍스트4.45:1#7f838b현재본문 보조 텍스트5.36:1본문 보조 텍스트4.70:1#7b7f87 은 –bg 위에선 통과하지만 카드 배경 위에선 4.45:1 로 미달이었다
같은 회색이라도 어느 배경 위에서 재느냐로 AA 합불이 갈린다

이 사이트에는 배경이 하나가 아닙니다. 페이지 배경(--bg #050506) 위에도 글자가 놓이지만, 카드·검색창·표 머리는 더 밝은 --surface-2(#17171a) 를 씁니다. 배경이 밝아지면 밝은 회색 글자와의 대비는 떨어집니다.

글자색 --bg --surface-2
#63666d (처음) 3.54:1 미달 3.11:1 미달
#7b7f87 (1차 수정) 5.07:1 통과 4.45:1 미달
#7f838b (현재) 5.36:1 통과 4.70:1 통과

“AA 통과”라고 적어둔 색이 실제로는 사이트에서 제일 흔한 카드 배경 위에서 미달이었습니다. 0.05 차이라 눈으로는 절대 못 잡습니다.

처방: 최악 배경을 기준으로 재라

규칙 하나로 정리했습니다. 대비는 가장 밝은 배경 토큰 위에서 잰다. 거기서 통과하면 나머지는 자동으로 통과합니다. 계산은 눈이 아니라 코드로 합니다.

python3 - <<'PY'
def lin(c):
    c /= 255
    return c/12.92 if c <= 0.03928 else ((c+0.055)/1.055)**2.4

def L(h):
    h = h.lstrip('#'); r,g,b = (int(h[i:i+2],16) for i in (0,2,4))
    return 0.2126*lin(r) + 0.7152*lin(g) + 0.0722*lin(b)

def ratio(a, b):
    la, lb = L(a), L(b)
    hi, lo = max(la,lb), min(la,lb)
    return (hi+0.05) / (lo+0.05)

for bg in ('#050506', '#101012', '#17171a'):      # 배경 토큰 전부
    print(bg, round(ratio('#7f838b', bg), 2))
PY

토큰 주석에 적는 숫자도 최악 배경 기준으로 바꿨습니다. 통과하는 숫자만 골라 적어두면 나중의 나를 속이게 됩니다.

모바일에서 이미지가 잘려나갈 때

세 번째는 aspect-ratio 였습니다. 이 블로그의 글 커버는 제목 뒤에 배경으로 깔립니다. 화면 폭에 따라 비율을 다르게 줬습니다.

.cover { aspect-ratio: 16/6; }                      /* 데스크톱 — 시네마틱한 띠 */
@media (max-width:700px) { .cover { aspect-ratio: 3/4; } }  /* 모바일 — 세로로 길게 */

커버 원본은 16:9 입니다. 그런데 object-fit: cover비율이 안 맞으면 잘라서 채웁니다. 16:9 를 3:4 칸에 넣으면 좌우가 대량으로 잘립니다. 커버에 글자가 들어 있으면 그대로 토막이 납니다.

모바일 화면 두 개를 나란히 놓은 비교. 왼쪽 BEFORE 는 커버를 3대4 로 강제 크롭해 16대9 원본의 좌우가 잘려 글자가 토막 나 있고 제목과 겹친다. 오른쪽 AFTER 는 커버를 16대9 그대로 보여주고 제목을 그 아래로 내려 둘 다 온전히 읽힌다.
왼쪽은 3:4로 강제 크롭돼 커버 글자가 토막 나고 제목과 겹쳤다

왼쪽이 그 결과입니다. 커버의 글자가 잘린 채 제목과 겹쳐서 둘 다 안 읽힙니다. 그런데 저는 이걸 몇 주 동안 몰랐습니다. 글을 쓰고 확인하는 화면이 늘 노트북이었기 때문입니다.

처방: 모바일에서는 겹치기를 포기한다

좁은 화면에서 이미지 위에 긴 한글 제목을 얹는 건 애초에 무리입니다. 그래서 모바일에서는 겹치지 않게 분리했습니다.

@media (max-width:700px) {
  .cover { aspect-ratio: auto; overflow: visible; }
  .cover > img { aspect-ratio: 16/9; height: auto; }  /* 원본 비율 그대로 */
  .cover::after { display: none; }                    /* 그라데이션 제거 */
  .cover .cap { position: static; }                   /* 제목을 이미지 아래로 */
}

데스크톱은 겹치기를 유지하되 크롭을 16/6 에서 9/4 로 완화하고, 제목 뒤 그라데이션을 진하게 올렸습니다. 그리고 커버를 만들 때 지킬 규칙을 하나 정했습니다 — 제목이 앉는 왼쪽 아래는 비워둔다.

세 함정의 공통점

정리해놓고 보니 셋 다 원인이 하나였습니다. 내 화면 하나에서만 확인했다는 것.

함정 내 화면에선 실제로는
그리드 넘침 짧은 코드라 멀쩡 긴 줄 하나에 78px 넘침
회색 대비 페이지 배경에서만 확인 카드 배경에서 AA 미달
모바일 크롭 노트북에서만 확인 390px 에서 글자 토막

그래서 발행 절차에 한 단계를 넣었습니다. 헤드리스 브라우저로 실제 폭에서 한 장 찍어보는 것입니다.

# 모바일 폭으로 전체 페이지 캡처
npx -y playwright screenshot --viewport-size=390,844 --full-page \
  https://example.com/my-post/ mobile.png

넘침을 잡는 요령도 있습니다. --full-page 로 찍은 PNG 의 가로 픽셀이 지정한 뷰포트보다 크면 그만큼 넘친 것입니다. 위의 그리드 사례에서 1000 으로 찍었는데 1078 이 나온 게 정확히 그 신호였습니다.

$ npx -y playwright screenshot --viewport-size=1000,560 --full-page demo.html out.png
$ sips -g pixelWidth out.png
  pixelWidth: 1078        # 78px 넘침

정리

  1. 1fr 의 최소치는 auto — 안 줄어들면 minmax(0,1fr)min-width:0
  2. 플렉스도 같다 — flex:1 만으로는 부족하고 min-width:0 이 있어야 한다
  3. 대비는 가장 밝은 배경 토큰 위에서 잰다 — 페이지 배경에서만 재면 카드 위에서 미달난다
  4. 대비 숫자는 눈이 아니라 코드로 계산하고, 토큰 주석에는 최악값을 적는다
  5. object-fit:cover + aspect-ratio비율이 다르면 자른다 — 글자 든 이미지에 쓰지 말 것
  6. 좁은 화면에서는 이미지와 제목을 겹치지 말고 분리하는 편이 낫다
  7. 발행 전에 실제 폭으로 한 장 찍어볼 것 — 전체 캡처 폭이 뷰포트보다 넓으면 넘친 것이다

간격과 타입 스케일을 정할 때는 규칙을 미리 정해두면 고민이 사라진다고 썼습니다. 이번 셋은 반대쪽 이야기입니다. 규칙을 정해놔도 확인하는 조건이 하나뿐이면 그 규칙이 지켜지고 있는지 알 수가 없습니다.

검증 기록: 2026-08-13 실측. 그리드 넘침은 테마와 같은 2단 구성을 재현해 Playwright(Chromium)로 뷰포트 1000px 에서 캡처, 전체 페이지 폭 1078px 확인. 대비값은 WCAG 상대휘도 공식으로 직접 계산(#7b7f87 은 –bg 위 5.07:1 · –surface-2 위 4.45:1). 모바일 크롭은 수정 전후 aimakelog.app 을 390×844 로 캡처해 대조. 세 처방 모두 이 사이트 테마에 반영해 재배포 후 확인.

Author

재박

AI로 뭔가를 만들다 막힌 지점과, 그걸 어떻게 뚫었는지 기록합니다.

RSS
구독
이어서 읽기
aimake.log AI로 실제 결과물 만드는 실전 기록 소개 개인정보처리방침 RSS