← 제작기

검사는 통과했는데 화면이 틀렸다

이 사이트에 한국어와 영어를 바꿔 보는 기능을 붙였습니다. 글이 일곱 편이라 번역만으로도 일이 꽤 많았는데, 정작 시간을 가장 많이 쓴 것은 제대로 됐는지 확인하는 일이었습니다.

검사를 여러 개 짰습니다. 풀리지 않은 문구 키가 남았는지, 영어 화면에 한글이 새어 나오는지, 언어를 바꿔도 정렬 순서가 같은지, 좁은 화면에서 가로로 밀리는지를 봤습니다. 마지막에는 4페이지 × 11가지 폭 × 2개 언어, 모두 88개 조합을 한 번에 돌려 전부 통과시켰습니다.

그런데도 두 번, 화면을 눈으로 보고서야 틀린 것을 알았습니다. 두 번 다 검사는 모두 통과한 상태였습니다.

1. 한 속성에 두 가지 일을 시켰다

전환 방식은 단순합니다. 본문을 한국어와 영어 두 벌로 넣어 두고, 지금 언어가 아닌 쪽을 CSS 로 숨깁니다.

html[lang="ko"] [lang="en"],
html[lang="en"] [lang="ko"] { display: none !important; }

잘 동작했습니다. 글도 제대로 바뀌었고 검사도 전부 통과했습니다.

문제는 lang 이 원래 "이 부분이 무슨 언어인가"를 뜻하는 속성이라는 점입니다. 영어 글 안에 한국어 단어가 하나 끼어 있으면, 표준에서는 스크린 리더가 그 부분만 한국어로 읽도록 <span lang="ko">고유진</span> 처럼 씁니다.

위의 규칙대로라면 그 단어가 화면에서 사라집니다.

하필 이 사이트의 영어 글에 그런 자리가 세 군데 있었습니다. 인증서의 CN=고유진과 한글 파일 이름 두 개입니다. 보조 기술이 제대로 읽게 하려면 lang="ko" 를 달아야 하는데, 다는 순간 화면에서 지워집니다. 오류도 경고도 없습니다.

당장은 아무 증상이 없었습니다. 아직 아무도 그 마크업을 쓰지 않았기 때문입니다. 이것은 나중에 올바른 방법으로 쓴 사람이 원인을 찾지 못할 종류의 결함입니다. "글자를 넣었는데 안 보인다"에서 시작해 CSS 선택자까지 거슬러 올라가야 하기 때문입니다.

원인은 속성 하나를 두 가지 뜻으로 쓴 것이어서 둘로 나눴습니다. lang 은 무슨 언어인지를 알려 주는 보조 기술용으로 두고, 전환에는 data-lang 을 쓰기로 했습니다.

html[lang="ko"] [data-lang="en"],
html[lang="en"] [data-lang="ko"] { display: none !important; }

2. 고치면서 같은 실수를 되풀이했다

바꾼 뒤 검사를 다시 돌렸고, 전부 통과했습니다. 인라인 한국어 세 곳이 되살아났고, 본문 전환도 정확했고, 콘솔에도 오류가 없었습니다.

스크린샷을 찍어 보니 KO 버튼이 없었습니다.

언어 버튼도 data-lang="ko" 를 달고 있었습니다. 방금 만든 규칙이 그 버튼까지 그대로 잡아서, 영어 화면에서 KO 버튼이 자기 자신을 숨겼습니다. 한국어로 돌아갈 방법이 없어진 것입니다.

"한 속성에 두 뜻을 담지 않는다"를 고친 직후에, 같은 파일 안에서 똑같은 실수를 했습니다. 버튼의 data-lang 은 "이 버튼이 고르는 언어"라는 뜻이었고, CSS 의 data-lang 은 "언어에 따라 숨길 덩어리"라는 뜻이었습니다. 이름이 같으니 규칙이 둘을 구분하지 못했습니다.

검사는 왜 이것을 잡지 못했을까요. 확인 항목에 "버튼이 보이는가"를 넣지 않았기 때문입니다. 본문이 바뀌는지, 인라인 한국어가 살아 있는지, 콘솔이 조용한지, 가로로 밀리는지는 모두 봤지만, 정작 누를 버튼이 그 자리에 있는지는 묻지 않았습니다.

