JS가 실행되기 전 화면이 틀려 있었어요 — SSR과 선택 상자의 함정
Vue로 공공 CMS 만들기 시리즈
(1편)- 1.JS가 실행되기 전 화면이 틀려 있었어요 — SSR과 선택 상자의 함정
공공기관 게시판에 분류 필터를 붙였어요. "채용", "입찰", "일반" 중에 고르고 검색을 누르면 그 분류만 보이는 평범한 기능이에요.
동작은 맞았어요. "채용"을 고르고 검색하면 채용 글만 나왔습니다. 그런데 선택 상자는 "전체"로 돌아가 있었어요. 잠깐 그랬다가 제대로 바뀌는 것도 아니고, 느린 연결에서는 꽤 오래 그랬습니다.
결과는 "채용"인데 화면은 "전체"라고 말하는 상태예요. 사용자 입장에선 필터가 안 먹은 것처럼 보입니다.
왜 GET 폼이었나
먼저 이 화면이 왜 이렇게 생겼는지부터요. 공개 사이트의 검색 폼은 이렇게 생겼어요.
method="get"인 평범한 폼이에요. JS로 가로채서 비동기로 쏘지 않습니다. 이렇게 한 이유는 두 가지예요.
- JS 없이도 동작해야 해요. 공공 사이트는 사용자 환경을 고를 수 없어요.
- 검색 결과에 주소가 있어야 해요. 분류·검색어가 쿼리스트링에 남으니, 결과 화면을 북마크하고 공유하고 뒤로 가기로 돌아올 수 있습니다.
그래서 검색하면 서버가 그 조건으로 그린 HTML을 새로 돌려줘요. Nuxt SSR이 담당합니다. 선택 상자의 현재 값도 당연히 그 HTML에 들어 있어야 해요.
그런데 안 들어 있었습니다.
바로 옆 입력창은 멀쩡했어요
제일 이상한 건 이거였어요. 같은 폼 안에서 제목 검색 입력창은 값이 남아 있었습니다. q=안내로 검색하면 입력창에 "안내"가 그대로 있어요. 선택 상자만 초기화된 것처럼 보였습니다.
둘 다 똑같이 :model-value로 값을 받는데 왜 한쪽만 될까요. Vue SSR이 실제로 뭘 출력하는지 찍어봤어요.
결과예요.
input에는 value="hello"가 붙었고, select에도 value="b"가 붙어 있어요. 둘 다 잘 나온 것처럼 보이죠.
문제는 <select value="b">가 아무 의미 없는 속성이라는 거예요.
HTML 명세에 select 요소의 value 속성은 없어요. 브라우저는 그 글자를 그냥 무시합니다. 선택 상태를 HTML로 말하는 방법은 하나뿐이에요 — option에 붙는 selected.
input의 value는 진짜 HTML 속성이라 서버가 보낸 그대로 화면에 반영돼요. select의 value는 **DOM 속성(property)**이라서 JS가 실행된 뒤에야 존재합니다. 그래서 JS 실행 전에는 브라우저 기본 동작대로 첫 번째 option이 선택돼 보여요. 우리 폼에서는 그 첫 번째가 "전체"였습니다.
여기까지 오면 왜 "잠깐 깜빡"이 아니라 "꽤 오래"였는지도 설명돼요. 하이드레이션이 끝나야 고쳐지는 거니까, 느린 연결일수록 틀린 화면이 길게 보입니다. 그리고 JS를 아예 끄면 끝까지 틀려 있어요. GET 폼으로 JS 없이도 되게 만들어 놨는데, 정작 화면이 거짓말을 하는 상태였죠.
고친 것
option마다 selected를 붙였어요.
컴포넌트 주석에도 이유를 남겼습니다. 나중에 이 줄을 보고 "이거 왜 있지?" 하면서 지우면 같은 문제가 돌아오니까요.
테스트는 SSR로 써야 해요
이 버그의 고약한 점은 일반 컴포넌트 테스트로는 안 잡힌다는 거예요. mount()로 띄우면 JS가 돌고 있으니 선택 상태가 맞게 보입니다. 통과해요. 그런데 사용자가 보는 첫 화면은 틀려 있어요.
그래서 renderToString으로 서버가 보내는 HTML 문자열을 직접 검사했어요.
두 번째 테스트의 정규식이 value=""가 아니라 value인 걸 눈치채셨나요. 빈 문자열 속성은 value=""가 아니라 value로 출력돼요. 처음에 value=""로 쓰고 왜 안 맞나 한참 봤습니다. 주석으로 남겨뒀어요.
접근성으로 보면
이건 "잠깐 깜빡이는 UI 버그"가 아니에요.
KWCAG·WCAG로 보면 지금 상태를 잘못 알려주는 문제입니다. 화면낭독기로 폼을 다시 확인할 때, JS 실행 전이라면 "전체, 선택 상자"라고 읽혀요. 실제 결과는 "채용"인데요. 눈으로 보는 사람에게도 똑같이 틀린 정보고요.
필터·정렬·페이지 크기 같은 GET 폼으로 상태를 주고받는 선택 상자는 다 해당돼요. 폼을 비동기로 처리하고 주소를 안 바꾸는 구조라면 안 만나는 버그라, 서버 렌더링으로 돌아가는 공공 사이트에서 더 잘 드러납니다.
오늘 정리
select의 선택 상태를 HTML로 말하는 방법은option의selected뿐이에요.<select value="...">는 브라우저가 무시합니다.input은 되고select는 안 되는 이유는 한쪽은 HTML 속성, 한쪽은 DOM 속성이기 때문이에요.- SSR 화면의 테스트는
renderToString으로.mount()는 JS가 돌고 있어서 이 부류의 버그를 통과시켜요.
본인 프로젝트가 SSR이라면, JS를 끄고 필터를 한 번 써보세요. 결과와 화면이 다른 말을 하는 곳이 있으면 거기가 같은 문제예요.
관련 링크
<select>— MDN- Vue SSR — renderToString
- @hanui/vue — KRDS 기반 Vue 컴포넌트
- HANUI CMS — 이 글의 게시판 화면이 들어간 공공기관 CMS (MIT)
- claude-settings —
/a11y,/review커맨드 저장소
시리즈: Vue로 공공 CMS 만들기
- 1편: JS가 실행되기 전 화면이 틀려 있었어요 — SSR과 선택 상자의 함정 ← 현재 글
- 2편: 화면과 서버에 같은 요청을 보내 비교했더니 20곳이 달랐어요 (예정)
함께 보면 좋은 글: KRDS를 그대로 가져다 쓰면 검수에 걸려요 — 같은 Select에서 찾은 다른 문제(글자 명도 대비)도 다룹니다.
HANUI
KRDS 기반 React 컴포넌트 라이브러리. 공공 웹 개발을 더 쉽게.