버튼 쪽 속성을 data-set-lang 으로 따로 떼어 해결했습니다. 코드에는 왜 이름이 달라야 하는지도 함께 적어 두었습니다. 적어 두지 않으면 언젠가 누군가, 아마 제가 이름을 통일하겠다며 되돌려 놓을 것이기 때문입니다.

3. 375px 에서는 나지 않았다

며칠 뒤 "휴대폰에서 프로젝트 부분이 옆으로 밀린다"는 말을 들었습니다.

제 검사에서는 보지 못한 증상이었습니다. 375px 에서 다시 재 봐도 넘치지 않았는데, 흔한 안드로이드 화면 폭인 360px 으로 줄이자 문제가 나타났습니다.

재 보니 360px 화면에서 카드가 464px 로 그려지고 있었습니다.

1fr 은 minmax(auto, 1fr) 이다

모바일에서 그리드를 한 칸으로 바꾸는 규칙이 있었습니다.

@media (max-width: 700px) {
  .grid { grid-template-columns: 1fr; }
}

1fr 은 minmax(auto, 1fr) 을 줄인 표기입니다. 여기서 auto 는 내용의 min-content 보다 좁아지지 않는다는 뜻입니다. 카드 안에 스크린샷 썸네일이 가로로 늘어선 줄이 있어서 카드의 min-content 가 464px 이었고, 트랙이 그만큼 벌어져 화면 밖으로 밀려난 것입니다.

minmax(0, 1fr) 로 바꾸자 324px 안에 들어왔습니다.

여기서 멈췄다면 절반만 고친 셈이었을 것입니다. 폭을 하나씩 바꿔 가며 재 보니 태블릿 구간(대략 700~1000px)에도 같은 문제가 있었습니다. 기본 규칙 minmax(320px, 1fr) 이 만드는 트랙이 카드보다 좁아지는 구간이 있었기 때문입니다. 휴대폰만 보고 끝냈다면 이 문제는 그대로 남았을 것입니다.

카드가 제 폭에 맞은 뒤에도 안쪽이 또 넘쳤습니다. 스크린샷 줄(346px)과 카드 아래의 링크 줄(321px)이 카드의 내용 영역보다 넓었습니다. 문제가 세 겹이었습니다.

한 폭에서 한 번 재고 "고쳤다"고 하면 안 됩니다. 이런 문제는 한 점이 아니라 구간에 걸쳐 있습니다.

다 고친 뒤 4페이지 × 11가지 폭 × 2개 언어를 다시 돌려, 88개 조합 모두에서 넘침이 없는 것을 확인했습니다.


남는 것

  • 한 이름에 두 가지 뜻을 담으면, 그 실수를 고친 직후에도 같은 실수를 합니다. lang 에서 data-lang 으로 옮기자마자 버튼에서 똑같이 부딪혔습니다. 주석으로 설명하는 것보다 이름을 나누는 쪽이 확실했습니다.
  • 자동 검사는 제가 물어본 것에만 답합니다. "본문이 바뀌었나"는 물었지만 "버튼이 아직 거기 있나"는 묻지 않았습니다. 검사가 늘어날수록 묻지 않은 것이 무엇인지는 더 안 보이게 됩니다.
  • 통과한 검사 88개보다 스크린샷 한 장이 먼저 찾았고, 두 번 다 그랬습니다. 숫자로 재는 방법은 재는 대상이 맞을 때만 쓸모가 있습니다.
  • 증상이 없는 결함이 가장 늦게 발견됩니다. 인라인 lang 문제는 아무 증상이 없었습니다. 아직 아무도 올바른 방법으로 쓰지 않았을 뿐이었습니다.
  • 한 폭에서 재고 끝내지 않습니다. 휴대폰에서 고치고 태블릿에 남겨 두면 고치기 전과 크게 다르지 않습니다.

검사기의 "통과"를 사람의 판정과 대조하게 만든 이야기는 계측기를 놓은 날에 있습니다.

I added Korean/English switching to this site. With seven posts, the translation alone was a fair amount of work — but what actually took the time was checking that it was right.

I wrote a lot of checks. Were any string keys left unresolved? Was Korean leaking into the English view? Did the sort order stay the same across a language switch? Did anything push sideways on a narrow screen? At the end I ran 4 pages × 11 widths × 2 languages = 88 combinations in one go, and passed all of them.

And twice, I only found out something was wrong by looking at the screen. Both times, the checks were green.

1. I gave one attribute two jobs

The switching mechanism is simple. Each post carries both language bodies, and CSS hides whichever one is not current.

html[lang="ko"] [lang="en"],
html[lang="en"] [lang="ko"] { display: none !important; }

It worked. The text swapped correctly and every check passed.

The problem is that lang means "what language is this" in the first place. When an English page contains a single Korean word, the correct markup is <span lang="ko">고유진</span> — so a screen reader pronounces that part as Korean.

Under the rule above, that disappears.

There were exactly three such places in this site's English text: the certificate's CN=고유진 and two Korean filenames. Marking them up correctly for assistive technology means adding lang="ko" — and the moment you add it, the text vanishes. No error, no warning.

There were no symptoms at the time, because nobody had written that markup yet. This is the kind of defect whose cause is unfindable by whoever eventually does it right — you would have to trace from "I typed text and it isn't showing" all the way back to a CSS selector.

The cause was using one thing for two purposes, so I split them: lang means what language something is (for assistive tech), and data-lang drives the switching.

html[lang="ko"] [data-lang="en"],
html[lang="en"] [data-lang="ko"] { display: none !important; }

2. Fixing it, I made the same mistake again

I made the change and re-ran the checks. All passing. The three inline Korean fragments were back, the body blocks swapped correctly, the console was quiet.

Then I took a screenshot, and the KO button was gone.

The language buttons also carried data-lang="ko". The rule I had just written caught them, so in English the KO button hid itself — and there was no way back to Korean.

Immediately after fixing "don't give one attribute two meanings," I made exactly that mistake again, in the same file. On the button, data-lang meant "the language this button selects." In the CSS, it meant "a block to be hidden by language." Same name, so the rule could not tell them apart.

Why didn't the checks catch it? Because "is the button visible" was not one of the things I asked. I asked whether the body swapped, whether the inline Korean survived, whether the console was quiet, whether anything overflowed — but never whether the thing you click is still there.

The buttons use data-set-lang now. The code also records why the names have to differ — without that, someone (probably me) will eventually "tidy it up" and put it back.

3. It didn't happen at 375px

A few days later I heard this: "on my phone the projects section slides sideways."

That symptom was not in my checks. Measuring again at 375px, there was no overflow. At 360px — a common Android width — there was.

The measurement: on a 360px screen, the card was rendering at 464px.

1fr is minmax(auto, 1fr)

There was a rule collapsing the grid to a single column on mobile.

@media (max-width: 700px) {
  .grid { grid-template-columns: 1fr; }
}

1fr is shorthand for minmax(auto, 1fr), and that auto means "never narrower than the content's min-content." The card contains a row of screenshot thumbnails laid out horizontally, which made the card's min-content 464px — so the track widened to match and pushed past the screen.

Changing it to minmax(0, 1fr) brought the card to 324px.

Stopping there would have been half a fix. Measuring across widths one at a time, the same problem existed in the tablet range (roughly 700–1000px), where the base rule minmax(320px, 1fr) produces tracks narrower than a card's min-content. Checking only the phone would have left that in place.

And even once the card fit its track, its insides overflowed: the screenshot row (346px) and the footer link row (321px) were both wider than the card's content box. Three layers.

Measuring once at one width and calling it fixed is not enough. This kind of problem exists as a range, not a point.

After fixing all three I re-ran 4 pages × 11 widths × 2 languages and confirmed no overflow in any of the 88 combinations.


What stays with me

  • Give one name two meanings and you will repeat the mistake right after fixing it. I moved from lang to data-lang and hit the identical problem on the buttons minutes later. Splitting the names worked where a comment would not have.
  • Automated checks answer only what you asked. I asked whether the body swapped; I never asked whether the button was still there. The more checks you have, the harder it gets to see what you didn't ask.
  • One screenshot beat 88 passing checks. Both times. Measuring is only useful when you are measuring the right thing.
  • Defects with no symptoms are found last. The inline lang problem produced nothing visible. Nobody had done it the right way yet.
  • Don't measure at one width and stop. Fixing the phone and leaving the tablet broken is not far from not fixing it.

How a checker's "pass" was put side by side with a person's verdict is in The Day the Gauges Went In.